亚马逊AWS官方博客

深入 Amazon Bedrock AgentCore Runtime 的可观测性:自动插桩机制、场景实测与自定义 telemetry

摘要:Amazon Bedrock AgentCore Runtime 提供了构建在 OpenTelemetry 之上的可观测性能力,但”自动插桩”并非无条件生效。本文通过多组真实部署对照实验,拆解 AgentCore 可观测性的两层来源。


1.前言

Amazon Bedrock AgentCore Runtime 提供了构建在 OpenTelemetry 之上的可观测性能力,但”自动插桩”并非无条件生效。本文通过多组真实部署对照实验,拆解 AgentCore 可观测性的两层来源,实际部署测试:裸部署、无 agentic 开发框架 workflow 场景、以及基于 Strands SDK 框架开发的 agent,三种形态下 trace/span/metric/log 的差异,演示如何在不引入第三方 agentic 开发框架的情况下用自定义 tracer 恢复业务语义的方式,并验证 Agent 调用 Memory 等其他 AgentCore 资源时跨资源 trace 的拼接条件。

2. 机制底座:Runtime 自动插桩是怎么实现的

Amazon Bedrock AgentCore Observability 服务构建在 OpenTelemetry(OTEL)之上,把 Agent 的 session / trace / span / metric / log 通过 AWS Distro for OpenTelemetry (ADOT)统一汇入 Amazon CloudWatch,并提供 GenAI Observability 专用仪表板。

但是其具体是如何操作的,如何更好的使用这个能力,理解它的关键,是先理清”自动插桩”的实现,是由谁实现、插桩在什么时候完成。

首先先看一下什么是 Telemetry

2.1 telemetry 的层级模型:从 Session 到 Trace 再到 Span

下图为以 Agentcore runtime 为例子,其整体的 opentelemery 的实现逻辑和流程。

[图 1]

其中涉及 Opentelemetry 的几个核心概念。

  • Session:贯穿多轮交互的上下文,带唯一 session.id。
  • Trace:一次 invoke 的完整执行路径。
  • Span:trace 内一个可度量的工作单元,有父子关系,构成执行树。

2.2 AgentCore 上的自动插桩是”两层叠加”

AgentCore Runtime 上的可观测性来自两个相互独立的来源:

1. 服务侧:即通过 agentcore 平台进行自动注入的,无需编写代码,无需依赖项,平台自带的 span。无论你的代码长什么样,Agentcore Runtime 都会为每次调用产出:

  • 一个根 span: AgentCore.Runtime.Invoke、
  • 一组运行时指标(Invocations / Latency / Session 等)
  • 应用日志

2. 进程内 ADOT(取决于依赖与配置):当满足条件时,Agentcore Runtime 用 opentelemetry-instrument 启动你的进程,由 AWS Distro for OpenTelemetry(ADOT)在进程内自动给常见库打点,产出 HTTP span、LLM span(含 GenAI 语义与 token 用量)等。

上面提到的第二层是否启用需要满足的条件,由 starter toolkit 在打包期决定。其源码 build_entrypoint_array 是关键:

def build_entrypoint_array(entrypoint_path, has_otel_distro, observability_enabled):
    # 这个函数定义了需要同时满足"安装了 aws-opentelemetry-distro"以及"打开了 observability"才打包 opentelemetry-instrument
    if has_otel_distro and observability_enabled:
        return ["opentelemetry-instrument", entrypoint_path]
    return [entrypoint_path]

这也就是说,如果想让 Agentcore Runtime 实现“自动插桩”并不是无条件的:

它要求代码的依赖里有 aws-opentelemetry-distro,且 agentcore 的 observability 处于开启状态(默认开)。这一点会在后面的第 3 章用一个”只加一个依赖”的对照实验例子来对比实现效果。

2.3 opentelemetry-instrument 的工作原理

opentelemetry-instrument 是 opentelemetry-instrumentation 包提供的命令行启动器。当 AgentCore Runtime 把 agent 代码入口设为 [“opentelemetry-instrument”, “agent.py”] 时,进程的启动流程是如下图所示的:

[图 2]

