この記事は何?
PythonでログをSlackに通知したいとき、logging.handlers.HTTPHandler
を継承したSlackHandler
のようなSlack通知用ハンドラを定義したくなると思います。
ここで、Slackになんでもかんでもログを通知してしまうとSlackのメッセージがどんどん流れてしまうため、重要なログだけ通知したくなります。
これを実現する方法として、例えばログレベルがWARN
以上のログを飛ばすなどが考えられます。
しかしINFO
のログも通知したい、とはいえINFO
のログを全て通知してしまうとメッセージが流れてしまう…という問題が出てきます。
そこでこの記事では、フィルタを利用してSlackに通知するログを振り分ける方法を紹介します。
Slack通知用ハンドラの定義
まずはSlackにログを飛ばすためのハンドラを定義します。
今回はIncoming Webhooksを利用します。
import logging from logging.handlers import HTTPHandler HOST = "hooks.slack.com" PATH = "/services/xxx" # Webhook URLを指定 class SlackHandler(HTTPHandler): def __init__(self): super().__init__(HOST, PATH, method="POST", secure=True) def mapLogRecord(self, record): text = self.format(record) return {"payload": {"text": text}}
HTTPHandler
を継承し、mapLogRecord
をオーバーライドしてあげればOKです。
以下のコードでSlackにログを飛ばせます。
logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) slack_handler = SlackHandler() formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s") slack_handler.setFormatter(formatter) logger.addHandler(slack_handler) logger.addHandler(logging.StreamHandler()) logger.info("Hello Slack!") logger.info("This message should not be posted on slack.")
Slackに通知するログを振り分ける
上コードではHello Slack!
およびThis message should not be posted on slack.
の2つのメッセージがSlackに通知されます。
ここで、1つ目のメッセージHello Slack!
はSlackに通知したいが、2つ目のメッセージThis message should not be posted on slack.
は通知したくないとします。
これを実現するために、Slackにログを通知するかどうか判定するフィルタを定義したいと思います。
フィルタに関する説明をドキュメントから引用します。
フィルタ (Filter) は、ハンドラ や ロガー によって使われ、レベルによって提供されるのよりも洗練されたフィルタリングを実現します。基底のフィルタクラスは、ロガー階層構造内の特定地点の配下にあるイベントだけを許可します。例えば、'A.B' で初期化されたフィルタは、ロガー 'A.B', 'A.B.C', 'A.B.C.D', 'A.B.D' 等によって記録されたイベントは許可しますが、'A.BB', 'B.A.B' などは許可しません。空の文字列で初期化された場合、すべてのイベントを通過させます。
https://docs.python.org/ja/3/library/logging.html#filter-objects
フィルタは関数としても定義することができます。
バージョン 3.2 で変更: 特殊な Filter クラスを作ったり、 filter メソッドを持つ他のクラスを使う必要はありません: 関数 (あるいは他の callable) をフィルタとして使用することができます。フィルタロジックは、フィルタオブジェクトが filter 属性を持っているかどうかチェックします: もし filter 属性を持っていたら、それは Filter であると仮定され、その filter() メソッドが呼び出されます。そうでなければ、それは callable であると仮定され、レコードを単一のパラメータとして呼び出されます。返される値は filter() によって返されるものと一致すべきです。
今回は、Slackにログを通知するかどうか判定するフィルタを関数で定義しました。
def slack_filter(record): return getattr(record, "notify_slack", False)
この関数は、指定されたrecord
のnotify_slack
属性(Booleanであることを期待)を取得して返します。
ここで、record
にnotify_slack
属性を持たせるために、ロギング関数を実行する際にキーワード引数extra
を指定します。
キーワード引数extra
に関する説明をドキュメントから引用します。
3番目のキーワード引数は extra で、当該ログイベント用に作られる LogRecoed の __dict__ にユーザー定義属性を加えるのに使われる辞書を渡すために用いられます。これらの属性は好きなように使えます。
https://docs.python.org/ja/3/library/logging.html#logging.debug
以下のように、logger.info
を実行する際にキーワード引数extra
でnotify_slack
属性をレコードに加えます。
logger.info("Hello Slack!", extra={"notify_slack": True})
{"notify_slack": True}
なので、上のメッセージはSlackに通知されることが期待されます。
コード全体は以下のようになります。
import logging from logging.handlers import HTTPHandler HOST = "hooks.slack.com" PATH = "/services/xxx" # Webhook URLを指定 class SlackHandler(HTTPHandler): def __init__(self): super().__init__(HOST, PATH, method="POST", secure=True) def mapLogRecord(self, record): text = self.format(record) return {"payload": {"text": text}} def slack_filter(record): return getattr(record, "notify_slack", False) logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) slack_handler = SlackHandler() formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s") slack_handler.setFormatter(formatter) slack_handler.addFilter(slack_filter) logger.addHandler(slack_handler) logger.addHandler(logging.StreamHandler()) logger.info("Hello Slack!", extra={"notify_slack": True}) logger.info("This message should not be posted on slack.")
slack_handler.addFilter(slack_filter)
でフィルタを追加しています。
上記コードを実行すると、1つ目のメッセージだけSlackに通知されますが、2つ目のメッセージはSlackに通知されません。
というわけで、やりたいことが達成できました。
ログレベルでSlackに通知するログを振り分けるのは?
以下の記事では、Slackに通知するログを振り分ける方法として、ログレベルINFO
とWARNING
の中間のログレベルを独自定義し、対象のログレベルについてSlackに通知するという方法を紹介しています。
jun-networks.hatenablog.com
この方法でもSlackに通知するログを振り分けることができるのですが、ドキュメントによるとログレベルを独自定義するのは非推奨のようです。
独自のレベルを定義することは可能ですが、必須ではなく、実経験上は既存のレベルが選ばれます。しかし、カスタムレベルが必要だと確信するなら、レベルの定義には多大な注意を払うべきで、ライブラリの開発の際、カスタムレベルを定義することはとても悪いアイデア になり得ます。これは、複数のライブラリの作者がみな独自のカスタムレベルを定義すると、与えられた数値が異なるライブラリで異なる意味になりえるため、開発者がこれを制御または解釈するのが難しくなるからです。
https://docs.python.org/ja/3/howto/logging.html#custom-levels
参考
以下のプロジェクトを大いに参考にさせていただきました。
https://github.com/junhwi/python-slack-loggergithub.com