AgentCore Observability が CloudWatch のどのロググループに、どの OpenTelemetry スキーマで着地するかを仕様理解が浅かったので整理した。
モチベーションとしては、AgentCore Evaluations に渡すデータをどのように準備するといいのかを理解したかった。(これは次回)
Strands と LangGraph で若干仕様の差があり混乱した。
- 結論
- 前提: OTel GenAI semantic conventions
- CloudWatch Logs ロググループは 4 種類
- メッセージの分離機構
- otel-rt-logs に message log record が出る条件
- ログ構造
- Runtime / ローカルで動かすための実装
- AgentCore Evaluations との連携
- 参考
結論
ポイントは 2 つ。
- AgentCore O11y のロググループは 4 種類で、評価に使うのは 2 種類。
aws/spans(構造情報)と runtime のotel-rt-logs(意味情報)。 - その 2 本の中身がフレームワークで変わる。
- Strands: 構造を
aws/spans、メッセージ(ユーザー発話・応答)をotel-rt-logsに分けて出す - LangGraph: メッセージを
aws/spansの span attributes に入れる。加えて AWS distro(aws-opentelemetry-distro)の GenAI 計装が同じ内容を message log record として出し、これがotel-rt-logsに着地する
- Strands: 構造を
どちらも spanId / traceId で span と log record が紐づく。otel-rt-logs に message log record が出るかは local / Runtime の差ではなく、aws_distro の log exporter が有効かで決まる。
前提: OTel GenAI semantic conventions
AgentCore の aws/spans に出てくる gen_ai.* は AgentCore 独自ではなく、OpenTelemetry の GenAI semantic conventions に従う。Strands / LangGraph の差も、突き詰めればこの仕様のどこに乗っているかの違い。
GenAI semconv はまだ開発段階(development / experimental, semconv 1.42 時点)で、専用リポジトリ semantic-conventions-genai に分離された。telemetry の信号は trace(span)・metric・event/log の 3 系統で構成される(OpenTelemetry for Generative AI)。
操作を表す gen_ai.operation.name
span の種類は gen_ai.operation.name で識別する。well-known 値は次のとおり(registry/attributes/gen-ai)。該当が無ければ独自値も可。
chat/text_completion/generate_content— LLM 推論呼び出しembeddings— 埋め込み生成execute_tool— ツール実行create_agent/invoke_agent— エージェント生成・呼び出しinvoke_workflow— ワークフロー実行retrieval— 検索
span 名は操作と対象を組み合わせる規約。ツール実行 span は execute_tool {gen_ai.tool.name}、エージェント呼び出しは invoke_agent {agent} になる。AgentCore で見る invoke_agent test_agent や execute_tool web_search はこの規約そのもの。
主な attribute
採点や可視化で使う属性は scope をまたいで共通。
| attribute | 用途 |
|---|---|
gen_ai.input.messages / gen_ai.output.messages |
入出力メッセージ本体(parts 形式の構造体) |
gen_ai.system_instructions |
system プロンプト(chat history と分離して保持) |
gen_ai.tool.name / gen_ai.tool.call.id / .arguments / .result |
ツール名・呼び出し ID・入力・結果 |
gen_ai.usage.input_tokens / output_tokens |
トークン数 |
gen_ai.request.model / gen_ai.provider.name |
モデル ID・プロバイダ識別子(例: aws.bedrock) |
gen_ai.provider.name は telemetry のフレーバーを示す discriminator。Bedrock 由来なら aws.bedrock を立て、aws.bedrock.* 属性を伴う(registry/attributes/gen-ai)。
2026/7/24 update:
メッセージのスキーマ
gen_ai.input.messages / output.messages は role + parts の配列。parts の type で text / tool_call / tool_call_response を切り替える。output 側には finish_reason が付く(registry/attributes/gen-ai)。
[ { "role": "user", "parts": [{ "type": "text", "content": "Weather in Paris?" }] }, { "role": "assistant", "parts": [{ "type": "tool_call", "id": "call_1", "name": "get_weather", "arguments": { "location": "Paris" } }] }, { "role": "tool", "parts": [{ "type": "tool_call_response", "id": "call_1", "result": "rainy, 57°F" }] } ]
content を event に置くか attribute に置くか
ここが Strands / LangGraph 分岐箇所。
GenAI semconv は当初、メッセージ本体を独立した log event(gen_ai.user.message / gen_ai.choice / gen_ai.system.message)として記録していた。現行仕様はこれを span attribute(gen_ai.input.messages / output.messages)へ移した(gen-ai-events は Moved 扱い、Traceloop 側の追従が openllmetry issue #3515)。
仕様上、記録先で形式の縛りが変わる。
- event に記録するなら structured 形式が MUST
- span に記録するなら structured が SHOULD、未対応なら JSON 文字列でも MAY
この「event か attribute か」の選択が、後述の Strands(Span Event → otel-rt-logs)と LangGraph(span attribute)の分かれ目に直結する。版差で attribute の置き場所(gen_ai.input.messages か gen_ai.task.input か)が動くのも、この移行の途上であるため。
CloudWatch Logs ロググループは 4 種類
| ロググループ | ストリーム | 用途 |
|---|---|---|
aws/spans |
default |
OTel span(構造情報。LangGraph はメッセージも attributes に含む) |
/aws/bedrock-agentcore/runtimes/<id>-DEFAULT |
otel-rt-logs |
OTel log record(input/output。Strands は常時、LangGraph は aws_distro の log exporter 有効時) |
/aws/bedrock-agentcore/runtimes/<id>-DEFAULT |
runtime-logs-<session> |
アプリの stdout/stderr(評価には不使用) |
/aws/bedrock-agentcore/evaluations/results/<id> |
evaluation-results-<id> |
評価結果(score + explanation) |
evaluate に必要なのは上 2 行。3 行目はアプリログ、4 行目は評価結果の配置先。
メッセージの分離機構
OTel の Span には 3 種類のデータを付けられる。
Span ├── Attributes ... span 全体のメタデータ(key-value)。例: gen_ai.request.model, session.id ├── Events ... span 実行中の「出来事」のタイムスタンプ付き記録。span.add_event() で追加 └── Links ... 別トレースへの参照
2 つの経路で otel-rt-logs の中身は同じ封筒構造(body.input.messages)に見えるが、生成元が異なる。
Strands: Span Event → ADOT 変換
tracer.py が span.add_event("gen_ai.client.inference.operation.details", {"gen_ai.input.messages": ...}) で Span Event を出す。AgentCore Runtime 内の ADOT パイプラインがその Span Event を検知して body.input.messages 形式の log record に変換し、otel-rt-logs に書く。SDK が出した形と着地形は別物で、変換は ADOT の責任範囲。メッセージを span attributes に置かないため、aws/spans のレコードにはメッセージが現れない。

LangGraph: span attribute + AWS distro GenAI 計装
Traceloop の langchain instrumentor(opentelemetry-instrumentation-langchain)は span.set_attribute() でメッセージを span attributes(gen_ai.input.messages / gen_ai.output.messages、版により gen_ai.task.input/output)に置く。Span Event は出さない。AWS distro に組み込まれた GenAI 計装が span の中身を見て body.input.messages 形式の log record を独立に logger.emit() する。AgentCore Runtime が span に付ける telemetry.extended フラグは「メッセージ本体を log record 側に切り出した」という印。Strands にはこのフラグが付かない(span attributes にメッセージを入れていないため切り出し操作が発生しない)。

otel-rt-logs に message log record が出る条件
LangGraph の message log record は AWS distro の log exporter が運ぶ。送り先が決まる条件で 3 パターンに分かれる。
| 実行形態 | message log record | 理由 |
|---|---|---|
| ローカル + InMemorySpanExporter | 出ない | log 経路が無く span のみ。evaluate は span attribute を直読みするので採点は通る |
| ローカル + aws_distro + log exporter | 出る | console / collector / OTLP など exporter の送り先に着地 |
| AgentCore Runtime | otel-rt-logs に出る |
AgentCore が aws_distro + OTLP log export(送り先 otel-rt-logs)+ capture flag を自動配線 |
分岐は local / Runtime ではなく aws_distro の log exporter の有無。Runtime で本体を log record へ切り出した span には telemetry.extended の印が付く。
ログ構造
ロググループ別のデータ比較
| Strands | LangGraph | |
|---|---|---|
aws/spans |
構造情報のみ(メッセージなし) | 構造 + span attributes にメッセージ |
otel-rt-logs |
body.input.messages(常時) |
aws_distro の log exporter 有効時に message log record |
LangGraph は構造側(span attributes)にもメッセージを重複して持つ点が Strands と違う。
span の識別キー
ルートスパン・ツールスパンの識別、ツール名の取り出し先は scope ごとに別物。
| 観点 | Strands | LangGraph |
|---|---|---|
scope.name |
strands.telemetry.tracer |
opentelemetry.instrumentation.langchain |
| ルートスパンの識別 | gen_ai.operation.name: invoke_agent |
traceloop.span.kind: workflow |
| ツールスパンの識別 | gen_ai.operation.name: execute_tool |
traceloop.span.kind: tool |
| ツール名の取り出し | attributes.gen_ai.tool.name |
attributes.traceloop.entity.name |
span の実データ
値は中立な例に置換、構造はそのまま。
Strands ルートスパン(invoke_agent)。
{ "scope": { "name": "strands.telemetry.tracer" }, "name": "invoke_agent test_agent", "traceId": "69fef8a049b528ab76919816242cf797", "spanId": "f14f97728bf5084a", "parentSpanId": "bedeaf1409c9d978", "kind": "INTERNAL", "attributes": { "gen_ai.agent.name": "test_agent", "gen_ai.operation.name": "invoke_agent", "gen_ai.request.model": "us.amazon.nova-lite-v1:0", "session.id": "sess-001", "gen_ai.system": "strands-agents" } }
Strands ツールスパン(execute_tool)。ツール名は gen_ai.tool.name。
{ "scope": { "name": "strands.telemetry.tracer" }, "name": "execute_tool web_search", "traceId": "69fef8a064084ae046df5182052e7080", "spanId": "e48797ee5c0920c2", "parentSpanId": "19c00bac9e1d9616", "attributes": { "gen_ai.tool.name": "web_search", "gen_ai.tool.call.id": "tooluse_nmlu6BwDzZ4ZccXmwhkLun", "gen_ai.tool.status": "success", "session.id": "sess-001" } }
LangGraph ツールスパン。名前は <tool>.tool 形式、ツール名は traceloop.entity.name。
{ "scope": { "name": "opentelemetry.instrumentation.langchain" }, "name": "web_search.tool", "traceId": "691f4a5c0a7ab761407a1a9a36991613", "spanId": "b58bd6568e00fc64", "parentSpanId": "aaee94b5bd16f3b0", "kind": "INTERNAL", "attributes": { "traceloop.span.kind": "tool", "traceloop.entity.name": "web_search", "traceloop.entity.path": "tools", "session.id": "sess-001" } }
log record の実データ
otel-rt-logs の log record は aws/spans の span と同じ spanId で紐づく。input/output の中身がここに入る。
Strands。
{ "scope": { "name": "strands.telemetry.tracer" }, "traceId": "6a267ec723c4be72618164676ff4c17d", "spanId": "c1d39bc358b83913", "attributes": { "session.id": "sess-001" }, "body": { "input": { "messages": [ { "role": "system", "content": { "content": "[{\"text\": \"あなたは検索アシスタントです\"}]" } }, { "role": "user", "content": { "content": "[{\"text\": \"東京の明日の天気を教えて\"}]" } } ] }, "output": { "messages": [ { "role": "assistant", "content": { "message": "[{\"text\": \"晴れ時々曇りです\"}]", "finish_reason": "end_turn" } } ] } } }
content.content が文字列化された JSON 配列なのは、Strands SDK の tracer.py が serialize() でそう出力するため。ただし body.input.messages という封筒構造は Strands の OTel 仕様ではない。責任を 3 層に分けると区別がつく。
- Strands SDK:
gen_ai.input.messagesの Span Event を出す。content の二重シリアライズはここ由来 - Runtime / O11y パイプライン: その Span Event を
otel-rt-logsの log record(body.input.messages形式)へ変換。aws/spansとotel-rt-logsへの振り分けもここ - Evaluate API: 変換後の
body.input.messagesを読んでuser_queryを取り出す
手書きで evaluate を直叩きするとき再現すべきは、SDK の生出力ではなく Runtime/O11y 変換後の形。content 値の二重シリアライズが SDK 由来、封筒は下流由来。
LangGraph の log record も otel-rt-logs 上は同じ body.input/body.output 形式(scope は opentelemetry.instrumentation.langchain)。ただし content は parts 形式({"role": ..., "parts": [{"type": "text", "content": ...}]})を文字列化したもので、Strands と中身が違う。メッセージは span attributes(gen_ai.input.messages / gen_ai.task.input / traceloop.entity.input、版により置き場所が変わる)にも重複して入る。
Runtime / ローカルで動かすための実装
Strands(Runtime): 追加コード不要。BedrockAgentCoreApp を opentelemetry-instrument で起動すれば両ロググループに書き込む。
from bedrock_agentcore.runtime import BedrockAgentCoreApp app = BedrockAgentCoreApp()
CMD ["opentelemetry-instrument", "python", "-m", "eval_agent_strands"]
LangGraph(Runtime): 明示 LangchainInstrumentor().instrument() は不要。opentelemetry-instrumentation-langchain を入れておけば opentelemetry-instrument が entry point(opentelemetry_instrumentor group の langchain)から自動計装する。capture flag を立てると AWS distro が otel-rt-logs に message log record を出す。
import os os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "true") from bedrock_agentcore.runtime import BedrockAgentCoreApp app = BedrockAgentCoreApp()
ENV OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true CMD ["opentelemetry-instrument", "python", "-m", "eval_agent_langgraph"]
依存はバージョンをピン留めする。semconv が非互換だと langchain instrumentor が import 時にクラッシュして計装が立ち上がらない(Appendix「版非互換」)。
opentelemetry-instrumentation-langchain>=0.59.0 # 実機で 0.61.0 解決 opentelemetry-semantic-conventions==0.61b0 aws-opentelemetry-distro==0.17.1
分かれ目は明示呼び出しの有無ではなく、langchain instrumentor が import に成功して GenAI span が出ているか。span が出ていれば、message log record は capture flag + log exporter の条件で出る。
AgentCore Evaluations との連携
evaluate に渡すデータの収集経路
| Strands Runtime | LangGraph Runtime | LangGraph ローカル | |
|---|---|---|---|
| 収集元 | aws/spans + otel-rt-logs |
span に message 内容あり。otel-rt-logs にも record(UserIntent 等はこちらを読む) |
InMemorySpanExporter |
| ジョイン | spanId で必要 |
evaluate は span 1 本で可。StartBatchEvaluation は API 側が処理 |
不要 |
evaluate(オンデマンド)を手で呼ぶ場合、Strands は 2 本のクエリで集めて spanId でジョインする。StartBatchEvaluation(public preview)はロググループ名と sessionId を渡せば API 側が収集とジョインを引き受ける。
# ① aws/spans から span レコード aws logs filter-log-events \ --log-group-name "aws/spans" \ --filter-pattern '"strands.telemetry.tracer"' \ --start-time <epoch_ms> --end-time <epoch_ms> # ② otel-rt-logs から log record(Strands、および UserIntent 等を回す LangGraph) aws logs filter-log-events \ --log-group-name "/aws/bedrock-agentcore/runtimes/<id>-DEFAULT" \ --log-stream-name "otel-rt-logs" \ --filter-pattern '"strands.telemetry.tracer"' \ --start-time <epoch_ms> --end-time <epoch_ms>
手書きで span を組む場合
ライブエージェントが無い手元データから手書きで span を組むときも、置き場所の違いがそのまま効く。
| scope | メッセージの置き場所 | tool 評価に必須の attribute |
|---|---|---|
strands.telemetry.tracer |
log record の body.input.messages(content 二重シリアライズ) |
tool span + log record のペア |
opentelemetry.instrumentation.langchain |
span attributes | gen_ai.tool.call.arguments / gen_ai.tool.call.result |
LangGraph で tool 評価(ToolSelectionAccuracy 等)まで通すには、ツール I/O を gen_ai.tool.call.arguments / gen_ai.tool.call.result に入れる。traceloop.entity.input/output だけだと LogEventMissingException になる。UserIntent など意味情報を読む評価は body.input.messages 形式の log record を要求する。
ライブの LangGraph 実 span を evaluate に渡すときは、tuple 値の正規化が要る。OTel の attribute 値は tuple になることがあり(langgraph_triggers など)、boto3 の evaluate は tuple を受け付けず ParamValidationError になる。
attrs = {k: (list(v) if isinstance(v, tuple) else v)
for k, v in dict(span.attributes).items()}
評価結果の保存先
評価結果は /aws/bedrock-agentcore/evaluations/results/<id> に書かれる。
{ "name": "gen_ai.evaluation.result", "attributes": { "gen_ai.evaluation.name": "Builtin.InstructionFollowing", "session.id": "sess-001", "gen_ai.evaluation.score.value": 1.0, "gen_ai.evaluation.score.label": "Yes", "aws.bedrock_agentcore.evaluation_level": "Trace" } }
書き込むのはオンライン評価のときだけ。オンデマンドの evaluate は結果を API レスポンスの evaluationResults で返すだけで、このロググループには書かない。
| 評価方式 | 結果の返り先 | CloudWatch 書き込み |
|---|---|---|
| オンライン評価(config 作成) | evaluations/results/<config-id> |
あり(自動) |
オンデマンド evaluate |
API レスポンス | なし |
Evaluate API と OpenTelemetry の関係
evaluate は OTel プロトコルで通信するわけではない。普通の boto3 / HTTPS リクエストで、OTel が関わるのはデータのスキーマだけ。
エージェント実行の記録 ↓ 表現形式として採用 OpenTelemetry の span/trace 形式 ↓ API のスキーマとして採用 Evaluate API の sessionSpans(OTEL-like JSON の配列)
evaluateは OTEL プロトコルで通信するか → しない。boto3 / HTTPSevaluateは OTEL 形式のデータを入れるか → 入れる。sessionSpansが OTEL-like JSON- OTEL SDK がないと使えないか → 使える。自分で JSON を組めばいい
- CloudWatch がないと使えないか → 使える。手書き span を直接投げてもいい
OTel はデータのスキーマを規定する存在で、評価そのものとは独立している。
参考
- Understanding input spans
- Getting started with on-demand evaluation
- Evaluate API リファレンス
- Ground truth evaluations
- openllmetry issue #3515(gen_ai.prompt/completion 廃止と新 semconv 移行)
- OTel GenAI semantic conventions: registry/attributes/gen-ai / 専用リポジトリ semantic-conventions-genai
- OpenTelemetry for Generative AI(信号 3 系統の概説)
- Strands SDK ソース:
src/strands/telemetry/tracer.py - bedrock-agentcore-starter-toolkit:
operations/observability/client.py