整个流程有三个关键 entry-point 组:

  • opentelemetry_distro:aws_distro / distro —— 设默认值(导出协议、propagator 等)。
  • opentelemetry_configurator:aws_configurator / configurator —— 装配 SDK:TracerProvider / MeterProvider / LoggerProvider、OTLP exporter、采样器、AWS 资源属性。
  • opentelemetry_instrumentor:所有已安装的 instrumentor,逐个 monkey-patch 目标库(库可 import 时才生效)。

整个流程的实现自动插桩的核心是通过 “entry-point 发现 + import 时 monkey-patch”来实现,用户无需更改业务代码。 整个流程选哪个 distro/configurator、关哪些 instrumentor,都由环境变量控制,而这些 OTEL_* 环境变量则由 AgentCore Runtime 平台在运行时注入。

整个机制并不是 Amazon Bedrock AgentCore 的私有方案,而是基于 OpenTelemetry Python 官方”zero-code instrumentation”的标准设计。OpenTelemetry Python 的自动插桩示例,做了非常好的一个示例,例子中用一个 Flask 路由做了三个场景的对比:

1)手写 tracer.start_as_current_span(…) 的 server_manual.py、

2)什么都不改直接用 opentelemetry-instrument 启动的 server_automatic.py,

以及 3)显式 FlaskInstrumentor().instrument_app(app) 的 server_programmatic.py——三者产出的 span 结构完全一致。感兴趣的读者可以自行查看尝试。

2.4 aws-opentelemetry-distro:ADOT 是什么,以及它和 opentelemetry-instrument 的关系

上一节讲的 opentelemetry-instrument 只是一个通用的启动器——它本身不知道该用哪个 distro、装哪些 instrumentor,全靠 Python 的 entry-point 机制在环境里“发现”谁注册了这些角色。

ADOT 就是 AWS 提供的那个“被发现的角色”:它是社区版 OpenTelemetry SDK 的一个发行版(distro),通过 PyPI 包 aws-opentelemetry-distro 分发,内容包括:

  • 一个 opentelemetry_distro 入口(aws_distro):设置面向 AWS 后端的默认导出协议、propagator 等。
  • 一个 opentelemetry_configurator 入口(aws_configurator):把 TracerProvider / MeterProvider / LoggerProvider、OTLP exporter、采样器、AWS 资源属性一次性装配好,让 span/metric/log 能直送 CloudWatch。
  • 一整套预置好的 instrumentor(比如 Web 框架、HTTP 客户端、AWS SDK、数据库、消息队列等),随包一起安装,opentelemetry-instrument 启动时会把它们全部枚举出来逐个 .instrument()。

也就是说,ADOT 是“内容”,opentelemetry-instrument 是“加载器”:装了 aws-opentelemetry-distro 后,opentelemetry-instrument 才有 AWS 定制的 distro/configurator 可选,从而才会把这一整套 instrumentor 通过 monkey-patch ,关联到你的进程

AWS 官方文档和示例并不建议锁死具体的 patch 版本。参考 Get started with AgentCore Observability ,使用 AgentCore Observability 时,只要求 requirements.txt 里有 aws-opentelemetry-distro 即可,AWS 官方的多智能体 SRE 示例用的是开放式约束 aws-opentelemetry-distro~=0.10.1;本文所有的测试 case 的 requirements.txt 同样写的是开区间 aws-opentelemetry-distro>=0.10.0,都没有把 patch 版本锁死到某一个具体值。

使用 uv/pip 部署时会解析到当时能拿到的最新兼容版本。截至本文撰写时(2026 年 7 月),PyPI 上 aws-opentelemetry-distro 的最新版本就是 0.18.0,因此后续所有实测部署时虽然没有明确指定使用 0.18.0 版本,但是默认使用的为这个版本。如果你在更晚的时间在自己环境下复现本文中的实验用例,拉到的版本号可能已经更新,指纹和清单的具体内容值可能有出入,但整体机制不变。

2.5 aws-opentelemetry-distro 0.18.0 捆了哪些 instrumentor

