今日は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のヒストグラムが出てくるようだ。