#!/usr/bin/env ruby require 'sqlite3' require 'syslog' DB_FILE = "metrics.db" # Syslog と DB の準備 facility= if STDERR.tty? then Syslog::LOG_USER else Syslog::LOG_NEWS end Syslog.open('iwg_watch', Syslog::LOG_PID, facility) db = SQLite3::Database.new(DB_FILE) # メッセージカウンタ $nalert=0 # DB最新更新時刻の取得。取得できなければDBがカラなので終了 now_ts = db.get_first_value( "select max(ts) from metrics" ) unless now_ts Syslog.error("metrics table empty") abort("metrics table empty") end # syslog用英語メッセージを日本語訳する def translate msg msg.gsub!(/ALERT/, "【注意】") msg.gsub!(/INFO/, "〔参考〕") msg.gsub!(/RECOVER/, "〖解除〗") msg.gsub!(/CLEAR/, "〔終了〕") msg.gsub!(/low(\d+)/, "\\1h減少") msg.gsub!(/high(\d+)/, "\\1h増加") msg.gsub!(/\.not_stored/, ".電文数(不保存)") msg.gsub!(/\.stored/, ".電文数(保存)") msg.gsub!(/\.bulletins/, ".全電文数") msg.gsub!(/\.files_received/, ".受信ファイル数") msg.gsub!(/\.errors/, ".エラーファイル数") msg.gsub!(/\.invalid/, ".不正ファイル数") msg.gsub!(/current=/, "現状=") msg.gsub!(/threshold=/, "閾値=") msg.gsub!(/\(null\)/, "記録なし") msg.gsub!(/last_nonzero=/, "直近非ゼロ=") msg.gsub!(/last_zero=/, "直近ゼロ=") msg.gsub!(/hours_since_(\S+)/, "\\1時間前") msg.gsub!(/days_since_(\S+)/, "\\1日前") msg.gsub!(/duration_hours=(\S+)/, "継続=\\1時間") end # メッセージをSyslogと標準出力に出す $nolog=true def sys_notice msg $nolog=false case msg when /^(ALERT|RECOVER)/ then Syslog.warning(msg) else Syslog.notice(msg) end translate(msg) puts msg end # 時刻表示用フォーマット:スペースを含まないのが特徴 class Time def show self.utc.strftime('%Y-%m-%dT%H:%M:%SZ') end end # アラート発報状態表 db.execute <<~SQL CREATE TABLE IF NOT EXISTS alert_state ( center_id TEXT, metric TEXT, since_low1 INTEGER, since_low3 INTEGER, since_low24 INTEGER, since_high1 INTEGER, since_high3 INTEGER, PRIMARY KEY(center_id, metric) ); SQL db.execute <<~SQL INSERT OR IGNORE INTO alert_state (center_id,metric) SELECT center_id,metric FROM thresholds; SQL # センター・メトリックに対して閾値、最新ゼロ経緯、発報状態を取得 thresholds = db.execute( <<~SQL SELECT t.center_id, t.metric, t.low1, t.low3, t.low24, t.high1, t.high3, s.last_zero_time, s.last_nonzero_time, a.since_low1, a.since_low3, a.since_low24, a.since_high1, a.since_high3 FROM thresholds t LEFT JOIN last_state s USING(center_id, metric) LEFT JOIN alert_state a USING(center_id, metric) SQL ) def alert_begin(db, center_id, metric, kind, now_ts) db.execute( <<~SQL, [now_ts, center_id, metric] UPDATE alert_state SET #{kind} = ? WHERE center_id=? AND metric=? ; SQL ) $nalert+=1 end def alert_end(db, center_id, metric, kind) db.execute( <<~SQL, [center_id, metric] UPDATE alert_state SET #{kind} = NULL WHERE center_id=? AND metric=? ; SQL ) $nalert+=1 end def add_history(msg, last_ts, label, now_ts) if last_ts then unit='days' age=((now_ts-last_ts)/86400.0).round(2) if age < 1.0 then unit='hours' age=((now_ts-last_ts)/3600.0).round(2) end msg << " last_#{label}=#{Time.at(last_ts).show}"\ " #{unit}_since_last_#{label}=#{age}" else msg << " last_#{label}=(null)" end end thresholds.each do |row| center_id, metric, low1, low3, low24, high1, high3, last_zero_time, last_nonzero_time, since_low1, since_low3, since_low24, since_high1, since_high3 = row # # 最新24時間分取得 # data = db.execute( <<~SQL, [center_id, metric, now_ts - 24 * 3600] select ts, value from metrics where center_id = ? and metric = ? and ts >= ? order by ts SQL ) # max(ts) が取れても、新規追加のセンターなどで空になるかもしれない next if data.empty? values = data.map { |_, value| value.to_f } # # v1 # next if values.size < 2 curr_v1 = values[-1] # # v3 # curr_v3 = nil if values.size >= 3 curr_v3 = values.last(3).sum end # # v24 # curr_v24 = values.sum # # low1 # # 閾値未満の異常値かつ未発報状態なら発報する if curr_v1 < low1 and not since_low1 then msg="ALERT low1 #{center_id}.#{metric} "\ "current=#{curr_v1} threshold=#{low1.round(2)}" # 現在値がゼロになって発報するときは履歴情報を追加 if curr_v1==0 then add_history(msg, last_zero_time, 'zero', now_ts) end msg.sub!(/^ALERT/, 'INFO') if /stored/===metric sys_notice(msg) alert_begin(db, center_id, metric, 'since_low1', now_ts) # 正常値かつ既発報状態なら解除報 elsif curr_v1 >= low1 and since_low1 hours = ((now_ts - since_low1) / 3600.0).round(2) msg="RECOVER low1 #{center_id}.#{metric} "\ "current=#{curr_v1} threshold=#{low1.round(2)}"\ " duration_hours=#{hours}" add_history(msg, last_nonzero_time, 'nonzero', now_ts) msg.sub!(/^RECOVER/, 'CLEAR') if /stored/===metric sys_notice(msg) alert_end(db, center_id, metric, 'since_low1') end # # high1 # if curr_v1 > high1 and not since_high1 then msg= "ALERT high1 "\ "#{center_id}.#{metric} "\ "current=#{curr_v1} threshold=#{high1.round(2)}" if high1==0.0 then add_history(msg, last_nonzero_time, 'nonzero', now_ts) end msg.sub!(/^ALERT/, 'INFO') if /stored/===metric sys_notice(msg) alert_begin(db, center_id, metric, 'since_high1', now_ts) end if curr_v1 <= high1 and since_high1 then msg= "RECOVER high1 "\ "#{center_id}.#{metric} "\ "current=#{curr_v1} threshold=#{high1.round(2)}" add_history(msg, last_zero_time, 'zero', now_ts) msg.sub!(/^RECOVER/, 'CLEAR') if /stored/===metric sys_notice(msg) alert_end(db, center_id, metric, 'since_high1') end # # low3 # if curr_v3 < low3 and not since_low3 then msg = "ALERT low3 #{center_id}.#{metric} "\ "current=#{curr_v3} threshold=#{low3.round(2)}" if curr_v3==0 then add_history(msg, last_zero_time, 'zero', now_ts) end msg.sub!(/^ALERT/, 'INFO') if /stored/===metric sys_notice(msg) alert_begin(db, center_id, metric, 'since_low3', now_ts) elsif curr_v3 >= low3 and since_low3 then hours = ((now_ts - since_low3) / 3600.0).round(2) msg = "RECOVER low3 #{center_id}.#{metric} "\ "current=#{curr_v3} threshold=#{low3.round(2)}"\ " duration_hours=#{hours}" msg.sub!(/^RECOVER/, 'CLEAR') if /stored/===metric sys_notice(msg) alert_end(db, center_id, metric, 'since_low3') end # # high3 # if curr_v3 > high3 and not since_high3 then msg= "ALERT high3 "\ "#{center_id}.#{metric} "\ "current=#{curr_v3} threshold=#{high3.round(2)}" if high3==0.0 then add_history(msg, last_nonzero_time, 'nonzero', now_ts) end msg.sub!(/^ALERT/, 'INFO') if /stored/===metric sys_notice(msg) alert_begin(db, center_id, metric, 'since_high3', now_ts) end if curr_v3 <= high3 and since_high3 then msg= "RECOVER high3 "\ "#{center_id}.#{metric} "\ "current=#{curr_v3} threshold=#{high3.round(2)}" add_history(msg, last_zero_time, 'zero', now_ts) msg.sub!(/^RECOVER/, 'CLEAR') if /stored/===metric sys_notice(msg) alert_end(db, center_id, metric, 'since_high3') end # # low24 # if curr_v24 < low24 and not since_low24 then msg="ALERT low24 #{center_id}.#{metric} "\ "current=#{curr_v24} threshold=#{low24.round(2)}" if curr_v24==0 then add_history(msg, last_nonzero_time, 'nonzero', now_ts) end msg.sub!(/^ALERT/, 'INFO') if /stored/===metric # tested sys_notice(msg) alert_begin(db, center_id, metric, 'since_low24', now_ts) elsif curr_v24 >= low24 and since_low24 hours = ((now_ts - since_low24) / 3600.0).round(2) msg="RECOVER low24 #{center_id}.#{metric} "\ "current=#{curr_v24} threshold=#{low24.round(2)}"\ " duration_hours=#{hours}" add_history(msg, last_nonzero_time, 'nonzero', now_ts) msg.sub!(/^RECOVER/, 'CLEAR') if /stored/===metric sys_notice(msg) alert_end(db, center_id, metric, 'since_low24') end end if $nalert>0 then puts "" puts "monitor: https://toyoda-eizi.net/rrd/wis.html" puts "source: https://www.wis-jma.go.jp/iwgmonitor/iwgmonitor.jsp" end Syslog.info('okay no alert this time') if $nolog db.close Syslog.close