kmuto’s blog

はてな社でMackerel CREをやっています。料理と旅行といろんなIT技術

OpenTelemetryのzero-code計装を試している〜その4。Python

今日はPythonでのzero-code計装をやってみる。

kmuto.hatenablog.com kmuto.hatenablog.com kmuto.hatenablog.com

いくつか罠があるようなのだが、とりあえず今見たところではopentelemetry-pythonもopentelemetry-python-contribも頻繁にアップデートされており、かつ対象のわかっている小さな範囲では問題なさそうだった。

モンキーパッチを使って挙動に割り込む方法が採用されている。

まずはいつものように雑なWebサーバーを立てよう。わかってはいたがhttp.serverモジュールは対象外(サポートレジストリ)なので、Aiohttpを使うことにした。

計装パッケージとともにrequirements.txtでインストール対象にする。

aiohttp
opentelemetry-distro
opentelemetry-exporter-otlp

pip3でインストールしたあと(Debian GNU/Linux上だとPythonパッケージ関連が厄介めなのでvenvで環境作った)、さらにopentelemetry-bootstrap -a installで計装ライブラリ一式をダウンロードする。

pip3 install -r requirements.txt
opentelemetry-bootstrap -a install

適当Webアプリケーションhello-server.pyを書く。

from aiohttp import web

app = web.Application()
app.add_routes([web.get('/', lambda _: web.Response(text='Hello, world!')),
                web.get('/error', lambda _: 1/0),
                web.get('/500', lambda _: web.Response(status=500, text='Internal Server Error'))])
web.run_app(app, port=5000)

Pythonで適当ランタイム例外どう出すんだっけ、と悩んだけど、雑に0除算した(ひどい)。

python3 hello-server.pyで実行して、curlから叩いてみる。

$ curl http://localhost:5000
Hello, world!
$ curl http://localhost:5000/error
500 Internal Server Error

Server got itself in trouble
$ curl http://localhost:5000/500
Internal Server Error

よさそうだ。

OpenTelemetry Collectorをいつものように用意し、実行しておく。

receivers:
  otlp:
    protocols:
      http:

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    metrics:
      receivers: [otlp]
      exporters: [debug]
    logs:
      receivers: [otlp]
      exporters: [debug]
    traces:
      receivers: [otlp]
      exporters: [debug]

では、zero-code計装を実行してみる。opentelemetry-instrumentというコマンドがzero-code計装ラッパーになっている。

opentelemetry-instrument --service_name python-zerocode python3 hello-server.py

環境変数を使ってもよい。

OTEL_SERVICE_NAME=python-zerocode opentelemetry-instrument python3 hello-server.py

curlを叩いて、トレースが送られる様子を見る。

2025-01-21T21:40:28.706+0900    info    Traces  {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
2025-01-21T21:40:28.706+0900    info    ResourceSpans #0
Resource SchemaURL: 
Resource attributes:
     -> telemetry.sdk.language: Str(python)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.29.0)
     -> service.name: Str(python-zerocode)
     -> telemetry.auto.version: Str(0.50b0)
ScopeSpans #0
ScopeSpans SchemaURL: 
InstrumentationScope opentelemetry.instrumentation.aiohttp_server 
Span #0
    Trace ID       : b035a7acc1fbe6a2eb4d76dfbfc7688e
    Parent ID      : 
    ID             : c6d834325bea6982
    Name           : /
    Kind           : Server
    Start time     : 2025-01-21 12:40:26.005757581 +0000 UTC
    End time       : 2025-01-21 12:40:26.005991817 +0000 UTC
    Status code    : Unset
    Status message : 
