😺

PythonのOpenTelemetryを触ってみた

に公開

初めに

この記事を書いているのは2025年の3月で2024年度が終わろうとしています。
4月にOpenTelemetryを勉強しようと思っていたのですが、
ずるずる着手しなかった反省をこめて滑りこみでお勉強した内容をまとめました。

OpenTelemetryとは

Gemini先生に100文字で要約してもらうと以下の回答になりました。

様々な環境で動作するアプリケーションの監視に必要な、分散トレーシング、メトリクス、ログといったテレメトリーデータを収集・管理するためのオープンソースプロジェクトです。ベンダーに依存せず、オブザーバビリティを高めるための標準的な仕組みを提供します

一つ注意が必要なのは、OpenTelemetryはテレメトリーデータを収集が役割ですので、
収集したデータを描画するためのツールは別で準備する必要があります。

触ってみた

OpenTelemetryのサイトにあるサンプルを触ってみました。
https://opentelemetry.io/docs/zero-code/python/example/

このサンプルは、Flaskのサーバー用のpythonスクリプトとそこにリクエストするpythonスクリプトを準備して、クライントからの通信がトレースできることを確認するものです。

またこの記事では追加で
サンプルを少し弄ってローカルのDockerで起動するJaegerに表示できるようにします。

pythonの環境を準備

仮想環境を作成

python3 -m venv venv
source ./venv/bin/activate

必要なライブラリをインストール

pip install opentelemetry-distro
pip install flask requests
opentelemetry-bootstrap -a install

ソースコードの準備

以下のclient.pyとserver_manual.pyをお借りします。

https://github.com/open-telemetry/opentelemetry-python/blob/main/docs/examples/auto-instrumentation/client.py

https://github.com/open-telemetry/opentelemetry-python/blob/main/docs/examples/auto-instrumentation/server_manual.py

まずサンプルどおりに実行

サーバー側のserver_manual.pyをまず実行

source ./venv/bin/activate
python server_manual.py

別のターミナルを動かしてクライアント側のclient.pyを実行

source ./venv/bin/activate
python client.py testing

クライアントからサーバーへリクエストが一件飛びます。
それに合わせてOpenTelemetryのトレース結果がクライアント側とサーバー側に一件ずつ出力されます。

クライアント側の出力
{
    "name": "client-server",
    "context": {
        "trace_id": "0xec05ef8aa5297d2414149afd9ffed128",
        "span_id": "0x20fe72c60bbd9f5a",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": "0x329fe1907aef791a",
    "start_time": "2025-03-21T06:59:19.488264Z",
    "end_time": "2025-03-21T06:59:19.505840Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.31.1",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
{
    "name": "client",
    "context": {
        "trace_id": "0xec05ef8aa5297d2414149afd9ffed128",
        "span_id": "0x329fe1907aef791a",
        "trace_state": "[]"
    },
    "kind": "SpanKind.INTERNAL",
    "parent_id": null,
    "start_time": "2025-03-21T06:59:19.488190Z",
    "end_time": "2025-03-21T06:59:19.505876Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {},
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.31.1",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}
サーバー側出力
{
    "name": "server_request",
    "context": {
        "trace_id": "0xec05ef8aa5297d2414149afd9ffed128",
        "span_id": "0x5cf8fe2ab289e0b3",
        "trace_state": "[]"
    },
    "kind": "SpanKind.SERVER",
    "parent_id": "0x20fe72c60bbd9f5a",
    "start_time": "2025-03-21T06:59:19.503744Z",
    "end_time": "2025-03-21T06:59:19.503906Z",
    "status": {
        "status_code": "UNSET"
    },
    "attributes": {
        "http.method": "GET",
        "http.server_name": "127.0.0.1",
        "http.scheme": "http",
        "net.host.name": "localhost:8082",
        "http.host": "localhost:8082",
        "net.host.port": 8082,
        "http.target": "/server_request?param=testing",
        "net.peer.ip": "127.0.0.1",
        "net.peer.port": 61331,
        "http.user_agent": "python-requests/2.32.3",
        "http.flavor": "1.1"
    },
    "events": [],
    "links": [],
    "resource": {
        "attributes": {
            "telemetry.sdk.language": "python",
            "telemetry.sdk.name": "opentelemetry",
            "telemetry.sdk.version": "1.31.1",
            "service.name": "unknown_service"
        },
        "schema_url": ""
    }
}

Jaegerに出力されるように修正

ライブラリのインストール

OTLPSpanExporterを使えるようにするためのライブラリを追加

source ./venv/bin/activate
pip install opentelemetry.exporter.otlp

クライアント側のclient.pyを修正

OTLPSpanExporterを設定します。
ついでにSERVICE_NAMEとして"client"を設定します。

--- client.py.org       2025-03-21 16:36:28.000000000 +0900
+++ client.py   2025-03-21 17:50:08.000000000 +0900
@@ -24,11 +24,21 @@
     ConsoleSpanExporter,
 )
 
-trace.set_tracer_provider(TracerProvider())
+from opentelemetry.sdk.resources import SERVICE_NAME, Resource
+from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+
+
+# Service name is required for most backends
+resource = Resource(attributes={
+    SERVICE_NAME: "client"
+})
+
+
+trace.set_tracer_provider(TracerProvider(resource=resource))
 tracer = trace.get_tracer_provider().get_tracer(__name__)
 
 trace.get_tracer_provider().add_span_processor(
-    BatchSpanProcessor(ConsoleSpanExporter())
+    BatchSpanProcessor(OTLPSpanExporter())
 )
clinet.pyの全文
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from sys import argv

from requests import get

from opentelemetry import trace
from opentelemetry.propagate import inject
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
    BatchSpanProcessor,
    ConsoleSpanExporter,
)

from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

# Service name is required for most backends
resource = Resource(attributes={
    SERVICE_NAME: "client"
})

trace.set_tracer_provider(TracerProvider(resource=resource))
tracer = trace.get_tracer_provider().get_tracer(__name__)

trace.get_tracer_provider().add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter())
)


