Mastra OpenTelemetry Bridge(@mastra/otel-bridge)完整指南:双向打通 Mastra 与 OTEL 可观测性
2026/9/15 1:58:47 网站建设 项目流程

Mastra OpenTelemetry Bridge(@mastra/otel-bridge)完整指南:双向打通 Mastra 与 OTEL 可观测性

【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

@mastra/otel-bridge是 Mastra 可观测性体系中的 OpenTelemetry 桥接层,它实现 Mastra 与 OTEL 基础设施的双向集成:既能从 OTEL 的活跃 span 上下文(AsyncLocalStorage)读取 trace ID 与父 span ID,也能为 Mastra 的每个 span 创建真实的 OTEL span,从而维护正确的 trace 层级。读完本文,你将掌握如何把 Mastra Agent 的追踪数据无缝汇入 OTEL 生态(如 Jaeger、OTLP Collector、云厂商 APM),让 Agent、工具调用、工作流步骤与数据库/HTTP 客户端调用落在同一条分布式 trace 里,并理解桥接背后的 span 生命周期与上下文传播原理。

一、为什么需要 OTEL Bridge:双向集成的动机

Mastra 自带一套完善的 observability 模型(span/trace/log/metric/score/feedback),但它内部的 span 并不是标准 OTEL span。如果你已经拥有 OTEL 基础设施(自动注入 HTTP/DB 埋点的 SDK、OTLP Collector、统一 trace 后端),会出现两个问题:

  1. Mastra 的 span 进不了 OTEL 生态:Mastra 产生的 trace 数据与 OTEL 采集的 trace 各自独立,无法在同一个 trace 视图里聚合。
  2. 上下文断裂:Mastra 运行 Agent 时调用的外部代码(数据库驱动、HTTP 客户端)如果被 OTEL 自动埋点,这些 span 无法正确挂到 Mastra span 之下,导致 trace 树断裂。

OtelBridge正是为解决这两个问题而设计。从源码注释(bridge.ts)和包入口(index.ts)可以看到它的双向职责:

  • OTEL → Mastra:自动读取 OTEL 环境上下文(AsyncLocalStorage),继承活跃 OTEL span 的 trace ID 与父 span ID,必要时从请求头提取 W3C trace 上下文;
  • Mastra → OTEL:为每个 Mastra span 创建真实的 OTEL span,维护父子关系,并让 OTEL 自动埋点的代码(HTTP、DB)正确嵌套在 Mastra span 之下。

桥的名称被定义为otel(见 bridge.ts 中name = 'otel',测试 bridge.test.ts 也对此做了断言),在 observability 配置中通过bridge字段挂载。

二、安装与前置依赖

2.1 安装

npm install @mastra/otel-bridge

根据 package.json,该包以 ESM 为主("type": "module"),同时通过exports同时提供importdist/index.js)与requiredist/index.cjs)两种入口,Node.js 版本要求>=22.13.0

2.2 依赖与 peer 依赖

包内部依赖:

  • @mastra/observability(workspace 依赖,提供BaseExportergetExternalParentId);
  • @mastra/otel-exporter(workspace 依赖,提供SpanConverterconvertLoggetSpanKind等转换工具);
  • @opentelemetry/api(^1.9.1)与@opentelemetry/api-logs(^0.221.0)。

peer 依赖(均为可选,见peerDependenciesMeta):

  • @mastra/core>=1.16.0-0 <2.0.0-0
  • @opentelemetry/auto-instrumentations-node>=0.50.0(可选);
  • @opentelemetry/sdk-node>=0.50.0(可选)。

这里需要特别强调一个关键前提(源码与测试都反复验证):OtelBridge只有在 OTEL SDK(如@opentelemetry/sdk-node@opentelemetry/sdk-logs)已注册全局 TracerProvider / LoggerProvider 时才能产出有效 span。若未注册,OTEL API 会回退到 no-op tracer,span 的 ID 全是零值(0000000000000000/00...00),桥会检测到无效 span context 并返回undefined(见下文"错误处理"),让 Mastra 核心用自己的 ID 生成器兜底。因此生产使用请务必同时安装并初始化 OTEL SDK。

三、最小接入:把 OtelBridge 挂到 Mastra

OtelBridge通过Mastra构造函数的observability.configs配置注入,bridge.ts 与包 README 中的示例一致:

import { OtelBridge } from '@mastra/otel-bridge'; import { Mastra } from '@mastra/core'; import { Observability } from '@mastra/observability'; const mastra = new Mastra({ agents: { myAgent }, observability: new Observability({ configs: { default: { serviceName: 'my-service', bridge: new OtelBridge(), }, }, }), });

