こんにちは、nekoyaです。
システムを日々運用していく中で、その処理結果の記録や異常検知の仕組みは地味ながらも大切な存在です。
各種監視ツールからの通知や、ブラウザから利用可能なWebインタフェースなど、その形態も様々です。
今回はその中から、バッチ処理の結果通知について、我々のチームが実践している方式をご紹介します。
loggerを通して記録する
まず前提として、通知する内容はプログラマ自身が出力することが基本になります。
自分はここ数年はPythonをメインに使っていて、標準のloggingモジュールを通して
import logging logger = logging.getLogger(__name__) logger.info('hello!')
のようにログを吐いておくと、スクリプトの終了時にそれまで出力したログがいい感じに集約されて通知されるようにしています。
ログレベル
ログを出力する際は「そのログがどのレベルに属するか」を併せて指定します。
Pythonのloggingにもいろいろなレベルが定義されていますし、Log4j的な実装は様々な言語に浸透していると思われます。
あまり種別が増えすぎると煩雑になるので、我々はこの4つのレベルを使うようにしています。
- debug
- debugモードで実行した場合のみ出力される
- info
- 後で参照するために残す正常な処理結果の情報
- warn
- 処理は継続するが把握すべき異常があった
- critical
- 処理を継続できないか、バッチが目的を達成できない異常があった
何をどのレベルで出力するかは開発者次第ですが、大まかな基準は上記のようになっています。
また、スクリプトが途中で異常終了した場合はエラー内容をcriticalで出力し、併せてstacktraceを出力する仕組みを用意しています。
通知の仕組み
スクリプトが終了した時点で、それまでに出力された最も高いレベルのログに合わせて通知が飛びます。
通常はwarn, criticalが発生した場合に、スクリプトをverboseモードで実行した場合はnoticeでも通知します。
通知内容にはスクリプトの実行中に発生した全てのログと、以下の項目が含まれます。
- 検知した最大ログレベル
- 実行ホスト
- 実行スクリプト名
- 実行開始と終了日時
例えば、こんな具合です。
---------------------------------------- From: system@localhost To: warn@localhost Subject: [warn] nekoya.dev.local - alert_sample.py ---------------------------------------- script /home/nekoya/alert_sample.py running at 2016-07-01 16:46:34 ---------------------------------------- 2016-07-01 16:46:34 [INFO] --- find friends --- 2016-07-01 16:46:34 [DEBUG] - found id: 3 2016-07-01 16:46:35 [DEBUG] - found id: 5 2016-07-01 16:46:35 [DEBUG] - found id: 8 2016-07-01 16:46:35 [INFO] found 3 friends 2016-07-01 16:46:35 [INFO] --- send message --- 2016-07-01 16:46:39 [WARNING] failed id: 5 2016-07-01 16:46:41 [INFO] sent 2 messages ---------------------------------------- finished at 2016-07-01 16:46:41
こうしたメッセージをメールやSlackで受けています。
Slackは文字数制限が厳しい上に、クライアントによって上限が異なったりして途中で途切れたりすることもありますが「その場合はメールで全文が確認できるからいいよね」ぐらいの温度感で回しています。
手軽にWebHookで済まさずにAPIを使ってSnippetにするみたいな方法もありそうですが、現時点ではそこまではしていません。
アラートの送信先
こうした通知に送信するか、どこに送信するかは「そのスクリプトを実行している間に発生した中で最も高いレベルのログ」で判断しています。
メッセージにはスクリプトの開始から終了までに出力した全てのログが含まれるので、どのように処理がなされたかの流れが追えます。
通常はwarnかcriticalが発生した場合のみ送るようになっており、バッチをverboseモードで実行した場合には異常がなくても常に通知が送られます。
Slackに送るメッセージはcriticalの場合はグループメンションを付けるようにしています。メールはレベルごとの送信先をプロジェクトの設定ファイルに記述しておいて、スクリプトの実行状況によってそれぞれのアドレスに送っています。
常駐プロセスのアラート管理
この通知の仕組みは「スクリプトの実行開始から終了までのログをひとつの通知にまとめること」を大前提としています。
HTTPのリクエストを処理し続けるWebアプリケーションのように常駐するプロセスにはそのまま適用はできません。
こうしたアプリケーションではログの出力を随時やらせておいて、一定期間ごとにそのログを集約・通知するスクリプトを別で回します。
「アプリケーション自身に一定期間ごとにログを集約して通知する」みたいな処理を担わせることも不可能ではありませんが、
- アプリケーション本来の仕事に集中させたい
- サーバ台数が増えても通知はまとめてやって欲しい
といった理由から切り分けています。
アラートに対する温度感
ログを出力する際のレベル分けは前述のとおりですが、それぞれのレベルのアラートを受けた際には下記のような温度感で動きます。
- critical
- すぐに何らかの対応が必要
- warn
- 営業時間ベースで何らかの対応をする
- notice
- アラートではなく、正常終了の記録を残す目的で使う
上記でいう「対応」とは、全ての問題をその時点で解決することではありません。
「一時的なイレギュラーなのでスルーしていい」とか「明日出社後に対応しよう」といった判断を下すことも立派な対応のひとつです。
アプリケーション以外のアラート
今回のトピックからは少し外れますが、
- 監視ツールからのアラート
- cronからのメール
なども同様の運用に乗せています。
cronで実行したスクリプトの標準出力などは内容によって送信先を変えることが出来ないため、原則として全てcritical扱いにします。
そうなると「あれもこれも/dev/nullに捨ててしまおう」という流れになりがちですが、cronジョブの記述においては>/dev/nullは基本的に禁止しています。
「何らかの出力がある時点でそれは想定外の事態であり、アラートとして受け取るべきである」という考えに基づいています。
サービスの維持に必要ではないものや、一時的な実験などでは例外的に記述を認めることもありますが、あくまでイレギュラーであることを意識した上で実施します。
なお、シェルスクリプトの中で個別のコマンドに対して>/dev/nullを設定することは禁止しておらず、Slackにメッセージを流すコマンドも用意しており、そのあたりで柔軟性を確保しています。
運用の勘所
アラート運用において大きな課題は「通知件数が増えると慣れてしまって見なくなる」というものです。
特に警戒すべきは「定常的に発生するwarning」です。
例えばログ集計を実施するスクリプトを例に挙げます。
- 異常な行があればwarnで記録する
- その行はスキップして処理を継続する
という実装をしたとしましょう。
開発時に「何かあれば気付けた方がよい」という思いでwarnを発生させたところ、何らかの事情で日常的に異常な行が混ざることが発覚します。
この時に前述の実装をそのままにしておくと、「このスクリプトから来るwarnは無視してよい」といった空気が醸成されてしまいます。
その結果、他の部分で発生したwarnのログに誰も気付かずに問題を放置してしまうといった事態が起こりえます。
様々なアラートに関して言えることですが、狼少年化を防ぐために状況に合わせて異常を検出する閾値を変えたり、出力するログの内容やレベルを見直し続けることが大切です。
言うは易しの典型みたいなもので、年数を経たプロジェクトやチームだとメンバーが慣れてしまって、特にやり取りしなくても「これはスルーでOK」とか「これはいつものやつ」みたいになってしまいがちです。
これに対しては新しくチームメンバーが増えた時がチャンスで、新鮮な視点からの疑問や未知のアラートに対する不安などを忌憚なく話してもらえると見直しのいいきっかけになります。
アラートを見ることでコードを書くだけではない、プロジェクトが生きている様子を実感できたり、新たな課題の発見にもつながるので「アラート受けるの不安だわ」みたいに思わずにあらゆる開発者は積極的にアラートを受けていきましょう。