这里我们看一下 ADOT 0.18.0 版本 distro 现阶段都有哪些 instrumentor,通过实际测试该版本 distro 注册了约 51 个 instrumentors,按用途分类如下列出供参考:

类别 instrumentor
Web 框架(服务端) starlette、fastapi、flask、django、falcon、pyramid、tornado
HTTP 客户端 requests、httpx、urllib、urllib3、aiohttp-client
AWS SDK / GenAI botocore、boto3、aiobotocore、aws-lambda
GenAI 框架(AWS 定制) aws_langchain、aws_crewai、aws_llama-index、aws_openai_agents、aws_mcp、openai_agents
数据库 sqlalchemy、psycopg2、pymysql、mysql、mysqlclient、sqlite3、pymongo、redis、elasticsearch、cassandra、asyncpg、aiopg、pymemcache、tortoiseorm
消息/队列 kafka、aiokafka、confluent_kafka、celery、pika、aio-pika、remoulade
gRPC grpc_client、grpc_server、grpc_aio_client、grpc_aio_server
运行时/基础 logging、threading、system_metrics、jinja2

上面表格列出的 instrumentor 中,值得注意的有以下两点:

  • 列表里没有 Strands SDK 相关的 instrumentor。Strands SDK 的 invoke_agent / execute_tool span 来自 Strands 自己原生的 OTEL tracer, 对应的 scope 为 strands.telemetry.tracer),不是 ADOT instrumentor 产出的。所以”Strands SDK 框架原生埋点”与”ADOT 自动插桩”是两条并行 span 来源,在同一棵 trace 里叠加。
  • Bedrock 的 GenAI 语义来自 botocore instrumentor(scope 为 opentelemetry.instrumentation.botocore.bedrock-runtime),而非某个 bedrock 专用包。只要用 boto3 调 Bedrock,无论是否使用框架,都能拿到 GenAI span + token 用量——这正是第 3 章 case2 无框架场景也有 token 用量的原因。

3. 基于实验下的三种部署场景实测对比:metric / telemetry / log

为了验证上面的内容,本文在 us-east-1 区域部署了三个 AgentCore runtime,模型统一用 us.amazon.nova-lite-v1:0:

  • case1 — 使用 Strands SDK 框架开发的 Agent:BedrockAgentCoreApp + Strands Agent + 2 个工具,requirements 中的依赖库含 strands-agents 与 aws-opentelemetry-distro。
  • case2 — 无任何 Agentic 开发框架的多次 LLM 调用的 workflow:直接使用 boto3.converse() API 对 Nova 模型进行调用的三步流程(抽取主题→起草→批改),requirements 含 aws-opentelemetry-distro,但无没有使用任何 agentic 开发框架。
  • case3 — 裸部署(bare),没有用 ADOT 依赖项目:与 case2 相同的代码,但 requirements 不含 aws-opentelemetry-distro。

Case 1 的代码参考如下:

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

app = BedrockAgentCoreApp()

MODEL_ID = "us.amazon.nova-lite-v1:0"

@tool
def get_weather(city: str) -> str:
    """获取城市的天气"""
    # Static stub so the trace is deterministic.
    table = {"seattle": "rainy, 12C", "tokyo": "sunny, 21C", "cairo": "hot, 35C"}
    return table.get(city.lower(), "unknown")

@tool
def add(a: int, b: int) -> int:
    """求两个整数的和."""
    return a + b

model = BedrockModel(model_id=MODEL_ID)
agent = Agent(
    model=model,
    tools=[get_weather, add],
    system_prompt=(
        "You are a concise assistant. Use tools when relevant. "
        "Answer in one short sentence."
    ),
)

@app.entrypoint
def invoke(payload):
    """AgentCore Runtime entrypoint."""
    user_input = payload.get("prompt", "Hello")
    result = agent(user_input)
    return result.message["content"][0]["text"]

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

3.1 测试中案例使用相同的代码,通过修改依赖项目,观察 cloudwatch 上的 span 的变化

case2 和 case 3 的代码相同,case3 一开始不添加 ADOT 的依赖。然后调用 runtime,观察 GetAgentRuntime 返回的部署入口与 trace:

首先 case3 requirements.txt 内容如下:

