/** * trace 面(span 侧)—— `startSpan → Span → CompletedSpan` 与默认结构化日志 sink。 * * 属性对齐 OpenTelemetry `gen_ai.*` 语义约定:`@drama/llm` base.js 传入的中性键 * (`provider`/`model`/usage 分量)在本包唯一一处(`mapAttrKey`/`mapEventAttrs`) * 映射为 gen_ai 语义键;下游两个 sink 消费同一 `CompletedSpan`,不再判类型。 * * 边界纪律:时钟 `now`、`logger`、`sink` 全部注入;包内不读 env、不建连接、不写盘。 */ /** * @typedef {import('./logger.js').Logger} Logger */ /** * @typedef {object} SpanEvent * @property {string} name * @property {Record} attributes */ /** * `end()` 写给 sink 的完结产物(两个 sink 消费的唯一结构)。 * @typedef {object} CompletedSpan * @property {string} name * @property {number} durationMs end 时刻 − startSpan 时刻。 * @property {number} startUnixNano * @property {number} endUnixNano * @property {Record} attributes 已映射为 gen_ai.* 语义键。 * @property {Array} events */ /** * 上报目标端口 —— 唯一方法 `write`。新增上报目标 = 新增一个 sink 工厂,不改本文件。 * @typedef {object} Sink * @property {(span: CompletedSpan) => void} write */ /** * @typedef {object} Span * @property {(k: string, v: any) => void} setAttr * @property {(n: string, d?: object) => void} addEvent * @property {() => void} end */ /** `setAttr` 键 → gen_ai 语义属性键。 */ const ATTR_KEY_MAP = { provider: 'gen_ai.system', model: 'gen_ai.request.model', } /** `usage` 事件属性键 → gen_ai 语义属性键。 */ const USAGE_KEY_MAP = { prompt_tokens: 'gen_ai.usage.input_tokens', completion_tokens: 'gen_ai.usage.output_tokens', total_tokens: 'gen_ai.usage.total_tokens', cached_tokens: 'gen_ai.usage.cached_tokens', cache_write_tokens: 'gen_ai.usage.cache_write_tokens', reasoning_tokens: 'gen_ai.usage.reasoning_tokens', } /** * setAttr 键映射:命中语义表返回 gen_ai 键,未命中原样返回。 * @param {string} k * @returns {string} */ function mapAttrKey(k) { return ATTR_KEY_MAP[k] ?? k } /** * 事件属性键映射:仅 `usage` 事件按 `USAGE_KEY_MAP` 重命名(只映射表内出现的键), * 其余事件(`content`/`error`/`aborted`)属性原样透传。 * @param {string} name * @param {Record} data * @returns {Record} */ function mapEventAttrs(name, data) { if (name !== 'usage') return data /** @type {Record} */ const out = {} for (const [k, val] of Object.entries(data)) { out[USAGE_KEY_MAP[k] ?? k] = val } return out } /** * 开一个 span。返回值恒为 `{ setAttr, addEvent, end }` 三方法对象。 * `end()` 恰调一次 `sink.write(CompletedSpan)`。 * * @param {string} name * @param {object} deps * @param {Sink} deps.sink 必注入;缺 tracePort 的降级由 @drama/llm noopTracePort 承担。 * @param {() => number} [deps.now=Date.now] 注入时钟(毫秒),测试可推进。 * @returns {Span} */ export function startSpan(name, { sink, now = Date.now }) { const startMs = now() /** @type {Record} */ const attributes = {} /** @type {Array} */ const events = [] return { setAttr(k, v) { attributes[mapAttrKey(k)] = v }, addEvent(n, d) { events.push({ name: n, attributes: mapEventAttrs(n, d ?? {}) }) }, end() { const endMs = now() sink.write({ name, durationMs: endMs - startMs, startUnixNano: startMs * 1e6, endUnixNano: endMs * 1e6, attributes, events, }) }, } } /** * 默认 sink:把一个完结 span 写成 `createLogger` 的**一条**结构化 JSON 行。 * `logger` 为 `createLogger(...)` 实例(注入,本包不 new)。 * * @param {object} deps * @param {Logger} deps.logger * @returns {Sink} */ export function createLogSink({ logger }) { return { write(span) { logger.info('llm.trace', { span: span.name, duration_ms: span.durationMs, ...span.attributes, events: span.events, }) }, } }