要点说明:

  • serviceName:观测实例的服务名,会被透传给桥,用于 OTEL span 的资源属性与SpanConverter的格式化(见 bridge.ts 的init方法);
  • bridge:传入OtelBridge实例即可,同一实例可被多个配置共享;
  • OtelBridge实现了ObservabilityBridge接口(定义于 packages/core/src/observability/types/core.ts),该接口要求实现namecreateSpanflushshutdown,并可选择性实现executeInContext/executeInContextSync/releaseSpan等。

3.1 可选配置:自定义 Tracer/Logger Provider

OtelBridge的构造函数接受OtelBridgeConfig(bridge.ts):

type OtelBridgeConfig = BaseExporterConfig & { tracerProvider?: TracerProvider; // 默认取全局 otelTrace.getTracerProvider() loggerProvider?: LoggerProvider; // 默认取全局 otelLogs.getLoggerProvider() };

默认情况下,桥使用全局注册的 provider。当你不希望触碰全局 provider(例如多租户场景,或测试隔离)时,可以显式传入自定义 provider:

import { BasicTracerProvider } from '@opentelemetry/sdk-trace-node'; import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-logs'; const bridge = new OtelBridge({ tracerProvider: customTracerProvider, loggerProvider: customLoggerProvider, });

测试 bridge.test.ts 验证了自定义 provider 的行为:自定义 provider 创建 span / 发射日志时不会触碰全局 provider,且flush()只会冲刷传入的自定义 provider 而不是全局的。

桥内部会为 provider 注册两个命名 instrument:

  • tracer 名称@mastra/otel-bridge,版本1.0.0
  • logger 名称@mastra/otel-bridge,版本1.0.0

(见 bridge.ts。)

四、核心机制一:createSpan —— 为 Mastra span 创建真实 OTEL span

createSpan是桥最关键的方法,在 Mastra 创建 span 时被调用,用于获取桥生成的 ID(bridge.ts)。它完成以下步骤:

4.1 确定父上下文(Parent Context)

父上下文的解析优先级如下:

  1. 活跃环境上下文:默认取otelContext.active()(即 AsyncLocalStorage 中的当前 OTEL 上下文);
  2. 外部父 span:通过getExternalParentId(options)沿链向上查找非内部父 span(该函数实现在 observability/mastra/src/spans/base.ts),若命中桥的otelSpanMap中记录的 span,则以其存储的 OTEL context 作为父上下文;
  3. 持久化恢复的 trace:若 span 带有traceId+parentSpanId(例如工作流 suspend/resume 后从持久化快照恢复),且父 OTEL span 已不在 map 中(父 span 可能在另一个进程中早已结束),则用持久化 ID 构造一个isRemote: true的 span context 作为父级,从而延续原 trace而不是开启一条新 trace。该逻辑配合TraceFlags.SAMPLED且会用isSpanContextValid校验 ID 合法性,杜绝注入畸形 ID 造成垃圾 trace link。

相关回归测试:针对 issue #20771(工作流恢复后应延续持久化 trace),测试 bridge.test.ts 覆盖了"父 span 已死时延续持久化 trace""存在活父 span 时优先用活父""畸形持久化 ID 回退到新 trace"三个场景。

4.2 创建 OTEL span 并确定 SpanKind

const otelSpan = this.otelTracer.startSpan( options.name, { kind: getSpanKind(options.type), // SpanKind 在创建时必须确定,不可更改 ...(options.startTime ? { startTime: options.startTime } : {}), }, parentOtelContext, );

getSpanKind来自@mastra/otel-exporter(导出见 observability/otel-exporter/src/index.ts),它将 Mastra 的SpanType(如AGENT_RUNWORKFLOW_RUNTOOL_CALLLLM)映射为 OTEL 的SpanKind(如INTERNALSERVERCLIENT等)。由于 SpanKind 是创建时不可变属性,必须在startSpan时确定。startTime若存在也会一并传入,保证时间线对齐。

4.3 无效 span context 的兜底

创建 span 后,桥会检查otelSpan.spanContext()是否有效:

  • 若无效(说明没有注册 OTEL SDK,全局 tracer 回退到 no-op),桥会结束刚创建的 span并返回undefined,让 Mastra 核心回退到自己的 ID 生成器(bridge.ts);
  • 这是针对 issue #15589 的回归修复:此前桥会把全零 ID 返回给核心,导致所有 span 共享同一 ID,破坏 TrackingExporter 的父子匹配队列并引发 CPU 空转。对应测试见 bridge.test.ts("when no OTEL SDK is registered" 分组)。

4.4 返回的 SpanIds 结构

return { spanId, // OTEL span ID,16 位十六进制 traceId, // OTEL trace ID,32 位十六进制 ...(parentIsMastraSpan ? { parentSpanId } : { externalParentSpanId: parentSpanId }), };

返回值区分两类父级:

  • parentIsMastraSpan为 true:父 span 也是 Mastra span(在otelSpanMap中或来自持久化恢复),返回parentSpanId,说明父子都在 Mastra trace 内;
  • 否则:父 span 属于外部 OTEL 系统(如自动埋点产生的 ambient span),返回externalParentSpanId,表明这是一个桥接的根 span。

测试 bridge.test.ts 验证了四种父子分类:恢复的 Mastra 父级不会被误判为外部、ambient 父级会被正确报告为 external、以及通过executeInContext创建的嵌套 Mastra span 其父级仍被识别为内部。

4.5 Span 映射表与生命周期

桥内部维护otelSpanMap: Map<string, { otelSpan, otelContext }>(bridge.ts),以 Mastra span ID 为键记录对应 OTEL span 及其激活上下文。span 结束时:

  • handleSpanEnded先从 map 中删除条目(防止内存泄漏),再用SpanConverter将 Mastra span 转换为符合 GenAI 语义约定的 OTEL ReadableSpan,回写属性、状态、异常事件后以真实 end time 结束 OTEL span(bridge.ts);
  • 若 span 被导出过滤(excludeSpanTypesspanFilter、span output processor)丢弃,则调用releaseSpan仅删除 map 条目而不结束底层 OTEL span,避免导出用户已过滤掉的数据(bridge.ts)。

测试 bridge.test.ts 的 "span map cleanup" 分组验证了四种情况下 map 都能清空为 0:正常导出、被excludeSpanTypes丢弃、被spanFilter丢弃、被 output processor 丢弃。

五、核心机制二:executeInContext —— 让自动埋点代码挂到正确父级

这是桥实现双向集成的另一个关键能力,对应接口定义在 packages/core/src/observability/types/core.ts。executeInContext/executeInContextSync都委托给executeWithSpanContext(bridge.ts):

private executeWithSpanContext<T>(spanId: string, fn: () => T): T { const entry = this.otelSpanMap.get(spanId); const spanContext = entry?.otelContext; if (spanContext) { return otelContext.with(spanContext, fn); // 在 OTEL context 中执行 fn } return fn(); // span 不存在时直接执行 }

原理:otelContext.with(spanContext, fn)将 OTEL 的 context(AsyncLocalStorage)设为指定的 span context,再执行fn。这样 fn 内部任何被 OTEL 自动埋点的操作(HTTP 客户端、数据库驱动)都会以该 Mastra span 为父级创建 span,保证 trace 树完整。

  • executeInContext支持异步函数(返回Promise<T>);
  • executeInContextSync支持同步函数;
  • 当 span 不存在时两者都退化为直接执行fn(),不会报错(对应测试 bridge.test.ts 与 L456-L469)。

六、日志桥接:onLogEvent 与 trace 关联

OtelBridge同时实现了日志桥接,将 Mastra 的日志事件转发到全局(或自定义)OTEL LoggerProvider(bridge.ts):

async onLogEvent(event: LogEvent): Promise<void> { if (this.isDisabled) return; const params = convertLog(event.log); // 复用 @mastra/otel-exporter 的日志转换器 const attributes = { ...params.attributes }; if (params.traceId) attributes['mastra.traceId'] = params.traceId; if (params.spanId) attributes['mastra.spanId'] = params.spanId; const logContext = this.resolveLogContext(params.traceId, params.spanId); this.otelLogger.emit({ timestamp, severityNumber, severityText, body, attributes, context: logContext }); }

日志的 trace 关联遵循三级回退(resolveLogContext,bridge.ts):

  1. 优先使用 map 中存储的 OTEL context:若日志携带的 spanId 对应一个活跃的 Mastra span,则在该 span 的 context 下发射日志,日志会嵌套在 trace 中对应 span 之下;
  2. 用原始 ID 构造 SpanContext:span 不在本地 map(如跨进程、孤儿子日志)时,若 traceId+spanId 能通过isSpanContextValid校验,则构造TraceFlags.SAMPLED的 span context 让后端仍能按 ID 关联;
  3. 回退到当前活跃 context

同时,日志会附带mastra.traceIdmastra.spanId属性(JSON 兼容,见测试 bridge.test.ts)。日志级别会映射为 OTEL 的SeverityNumber/SeverityText(例如warnSeverityNumber.WARN/'WARN',见测试 L595-L606)。

优雅降级:如果用户没有注册 LoggerProvider,api-logs会返回 no-op logger,emit()是静默空操作,桥不会抛错(bridge.ts 与测试 bridge.test.ts)。

七、span 属性与 GenAI 语义约定

span 结束时,桥通过SpanConverter(来自@mastra/otel-exporter)统一格式化 span(bridge.ts):

  • 初始化时指定format: 'GenAI_v1_38_0',即采用 GenAI 语义约定格式(bridge.ts);
  • span 名称会更新为 converter 格式化后的名称(otelSpan.updateName(readableSpan.name));
  • 所有标量属性(非对象、非 null/undefined)写入 OTEL span 属性,包括 OTEL 语义约定属性;
  • 状态、异常事件(recordException)也会回写,异常消息取自exception.message属性。

例如根 span 的标签(tags)会被序列化为mastra.tags属性(JSON 字符串),而子 span 不会携带该属性,空数组也不会出现(测试 bridge.test.ts)。这些转换逻辑的具体实现在 observability/otel-exporter/src/span-converter.ts 与 observability/otel-exporter/src/gen-ai-semantics.ts,如果你想了解某个属性从 Mastra span 到 OTEL span 的具体映射规则,可以继续深入这两个文件。

八、flush 与 shutdown:生命周期管理

8.1 flush()

flush()用于在不关闭桥的前提下冲刷缓冲的 span/日志(bridge.ts),非常适合 Serverless 场景:在运行时实例被终止前确保所有 span 已导出,同时保持桥可用于后续请求。

async flush(): Promise<void> { await this.flushProvider(this.tracerProvider, 'tracer'); await this.flushProvider(this.loggerProvider, 'logger'); }

flushProvider会检查 provider 是否实现了forceFlush方法,实现了才调用;否则仅记录 debug 日志,不会抛错。

8.2 shutdown()

shutdown()(bridge.ts)按顺序执行:

  1. flush()冲刷所有待导出数据;
  2. 遍历otelSpanMap,强制结束所有未正常关闭的 span(记录 warn 日志);
  3. 清空 span map。

九、错误处理与健壮性设计

桥的错误处理贯穿所有关键路径,可总结为以下几点:

场景行为依据
未注册 OTEL SDK,span context 无效createSpan结束 span 并返回undefined,核心回退到自研 ID 生成bridge.ts,测试 L300-L387
createSpan内部异常catch 后记录错误日志并返回undefinedbridge.ts,测试 L208-L217(传null触发)
span 结束事件找不到对应 OTEL spanwarn 日志后返回,不阻塞bridge.ts
日志发射异常catch 后记录[OtelBridge] Failed to emit log错误bridge.ts
无 LoggerProviderno-op logger,静默空操作bridge.ts
flush时 provider 不支持forceFlushdebug 日志,不抛错bridge.ts
畸形持久化 trace ID忽略并回退到当前活跃上下文 / 新 trace测试 bridge.test.ts

十、实战验证与深入路径

  • 单元测试:observability/otel-bridge/src/bridge.test.ts 覆盖了 span 创建、ID 格式(16 位 spanId / 32 位 traceId)、父子分类、持久化恢复、无 SDK 回退、日志关联、自定义 provider、span map 清理等全部核心行为。运行方式:

    # 在仓库 observability/otel-bridge 目录下 pnpm test # vitest run pnpm test:watch # 监听模式
  • 完整集成测试:单元测试文件头部注释指出,带真实 OTEL 基础设施的集成测试位于observability/_examples/agent-hub/src/integration.test.ts,想要验证桥与真实 SDK 协同工作的读者可以前往该目录查看。

  • 进一步阅读源码

    • 桥的实现:observability/otel-bridge/src/bridge.ts;
    • 桥的接口契约:packages/core/src/observability/types/core.ts;
    • span 转换器(GenAI 语义格式化):observability/otel-exporter/src/span-converter.ts;
    • 日志转换器:observability/otel-exporter/src/log-converter.ts;
    • 父级解析工具getExternalParentId:observability/mastra/src/spans/base.ts;
    • 版本历史:observability/otel-bridge/CHANGELOG.md。

小结

@mastra/otel-bridge是 Mastra 与 OpenTelemetry 生态之间的关键桥梁:createSpan保证 Mastra 每次 span 创建都能映射为真实 OTEL span 并正确继承父上下文,executeInContext让自动埋点的外部调用嵌套到正确父级,onLogEvent让日志与 trace 关联,flush/shutdown保证数据可靠导出。理解 span map 的生命周期与无效 context 的兜底策略,是正确使用桥的前提——只要记住"使用前先注册 OTEL SDK,必要时显式传入自定义 provider",就能把 Mastra Agent 的完整执行链路平滑接入你现有的 OTEL 可观测平台。

【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询