assert len(argv) == 2

with tracer.start_as_current_span("client"):
    with tracer.start_as_current_span("client-server"):
        headers = {}
        inject(headers)
        requested = get(
            "http://localhost:8082/server_request",
            params={"param": argv[1]},
            headers=headers,
        )

        assert requested.status_code == 200

サーバー側のserver_manual.pyを修正

こちらもOTLPSpanExporterを設定します。
ついでにSERVICE_NAMEとして"server"を設定します。

+
+from opentelemetry.sdk.resources import SERVICE_NAME, Resource
+from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+
+# Service name is required for most backends
+resource = Resource(attributes={
+    SERVICE_NAME: "server"
+})
+
 app = Flask(__name__)
 
-set_tracer_provider(TracerProvider())
+set_tracer_provider(TracerProvider(resource=resource))
 tracer = get_tracer_provider().get_tracer(__name__)
 
 get_tracer_provider().add_span_processor(
-    BatchSpanProcessor(ConsoleSpanExporter())
+    BatchSpanProcessor(OTLPSpanExporter())
 )

server_manual.pyの全文
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from flask import Flask, request

from opentelemetry.instrumentation.wsgi import collect_request_attributes
from opentelemetry.propagate import extract
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
    BatchSpanProcessor,
    ConsoleSpanExporter,
)
from opentelemetry.trace import (
    SpanKind,
    get_tracer_provider,
    set_tracer_provider,
)
app = Flask(__name__)

set_tracer_provider(TracerProvider())
tracer = get_tracer_provider().get_tracer(__name__)

get_tracer_provider().add_span_processor(
    BatchSpanProcessor(ConsoleSpanExporter())
)

@app.route("/server_request")
def server_request():
    with tracer.start_as_current_span(
        "server_request",
        context=extract(request.headers),
        kind=SpanKind.SERVER,
        attributes=collect_request_attributes(request.environ),
    ):
        print(request.args.get("param"))
        return "served"


if __name__ == "__main__":
    app.run(port=8082)

Jaegerの起動

以下のコマンドでJaegerをdockerで起動します。

docker run -d --name jaeger \
  -e COLLECTOR_OTLP_ENABLED=true \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  jaegertracing/all-in-one:1.41

再度リクエストをサーバーにリクエストしてみる。

サーバー側のserver_manual.pyをまず実行

source ./venv/bin/activate
python server_manual.py

別のターミナルを動かしてクライアント側のclient.pyを実行

source ./venv/bin/activate
python client.py testing

今回はターミナルにはログには出てきません。
Jaegerに転送がうまくいったようです。

Jaegerの確認

Jaegerは http://localhost:16686/ で起動しているのでブラウザでアクセスします。
するとちゃんとトレースとしてJaegerにとりこまれていることが確認できます。

Jaeger

感想

正直な話Jaegerの表示させるまで色々つまづいてやっと表示できた感じです。
まだまだ学ぶことは多そうです。。

今回は触らなかったですがOpenTelemetryにはコレクターという機能もあるので
そこを使えるともっと幅が広がりそうだなとは感じています。

https://opentelemetry.io/docs/collector/

Discussion