span.js
4.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
* 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<string, any>} attributes
*/
/**
* `end()` 写给 sink 的完结产物(两个 sink 消费的唯一结构)。
* @typedef {object} CompletedSpan
* @property {string} name
* @property {number} durationMs end 时刻 − startSpan 时刻。
* @property {number} startUnixNano
* @property {number} endUnixNano
* @property {Record<string, any>} attributes 已映射为 gen_ai.* 语义键。
* @property {Array<SpanEvent>} 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<string, any>} data
* @returns {Record<string, any>}
*/
function mapEventAttrs(name, data) {
if (name !== 'usage') return data
/** @type {Record<string, any>} */
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<string, any>} */
const attributes = {}
/** @type {Array<SpanEvent>} */
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,
})
},
}
}