"Missing Signed-By in the sources.list(5) entry for" を解消する

開発機をUbuntu 24.04 にアップデートしてしばらく経って、 apt update したときに表示される次のwarningが今更気になった。

N: Missing Signed-By in the sources.list(5) entry for 'http://ftp.riken.go.jp/Linux/ubuntu'

試しに /etc/apt/sources.list をのぞいてみると、次の1行になっていた。

# Ubuntu sources have moved to /etc/apt/sources.list.d/ubuntu.sources

/etc/apt/sources.list.d には third-party.sources, ubuntu.sources の2つのファイルがあった。

/etc/apt/sources.list.d$ cat ubuntu.sources
Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

/etc/apt/sources.list.d$ cat third-party.sources
Types: deb
URIs: http://ftp.riken.go.jp/Linux/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse

Ubuntuをアップグレードした時に sources.list 内のソースが、公式のものは ubuntu.sources に、ミラーのものは third-party.sources に分割されたようだ。

third-party.sources には Signed-By: が付いていないが、本家のミラーなので ubuntu.sources の Signed-By: をコピーしてみたところ、冒頭のwarningが消えた。

ワイヤレスイヤホンの高音質通話をやっと体験できた

SOUNDPEATS Air4 Pro というイヤホンを使っていて、結構通話品質が良い。去年発売のモデルだと結構有名なメーカーの製品でも金属管の中を通したかのようなエコーがかかる製品があったりするけれども、このイヤホンはかなり自然な音で通話できる。今はセール価格とクーポン合わせると7000円以下で買えるのでけっこうお勧め。

このイヤホンは高音質で通話できるaptX VoiceとLE Audioに対応しているのだが、Pixel 8でLE Audioを使ってみるとマイクが動作しない。イヤホン側の問題なのかスマホ側の問題なのかはまだ不明。最近aptX Voice対応スマホを安く譲っていただいて、やっと高音質通話を体験することができた。

Pixel 8と接続したときは、十分にクリアだとはいえ、やはり高音が無いこもり感や、狭い押し入れの中で通話しているかのようなエコーがある。

一方でaptX Voiceで接続したときは圧倒的に音質がクリアになり、こもり感もエコーも感じない。有線イヤホンと比べても遜色ないだろう。

aptX Voiceを使えるのは、だいたいaptX Losslessに対応したイヤホンとスマホだ。Snapdragon Sound認証が必要になる。aptX Losslessはオーディオにお金を払う人がいるからまだわかるが、aptX Voiceまで抱き合わせでプレミア化する必要はあったのだろうか?スマホもイヤホンもQualcommのチップを使ってれば通話音質がいいよ、って方が、ユーザーにとってもQualcommにとっても良かったのではないか。

あと、YouTubeでイヤホンのレビューをしてくれている方達は、aptX Lossless対応イヤホンの通話性能のテストをするときは、録音するスマホ側もaptX Lossless対応スマホで録音してみて欲しい。

ちなみに、現在の高音質通話機能はざっくりとこれくらいある。

  • LE Audio - Sonyの最近のイヤホンとXperia、Pixel Buds Pro2とPixel 8以降では実用化済み?まだまだ不安定な組み合わせが多いが将来的に主流になるはず。
  • SWB - Pixel 7 Pro以降とPixel Buds Proで使える。BluetoothのクラシックオーディオのHFP v1.9で追加された、コーデックにLC3を使うもの。標準規格なので、後述の独自コーデックと違って今後使える機種が増えるだろう。でも同時にLE Audioにも対応するだろうから、どれくらい利用場面が増えるかは不明。
  • aptX Voice - QualcommのSoCを利用した一部のスマホ&イヤホンの組み合わせで使える。aptX Losslessに対応してたら使える可能性が高い。仕組みはSWBと同じで、コーデックにaptX Voiceを使う。
  • AAC-ELD - AppleiPhoneAirPods 第三世代以降で使えるもの。同じくHFPのコーデックにAAC-ELDを使う。従来のHD Voice(mSBC)のサンプリングレートが16kHz、SWBやaptX Voiceが32kHzなのに対して、AAC-ELDは24kHzらしいので、中間くらいの性能と思われる。

Python 3.12.7 で msgpack におけるメモリリークが修正された

github.com

