OpenTelemetry の Generative AI Observability の話
Generative AI Observability とは
最近は LLM や生成 AI、AI エージェントを活用するユースケースが増えてきていますが、需要の増加に伴って性能や安全性の評価、パフォーマンスの最適化、メトリクスの可視化といった本格的な運用を考える上で欠かせない要素の重要性も増しています。このような generative AI の Observability の話題に関して、先日 CNCF のブログに OpenTelemetry for generative AI という記事が公開されました。
元となった記事は opentelemetry の blog の投稿で、以下から確認できます。こちらは 2024/12 に公開済みですが内容はだいたい同じ。
記事を AI で要約すると以下のようになりました。
CNCFのブログ記事「OpenTelemetry for Generative AI」では、生成AI(Generative AI)における観測性(Observability)の重要性について説明しています。生成AIは急速に成長しており、その性能や安全性を評価・最適化するための観測が求められています。OpenTelemetryは、これらのニーズに応えるために、生成AIの出力評価、パフォーマンス最適化、安全性の確保を目指した観測ツールを提供しています。特に、Auto-instrumentationを用いて、これらの要素を自動的に計測することが可能です。現時点では、OpenAI用のJavaScriptおよびPythonライブラリが提供されています。
だいたい合っていますが少し補足すると、Gen AI を組み込んだシステムやアプリケーションの Observability を考える際にログ・メトリクス・トレースといった従来の 3 大要素は依然として有効ですが、その他に応答におけるトークン使用量や応答速度といった GenAI 固有の観測量を可視化・分析することも重要になっています。この課題を解決するために OpenTelemetry では新しく Generative AI Observability というプロジェクトを立ち上げ、Gen AI の Observability におけるセマンティクス規則や計測ライブラリの開発に取り組んでいるという話になっています。
プロジェクトの概要、目標、進捗などは以下リポジトリの Generative AI Observability でまとめられています。
GenAI Observability に焦点を当てたプロジェクトとして Langfuse や OpenLLMetry など既にいくつかのホスティングサービスや OSS が存在していますが、Generative AI Observability では特定のベンダーに依存せず、従来の opentelemetry の計測方法 (OpenTelemetry SDK を使った計測など) と統合できる汎用的な計測ライブラリを開発することが目標の一つとなっています。さらに長期的な目標として、GenAI 計測を包括的に管理できる orchestrator やフレームワークを開発して RAG などの複雑なユースケースもカバーできるようにするといった内容も掲げられています。
現状
Generative AI Observability はまだ歴史の浅いプロジェクトなので実装済みの部分は少ない状況ですが、python で OpenAI API 実行時の計測を行うための OpenTelemetry ライブラリが提供されています。
これは python コードで OpenAI の api を実行する際に GenAI 用のライブラリをコードに埋め込むことで計測を行い、実行時のログやチャットの応答、トークン量、実行時間などの情報を収集する仕組みになっています。
Python の計測ライブラリを使ってみる
python 用の計測ライブラリ opentelemetry-instrumentation-openai-v2
は以下に example があるので、これを使ってどのような情報が取れるか見てみます。
opentelemetry-instrumentation-openai-v2 は python 用の OpenAI パッケージ openai-python を使って OpenAI 互換のモデルで api を実行する際に計測を行います。これによって api 経由でチャットを行う際の trace や log, metric を計測するようになっています。
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
OpenAIInstrumentor().instrument()
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "user", "content": "Write a short poem on open telemetry."},
],
)
具体的な example のコードは以下。
現時点での実装では chatGPT や Ollama などの OpenAI と互換性のある Gen AI サービスであれば計測できるようになっているので、ここでは今流行りの deepseeker を使ってチャットを実行した際のデータを計測してみます。
前準備
前準備として deepseek にアカウント作成して api key を作成しておきます。
opentelemetry-instrumentation-openai-v2 の example コードを使用するため opentelemetry-python-contrib のリポジトリを clone.
git clone https://github.com/open-telemetry/opentelemetry-python-contrib.git
cd opentelemetry-python-contrib/instrumentation-genai/opentelemetry-instrumentation-openai-v2/examples/manual
.env
を以下のように編集。
項目名 | 説明 |
---|---|
OPENAI_API_KEY | DeepSeek の api key |
OPENAI_BASE_URL | DeepSeek の api url https://api.deepseek.com を指定 |
CHAT_MODEL | DeepSeek の chat のモデル名 deepseek-chat を指定 |
OTEL_EXPORTER_OTLP_ENDPOINT | 後述の otel collector エンドポイント http://0.0.0.0:4317 を指定 |
OTEL_EXPORTER_OTLP_PROTOCOL | grpc |
OTEL_SERVICE_NAME | 好きな名前を指定 |
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT | チャットにおけるリクエスト文と応答を log で取得する場合は true に設定 |
# Update this with your real OpenAI API key
OPENAI_API_KEY=xxxx
OPENAI_BASE_URL=https://api.deepseek.com
CHAT_MODEL="deepseek-chat"
# Uncomment and change to your OTLP endpoint
OTEL_EXPORTER_OTLP_ENDPOINT=http://0.0.0.0:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_SERVICE_NAME=opentelemetry-python-openai
# Change to 'false' to hide prompt and completion content
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
また、opentelemetry-instrumentation-openai-v2 では計測したログ/トレース/メトリクスを OpenTelemetry エンドポイントに送信する仕組みなので、データを受け付ける otel collector などが必要になります。ここでは取得した各データを確認するためにそれぞれ以下のアプリケーションを使います。
- otel エンドポイント: opentelemetry collector
- ログ: Loki + grafana
- メトリクス: prometheus + grafana
- トレース: Jaeger
構成は以下。
構成図
各アプリは docker compose で作成。ファイルは https://github.com/git-ogawa/zenn_resource/tree/main/articles/otel-genai にも置いてあります。
services:
jaeger:
container_name: jaeger
image: jaegertracing/jaeger:${JAEGER_VERSION:-latest}
volumes:
- "./jaeger-ui.json:/etc/jaeger/jaeger-ui.json" # Do we need this for v2 ? Seems to be running without this.
- "./config.yml:/etc/jaeger/config.yml"
command: ["--config", "/etc/jaeger/config.yml"]
ports:
- "16686:16686"
otel-collector:
container_name: otel-collector
image: otel/opentelemetry-collector:0.119.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yml:/etc/otel-collector-config.yaml
ports:
- "1888:1888" # pprof extension
- "8899:8888" # Prometheus metrics exposed by the collector
- "8889:8889" # Prometheus exporter metrics
- "13133:13133" # health_check extension
- "4317:4317" # OTLP gRPC receiver
- "55679:55679" # zpages extension
depends_on:
- jaeger
- loki
- prometheus
loki:
container_name: loki
image: grafana/loki
ports:
- "3100:3100"
grafana:
container_name: grafana
image: grafana/grafana
ports:
- "3000:3000"
depends_on:
- loki
prometheus:
container_name: prometheus
image: prom/prometheus
command: ["--config.file=/etc/promtheus/protmetheus.yml"]
volumes:
- ./prometheus.yml:/etc/promtheus/protmetheus.yml
ports:
- "9090:9090"
設定ファイル
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
exporters:
prometheus:
endpoint: "0.0.0.0:9090"
const_labels:
label1: value1
otlphttp:
endpoint: http://loki:3100/otlp
debug:
otlp:
endpoint: jaeger:4317
tls:
insecure: true
processors:
batch:
extensions:
health_check:
pprof:
endpoint: :1888
zpages:
endpoint: :55679
service:
extensions: [pprof, zpages, health_check]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlp]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [debug, prometheus]
logs:
receivers: [otlp]
processors: []
exporters: [debug, otlphttp]
telemetry:
metrics:
readers:
- pull:
exporter:
prometheus:
host: "0.0.0.0"
port: 8888
service:
extensions: [jaeger_storage, jaeger_query]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [jaeger_storage_exporter, spanmetrics]
metrics/spanmetrics:
receivers: [spanmetrics]
exporters: [prometheus]
telemetry:
resource:
service.name: jaeger
metrics:
level: detailed
address: 0.0.0.0:8888
logs:
level: DEBUG
extensions:
jaeger_query:
storage:
traces: some_storage
metrics: some_metrics_storage
jaeger_storage:
backends:
some_storage:
memory:
max_traces: 100000
metric_backends:
some_metrics_storage:
prometheus:
endpoint: http://prometheus:9090
normalize_calls: true
normalize_duration: true
connectors:
spanmetrics:
receivers:
otlp:
protocols:
grpc:
endpoint: "0.0.0.0:4317"
http:
endpoint: "0.0.0.0:4318"
processors:
batch:
exporters:
jaeger_storage_exporter:
trace_storage: some_storage
prometheus:
endpoint: "0.0.0.0:8889"
{
"monitor": {
"menuEnabled": true
},
"dependencies": {
"menuEnabled": true
}
}
scrape_configs:
- job_name: "otel-collector"
static_configs:
- targets: ["otel-collector:9090"]
opentelemetry-instrumentation-openai-v2 の動作に必要なパッケージをインストール。venv による仮想環境の作成は任意。
python3 -m venv .venv
source .venv/bin/activate
pip install "python-dotenv[cli]"
pip install -r requirements.txt
requirements.txt
の中身は以下のようになっています。
openai~=1.57.3
opentelemetry-sdk~=1.29.0
opentelemetry-exporter-otlp-proto-grpc~=1.29.0
opentelemetry-instrumentation-openai-v2~=2.0b0
deepseek の chat api を実行する main.py
では OpenTelemetry SDK と組み合わせてコード内で TracerProvider など各種 Provider を有効化しています。
from openai import OpenAI
# NOTE: OpenTelemetry Python Logs and Events APIs are in beta
from opentelemetry import _events, _logs, trace
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# configure tracing
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(OTLPSpanExporter())
)
# configure logging and events
_logs.set_logger_provider(LoggerProvider())
_logs.get_logger_provider().add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter())
)
_events.set_event_logger_provider(EventLoggerProvider())
# instrument OpenAI
OpenAIInstrumentor().instrument()
ただデフォルトではログとトレースのみ収集されるようになっていてメトリクスは収集されません。メトリクスも取得するには Usage に記載の通り MeterProvider
などをコードに追加する必要があります。ここではメトリクスも取りたいので main.py
にコードを追加しましたが、上記の箇所を追加するだけでは実行時にいくつかエラーが発生したので以下のような修正を加えました。
import os
from openai import OpenAI
# NOTE: OpenTelemetry Python Logs and Events APIs are in beta
from opentelemetry import _events, _logs, metrics, trace
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk._events import EventLoggerProvider
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider, view
from opentelemetry.sdk.metrics._internal.aggregation import (
ExplicitBucketHistogramAggregation,
)
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# configure tracing
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
# configure logging and events
_logs.set_logger_provider(LoggerProvider())
_logs.get_logger_provider().add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter())
)
_events.set_event_logger_provider(EventLoggerProvider())
# configure metrics
views = [
view.View(
instrument_name="gen_ai.client.token.usage",
aggregation=ExplicitBucketHistogramAggregation(
[
1,
4,
16,
64,
256,
1024,
4096,
16384,
65536,
262144,
1048576,
4194304,
16777216,
67108864,
]
),
),
view.View(
instrument_name="gen_ai.client.operation.duration",
aggregation=ExplicitBucketHistogramAggregation(
[
0.01,
0.02,
0.04,
0.08,
0.16,
0.32,
0.64,
1.28,
2.56,
5.12,
10.24,
20.48,
40.96,
81.92,
]
),
),
]
metric_exporter = OTLPMetricExporter()
metric_reader = PeriodicExportingMetricReader(metric_exporter)
provider = MeterProvider(metric_readers=[metric_reader], views=views)
metrics.set_meter_provider(provider)
# instrument OpenAI
OpenAIInstrumentor().instrument()
def main():
client = OpenAI()
chat_completion = client.chat.completions.create(
model=os.getenv("CHAT_MODEL", "gpt-4o-mini"),
messages=[
{
"role": "user",
"content": "Write a short poem on OpenTelemetry.",
},
],
)
print(chat_completion.choices[0].message.content)
if __name__ == "__main__":
main()
以上で準備ができたので docker compose up -d
で各アプリケーションを起動します。
計測の実行とデータの確認
各 docker コンテナが起動していることを確認したのち main.py
を実行して deepseek の chat api を呼び出します。
$ dotenv run -- python main.py
main.py
内のプロンプトで短い詩を書くように指定しているので、応答としてはそれっぽい詩が返ってきます。
chat_completion = client.chat.completions.create(
model=os.getenv("CHAT_MODEL", "gpt-4o-mini"),
messages=[
{
"role": "user",
"content": "Write a short poem on OpenTelemetry.",
},
],
)
In the realm of code, where systems entwine,
OpenTelemetry shines, a beacon divine.
With traces and spans, it weaves a tale,
Of services linked, through success or fail.
Metrics and logs, it gathers with care,
A map of the chaos, laid bare and clear.
From cloud to edge, it bridges the gap,
A universal language, no detail left unwrapped.
Observability's key, in its hands it holds,
Unlocking insights, as the story unfolds.
OpenTelemetry, a guardian true,
For every developer, and every crew.
chat api が実行されると裏では main.py 内で opentelemetry による計測が行われ、ログ、メトリクス、トレースが収集、otel エンドポイントに送信されます。otel-collector コンテナのログを見ると以下のように log, metrics, trace が収集されていることが確認できます。
2025-02-06T16:32:39.153Z info Logs {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 1}
2025-02-06T16:32:46.326Z info Logs {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 1}
2025-02-06T16:32:46.349Z info Metrics {"kind": "exporter", "data_type": "metrics", "name": "debug", "resource metrics": 1, "metrics": 2, "data points": 3}
2025-02-06T16:32:46.395Z info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
ログ
ログは otel collector から loki に送信されるので grafana で内容を確認してみます。
ブラウザで http://0.0.0.0:3000
にアクセスして admin/admin でログイン。
Data sources > Add new Data Source > Loki でデータソースを追加。
Connection > URL に http://loki:3100 を追加すれば ok
explorer で service_name=opentelemetry-python-openai"
で検索すると 2 つのログエントリが追加されていることが確認できます。
1 つめは chat api のリクエストに対応していて、プロンプトで指定したメッセージが確認できます。
{"content":"Write a short poem on OpenTelemetry."}
2 つめは chat api のレスポンスで、返信で返ってきた内容が確認できます。
{"finish_reason":"stop","index":0,"message":{"content":"In the realm of code, where systems entwine, \nOpenTelemetry shines, a beacon divine. \nTracing the paths where data does flow, \nMetrics and logs in harmony grow. \n\nFrom microservices to monolithic might, \nIt captures the whispers of every byte. \nA bridge to clarity, a lens so clear, \nUnveiling the chaos, dispelling the fear. \n\nThrough spans and traces, it weaves a tale, \nOf latency, errors, and journeys frail. \nA universal tongue for observability's art, \nOpenTelemetry, the observant heart. \n\nSo here's to the tool that binds us together, \nThrough storms of complexity, it tames the weather. \nIn the dance of systems, it leads the way, \nOpenTelemetry, guiding night into day.","role":"assistant"}}
リクエスト・レスポンスの内容の他に各ログエントリにはイベント名や opentelemetry のバージョン、trace id などの情報がラベルとして設定されるため、他のデータと連携しやすくなっています。設定されるラベルを見ると以下のようになっています。
label key | value |
---|---|
detected_level | info |
event_name | gen_ai.user.message |
flags | 1 |
gen_ai_system | openai |
observed_timestamp | 1738859554180767081 |
scope_name | opentelemetry.instrumentation.openai_v2 |
service_name | opentelemetry-python-openai |
severity_number | 9 |
span_id | ba19bcc93d3afaff |
telemetry_sdk_language | python |
telemetry_sdk_name | opentelemetry |
telemetry_sdk_version | 1.29.0 |
trace_id | 58cbf02d848ff8e2328f345b682798ce |
メトリクス
メトリクスは otel collector から prometheus に送信されているので、こちらも grafana で確認してみます。
実際見てみると以下のようなメトリクスが保存されていることがわかります。
- gen_ai_client_operation_duration_seconds_bucket
- gen_ai_client_operation_duration_seconds_count
- gen_ai_client_operation_duration_seconds_sum
- gen_ai_client_token_usage_bucket
- gen_ai_client_token_usage_count
- gen_ai_client_token_usage_sum
いくつかプロットした図
operation_duration_seconds の方は operation にかかった時間を表しており、operation_duration_seconds_sum
が実際の時間に対応しています。
コードでは ExplicitBucketHistogramAggregation
を指定しているので、bucket, count に関してはおそらく https://opentelemetry.io/docs/specs/otel/metrics/data-model/#histogram に沿って計算される値が記録されるようです。
token_usage_bucket は応答に使用された token を表しています。gen_ai_token_type
ラベルに input/output が設定されるので、これで入出力のどちらに対応しているか区別できます。
今回は main.py
のコードに以下の instrument_name を指定したので上記 2 つのメトリクスが取得できるようになっています。
- gen_ai.client.token.usage
- gen_ai.client.operation.duration
現時点で client side で取得可能なメトリクスはこの 2 つしかありませんが、server side の場合には応答速度、出力トークンの生成にかかった時間などのメトリクスも取得できるようになっています。
chatGPT などの外部にある AI サービスを使用する際は token や operation を見ることで無駄に使用されていないか確認し、内部でホストするサービスでは server side メトリクスを見てパフォーマンスを評価するといった活用方法ができそうです。
トレース
トレースは Jaeger webui から確認できます。ブラウザで http://0.0.0.0:16686/
にアクセスして server name = opentelemetry-python-openai で検索すると、chat deepseeker-chat
のトレースが取得されていることが確認できます。
chat api のトレース。span は 1 つのみ
trace の詳細では operation にかかった時間や request mode などの詳細が確認できます。ログのラベルと重複している情報もいくつかありますが、リクエスト単位で応答に要した時間や input/output トークン数を確認できるなどが便利そうです。また、ログの方では Trace id が含まれているので、Trace id で検索して対象リクエストにどのぐらいの時間がかかっているか、token をどれぐらい使っているのか調査するといった使い方もできます。
これからの展望
Generative AI Observability プロジェクトのこれからの目標や予定は Deliverables, Timeline あたりに書かれています。ざっくりまとめると以下のような感じ。
- GenAI 用のクライアントライブラリやフレームワークを開発していく
- Langfuse など既存の Observability ベンダーと同程度の機能を提供できるようにする。
- GenAI 用のセマンティクス規則を拡張し、chat だけでなく embeddings, image, audio generation といった他の operation をカバーする。
- python, js 以外の言語で GenAI 用の計測ライブラリを開発し、OpenAI だけでなく一般的な GenAI を計測対象に追加する。
- RAG など複雑なシナリオも計測できるようにする。
おまけ
既存アプリケーションの組み込みについて
opentelemetry-instrumentation-openai-v2
は現時点で OpenAI の chat のみが計測対象となっています、ソースコードでは opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py で wrapt を使って openai の chat.completions create が呼び出された際に trace などを記録する wrapper として実装されています。呼び出し先の chat_completions_create
などは patch.py で定義されています。
from wrapt import wrap_function_wrapper
wrap_function_wrapper(
module="openai.resources.chat.completions",
name="Completions.create",
wrapper=chat_completions_create(
tracer, event_logger, instruments, is_content_enabled()
),
)
wrap_function_wrapper(
module="openai.resources.chat.completions",
name="AsyncCompletions.create",
wrapper=async_chat_completions_create(
tracer, event_logger, instruments, is_content_enabled()
),
)
そのため LLM を利用した自作のアプリケーションや既存のプロジェクトに opentelemetry-instrumentation-openai-v2
を組み込んで trace を記録したい場合、openAI の chat completion
を使ってチャット機能を実装している必要があります。
例えば LLM アプリプラットフォームとして有名な Dify では、model provider の OpenAI は openai chat を実行する際に chat.completions.create
を呼び出しているので opentelemetry-instrumentation-openai-v2
を組み込んで trace を取得することができます。
実際に llm.py
に opentelemetry 計測コードを追加、イメージをビルドしてアプリケーションを起動すると、以下のように openAI モデルに対してチャットを実行した際にログやトレースが取得されていることが確認できます。
Dify でのチャット
ログ
トレース
一方で DeepSeek や Ollama の model provider は openAI 互換ですが、chat.completions.create
ではなく python の requests
でエンドポイントにリクエストを送信してチャットする形式になっているので opentelemetry を入れても trace 等は取得されません。
# send a post request to validate the credentials
response = requests.post(endpoint_url, headers=headers, json=data, timeout=(10, 300), stream=stream)
なので opentelemetry-genai で計測できるかどうかは openai-python の chat.completions
を使っているかどうかに依存します。既存の LLM アプリケーションでは上記を使っているとも限らず対応が難しい場合があるので、このあたり今後他の対応が開発されることに期待。
まとめ
OpenTelemetry の Generative AI Observability プロジェクトはまだ歴史が浅いこともあって python と js のみ実装、OpenAI ライブラリのみに対応しているという状況ですが、今後は他の言語や GenAI サービス、embedding や image などより多くの GenAI 機能の計測に対応することが目標となっています。Langfuse や OpenLLMetry など他の GenAI Observability サービスと比較すると、OpenTelemetry 配下のプロジェクトであるため既存の OpenTelemetry を使ったアプリケーションとの統合が容易な点(まだそこまで実装されていはいないが)や、特定のベンダーに依存しない実装が特徴的になっています。
opentelemetry のコミュニティは CNCF の中でも結構大きいので、これからの GenAI サービスの拡大に合わせて Generative AI Observability プロジェクトの機能も充実していくことが期待されます。
Discussion