ねののお庭。

かりかりもふもふ。

OpenTelemetry の Python 実装には気をつけろ!

OpenTelemetry の Python 実装であるところの opentelemetry-python と計装ライブラリ群である opentelemetry-python-contrib。 この記事ではこれらを使うときに自分が色々踏み抜いた点を紹介します。

Django に対する計装が機能しない

opentelemetry-instrumentation-django を追加して DjangoInstrumentor().instrument() を呼び出して終了...では済まない場合があります。残念ながら。

これは自分が実際に踏んだパターンですが、プロダクトの django プロダクトに opentelemetry-instrumentation-django を導入し、DjangoInstrumentor().instrument() を呼び出しても全く inbound http request に対するテレメトリデータが全く収集できませんでした。 何か呼び出し方が悪いのかと思い、新規に django プロジェクトを作成し opentelemetry-instrumentation-django を追加し DjangoInstrumentor().instrument() を呼び出したところ、全く問題なくテレメトリデータを取集できました。

まぁプロダクション用のコードはいろいろカスタマイズされてるからそんな事もあるよね、と思いドキュメントexamplesを読み返すもぶっちゃけ DjangoInstrumentor().instrument() を呼び出せ以上の事は書いてありません。 また issue を眺めてもテレメトリデータが取れず困っている人々はいたのですが、ズバリな解決策が提示されている事はありませんでした。 深まる謎。

そこで致し方なく opentelemetry-instrumentation-django のコードを読み、涙を流しながら print デバッグをしたところ、何故プロダクトのコードでのみテレメトリデータが取得できないのか原因が判明しました。 それは WSGI を使っているか ASGI を使っているかで挙動が変わるという事でした。

実は WSGI と ASGI で計装パッケージが異なります。

  • opentelemetry-instrumentation-wsgi
  • opentelemetry-instrumentation-asgi

opentelemetry-instrumentation-django は実装としては双方に依存しているのですが、パッケージの依存関係的には opentelemetry-instrumentation-wsgi には依存している一方で、opentelemetry-instrumentation-asgi に依存していません。 これは実装を見ると分かりやすいでしょう。 opentelemetry-instrumentation-django をプロジェクトに追加しているだけだと opentelemetry-instrumentation-asgi を import しようとしたタイミングで ImportError が飛んでくるのです。

# https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v1.16.0/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py#L91-L109
try:
    from opentelemetry.instrumentation.asgi import asgi_getter, asgi_setter
    from opentelemetry.instrumentation.asgi import (
        collect_custom_headers_attributes as asgi_collect_custom_headers_attributes,
    )
    from opentelemetry.instrumentation.asgi import (
        collect_request_attributes as asgi_collect_request_attributes,
    )
    from opentelemetry.instrumentation.asgi import set_status_code

    _is_asgi_supported = True
except ImportError:
    asgi_getter = None
    asgi_collect_request_attributes = None
    set_status_code = None
    _is_asgi_supported = False

この結果何が起きるかというと、ASGI を使っているにも関わらず opentelemetry-instrumentation-django のみをプロジェクトに追加している場合、何もテレメトリデータが取れません。 以下のように何もテレメトリデータを生み出さずに early return してまいます

# https://github.com/open-telemetry/opentelemetry-python-contrib/blob/v1.16.0/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py#L198-L200
class _DjangoMiddleware(MiddlewareMixin):
    # ~略~
    def process_request(self, request):
        if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
            return

        is_asgi_request = _is_asgi_request(request)
        if not _is_asgi_supported and is_asgi_request:
            return # ASGI が使われているが、`opentelemetry-instrumentation-asgi` が追加されていない場合 early return

解決策自体は簡単で、opentelemetry-instrumentation-asgipip install でも rye add でもいいからしてプロジェクトに追加しましょう。 それでちゃんと計装が機能するようになり、テレメトリデータが収集できるようになります。

ごく個人的なお気持ちとしては、ASGI それなりに使われているのだからドキュメントに一言 ASGI 使っている場合は opentelemetry-instrumentation-asgi を追加しないとダメだぜとか書くなり、ログに opentelemetry-instrumentation-asgi を追加しろ的な警告出すなりしろよと強く思ったのでした。 強く思って PR 投げたのですが、1行のログのためにテスト書けとか言われて怠くて放置中。 テスト書く土壌整ってたら書いたんですけどね、そもそも ASGI が使われているが opentelemetry-instrumentation-asgi が install されていないテストケースがそもそも存在しないので、環境整備からやらないといけなくて流石に python に対してそこまでやってられん、という。

まぁとはいえ誰かが困るやろ、と思ってブログでの紹介と相成りました。 ログに警告流した方が多くのユーザのためになると思うんだけどなぁ...。

Processor/Exporter の設定方法

テレメトリデータを Observability backend に送信するためにも Exporter の設定は必須ですが、この Exporter の設定にも一癖あります。 Processor/Exporter の設定は、必ず trace.get_tracer_provider() で取得した TracerProvider に対して設定しないと期待通りに機能しません。 型的にはどちらでも問題ないハズなんですけどね...。気を付けましょう。

# これは不正解
# 以下のように書かれているドキュメントが存在するが、これだと期待通りに動作しない
# メソッドの呼び出し自体は問題なく成功するのが渋いポイント。
tracer_provider = TracerProvider(resource=resource) \
    .add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(tracer_provider)

# これが正解
# Processor/Exporter は trace.get_tracer_provider() で取得した
# TracerProvider に対して追加しなければいけない。
trace.set_tracer_provider(TracerProvider(resource=resource))
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))

最新の semantic conventions に則っていない

Python の OpenTelemetry 実装はかなり古い semantic conventions (semconv) に則っており、最新の semconv に則っていません。 どれくらい古い semconv に則っているかというと、少なくとも v1.12.0 (2022-06-10) より古いものです。 この記事書いている時点の最新が v1.26.0 (2024-05-22) で、この間にかなり breaking changes が発生しています。 そのため他の言語のライブラリが最新の semconv に則っている場合 attribute が一致しないので、マイクロサービス間を横断してテレメトリデータを探索する事が困難です。 めちゃくちゃ不便。 というかここまで古い semconv のまま塩漬けされているの python だけなんじゃないか...?

この問題に対する暫定的な対処策は OpenTelemetry Collector で attribute を加工する事です。 具体的な方法などは過去記事で紹介しているのでご覧ください。

とはいえこの問題は opentelemetry-python 側に修正の PR が上がっていて先月マージされ、先週リリースされていたので修正される見込みがないわけではありません。 まぁ opentelemetry-python 側に更新が入っても opentelemetry-python-contrib 側の各計装ライブラリがちゃんと追従するのはもうちょっと先の話だと思うので、気長に待つしかないですが...。

まとめ

OpenTelemetry の Python 実装はいろいろ渋い。 もちろん、現在に至るまで実装とメンテしている Contributor 達に感謝と敬意を示すところではあるのですが...。 ドキュメントやログが色々不足していたり、一行のログを出すのにテスト求めるわりにそのテストケースがそもそも存在しなかったり、2年以上 semconv を塩漬けしていたりと、OpenTelemetry そのものの勢いのわりにちょっと...みたいな事が多い。

とはいえ開発に携わっているプロジェクトが Python を利用している事は普通にあるし、その上で OpenTelemetry を導入するなら付き合わざるを得ないので頑張っていきましょう...。