structlogのBoundLoggerについて (2/2)

前回はBoundLoggerのコンテキスト管理とログメソッドについて解説しました。

methane.hatenablog.jp

今回はログメソッドが呼ばれた後の処理についてみていきます。

_proxy_to_logger()

まずはログメソッドがどうなっていたかのおさらいです。

# FilteringBoundLogger の方 (_native.py)

        def meth(self: Any, event: str, *args: Any, **kw: Any) -> Any:
            if not args:
                return self._proxy_to_logger(name, event, **kw)

            return self._proxy_to_logger(name, event % args, **kw)

# stdlib.BoundLogger の方 (stdlib.py)

    def debug(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
        return self._proxy_to_logger("debug", event, *args, **kw)

    def _proxy_to_logger(
        self,
        method_name: str,
        event: str | None = None,
        *event_args: str,
        **event_kw: Any,
    ) -> Any:
        if event_args:
            event_kw["positional_args"] = event_args

        return super()._proxy_to_logger(method_name, event=event, **event_kw)

BoundLoggerBase._proxy_to_logger() の第一引数にメソッド名 ("debug"), 第二引数は event という名前でログ文字列、そしてキーワード引数を渡しています。

stdlib側はloggingのLoggerと互換性を持たせるために _proxy_to_logger() をオーバーライドして、位置引数のタプルをキーワード引数 positional_args に格納しています。これはその後logging互換用のprocessorが処理に利用します。

BoundLoggerBase._proxy_to_logger() の実装は次のようになっています。

structlog/src/structlog/_base.py at 527f492fcdef65619ae1de9d4f9ec692ca64a81e · hynek/structlog · GitHub

    def _proxy_to_logger(
        self, method_name: str, event: str | None = None, **event_kw: Any
    ) -> Any:

        try:
            args, kw = self._process_event(method_name, event, event_kw)
            return getattr(self._logger, method_name)(*args, **kw)
        except DropEvent:
            return None

self._process_event() が processors を呼び出すなどの処理を行い、 args, kw を返します。 getattr(self._logger, method_name) の部分は、 self._logger が wrapped logger、 method_name がログメソッド名なので、例えば BoundLogger.debug() が呼ばれたのだとしたら self._logger.debug(*args, **kw) を呼び出すことになります。

最後の except DropEvent: ですが、 processors で動的なログのフィルタリングを行うことができて、そのログをスキップしたい場合に DropEvent を発生させることになっています。この except 句はそのスキップを行う部分になります。

_process_event()

次に self._process_event(method_name, event, event_kw) の中を見ていきましょう。

この関数の役割は method_name ("debug"など), event (ログ文字列), event_kw (キーワード引数) から、 wrapped logger に渡すための args, kw を作ることでした。 少し行数があるので、コード内にコメントで解説を書いていきます。

structlog/src/structlog/_base.py at 527f492fcdef65619ae1de9d4f9ec692ca64a81e · hynek/structlog · GitHub

    def _process_event(
        self, method_name: str, event: str | None, event_kw: dict[str, Any]
    ) -> tuple[Sequence[Any], Mapping[str, Any]]:

        # context.copy()はdictを返すが、後でprocessorがdict以外を返す可能性があるので typing としては Any を指定する。
        event_dict: Any = self._context.copy()

        # contextから作った辞書とキーワード引数辞書をマージする
        event_dict.update(**event_kw)

        # イベント(ログ文字列)を "event" という名前で event_dict に格納する。
        # processorはイベント文字列を独立した引数としてではなく、 event_dict に入った状態で受け取ることになる。
        if event is not None:
            event_dict["event"] = event

        # processors をループで適用していく。
        for proc in self._processors:
            event_dict = proc(self._logger, method_name, event_dict)

        # 最後の processors の出力が文字列かバイト列なら、それを単独の引数として wrapped logger に渡す
        if isinstance(event_dict, (str, bytes, bytearray)):
            return (event_dict,), {}

        # タプルであれば、それは (*args, **kw) の2要素タプルとして wrapped logger に渡す
        if isinstance(event_dict, tuple):
            return event_dict

        # dictであれば、それをキーワード引数とし、位置引数はなしとして wrapped logger に渡す
        if isinstance(event_dict, dict):
            return (), event_dict

        # それ以外の形であればエラーにする
        msg = (
            "Last processor didn't return an appropriate value.  "
            "Valid return values are a dict, a tuple of (args, kwargs), bytes, or a str."
        )
        raise ValueError(msg)

processor の書き方

Processorを正しく設定したり独自のProcessorを作る場合のためにドキュメントがあります。 _proxy_to_logger()_process_event() の実装を見てきたので、このドキュメントを完全に理解できるはずです。

www.structlog.org

  • processorは wrapped_logger, method_name, event_dict を受け取り、 event_dict を返す
  • 最初の processor に渡される event_dict は dict 型になっている
    • ログメソッドに渡されたログメッセージ文字列は、(あれば) event_dict["event"] に入っている
  • 途中の processor に渡される event_dict は、その前に処理された processor の戻り値である
    • 汎用のprocessorであれば dict にするべき
    • 何かの処理を前段と後段の2つのprocessorの組み合わせとして実装する時は、前段の戻り値=後段のevent_dict引数にdict以外を渡せる
  • 最後の processor は wrapped logger の引数を組み立てる役割を持つ
  • ログのフィルタリングをするなら、 DropEvent を raise する
    • processororsの中のなるべく早い場所でフィルタリングすれば、後続のprocessorsの処理をまるごとスキップできる
このブログに乗せているコードは引用を除き CC0 1.0 で提供します。