【Python】フィルタを利用してSlackに通知するログを振り分ける

この記事は何?

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.")

f:id:sz_dr:20200814234647p:plain

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)

この関数は、指定されたrecordnotify_slack属性(Booleanであることを期待)を取得して返します。

ここで、recordnotify_slack属性を持たせるために、ロギング関数を実行する際にキーワード引数extraを指定します。

キーワード引数extraに関する説明をドキュメントから引用します。

3番目のキーワード引数は extra で、当該ログイベント用に作られる LogRecoed の __dict__ にユーザー定義属性を加えるのに使われる辞書を渡すために用いられます。これらの属性は好きなように使えます。

https://docs.python.org/ja/3/library/logging.html#logging.debug

以下のように、logger.infoを実行する際にキーワード引数extranotify_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に通知されません。

f:id:sz_dr:20200815001538p:plain

というわけで、やりたいことが達成できました。

ログレベルでSlackに通知するログを振り分けるのは?

以下の記事では、Slackに通知するログを振り分ける方法として、ログレベルINFOWARNINGの中間のログレベルを独自定義し、対象のログレベルについて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