Python 3.12から参照カウントを固定化するImmortal Objectが導入されたのですが、それがinterned string全てを強制的に固定参照カウントにしてしまっていました。

Pythonの文字列はimmutableなので、短くて大量に同じ値が出てくる可能性がある文字列は積極的にオブジェクトを1つにまとめて省メモリ化したいです。 さらにその文字列が変数名などと共通する可能性があるときは、インターン化することで単にインスタンスを一つにまとめることによる省メモリ効果だけでなく、その文字列をキーにして名前を参照するときに文字列比較がポインタ比較だけで終わるという効果も期待できます。

ということで、Pythonpathlibの一部sys.intern() が使われていたり、私が開発しているmsgpack-pythonでもオブジェクトのキーに対してインターンを実施していました。 それがインターン文字列のimmortal化の影響で、pathlibでランダムなディレクトリ名(ハッシュ値ディレクトリ名になってるとか)を扱ったり、外部から入力されるmsgpackのキーが固定の文字列ではなく多様な文字列を含んでいる場合などに、緩やかにメモリ使用量が増え続けることになってしまっていました。

それがrevertされてintern文字列全てがimmortalにならなくなったことで、Python 3.12.7で問題が解消されました。該当するユースケースを持つアプリは早めにアップデートをお勧めします。

他に影響を受けそうなモジュールとして標準ライブラリのjsonモジュールとorjsonを見てみたのですが、jsonインターン化せず独自のdictを使ってオブジェクト再利用を行なっていて、orjsonもassociative_cacheを使った再利用を行なっているので、インターン文字列永続化の影響はありませんでした。

structlog.get_logger() と configure()

structlogシリーズ、今回(たぶん最終回)は get_logger() について。ソースコードを解説していく前に、まず重要な挙動から。

>>> import structlog
>>> from structlog import PrintLogger
>>> def logger_factory():
...     print("creating logger")
...     return PrintLogger()
...

>>> structlog.configure(logger_factory=logger_factory)
>>> logger = structlog.get_logger()
>>> logger
<BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
>>> logger.info("hello")
creating logger
2024-06-24 15:31:41 [info     ] hello
>>> logger.info("hello")
creating logger
2024-06-24 15:31:43 [info     ] hello
  • logger_factory は Bound Logger に渡す wrapped logger を生成する関数
  • get_logger() は Bound Logger ではなく、BoundLoggerLazyProxy というクラスのインスタンスを返す
  • BoundLoggerLazyProxyのメソッドを呼び出すたびに Bound Logger が作られ、その度に logger_factory も実行される

structlog は単なるアプリのロギングだけでなく標準ライブラリの logging の代わりに使われることも考えて作られているので、構成設定前に get_logger が呼ばれることも考えてデフォルトでは毎回設定からBoundLoggerを生成するようになっています。

しかし流石にこれは効率や性能面で良くないので、 cache_logger_on_first_use というオプションを使うことをお勧めします。このオプションを有効にすると logger (BoundLoggerLazyProxy) は一度作ったBoundLoggerを再利用するようになります。

>>> structlog.configure(cache_logger_on_first_use=True)
>>> logger.info("hello")
creating logger
2024-06-24 15:56:03 [info     ] hello
>>> logger.info("hello")
2024-06-24 15:56:06 [info     ] hello
  • BoundLoggerLazyProxy はデフォルトでグローバルの cache_logger_on_first_use 設定を参照する
    • get_logger() で logger が作られた後に cache_logger_on_first_use=True を設定しても良いようになっている
  • ただし、異なる BoundLoggerLazyProxy 間ではこのキャッシュは共有されていない
    • 同一の logger インスタンスを複数の logger から使いたい場合は、 logger factory が何度呼ばれても同一の logger インスタンスを返すようにする

ソースコード解説

get_logger() も BoundLoggerLazyProxy も、 structlog._config というモジュールで実装されています。

https://github.com/hynek/structlog/blob/527f492fcdef65619ae1de9d4f9ec692ca64a81e/src/structlog/_config.py

def get_logger(*args: Any, **initial_values: Any) -> Any:
    return wrap_logger(None, logger_factory_args=args, **initial_values)

