#!/usr/bin/env ruby =begin `last_state` 表と `iwg_state.rb` を追加する理由は、**アラートの意味付けを強化するため**です。現在の `iwg_watch.rb` は「閾値を超えた」「閾値を下回った」という事実は検出できますが、その事象がどれほど珍しいのかは分かりません。例えば `files_received` が 0 になった場合でも、それが「3時間ぶり」なのか「2か月ぶり」なのかでニュースバリューは大きく異なります。`last_state` 表に `last_zero_time` と `last_nonzero_time` を保持しておけば、アラート発報時に「前回ゼロになったのは○日前」「このメトリックは観測期間中一度もゼロになったことがない」といった補足情報を付与でき、運用者が異常の重大性を素早く判断できるようになります。 また、`last_state` を独立した表として持つことで、アラート判定処理が単純になります。毎回 `metrics` 表全体を検索して「最後のゼロ時刻」を求める必要がなく、`center_id` と `metric` をキーに1行取得するだけで済みます。現状の `iwg_state.rb` は `metrics` の内容から定期的に `last_state` を再生成する方式を採っており、ロジックも単純で保守しやすいという利点があります。さらに、将来的に `last_state` に `first_seen_time` や `zero_count_30d` のような情報を追加すれば、閾値だけでは表現できない「珍しさ」をアラート文面に反映できる拡張余地もあります。 現在の運用では `iwg_watch` が毎時の監視を行い、その後に `iwg_state.rb` が更新されるため、監視時点では常に「直前までの履歴」を参照できます。これは「今回ゼロになった時刻」を誤って「前回ゼロになった時刻」と表示してしまう問題を避けるうえで重要な性質です。したがって `last_state` は単なる高速化用キャッシュではなく、**アラートに履歴情報を付加して運用上の判断材料を増やすための補助データベース**である、という位置付けになります。 =end require 'sqlite3' require 'syslog' DB_FILE = "metrics.db" db = SQLite3::Database.new(DB_FILE) now = Time.now.to_i db.execute <<~SQL CREATE TABLE IF NOT EXISTS last_state ( center_id TEXT, metric TEXT, last_zero_time INGEGER, last_nonzero_time INTEGER, PRIMARY KEY(center_id, metric) ); SQL tsize=db.get_first_value(<<~SQL) SELECT COUNT(*) FROM last_state; SQL nins=0 db.execute(<<~SQL) INSERT INTO last_state SELECT center_id, metric, MAX(CASE WHEN value=0 THEN ts END), MAX(CASE WHEN value<>0 THEN ts END) FROM metrics GROUP BY center_id, metric ON CONFLICT(center_id, metric) DO UPDATE SET last_zero_time = COALESCE( excluded.last_zero_time, last_state.last_zero_time ), last_nonzero_time = COALESCE( excluded.last_nonzero_time, last_state.last_nonzero_time ); SQL nins=db.changes msg="event=iwg_state upd=#{nins}/#{tsize}" warn(msg) if STDERR.tty? fac=if STDERR.tty? then Syslog::LOG_USER else Syslog::LOG_NEWS end Syslog::open('iwg_state', Syslog::LOG_PID, fac) do |log| log.info(msg) end db.close