Attributes:
     -> http.scheme: Str(http)
     -> http.host: Str(localhost)
     -> net.host.port: Int(5000)
     -> http.route: Str(<lambda>)
     -> http.flavor: Str(1.1)
     -> http.target: Str(/)
     -> http.url: Str(http://localhost:5000/)
     -> http.method: Str(GET)
     -> http.server_name: Str(localhost:5000)
     -> http.user_agent: Str(curl/7.88.1)
     -> http.status_code: Int(200)
        {"kind": "exporter", "data_type": "traces", "name": "debug"}
2025-01-21T21:40:38.706+0900    info    Traces  {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 2}
2025-01-21T21:40:38.706+0900    info    ResourceSpans #0
Resource SchemaURL: 
Resource attributes:
     -> telemetry.sdk.language: Str(python)
     -> telemetry.sdk.name: Str(opentelemetry)
     -> telemetry.sdk.version: Str(1.29.0)
     -> service.name: Str(python-zerocode)
     -> telemetry.auto.version: Str(0.50b0)
ScopeSpans #0
ScopeSpans SchemaURL: 
InstrumentationScope opentelemetry.instrumentation.aiohttp_server 
Span #0
    Trace ID       : b00c92c749823cfe5bfbbdd3bb82a5e2
    Parent ID      : 
    ID             : 58ba38395d6a83ca
    Name           : /error
    Kind           : Server
    Start time     : 2025-01-21 12:40:34.303458872 +0000 UTC
    End time       : 2025-01-21 12:40:34.304187316 +0000 UTC
    Status code    : Error
    Status message : ZeroDivisionError: division by zero
Attributes:
     -> http.scheme: Str(http)
     -> http.host: Str(localhost)
     -> net.host.port: Int(5000)
     -> http.route: Str(<lambda>)
     -> http.flavor: Str(1.1)
     -> http.target: Str(/error)
     -> http.url: Str(http://localhost:5000/error)
     -> http.method: Str(GET)
     -> http.server_name: Str(localhost:5000)
     -> http.user_agent: Str(curl/7.88.1)
Events:
SpanEvent #0
     -> Name: exception
     -> Timestamp: 2025-01-21 12:40:34.304175408 +0000 UTC
     -> DroppedAttributesCount: 0
     -> Attributes::
          -> exception.type: Str(ZeroDivisionError)
          -> exception.message: Str(division by zero)
          -> exception.stacktrace: Str(Traceback (most recent call last):
  File ".../lib/python3.11/site-packages/opentelemetry/trace/__init__.py", line 589, in use_span
    yield span
  File ".../lib/python3.11/site-packages/opentelemetry/sdk/trace/__init__.py", line 1104, in start_as_current_span
    yield span
  File ".../lib/python3.11/site-packages/opentelemetry/instrumentation/aiohttp_server/__init__.py", line 230, in middleware
    resp = await handler(request)
           ^^^^^^^^^^^^^^^^^^^^^^
  File ".../python-zerocode/lib/python3.11/site-packages/aiohttp/web_urldispatcher.py", line 208, in handler_wrapper
    result = old_handler(request)  # type: ignore[call-arg]
             ^^^^^^^^^^^^^^^^^^^^
  File ".../hello-server.py", line 5, in <lambda>
    web.get('/error', lambda _: 1/0),
                                ~^~
ZeroDivisionError: division by zero
)
          -> exception.escaped: Str(False)
Span #1
    Trace ID       : f434c991c1933b82f576d2ff73eed4ad
    Parent ID      : 
    ID             : efde73433bf5aef4
    Name           : /500
    Kind           : Server
    Start time     : 2025-01-21 12:40:36.371515683 +0000 UTC
    End time       : 2025-01-21 12:40:36.37165803 +0000 UTC
    Status code    : Error
    Status message : 
Attributes:
     -> http.scheme: Str(http)
     -> http.host: Str(localhost)
     -> net.host.port: Int(5000)
     -> http.route: Str(<lambda>)
     -> http.flavor: Str(1.1)
     -> http.target: Str(/500)
     -> http.url: Str(http://localhost:5000/500)
     -> http.method: Str(GET)
     -> http.server_name: Str(localhost:5000)
     -> http.user_agent: Str(curl/7.88.1)
     -> http.status_code: Int(500)
        {"kind": "exporter", "data_type": "traces", "name": "debug"}

Exceptionイベントが記録されたのはPythonが初めてだな。リッチなエラーバックトレースが書き出された(ほかの言語でも0除算みたいなことをすればもしかして何か出るのかな。コンパイラだますのが面倒だが)。

また、Aiohttp対象の場合はメトリックとしてhttp.server.duration, http.server.active_requestsのヒストグラムが出てくるようだ。