def wrap_logger(
    logger: WrappedLogger | None,
    processors: Iterable[Processor] | None = None,
    wrapper_class: type[BindableLogger] | None = None,
    context_class: type[Context] | None = None,
    cache_logger_on_first_use: bool | None = None,
    logger_factory_args: Iterable[Any] | None = None,
    **initial_values: Any,
) -> Any:

    return BoundLoggerLazyProxy(
        logger,
        wrapper_class=wrapper_class,
        processors=processors,
        context_class=context_class,
        cache_logger_on_first_use=cache_logger_on_first_use,
        initial_values=initial_values,
        logger_factory_args=logger_factory_args,
    )
  • get_logger() は logger_factory_args という引数(後述)を渡しやすくなっている以外は、 wrap_logger() をデフォルト引数のまま呼んでいるだけ
  • wrap_logger() は BoundLoggerLazyProxy() を生成する。グローバルの設定を引数で上書きできるようになっている。
class BoundLoggerLazyProxy:

    def __getattr__(self, name: str) -> Any:
        bl = self.bind()
        return getattr(bl, name)

    def bind(self, **new_values: Any) -> BindableLogger:
        ...
        cls = self._wrapper_class or _CONFIG.default_wrapper_class
        logger = cls(_logger, processors=procs, context=ctx)

        def finalized_bind(**new_values: Any) -> BindableLogger:
            """
            Use cached assembled logger to bind potentially new values.
            """
            if new_values:
                return logger.bind(**new_values)
            return logger

        if self._cache_logger_on_first_use is True or (
            self._cache_logger_on_first_use is None
            and _CONFIG.cache_logger_on_first_use is True
        ):
            self.bind = finalized_bind  # type: ignore[method-assign]

        return finalized_bind(**new_values)
  • BoundLoggerLazyProxy はログメソッドを呼ばれるたびに bind() を実行している
  • 先に明示的に log = logger.bind() しておけば、 BoundLoggerLazyProxy を回避して直接 Bound Logger のメソッドを呼び出せる

structlog.stdlibについて

structlogのデフォルトの設定はPrintLoggerを使うようになっているのですが、 structlog.stdlib モジュールの recreate_defaults() を使うとほぼ同じ出力を、 logging 経由で出力できるようになっています。

https://github.com/hynek/structlog/blob/527f492fcdef65619ae1de9d4f9ec692ca64a81e/src/structlog/stdlib.py#L57

def recreate_defaults(*, log_level: int | None = logging.NOTSET) -> None:

    if log_level is not None:
        kw = {"force": True}

        logging.basicConfig(
            format="%(message)s",
            stream=sys.stdout,
            level=log_level,
            **kw,  # type: ignore[arg-type]
        )

    _config.reset_defaults()
    _config.configure(
        processors=[
           ...  # デフォルトと似たテキストにフォーマットする設定
        ],
        wrapper_class=BoundLogger,
        logger_factory=LoggerFactory(),
    )

def get_logger(*args: Any, **initial_values: Any) -> BoundLogger:
    return _config.get_logger(*args, **initial_values)

class LoggerFactory:

    def __call__(self, *args: Any) -> logging.Logger:

        if args:
            return logging.getLogger(args[0])

        _, name = _find_first_app_frame_and_name(self._ignore)
        return logging.getLogger(name)
  • stdlib.recreates_defaults() を使うと、 stdlib.BoundLogger と stdlib.LoggerFactory を使うようになる
  • BoundLoggerは以前に説明した通り、 logging.Logger とシグネチャを似せて static type hint をつけた上で、ログメッセージの位置引数に対応したもの
  • ただしprocessorsには PositionalArgumentsFormatter が入っていないので、位置引数はフォーマットされず、 event_dict["positional_args"] に入ったままフォーマットされる
  • log_level を渡すと logging.basicConfig() もしてくれる。ログフォーマットは "%(message)s" で、すでに structlog がフォーマットしたログをそのまま表示するだけ(タイムスタンプ等はstructlogがつける)
  • stdlib.get_logger() は戻り値の型ヒントが stdlib.BoundLogger になるだけで、あとは structlog.get_logger() のまま。
    • recreates_defaults() を使ってなくても型ヒントのためだけに使うことはできるが、その場合は型ヒントでは位置引数を取ることになるので注意が必要。
  • stdlib.LoggerFactorylogging.getLogger() を呼ぶ。
    • structlog.get_logger() の引数が LoggerFactory に渡される仕組みがここで生きてくる。
    • structlog.get_logger("foo.bar")logging.getLogger("foo.bar") を呼ぶ。
    • 引数を渡さない場合は呼び出し元のモジュール名を利用する。