bedrock-agentcore
boto3

agent 的代码如下:

import boto3
from bedrock_agentcore.runtime import BedrockAgentCoreApp
app = BedrockAgentCoreApp()
MODEL_ID = "us.amazon.nova-lite-v1:0"
bedrock = boto3.client("bedrock-runtime")
#调用 bedrock coverse 的 api
def call_llm(system: str, user: str, max_tokens: int = 200) -> str:
    resp = bedrock.converse(
        modelId=MODEL_ID,
        system=[{"text": system}],
        messages=[{"role": "user", "content": [{"text": user}]}],
        inferenceConfig={"maxTokens": max_tokens},
    )
    return resp["output"]["message"]["content"][0]["text"]
@app.entrypoint
def invoke(payload):
    text = payload.get("prompt", "Tell me about coral reefs and why they matter.")
    print("workflow.start: extracting topic")  # plain stdout log
    topic = call_llm(
        "Extract the single core topic. Reply with just the topic.", text, 30
    )
    print(f"workflow.step: topic={topic!r}; drafting")
    draft = call_llm(
        "Write a 2-sentence intro paragraph about the given topic.",
        f"Topic: {topic}", 150,
    )
    print("workflow.step: critiquing")
    critique = call_llm(
        "Give one concrete suggestion to improve the paragraph.",
        f"Paragraph: {draft}", 80,
    )
    print("workflow.end")
    return {"topic": topic, "draft": draft, "critique": critique}
if __name__ == "__main__":
    app.run()

查询 runtime 的 entry-point,可以看到为 agent.py

[图 3]

Invoke agentcore runtime 之后,调用的结果在 GenAI Observability 中查看 case3_bare 的对应的 trace 时间线。

[图 4]

可以看到只记录了一个 AgentCore.Runtime.Invoke 的根 Span

下图为 case3-bare 的部署的日志,分别抓取了应用日志和 otel-rt-logs,可以看到有应用日志

[图 5]

[图 6]

在此基础上,后续只在 requirements.txt 增加一行 aws-opentelemetry-distro,而 agent.py 的代码一字不改,重新 deploy 到 AgentCore runtime(版本 v1 变更到 v2):

更改后的 requirements 截图如下,可以看到,增加了 aws-opentelemetry-distro>=0.10.0

[图 7]

重新 deploy 后,再次查看 runtime 的 entryPoint,可以看到 entryPoint 变为变为 [“opentelemetry-instrument”, “agent.py”]】

[图 8]

Invoke 这个新版本的 runtime 后,再次在 cloudwatch 的 GenAI Observability 验证 trace,可以看到同样的代码,但是经过增加 ADOT 的依赖项目,通过 opentelemetry-instrument 作为入口后,span 从 之前的仅有 1 个 root span 变为 5 个

[图 9]

同时,点开 span 的 meta data 面板,可以看到 gen_ai.usage.input_tokens / output_tokens 等 GenAI 语义属性。

添加了 ADOT 的依赖项目后的 trace 的结构如下结构:

[图 10]

其中除了之前第一版的 case3-bare 中的 runtime.invoke 的根 span 外,由 ADOT 自动插桩产生了 POST/Invocations 以及 chat span 的 span,这几个 span 中都可以在 metadata 中找到由 OTEL instrument 插桩的证明,如一下几个在 cloudwatch 中 trace metadata 截图,post 以及 chat 的 span 都有 telemetry.auto.version 的字段,且可以看到具体的 instrument scope。而服务原生产生的根 span 并无这些字段。

[图 11]

[图 12]

[图 13]

产生上面的结果现象的原因可以汇总如下

1. 部署在 runtime 的代码打包期的配置产生了变化:

添加了 distro 后 runtime 的 toolkit 的 build_entrypoint_array 探测到 has_otel_distro=True 从而将 runtime 部署的 codeConfiguration.entryPoint 从 [“agent.py”] 变成 [“opentelemetry-instrument”,”agent.py”]。这个结论可以从上面的实际测量中两次部署的 getRuntimeArtifect 的输出获得明确的看到现象。

2. 进程启动方式变化:

