
AgentOps OpenTelemetry 实现详解上下文传播、时间戳处理与语义属性规范【免费下载链接】agentopsPython SDK for AI agent monitoring, LLM cost tracking, benchmarking, and more. Integrates with most LLMs and agent frameworks including CrewAI, Agno, OpenAI Agents SDK, Langchain, Autogen, AG2, and CamelAI项目地址: https://gitcode.com/GitHub_Trending/ag/agentops本文基于 AgentOps 仓库中的 OpenTelemetry 实现笔记系统讲解 AgentOps 各 LLM 与 Agent 框架探针instrumentation背后的 OpenTelemetry 核心实践如何在跨执行上下文时维护 span 的父子关系、如何避免命名冲突与内存泄漏、为何不应手动设置时间戳以及如何通过agentops/semconv语义约定规范属性写入。读完本文你将能够在 AgentOps 的探针代码如 OpenAI、CrewAI、OpenAI Agents SDK 等中准确定位上下文传播链路并理解其 span 生命周期管理的底层设计。一、为什么上下文传播Context Propagation是探针设计的核心AgentOps 的探针模块位于agentops/instrumentation/下覆盖了两类目标LLM 提供商providers/下的 openai、anthropic、google_genai 等和 Agent 框架agentic/下的 crewai、agno、langgraph、openai_agents 等。所有这些探针都遵循同一套 OpenTelemetry 最佳实践其核心是上下文传播OpenTelemetry 依赖正确的 context 传播来维护 span 之间的父子关系这是以下三件事的基础在可视化中生成准确的 trace 瀑布图waterfall确保同一逻辑操作产生的所有 span 共享同一个 trace ID支持对相关操作进行正确的查询与过滤。从源码结构看AgentOps 将这套通用能力沉淀在 agentops/instrumentation/common/ 公共模块中各框架探针复用同一套 span 管理、属性处理与包装器机制。二、核心模式跨执行上下文维护 span 关系当探针需要跨越不同的执行上下文例如 SDK 回调、异步任务、事件流维护 span 关系时文档给出了四个核心模式。2.1 用弱引用字典保存 span 上下文探针内部常用一个字典缓存对象 → span 上下文的映射以便后续创建子 span 时显式指定父级。为避免内存泄漏必须使用weakref.WeakKeyDictionary()# Use weakref dictionaries to avoid memory leaks self._span_contexts weakref.WeakKeyDictionary() self._trace_root_contexts weakref.WeakKeyDictionary()弱引用字典以对象的弱引用作为键当业务对象如 SDK 的 trace/task 对象被垃圾回收时对应的 span 上下文条目会自动失效不需要额外的清理逻辑。2.2 用显式父上下文创建 span不要依赖隐式的当前上下文创建子 span 时应显式传入父 context并在 span 内部把新 span 的上下文存回字典parent_context self._get_parent_context(trace_obj) with trace.start_as_current_span( namespan_name, contextparent_context, kindtrace.SpanKind.CLIENT, attributesattributes, ) as span: # Span operations here # Store the spans context for future reference context trace.set_span_in_context(span) self._span_contexts[span_obj] context这一模式在 AgentOps 公共模块中有直接的对应实现。span_management.py 中的extract_parent_context函数封装了同一逻辑给定父 span 则用set_span_in_context(parent_span)构造父上下文否则回退到context_api.get_current()而create_span上下文管理器span_management.py#L44-L76则统一了 span 创建、通用属性写入与错误状态设置成功置StatusCode.OK异常则record_exception并置ERROR。2.3 实现父上下文解析的辅助方法父上下文的解析通常遵循先查缓存、再回退当前上下文的优先级def _get_parent_context(self, trace_obj): # Try to get the traces root context if it exists if trace_obj in self._trace_root_contexts: return self._trace_root_contexts[trace_obj] # Otherwise, use the current context return context_api.context.get_current()在 OpenAI Agents SDK 探针中这一思路被进一步细化为三级查找。exporter.py 中的OpenAIAgentsExporter._get_parent_context依次执行若事件携带显式parent_id用span:{trace_id}:{parent_id}作为键在_span_map中查找父 span并取其get_span_context()若无显式父 ID则尝试把该 trace 的根 span键为span:{trace_id}:{trace_id}作为父级都找不到时回退到context_api.get_current()中当前活跃的 span context。找到父 context 后_create_span_with_parentexporter.py#L249-L277通过trace_api.use_span(NonRecordingSpan(parent_ctx), end_on_exitFalse)临时把父上下文设为当前再调用tracer.start_span从而保证子 span 挂在正确的父级之下——这正是文档中总是显式提供父上下文原则的落地方式。2.4 调试 trace 连续性排查span 断链问题时先打印当前 span 的 trace IDcurrent_span trace.get_current_span() span_context current_span.get_span_context() trace_id format_trace_id(span_context.trace_id) logging.debug(fCurrent span trace ID: {trace_id})AgentOps 仓库提供了两处可直接复用的实现span_management.py 的get_span_context_info返回(trace_id, span_id)十六进制字符串trace ID 格式化为 32 位、span ID 格式化为 16 位未取到时返回unknownexporter.py 的log_otel_trace_id把当前 span 的 OTel trace ID 以 32 位 hex 写入 debug 日志[SPAN] Export | Type: ... | TRACE ID: ...。该函数的注释特别说明这个十六进制 OTel trace ID 与 Agents SDK 自身的trace_id不同它是查询后端数据库、关联本地调试日志与服务端 trace 数据的主键。三、常见陷阱Common Pitfalls文档列出了四个典型陷阱结合仓库源码逐一说明。3.1 命名冲突不要用trace作参数名OpenTelemetry 的trace模块from opentelemetry import trace是探针代码的高频依赖。若回调方法又用trace作参数名会遮蔽模块引用# Bad def on_trace_start(self, trace): # This will cause conflicts with the imported trace module # Good def on_trace_start(self, trace_obj): # No conflicts with OpenTelemetrys trace moduleexporter.py 顶部就展示了规范写法from opentelemetry import trace, context as context_api同时把context模块起别名为context_api把 SDK 侧对象命名为trace_obj/trace_id等全程不污染trace这个名字。3.2 缺失父上下文在父级可知的场景下必须显式提供父上下文而不能只依赖当前上下文自动继承——异步回调、事件处理器中当前上下文往往是空的隐式继承会导致 span 落入新 trace。3.3 内存泄漏用于跨回调保存 span 的字典必须使用weakref.WeakKeyDictionary()或以span_id这类字符串为键并在结束时主动pop让对象可以被垃圾回收。两种清理策略在仓库中都有体现弱引用字典文档 2.1 节给出的模式显式键控字典 生命周期清理OpenAIAgentsExporter使用_span_map/_active_spans两个字符串键字典在 start/end 事件处理完成后pop掉对应条目exporter.py#L384-L385并在cleanup()中于关闭时清空全部跟踪字典以防泄漏exporter.py#L462-L469。3.4 异步/回调中的上下文丢失调用异步函数或回调时必须显式保存并传递 context。AgentOps 的公共包装器对此有专门处理wrappers.py 中的awrapper针对WrapConfig.is_asyncTrue的方法返回 async 包装函数在async with tracer.start_as_current_span(...)内await wrapped(*args, **kwargs)保证异步方法内部产生的 span 仍挂在调用方上下文之下。值得注意的是WrapConfigwrappers.py#L26-L55的注释明确指出这里显式声明is_async是因为asyncio.iscoroutinefunction在该场景下不可靠。另外包装器在入口处检查_SUPPRESS_INSTRUMENTATION_KEYwrappers.py#L111-L112若当前上下文抑制了 instrumentation则直接透传原始方法调用避免嵌套探针造成重复 span——这也是context 随调用链显式流动原则的一个应用。四、公共包装机制WrapConfig 与 span 生命周期上文多次提到的WrapConfig是 AgentOps 所有提供商探针的统一入口其 READMEcommon/README.md给出的完整用法如下from agentops.instrumentation.common.wrappers import WrapConfig from opentelemetry.trace import SpanKind config WrapConfig( trace_namellm.completion, # Name that will appear in trace spans packageopenai.resources, # Path to the module containing the class class_nameCompletions, # Name of the class containing the method method_namecreate, # Name of the method to wrap handlermy_attribute_handler, # Function that extracts attributes span_kindSpanKind.CLIENT # Type of span to create )handler是AttributeHandler签名args, kwargs, return_value → AttributeMap在方法执行前提取输入属性、执行后提取输出属性出错时把已有信息一并写入 span。wrap(config, tracer)基于wrapt.wrap_function_wrapper应用包装unwrap(config)可逆向移除wrappers.py#L178-L212instrumentor.py 的CommonInstrumentor基类在_uninstrument中批量调用unwrap并清理 tracer/meter 引用保证卸载探针时不留残余状态。对于SDK 发送分离的 start/end 事件这类场景如 OpenAI Agents SDKspan 生命周期管理规则记录在 openai_agents/README.md 中start 事件只创建 span 而不结束它、状态保持 UNSET表示进行中引用存入跟踪字典end 事件按 ID 查找已有 span补全最终属性与状态后手动span.end()若 end 事件找不到对应 span则创建并立即结束一个完整 span。这样做的原因包括最终数据输出、token 用量只在 end 事件可用需要单一对应任务生命周期的 span 才能得到准确耗时且要避免同一任务产生重复 span。五、测试上下文传播的方法验证上下文传播是否正确的标准步骤开启 trace ID 的 debug 日志运行一个会生成多个 span 的简单端到端测试校验所有 span 共享同一个 trace ID检查父子关系是否正确建立。配套的调试日志示例logging.debug(fSpan {span.name} has trace ID: {format_trace_id(span.get_span_context().trace_id)})仓库中与该流程对应的实现与测试资产包括调试工具get_span_context_info前文 2.4 节端到端用例tests/integration/test_llm_providers.py、tests/integration/test_session_concurrency.py 等集成测试会驱动多 provider 的多 span 场景探针单测tests/unit/instrumentation/ 下按 provider/框架组织的测试openai_agents 探针 README 还特别提醒测试断言属性时应使用MessageAttributes常量逐一校验具体 content/tool 属性而不是去检查根级聚合属性如SpanAttributes.LLM_COMPLETIONS是否存在。六、时间戳处理Timestamp Handling关于 span 时间戳文档明确了五条原则自动时间戳跟踪span 的起止时间由 OpenTelemetry 自动管理——tracer.start_span()/tracer.start_as_current_span()创建时自动记录开始时间span.end()被调用时记录结束时间无需手动设置时间戳属性标准探针模式不需要在 span 上手动写入 timestamp 属性这由 SpanProcessor 和 Exporter 在内部完成时间戳表示在 OpenTelemetry 数据模型中时间戳存储为自 Unix 纪元1970-01-01以来的纳秒序列化职责在 Exporter从 OTel span 到 JSON 等输出格式的时间戳序列化由 Exporter 组件负责。如果输出 API 中时间戳异常例如start_time字段为空问题大概率出在 API 导出/序列化层而不是 span 创建代码调试思路排查时间戳问题时应验证 span 是否被正确 start/end而不是手动补写时间戳属性。推荐模式# Good pattern - timestamps handled by OpenTelemetry automatically with tracer.start_as_current_span(my_operation) as span: # Do work pass # span.end() is called automaticallyAgentOps 仓库中的实践与此完全一致span_management.py 的create_span上下文管理器把 start/end 完全交给start_as_current_spantimed_span装饰器span_management.py#L79-L102如需耗时也只是调用方额外的time.time()差值回调而不写入 span 属性common/README.md 的 attribute handler 示例同样只提取业务属性不触碰时间字段。流式场景则由 StreamingSpanManager 按stream_id管理 span 的显式start/end结束前统一set_status(StatusCode.OK)。七、Span 属性规范Attributes属性写入是探针与后端ClickHouse 存储、dashboard 展示之间的契约文档给出四条规则根attributes节点应保持为空API 输出 JSON 中根级的attributes对象始终为空是设计使然所有属性写入span_attributes用户自定义属性与语义属性数据都应存放在span_attributes对象中形成结构化的层级表示不要修复空对象根attributes为空是正常的不要直接向其填值也不要把span_attributes的数据复制进去一律使用语义约定命名属性名必须来自agentops/semconv模块定义的常量。from agentops.semconv import agent # Good pattern - using semantic conventions span.set_attribute(agent.AGENT_NAME, My Agent)7.1 semconv 模块的实际结构agentops/semconv/ 按领域拆分属性常量semconv/init.py 统一导出CoreAttributes、AgentAttributes、ToolAttributes、WorkflowAttributes、SpanAttributes、MessageAttributes、Meters、ResourceAttributes等并额外定义了SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY抑制探针的 context key。其中span_attributes.py 定义了SpanAttributes对齐 OpenTelemetry GenAI 语义约定gen_ai.system、gen_ai.request.model、gen_ai.usage.prompt_tokens、流式专属的gen_ai.streaming.time_to_first_token等特别地LLM_COMPLETIONS gen_ai.completion一行注释写着DO NOT SET DIRECTLYspan_attributes.py#L51与根聚合属性保持为空、由后端从明细属性推导的原则互相印证agent.py 定义了AgentAttributesagent.id、agent.name、agent.role、agent.tools等其中from_agent/to_agent两个属性在注释中明确标注为对 OTel GenAI 约定的有意偏离。探针侧的属性提取统一走 common/attributes.py 中的AttributeMap目标属性键 → 源字段名的映射字典与IndexedAttributeMap支持i/j双下标的索引化属性如gen_ai.request.tools.0.id各 provider 探针只需提供映射即可获得跨框架一致的属性命名。写属性时还建议经过 safe_set_attribute跳过None值、把超长字符串截断到max_length默认 1000末尾加...并吞掉set_attribute的异常防止探针故障影响业务调用。八、实践小结把文档与源码对照后可以提炼出 AgentOps 探针的 OpenTelemetry 工程守则关注点规则仓库依据父子关系显式传父上下文缓存用弱引用字典OpenTelemetry.md、exporter.py命名参数名避开tracecontext 模块别名context_apiexporter.py#L20-L25时间戳交给start_as_current_span/end()纳秒表示序列化归 Exporterspan_management.py属性只写span_attributes键名来自 semconv 常量禁用根级聚合属性span_attributes.py#L51、common/attributes.py生命周期start 事件不结束 spanend 事件查找-更新-结束结束时清理跟踪字典openai_agents/README.md可调试性打印 32 位 hex trace ID 关联后端exporter.py#L42-L69以上模式全部沉淀在 agentops/instrumentation/common/ 公共模块与 agentops/semconv/ 语义约定模块中阅读任何具体 provider 探针providers/openai/、providers/anthropic/、agentic/crewai/等之前先理解这一层能显著降低对整套探针体系的认知成本。【免费下载链接】agentopsPython SDK for AI agent monitoring, LLM cost tracking, benchmarking, and more. Integrates with most LLMs and agent frameworks including CrewAI, Agno, OpenAI Agents SDK, Langchain, Autogen, AG2, and CamelAI项目地址: https://gitcode.com/GitHub_Trending/ag/agentops创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考