アプリケーションでstructlogを使う場合

stdlib.LoggerFactoryやstdlib.BoundLoggerを使うかどうかはどちらでも良い。loggingとstructlogを組み合わせる別の方法は過去に書いた記事を参考に。

structlogとloggingの併用方法について - methaneのブログ

アプリケーション内でログを書く場合、ライブラリが書く場合と違ってアプリケーションの起動直後にまずロギングの設定を始められる。そうすると、 structlog.get_logger() を使う理由はあまりなくなる。 myapp.log モジュールで logger = structlog.BoundLogger(...) のように直接 BoundLogger インスタンスを作って、それ以外のモジュールは from myapp.log import logger すれば良い。

loggingを使っていた時に慣習として logging.getLogger(__name__) を使っていた場合は、structlogに移行する時にその慣習を残すかどうか考えなすと良いだろう。

  • stdlib.LoggerFactory とモジュールごとの logger = structlog.get_logger(__name__) を使う
  • logger = myapp.log.logger.bind(module=__name__) のようにして、構造化ログの中身に module を入れる
  • 完全にこの慣習を止め、代わりに呼び出し元の場所を CallsiteParameterAdder で行数まで残す。

また、複数種類のログを残したい場合も、 structlog.get_logger() を使う代わりに直接 BoundLogger インスタンスを作ると良いだろう。そうすればログの種類ごとに出力先やフォーマットなどの設定を変えられる。例えばデバッグ用のテキストログを標準出力に出して、別にリクエストごとに構造化イベントをJSONフォーマットでfluentbit経由でBigQueryに送るなどの組み合わせが可能だ。

structlogのwrapped loggerとrendererについて

前回までの記事でstructlogの中心となるBoundLoggerの役割とコードについて解説しました。

ざっくりとシーケンス図にまとめると次のようになっています。

sequenceDiagram
  participant U as App
  box Bound Logger
  participant B as BoundLogger
  participant P as _processors
  participant W as wrapped logger
  end
  U->>B: logger.debug()
  loop
    B->>+P: event_dict
    P-->>-B: event_dict
  end
  B->>+P: event_dict
  P-->>-B: (args, kw)
  B->>W: _logger.debug(*args, **kw)

processorの出力が次のprocessorの入力になり、最後のprocessorの出力がwrapped loggerの入力になっているのが重要なポイントです。この最後のprocessorのことをrendererとも呼びます。

今回はwrapped loggerとrendererの関係を見ていきます。

WriteLoggerとLogFmtRenderer

wrapped logger とそのための最終processorのシンプルな組み合わせとして WriteLogger と LogFmtRenderer を見ていきます。まずはWriteLoggerから。

(WriteLogger)

class WriteLogger:

    def msg(self, message: str) -> None:
        with self._lock:
            self._write(message + "\n")
            self._flush()

    log = debug = info = warn = warning = msg
    fatal = failure = err = error = critical = exception = msg

.debug() 等のログメソッドを用意していますが、それらは全部単一の実装を共有しています。その単一の実装は、受け取ったメッセージに改行をつけてファイルに書き出すだけのシンプルなものです。

メッセージ本体の中にログレベルを入れる等の処理はprocessorsの中で全て終わっているので、WriteLoggerの実装は単に書くだけになっています。そのため、rendererの役割は event_dict を1つのテキストメッセージに変換することです。次にLogFmtRendererを見ていきましょう。

(LogFmtRenderer)

    def __call__(
        self, _: WrappedLogger, __: str, event_dict: EventDict
    ) -> str:
        elements: list[str] = []
        for key, value in self._ordered_items(event_dict):
            # ...
            # bool を true/false にしたり必要ならクォート&エスケープしたりの処理
            # ...
            elements.append(f"{key}={value}")

        return " ".join(elements)

event_dict から key=value をスペース区切りで繋げたテキストを作って返していますね。このprocessorはevent_dictの文字列化だけに特化していて、ログレベルやタイムスタンプをevent_dictに入れるのはTimeStamperやadd_log_levelといった他のprocessorに任せることで、再利用性を高めています。