容器进程从 python agent.py 变为 opentelemetry-instrument python agent.py。后者在你的代码运行前先装配 ADOT exporter,并且 monkey-patch 已安装的 instrumentor。

3. 逐 span 发送:

  • POST /invocations 使用 starlette instrumentor,
  • Agent 代码中的三次 LLM 调用即 chat <model> 使用 botocore instrumentor 的 bedrock-runtime 专用逻辑,把每次 converse() 包成 CLIENT span 并补 gen_ai.* + token 用量的 metadata。

3.2 不同的使用方式,observability 场景的观测性的区别:telemetry / log / metric

以下均为通过对比 case1-3 实测 cloudwatch 中的各个数据观察的对比结果(case3 取其”裸”版 v1):

3.2.1 Trace / Span 的对比

span scope / 来源 case1 Strands case2 无框架+ADOT case3 裸部署无 ADOT
AgentCore.Runtime.Invoke(根) 服务侧平台注入
POST /invocations ADOT starlette
chat <model>(含 gen_ai.*+token) ADOT botocore.bedrock-runtime
invoke_agent / execute_event_loop_cycle / execute_tool 框架原生 strands.telemetry.tracer
单次调用 span 总数(实测) 11 5 1

具体呈现参考下面对于 case1 的 otel session 的 span 树

[图 14]

可以看到包含 tool_name 等框架原生属性

[图 15]

对应 case 2 无框架 workflow,三个 chat span 同名平级排列,看不出业务步骤

[图 16]

3.2.2 Log 对比

日志 来源 case1 case2 case3
stdout/stderr(runtime-logs 流) Print 等操作
结构化 app 日志(bedrock_agentcore.app,带 requestId/sessionId/latency) SDK 的 RequestContextFormatter
OTEL 结构化日志(otel-rt-logs 流,含 gen_ai.system.message/user.message/choice 事件) ADOT logging 导出 无(流存在但 0 事件)

3.2.3 Metric

指标 命名空间 / 来源 case1 case2 case3
Invocations / Latency / SessionCount / Errors / Throttles / CPU / Memory 服务侧 vended(GenAI 仪表板;作为 CloudWatch 指标有批处理延迟)
gen_ai.client.token.usage、gen_ai.client.operation.duration ADOT,bedrock-agentcore 命名空间
http.server.duration / active_requests / request.size / response.size ADOT starlette,bedrock-agentcore 命名空间
strands.event_loop.*、strands.tool.*(带 tool_name 维度) 框架原生,bedrock-agentcore 命名空间

3.3 数据对比总结

通过上面的实测结果的对比,可以有以下的结论汇总:

1. 裸部署(无 Strands 开发框架、无 ADOT):

仍有”请求级”的可观测性,能够做基础监控,但看不到内部步骤与 token。

2. 有添加 aws-opentelemetry-distro 但是未使用 strands 等开发框架:

能够给到”调用级”的可观测性,每次 LLM 调用会产生一个带 token 用量的 span 以及 HTTP server span;但多次 LLM 调用是平级匿名的,缺业务编排语义。

3. Strands 框架原生 ,添加 ADOT:

可以支持到”Agent 语义级”的可观测性,提供 agent / event-loop / tool 的层级 span 与对应指标齐全。

4. 在 AgentCore Observability 中自定义 telemetry

通过第 3 章的实测结果,可以发现 case2 这种不使用已有 agentic 开发框架的 workflow 这种类似工作负载直接迁移到 agentcore,产生的可观测性是有限的,看不出业务步骤。如何在不重新学习开源 agent 开发框架,通过用自定义 telemetry 更好的使用 AgentCore Observability 的能力将观测能力补全是本章的讨论重点

4.1 官方文档中的建议

AWS 文档对”非 Strands/LangChain/CrewAI 框架”的情况给出了明确的建议:可以通过定义 custom tracer 来发送 GenAI 语义约定的 telemetry 和 span;并列出官方支持的插桩库:OpenInference、OpenLLMetry、OpenLit、Traceloop。

基于此建议,有两种途径可以参考

