概要
SNMP Trap を送信できず、syslog でなら目的のログは送信できるが、緊急性の高い syslog が出た場合、どうしてもなるべくリアルタイムに Slack 通知したい、という稀によくある自体に遭遇したので、ログ収集に使っている Logstash を拡張して、特定の文字列があったら Slack 通知する Output Plugin を自作した。
構成の概要
現在、各ネットワーク機器は集約サーバを通すことで SNMP メトリック、syslog を収集している。
また、集約サーバは snmptrapd で SNMP Trap も受け付けて、自作のスクリプトを通して Slack 通知を行っている。
発生した問題
とあるネットワーク機器(しかも割りと重要ポジション)がやんごとなき事情により「SNMP での監視が不可能」という状況が発生した。
メトリックは幸いエージェント型のツールで取得することができたが、SNMP Trap で Link Up/Down を検知するといったことができず、判断できる情報は syslog にしか出ない、という状況だった。
しかしながら、Elasticsearch が得意とするのは集約されたログの(それも曖昧な)検索であり、特定の文字列が含まれたログが見つかったら即通知、ということはしづらい。
elastalert2 などを組み込んで、似たようなことは実現できるが、1 分以内など細かい間隔では検知できても通知が 1 回に丸められてしまったりと、望む機能は実現できなかった。
Go などで syslog サーバ自体を自作し、Logstash へ流す(ファイルへ書き込む)機能+Slack へ通知する機能を持たせる、ということも検討したがこれもメンテナンス性が悪くなりそうだったため最終手段とした。
Logstash Output Plugin で対応
そもそも、Logstash は(記述の面倒さは置いておいて)設定次第でそれなりに柔軟に送信するログを選別したり、送信先を分けたりといった機能が備わっている。
しかも Output Plugin を自作することも簡単にできる。
もちろん同じことを考えた人は過去にもいたようだが、Legacy Webhook を前提にしていたり、ずっと更新されていなかったりといった問題があったため、必要な機能を備えた Output Plugin を自作することにした。
以下、サーバは Rocky Linux 8 で Logstash のバージョンは 8.x で進める。
Plugin の作成
パクリ元 参考にしたのはこちらの記事。
https://inokara.hateblo.jp/entry/2016/10/23/091906
雛形の作成
logstash がインストールされているディレクトリ(/usr/share/logstash
)へ移動。
# cd /usr/share/logstash/
# ls
CONTRIBUTORS Gemfile Gemfile.lock JDK_VERSION LICENSE.txt NOTICE.TXT bin data jdk lib logstash-core logstash-core-plugin-api modules tools vendor x-pack
logstash-plugin generate
でひな形を作成。
--name
には任意の plugin 名(ここでは nekoslack
)を入れる。
./bin/logstash-plugin generate --type output --name nekoslack --path ./
実行結果
# ./bin/logstash-plugin generate --type output --name nekoslack --path ./
Using bundled JDK: /usr/share/logstash/jdk
Creating ./logstash-output-nekoslack
create logstash-output-nekoslack/docs/index.asciidoc
create logstash-output-nekoslack/lib/logstash/outputs/nekoslack.rb
create logstash-output-nekoslack/spec/outputs/nekoslack_spec.rb
create logstash-output-nekoslack/CHANGELOG.md
create logstash-output-nekoslack/CONTRIBUTORS
create logstash-output-nekoslack/DEVELOPER.md
create logstash-output-nekoslack/Gemfile
create logstash-output-nekoslack/LICENSE
create logstash-output-nekoslack/README.md
create logstash-output-nekoslack/Rakefile
create logstash-output-nekoslack/logstash-output-nekoslack.gemspec
logstash-output-nekoslack
ディレクトリが作成されているので移動。
# cd logstash-output-nekoslack
# ls
CHANGELOG.md DEVELOPER.md LICENSE Rakefile lib spec
CONTRIBUTORS Gemfile README.md docs logstash-output-nekoslack.gemspec
lib/logstash/outputs/nekoslack.rb
が本体なので、これをいじる。
# cat lib/logstash/outputs/nekoslack.rb
# encoding: utf-8
require "logstash/outputs/base"
# An nekoslack output that does nothing.
class LogStash::Outputs::Nekoslack < LogStash::Outputs::Base
config_name "nekoslack"
public
def register
end # def register
public
def receive(event)
return "Event received"
end # def event
end # class LogStash::Outputs::Nekoslack
なお、特に github などで gem として公開する予定は無いので放置しているが、公開する場合には gemspec を変更しておく。
デフォの gemspec ファイル
# cat logstash-output-nekoslack.gemspec
Gem::Specification.new do |s|
s.name = 'logstash-output-nekoslack'
s.version = '0.1.0'
s.licenses = ['Apache-2.0']
s.summary = 'Logstash Output Plugin for Nekoslack'
s.description = 'TODO: Write a longer description or delete this line.'
s.homepage = 'TODO: Put your plugin''s website or public repo URL here.'
s.authors = ['']
s.email = ''
s.require_paths = ['lib']
# Files
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
# Tests
s.test_files = s.files.grep(%r{^(test|spec|features)/})
# Special flag to let us know this is actually a logstash plugin
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "output" }
# Gem dependencies
s.add_runtime_dependency "logstash-core-plugin-api", "~> 2.0"
s.add_runtime_dependency "logstash-codec-plain"
s.add_development_dependency "logstash-devutils"
end
プラグインの開発(出来上がったもの)
このような感じにプラグインを作成した。
# encoding: utf-8
require "logstash/outputs/base"
require 'net/http'
require 'json'
# An nekoslack output that does nothing.
class LogStash::Outputs::Nekoslack < LogStash::Outputs::Base
config_name "nekoslack"
config :webhook_url, :validate => :string, :required => true
config :title, :validate => :string, :required => false, :default => "undefined title"
config :title_link, :validate => :string, :required => false, :default => ""
config :color, :validate => :string, :required => false, :default => "ffebcd"
config :text, :validate => :string, :required => false, :default => "undefined text"
config :notice_fields, :validate => :array, :required => false, :default => []
config :attachments_short, :validate => :boolean, :required => false, :default => true
config :attachments_code_block, :validate => :boolean, :required => false, :default => false
public
def register
require 'socket'
@hostname = Socket.gethostname
end # def register
public
def receive(event)
@pretext = "[`#{@hostname}`] (Pretext Message)"
@slack_field = []
if notice_fields.any?
notice_fields.each do |field|
field_value = attachments_code_block ? "```#{event.get(field)}```" : "#{event.get(field)}"
slack_field = {
title: field,
value: field_value,
short: attachments_short,
}
@slack_field.push slack_field
end
end
json = JSON.dump(
attachments: [
mrkdwn_in: ["text"],
pretext: @pretext,
title: title,
title_link: title_link,
color: color,
text: text,
fields: @slack_field,
]
)
Net::HTTP.new("hooks.slack.com", 443).tap do |http|
http.use_ssl = true
end.start do |http|
req = Net::HTTP::Post.new webhook_url
req['Content-Type'] = 'application/json'
req['Content-Length'] = json.bytesize
req.body = json
http.request req
end
end # def event
end # class LogStash::Outputs::Nekoslack
処理としては各オプション(webhook_url
, title
, title_link
, color
, text
, notice_fields
, attachments_short
, attachments_code_block
)を元に slack のメッセージを組み立てて webhook_url
宛に送信する、というシンプルなもの。
また、Logstash の field name を受け取って、その値を event.get(field)
で slack のメッセージに組み込んでいる。
こうすることで通知先や通知内容を Logstash 側で柔軟に組み立てられるようにしている(代わりに Logstash の設定ファイルに記述する内容は増える)。
プラグインのビルドとインストール
gem build を行う必要があるため、事前に ruby のインストールを行っておく必要がある。
# ruby --version
ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-linux]
logstash-output-nekoslack
ディレクトリで gem build を実行。
# gem build logstash-output-nekoslack.gemspec
WARNING: open-ended dependency on logstash-codec-plain (>= 0) is not recommended
use a bounded requirement, such as '~> x.y'
WARNING: open-ended dependency on logstash-devutils (>= 0, development) is not recommended
use a bounded requirement, such as '~> x.y'
WARNING: See https://guides.rubygems.org/specification-reference/ for help
Successfully built RubyGem
Name: logstash-output-nekoslack
Version: 0.1.0
File: logstash-output-nekoslack-0.1.0.gem
めんどいので WARNING は無視。
build に成功すれば logstash-output-nekoslack-0.1.0.gem
が出来上がる。
# ls -l logstash-output-nekoslack-0.1.0.gem
-rw-r--r-- 1 root root 8192 1月 4 15:54 logstash-output-nekoslack-0.1.0.gem
logstash-plugin install でインストールする。
# /usr/share/logstash/bin/logstash-plugin install ./logstash-output-nekoslack-0.1.0.gem
Using bundled JDK: /usr/share/logstash/jdk
Validating ./logstash-output-nekoslack-0.1.0.gem
Installing logstash-output-nekoslack
Installation successful
プラグインの使い方
インストールが成功すれば Logstash から output plugin として利用可能になる。
今回の要件としては、特定の文字列があったらこのプラグインを使って即時 Slack 通知することが目的で、受信した syslog に Interface の Up/Down に関する文字列があったら Slack へ通知する設定を Logstash に追加する。
例)
output {
(その他の分岐)
if "ifwatchdog" in [syslog_message] {
if "up->down" in [syslog_message] {
nekoslack {
webhook_url => "slack の webhook URL"
title => "Interface Notification"
title_link => "必要なリンク"
text => "Interface Down 検知"
color => "danger"
notice_fields => ["hostname", "fromhost_ip", "syslog_message"]
attachments_short => false
attachments_code_block => true
}
}
if "down->up" in [syslog_message] {
nekoslack {
webhook_url => "slack の webhook URL"
title => "Interface Notification"
title_link => "リンク"
text => "Interface Up 検知"
color => "good"
notice_fields => ["hostname", "fromhost_ip", "syslog_message"]
attachments_short => false
attachments_code_block => true
}
}
}
(ログ送信先の設定など)
}
この例では「hostname
, fromhost_ip
(メッセージを流した host の IP), syslog_message
(実際に流したログ)」の 3 つをコードブロックで囲み、attachment を short にまとめない形で通知が行われる。
また、Slack へ通知するだけでなく、そのまま Elasticsearch にもログが転送されるので後から検索することもできて便利になった。
終わりに
今回は通知内容を増やしたり変更したくなった場合にプログラミングが必要な箇所を極力減らすために汎用的(オプションで色々変えられる形)に作成したが、ruby で簡単にプラグインが自作できるので、Logstash では実現しづらいロジックをプラグインに作り込むこともできそうだし、今後のログ集約・分析にも応用できそう。
ただ、Logstash 自身がかなり重く、メモリの少ないサーバには載せづらいのが難点。
株式会社ねこじゃらしでは人材募集中です。
気になった方は是非お話だけでも聞きにきてください。
https://www.green-japan.com/company/2706