/** * `SpanFactory` 的 **Mastra 实现** —— 用 Mastra 的 Observability 组件产 span * (`@mastra` 只准 adapter 与 app,§6.2)。 * * 我们的执行不经 Mastra 图,所以它的自动 tracing 不会产出任何东西; * 但 span 的**形状与 id 体系**(traceId / spanId / 父子链)照用它的 —— * 将来真接了 Mastra workflow,这些 span 与它自己产的能拼进同一棵树。 * * 这里只要 id 与父子关系:落库、耗时、入出参由 trace 模块自己管 * (它要「一开就落」,而 Mastra 的 exporter 是缓冲 + 定时冲刷的批量路)。 */ import { DefaultObservabilityInstance } from '@mastra/observability'; import type { SpanFactory } from '@modules/trace'; /** * Mastra span 对象上我们要用的那几个字段。 * Mastra 的 `Span` 泛型极深且随版本变;这里只声明**我们真正用到的**面, * 由 `instance` 的窄化包装一次性对上 —— 断言收在 `spanApi` 这一处。 */ type MastraSpan = { id: string; traceId: string; createChildSpan(options: { type: string; name: string }): MastraSpan; }; /** Mastra 实例的窄化面:类型桥收在这一处,别撒到调用点上 */ type SpanApi = { startSpan(options: { type: string; name: string }): MastraSpan }; export function createMastraSpanFactory(serviceName = 'jcai'): SpanFactory { const instance: SpanApi = new DefaultObservabilityInstance({ config: { serviceName, sampling: { type: 'always' }, exporters: [] }, } as never) as SpanApi; /* * Mastra 的 span 对象要留着 —— 子 span 得从父对象上开(`createChildSpan`), * 光有 id 拼不出父子链。这张表按 spanId 存活着的 span; * 观测是**尽力而为**的:表里查不到父亲(进程重启、span 太老被清)就另起一棵树, * 不抛错、不让业务停摆。 */ const live = new Map(); const remember = (span: MastraSpan): { spanId: string; traceId: string } => { live.set(span.id, span); // 只留最近的一批:测试台一次跑一条流,几十个 span 足够;不设上限会随长跑泄漏 if (live.size > 2000) { const oldest = live.keys().next().value; if (oldest !== undefined) live.delete(oldest); } return { spanId: span.id, traceId: span.traceId }; }; return { root(name) { return remember(instance.startSpan({ type: 'generic', name })); }, child(parent, name) { const held = live.get(parent.spanId); if (held === undefined) { // 父亲不在手上:另起一棵树,别把整条链丢掉(观测尽力而为) return remember(instance.startSpan({ type: 'generic', name })); } return remember(held.createChildSpan({ type: 'generic', name })); }, }; }