途径 A:用 OTEL 原生 API 写 custom span/metric,复用 ADOT 在 Runtime 上已装配好的 provider。

这个方式的优点是零额外依赖、不碰 exporter 配置、风险最低。

途径 B:直接用上面那些现成库(如 Traceloop 的 @workflow/@task 装饰器),少写代码,但需注意别让它重复初始化 exporter,要复用 ADOT 的 provider。

下文使用途径 A 的方式进行演示

4.2 途径 A 实践演示

使用途径 A 可以直接复用 ADOT 在 runtime 上已经配置好的 provider。因此不需要创建 TracerProvider,也不需要配置 exporter

from opentelemetry import trace, metrics

tracer = trace.get_tracer("workflow.content_pipeline")   # 复用 ADOT 装配的 TracerProvider
meter  = metrics.get_meter("workflow.content_pipeline")   # 复用 ADOT 装配的 MeterProvider
然后用装饰器/上下文管理器给每个业务步骤建 span,属性命名对齐官方 Bedrock OTEL sample 的 OpenLLMetry 约定(traceloop.span.kind),并只记录脱敏后的预览,参考代码如下:
def run_step(step_name, system, user, max_tokens):
    with tracer.start_as_current_span(f"task.{step_name}") as span:
        span.set_attribute("traceloop.span.kind", "task")
        span.set_attribute("workflow.step", step_name)
        span.set_attribute("workflow.input.preview", _preview(user))   # 脱敏:只记截断预览
        span.set_attribute("workflow.input.char_count", len(user))
        result = call_llm(system, user, max_tokens)  # ← ADOT 自动产生的 chat span 会挂在本 span 下
        span.add_event("step.completed", {"output.preview": _preview(result)})
        return result

还可以自定义业务指标,这里以统计字数为例,创建定制 metric:

runs_counter = meter.create_counter("workflow.runs", unit="{run}")
draft_words  = meter.create_histogram("workflow.draft.word_count", unit="{word}")
...
draft_words.record(len(draft.split()), {"workflow.name": "content_pipeline"})
runs_counter.add(1, {"workflow.name": "content_pipeline", "outcome": "success"})

4.3 途径 A 添加自定义 trace 修改后的实测

部署方式不变(同样 agentcore deploy,依赖里本就有 ADOT),只改了 agent.py。同一句调用 prompt 的产生的真实 trace 如下图:

[图 17]

Cloudwatch 中的实际观测结果如下

[图 18]

点开 task.extract_topic span 的属性面板,可见 traceloop.span.kind=task、workflow.step、workflow.input.char_count 等自定义属性与 step.completed 事件

[图 19]

对照:

case2 原版本 case2-v2 自定义
顶层业务 span 是,
呈现自定义的 content_pipeline
步骤 span
(三个 chat 为平级匿名)
是,
呈现自定义的
task.extract_topic/draft/critique
LLM chat span 是,平铺 是,自动嵌到对应 task 下
span 总数 5 9

自定义指标也确认进入 bedrock-agentcore 命名空间并可查到真实值

CLI 查询 bedrock-agentcore 命名空间下的自定义指标 workflow.runs 与 workflow.draft.word_count 的数据点输出如下

[图 20]

[图 21]

4.4 自定义 trace 输出的结果分析

  1. 首先 Runtime 上 ADOT 已经把全局 TracerProvider/MeterProvider 装好,代码中通过 get_tracer/get_meter 拿到的就是这些 provider,后续代码中自定义的 span/metric 直接走 ADOT 的导出管线即可输出到 CloudWatch 中。
  2. 其次,代码中,start_as_current_span 把上下文设为”当前 span”,随后 bedrock.converse() 触发的 botocore 自动 span 通过 OTEL current-context 传播挂到 task.* 之下,形成 topic 和 chat 的父子关系,无需手动传递关系。
  3. 最后自定义 span 的 scope 使用的是代码中的 tracer 名 workflow.content_pipeline,ADOT 使用的是 opentelemetry.instrumentation.*,这样可以让用户在 CloudWatch 里一眼能分清哪些是业务埋点、哪些是自动插桩的内容。

5. 跨资源可观测性验证