logging.Logger と stdlib.render_to_log_kwargs

BoundLoggerからの出力先になるwrapped loggerとして、標準ライブラリの logging.Logger を使う事ができます。 logging.Logger の debug() メソッドは次のような実装になっています。

logging.Logger.debug

    def debug(self, msg, *args, **kwargs):
        """
        Log 'msg % args' with severity 'DEBUG'.
        """
        if self.isEnabledFor(DEBUG):
            self._log(DEBUG, msg, args, **kwargs)

このメソッドの引数を作るための renderer は structlog.stdlib.render_to_log_kwargs になります。

structlog.stdlib.render_to_log_kwargs

def render_to_log_kwargs(
    _: logging.Logger, __: str, event_dict: EventDict
) -> EventDict:

    return {
        "msg": event_dict.pop("event"),
        "extra": event_dict,
        **{
            kw: event_dict.pop(kw)
            for kw in ("exc_info", "stack_info", "stacklevel")
            if kw in event_dict
        },
    }

(args, kw) のペアではなくてキーワード引数のための辞書だけを返していますね。この場合はBoundLoggerは (), **kw を引数として wrapped logger を呼び出しますが、 "msg" というキーワード引数が Logger.debug() の第一引数にマッピングされるのでちゃんとログメッセージが伝わっています。しかし、標準ライブラリの logging.Logger では logger.debug("hello, %s", "world") みたいに追加の位置引数でログ文字列を作れていたのに、この呼び出し方では第二引数以降の位置引数がありませんね?

PositionalArgumentsFormatter

既存の logging を使ったコードを structlog に置き換えたい時には、 structlog.stdlib.BoundLogger を使います。このBoundLoggerの実装は以前の記事で紹介しましたが、改めて書いておきます。

    def debug(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
        """
        Process event and call `logging.Logger.debug` with the result.
        """
        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:
        """
        Propagate a method call to the wrapped logger.

        This is the same as the superclass implementation, except that
        it also preserves positional arguments in the ``event_dict`` so
        that the stdlib's support for format strings can be used.
        """
        if event_args:
            event_kw["positional_args"] = event_args

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

stdlib.BoundLogger は logging.Logger との互換性のために第二引数以降の位置引数 *event_args を受け取ります。そしてそれを event_kw["positional_args"] に格納しています。 event_kw["positional_args"] を処理するためのProcessorとして、 PositionalArgumentsFormatter があります。

    def __call__(
        self, _: WrappedLogger, __: str, event_dict: EventDict
    ) -> EventDict:
        args = event_dict.get("positional_args")  # !!!

        # Mimic the formatting behaviour of the stdlib's logging module, which
        # accepts both positional arguments and a single dict argument. The
        # "single dict" check is the same one as the stdlib's logging module
        # performs in LogRecord.__init__().
        if args:
            if len(args) == 1 and isinstance(args[0], dict) and args[0]:
                args = args[0]

            event_dict["event"] %= args  # !!!

        if self.remove_positional_args and args is not None:
            del event_dict["positional_args"]

        return event_dict

重要な部分に !!! というコメントをつけておきました。 event_dict から positional_args を取り出して、 event_dict["event"] %= args しています。

PositionalArgumentsFormatterを使えば、既存のlogging.Loggerを使った logger.debug("hello, %s", "world") のようなログを含むコードのloggerをstructlog.stdlib.BoundLoggerに置き換えたとき、出力先(wrapped logger)が logging.Logger であれ structlog の WriterLogger であれ位置引数を使った文字列フォーマットに対応できます。

ProcessorFormatter

wrapped logger に logging.Logger を使う場合、 PositionalArgumentsFormatter を使う代わりに、 logging.Logger.StreamHandler の Formatterとして structlog.stdlib.ProcessorFormatter を使う方法もあります。

このクラスを使うと logging.Logger と structlog をより高度に連携させられるのですが、構成が複雑になるので今回は(多分今後も)詳しくは説明しません。 structlog のドキュメントにある次の章を頑張って読み解いてみてください。

www.structlog.org

また、過去に書いたこの記事では、 "Rendering Using structlog-based Formatters Within logging" が ProcessorFormatter を利用する方法を指していました。

methane.hatenablog.jp

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。