前面几章都在看单一资源(Runtime)自身的 telemetry。但真实场景里,Agent 通常还会调用 Memory、Gateway、Tools 这些独立的 AgentCore 资源。这些资源自己会 vend 出 span(比如 Memory 的 CreateEvent),但一个关键问题是:这些 span 会不会自动和 Agent 自己的 trace 拼成一棵树,还是各自成一棵孤立的 trace,需要后续手动自己拼接? 答案直接决定了你能不能在 GenAI Observability 里一条 trace 点开就看到从 Agent 调用 LLM 到 写入 Memory 的完整链路。

验证整个 trace 的实现情况,这里设计了一组对照 case。case4 是与 case2 相同的一个 3 步 LLM workflow 附加了一次 bedrock-agentcore memory 数据面的 CreateEvent 调用;case5 是 Strands agent 通过官方 AgentCoreMemorySessionManager 库集成了 Memory 功能; (Memory 资源均为 STM_ONLY,即 short memory only,由 starter toolkit 自动创建),汇总各个案例的配置假设如下:

case 目录 框架 组件 ADOT
4a case4-memory-noadot/不使用 ADOT Memory(手写 boto3 create_event)
4b case4-memory-adot/使用 ADOT 与 4a 代码相同,代码一字不差,只在 requirments 中多一行 aws-opentelemetry-distro 依赖
5a case5-strands-memory-noadot/不使用 ADOT Strands Memory(官方 SessionManager 集成)
5b case5-strands-memory-adot/使用 ADOT Strands 同 5a,只在 requirments 中多一行 aws-opentelemetry-distro 依赖

5.1 案例实测结果

用 CloudWatch Logs Insights 查 aws/spans 日志组,按 traceId 把每次调用的完整 span 集合拉出来,三个 case 的结果如下:

5.1.1 Case 4b(无 strands 框架 ,有 ADOT)

  • 两棵 trace 树拼成了一棵

[图 22]

上图为 cloudwatch 上观测到的 trace 结果截图,以及查询 CreateEvent 的 log 的结果,首先,Memory 侧 CreateEvent 的 parentSpanId 精确指向 ADOT 产出的 Bedrock AgentCore.CreateEvent CLIENT span 的 spanId,且两者 trace_id 完全一致。也就是说 ADOT 的 botocore instrumentor 不仅给 bedrock-runtime 的 converse() 加了 span,也同样包了 bedrock-agentcore 数据面 client 的 create_event(),并且在出站请求上正确注入了 trace context header,让 Memory 服务端能把自己 vend 的 span 挂到同一棵树上。

[图 23]

5.1.2 Case 4a(无 Strands 框架 ,无 ADOT)

  • 两棵独立的树

两个 trace_id 完全不同。Memory 服务端确实 vend 出了 CreateEvent span,这个是服务的默认行为,不需要在资源上额外开关,但因为 Agent 进程里没有 botocore instrumentor,出站请求没有携带任何 trace context,Memory 服务端只能自己起一个全新的根 trace。这和第 3 章”1 span 基线”的结论完全一致。

如下图,为 log insights 的查询结果,Create Event 的 trace id 为 41a6 结尾的 id,而 runtime invoke 的 trace id 是 d67c 结尾的,两者不同

[图 24]

[图 25]

下图为 GenAI Observability 控制台中 case4a 的实际 trace 查看效果,Agent trace 与 Memory trace 彼此分离,可以看到对应的 trace id 是 d67c 结尾的 id

[图 26]

5.1.3 Case 5a (使用 Strands 框架 , 无 ADOT)

— Agent 侧退化到只剩根 span,Memory / Code Interpreter 的 span 以独立 trace 出现

Agent 自己的 trace 只剩 1 个服务侧根 span:

如下图为 cloudwatch GenAI Observability 控制台中 case5a 的实际 trace 查看效果,仅有 1 个 AgentCore.Runtime.Invoke 根 span,Strands 原生 tracer 的 span 全部缺失

[图 27]

Strands SDK 自己的 invoke_agent/execute_event_loop_cycle/execute_tool 原生 span 并不存在,此现象和第 3 章节的结论一致:没有 ADOT 提供的 exporter,框架自带的 tracer 拿到的是 OTEL 默认的 no-op provider。

AgentCore Memory 的写入是成功的,用 list-events 能查到完整事件列表

[图 28]

Memory / Code Interpreter 服务侧 vended span 的行为和 Case 4a 一致:span 照常产生,但都是独立 trace。

5.2 测试结果汇总

无 ADOT 有 ADOT
Agent 自身 span 1(服务侧根 span) 完整(HTTP / chat / 框架原生 span 全有)
Memory vended span(CreateEvent 等) 出现,独立 trace 出现,且能挂进 agent trace
跨资源 trace 拼接
各资源指标(Invocations/Latency 等) 默认有 默认有

回到本章开头提出的问题,通过实际测试,获得了以下三条结论:

  1. AgentCore 的服务侧 vended span 是无条件产生的,与 Agent 进程有没有装 ADOT、有没有用框架都无关,前提是该资源的 traces delivery 配置正常
  2. ADOT 的作用更侧重”拼接”而不是”产生”:在安装了 ADOT,botocore instrumentor 给所有 boto3 出站调用添加 CLIENT span 并注入 trace context,让资源侧的 SERVER span 挂进同一棵 trace;不装 ADOT 则各自成独立 trace,只能靠 session.id/时间戳人工关联。
  3. 框架不能替代 ADOT,但两者解决的是不同问题:Strands SDK 原生 tracer 负责”产生”agent 语义层的 span(invoke_agent/execute_tool 等),但没有 ADOT 装配的全局 exporter 这些 span 发不出去;跨资源拼接则是 ADOT botocore instrumentor 的功劳。要拿到完整的三层结构树:框架语义层 ,ADOT 调用层 ,资源服务侧的话,Strands SDK 框架和 ADOT 缺一不可。

6.总结

本文从机制原理到实测验证,系统性地拆解了 Amazon Bedrock AgentCore Runtime 的可观测性能力。核心结论如下:

第一,AgentCore 的”自动插桩”并非无条件生效,它依赖 aws-opentelemetry-distro 出现在依赖列表中,由 starter toolkit 在打包期决定进程启动方式

第二,可观测性呈三级递进:裸部署仅有请求级根 span 与基础指标;添加 ADOT 后获得调用级 span(含 token 用量与 HTTP 指标);再结合 Strands 等 Agentic 框架,则可达到 Agent 语义级的完整 span 树(agent / event-loop / tool)。

第三,对于不使用现成agentic开发框架的 workflow 类负载,可通过 OpenTelemetry 原生 API 创建自定义 span 与 metric,复用 ADOT 已装配的 provider,即可补全业务编排语义,无需额外配置 exporter。

第四,跨资源(Memory、Gateway 等)的 trace 拼接同样依赖 ADOT:服务侧 vended span 无条件产生,但只有 ADOT 的 botocore instrumentor 才能注入 trace context,将多棵独立 trace 树合并为一棵完整调用链。

希望本文的对照测试结果与代码示例能帮助读者根据自身场景,选择合适的可观测性配置策略。

➡️ 下一步行动:

相关产品:

相关文章:

7.参考文档

*前述特定亚马逊云科技生成式人工智能相关的服务目前在亚马逊云科技海外区域可用。亚马逊云科技中国区域相关云服务由西云数据和光环新网运营,具体信息以中国区域官网为准。

本篇作者

裴峰

亚马逊云科技合作伙伴解决方案架构师,主要负责合作伙伴架构咨询和方案设计,同时致力于 AWS 云服务在国内的应用及推广,有多年大型企业数据中心网络架构设计实战经验。

刘磊

亚马逊云科技合作伙伴解决方案架构师,致力于帮助初创企业在亚马逊云平台上实现业务部署以及生成式 AI 创新应用。在制造业和云计算领域有多年实践经验,目前专注生成式 AI 在 SAP 及相关行业领域的解决方案。


AWS 架构师中心:云端创新的引领者

探索 AWS 架构师中心,获取经实战验证的最佳实践与架构指南,助您高效构建安全、可靠的云上应用