@aipack-ai/observability 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,341 @@
1
+ import { Telemetry, RunStartTelemetryInfo, RunTelemetryInfo, ToolTelemetryInfo, ModelTelemetryInfo, RetryTelemetryInfo, PermissionDeniedTelemetryInfo } from '@aipack-ai/agent';
2
+
3
+ /**
4
+ * 上报 SDK 记录类型(@aipack-ai/observability)。
5
+ * 客户端 telemetry 事件转成这些原始记录后批量上报;收集服务(@aipack-ai/observability-server)
6
+ * 依赖这些类型做落盘与聚合。
7
+ */
8
+ interface RunRecord {
9
+ traceId: string;
10
+ startedAt: number;
11
+ endedAt: number;
12
+ sessionKey: string;
13
+ channel?: string;
14
+ model?: string;
15
+ /** 发布版本(接入方 agent 应用版本,如 '1.3.0')。可空:旧 SDK / 未配置时缺省,服务端归入 unknown */
16
+ appVersion?: string;
17
+ status: 'success' | 'error' | 'validation';
18
+ errorClass?: string;
19
+ turns: number;
20
+ durationMs: number;
21
+ activeMs: number;
22
+ queuedMs: number;
23
+ ttftMs?: number;
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ cacheRead?: number;
27
+ cacheWrite?: number;
28
+ }
29
+ interface SpanRecord {
30
+ traceId: string;
31
+ spanId: string;
32
+ kind: 'run' | 'model' | 'tool';
33
+ name: string;
34
+ startedAt: number;
35
+ durationMs: number;
36
+ status: 'ok' | 'error';
37
+ errorClass?: string;
38
+ attempts?: number;
39
+ inputTokens?: number;
40
+ outputTokens?: number;
41
+ cacheRead?: number;
42
+ cacheWrite?: number;
43
+ /** 会话标识(支撑 session 维度 token 统计) */
44
+ sessionKey?: string;
45
+ }
46
+ interface ToolCallRecord {
47
+ traceId: string;
48
+ spanId: string;
49
+ toolName: string;
50
+ status: 'ok' | 'error' | 'blocked' | 'skipped';
51
+ durationMs: number;
52
+ errorClass?: string;
53
+ }
54
+ /** 权限拦截记录(收集端仅计入聚合器计数,不落库) */
55
+ interface PermissionRecord {
56
+ traceId?: string;
57
+ sessionKey: string;
58
+ toolName: string;
59
+ reason: string;
60
+ /** 事件发生时刻(epoch ms) */
61
+ timestamp: number;
62
+ }
63
+ /** 自定义业务事件(P2-1:obs.emit 通用埋点,入 Trace 时间轴) */
64
+ interface EventRecord {
65
+ /** run 内 emit 自动注入;run 外缺省 */
66
+ traceId?: string;
67
+ sessionKey?: string;
68
+ /** 事件名(如 'tool_picked' / 'cancelled'),面板按此分组 */
69
+ name: string;
70
+ /** 任意 JSON 可序列化数据 */
71
+ data?: unknown;
72
+ /** 事件发生时刻(epoch ms) */
73
+ timestamp: number;
74
+ }
75
+ /** 单次 provider 内部重试(P2-2:per-attempt 维度,关联 model span) */
76
+ interface RetryRecord {
77
+ traceId: string;
78
+ /** 关联的模型调用 span */
79
+ spanId?: string;
80
+ provider: string;
81
+ modelId: string;
82
+ /** 第几次重试(从 1 开始) */
83
+ attempt: number;
84
+ errorClass?: string;
85
+ /** HTTP 状态码(如有,如 429/502) */
86
+ status?: number;
87
+ /** 本次退避延迟(ms) */
88
+ delayMs: number;
89
+ /** 重试发生时刻(epoch ms) */
90
+ timestamp: number;
91
+ }
92
+ /** 客户端一次上报的批量事件(POST /api/v1/ingest body,appId 附加) */
93
+ interface EventBatch {
94
+ runs: RunRecord[];
95
+ spans: SpanRecord[];
96
+ toolCalls: ToolCallRecord[];
97
+ permissions: PermissionRecord[];
98
+ retries: RetryRecord[];
99
+ events: EventRecord[];
100
+ }
101
+
102
+ /**
103
+ * Telemetry 接口实现(客户端 SDK):事件 → 原始记录 → 上报队列 → HttpReporter。
104
+ *
105
+ * 埋点模式:不在本地聚合/落盘,事件同步转成 RunRecord / SpanRecord / ToolCallRecord /
106
+ * PermissionRecord 后入队,由定时器批量 POST 到收集服务。上报失败由 reporter 缓存补报,
107
+ * 事件路径零阻塞、失败不阻断 run()。
108
+ *
109
+ * 载荷无 startedAt 字段:用「到达时刻 - durationMs」推导;run 级用 onRunStart.queuedAt。
110
+ *
111
+ * 注意:runtime 的 emitTelemetry 会解构方法后调用(fn(info)),故所有回调必须为
112
+ * 箭头函数属性以绑定 this,否则 this.queued / this.startedAt 为 undefined。
113
+ */
114
+
115
+ interface FlushQueueOptions {
116
+ /** 上报周期(ms),默认 5000 */
117
+ intervalMs?: number;
118
+ /** 积攒条数触发(含 runs/spans/toolCalls/permissions 总数),默认 50 */
119
+ batchSize?: number;
120
+ /** P2-1 明细采样率(0–1):只作用于 model/tool spans 与 toolCalls;runs/permissions/events 全量。缺省 1(全量) */
121
+ sampleRate?: number;
122
+ /** P2-1 脱敏钩子:send 前对整批改写(防 PII 明文上报) */
123
+ redact?: (batch: EventBatch) => EventBatch;
124
+ /** 发布版本:注入每条 run 记录(RunRecord.appVersion),供收集端按版本聚合。缺省不携带 */
125
+ appVersion?: string;
126
+ }
127
+ declare class ObservabilityTelemetry implements Telemetry {
128
+ private reporter;
129
+ private queued;
130
+ private timer;
131
+ private startedAt;
132
+ /** in-flight run 顺序(最近开始的排最后),供 logger 关联当前 traceId */
133
+ private inFlightOrder;
134
+ private batchSize;
135
+ private sampleRate;
136
+ private redact?;
137
+ private appVersion?;
138
+ constructor(reporter: {
139
+ send(batch: EventBatch): Promise<boolean>;
140
+ }, opts?: FlushQueueOptions);
141
+ onRunStart: (info: RunStartTelemetryInfo) => void;
142
+ onRunEnd: (info: RunTelemetryInfo) => void;
143
+ onToolCall: (info: ToolTelemetryInfo) => void;
144
+ onModelCall: (info: ModelTelemetryInfo) => void;
145
+ onRetry: (info: RetryTelemetryInfo) => void;
146
+ onPermissionDenied: (info: PermissionDeniedTelemetryInfo) => void;
147
+ /**
148
+ * 当前 run 上下文(供结构化 logger 注入 traceId)。
149
+ * 单并发时返回唯一 in-flight run;并发时返回最近开始的(约定语义,文档说明)。
150
+ */
151
+ currentContext(): {
152
+ traceId?: string;
153
+ };
154
+ /**
155
+ * P2-1 自定义业务事件(通用埋点)。
156
+ * run 内调用自动注入当前 traceId;run 外可通过 opts 显式传 traceId/sessionKey。
157
+ */
158
+ emit(name: string, data?: unknown, opts?: {
159
+ traceId?: string;
160
+ sessionKey?: string;
161
+ }): void;
162
+ /** 立即上报队列中残留的数据(fire-and-forget,失败由 reporter 缓存补报) */
163
+ flush(): void;
164
+ /** 停止定时器并等残留上报完成(进程退出前调用) */
165
+ close(): Promise<void>;
166
+ /** P2-1 明细采样判定:rate>=1 恒过、<=0 恒弃、否则按概率 */
167
+ private sample;
168
+ private runRecord;
169
+ private runSpan;
170
+ private isEmpty;
171
+ private maybeFlush;
172
+ }
173
+
174
+ /**
175
+ * 结构化 logger(P1-1 日志关联)。
176
+ *
177
+ * 约定:logfmt 或 JSON 输出,可通过 context() 动态注入当前上下文(如 traceId),
178
+ * 使应用日志与 Trace 关联(排障时按 traceId 检索日志流)。
179
+ *
180
+ * const logger = createLogger({ format: 'logfmt', context: () => telemetry.currentContext() });
181
+ * logger.info('tool called', { tool: 'search_flights', durationMs: 120 });
182
+ * // => time=2026-08-11T10:00:00.000Z level=info msg="tool called" tool=search_flights durationMs=120
183
+ *
184
+ * 与收集服务 webhook/告警日志不同:本 logger 只负责"应用日志"输出约定,
185
+ * 不参与上报(上报走 telemetry)。
186
+ */
187
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
188
+ type LogFormat = 'logfmt' | 'json';
189
+ interface LoggerOptions {
190
+ /** 输出格式,默认 logfmt */
191
+ format?: LogFormat;
192
+ /** 最低输出级别,默认 info */
193
+ level?: LogLevel;
194
+ /** 每行日志的固定字段(如 service/app),会被动态 context / 单行 fields 覆盖 */
195
+ tags?: Record<string, unknown>;
196
+ /** 动态上下文(每行调用),例如 () => telemetry.currentContext() 注入 traceId */
197
+ context?: () => Record<string, unknown> | undefined;
198
+ /** 输出目标,默认 console(line 已含换行) */
199
+ dest?: (line: string) => void;
200
+ /** 是否注入 ISO 时间戳字段 time,默认 true */
201
+ timestamps?: boolean;
202
+ }
203
+ interface Logger {
204
+ debug(msg: string, fields?: Record<string, unknown>): void;
205
+ info(msg: string, fields?: Record<string, unknown>): void;
206
+ warn(msg: string, fields?: Record<string, unknown>): void;
207
+ error(msg: string, fields?: Record<string, unknown>): void;
208
+ /** 派生子 logger:合并部分配置(如固定业务标签) */
209
+ child(overrides: Partial<LoggerOptions>): Logger;
210
+ }
211
+ declare function createLogger(opts?: LoggerOptions): Logger;
212
+
213
+ /**
214
+ * HttpReporter — 把批量事件上报到收集服务(埋点模式)。
215
+ *
216
+ * - POST {endpoint}/api/v1/ingest,携带 x-app-id / x-app-secret 鉴权头
217
+ * - 上报失败(网络错误 / 5xx / 429)→ 写入本地缓存文件({cacheDir}/{appId}.json),
218
+ * 下次 send 前先补报缓存,全部成功才删除
219
+ * - 4xx(鉴权失败、参数错误)判定为不可重试,直接丢弃并 warn,避免缓存无限重试
220
+ * - 缓存条数上限 maxCacheSize,超出后丢弃最旧记录
221
+ * - 并发保护:send 串行执行,重复调用合并为一次
222
+ */
223
+
224
+ interface ReporterOptions {
225
+ /** 收集服务地址,如 http://localhost:8787(必填) */
226
+ endpoint: string;
227
+ appId: string;
228
+ appSecret: string;
229
+ /** 缓存目录,默认 ./.aipack/observability */
230
+ cacheDir?: string;
231
+ /** 缓存条数上限(含 runs/spans/toolCalls/permissions),默认 2000 */
232
+ maxCacheSize?: number;
233
+ /** 上报超时(ms),默认 5000 */
234
+ timeoutMs?: number;
235
+ /** 便于测试注入;默认全局 fetch */
236
+ fetchImpl?: typeof fetch;
237
+ }
238
+ declare class HttpReporter {
239
+ private endpoint;
240
+ private appId;
241
+ private appSecret;
242
+ private cachePath;
243
+ private maxCacheSize;
244
+ private timeoutMs;
245
+ private fetchImpl;
246
+ /** 串行发送锁:true 表示有发送在进行中 */
247
+ private sending;
248
+ /** 发送期间新到的批次,发送完成后继续补发 */
249
+ private pending;
250
+ constructor(opts: ReporterOptions);
251
+ /** 上报批次;失败自动落缓存,返回是否成功(测试断言用) */
252
+ send(batch: EventBatch): Promise<boolean>;
253
+ /** 排空所有批次(含缓存补报)。返回最后一轮是否成功 */
254
+ private drain;
255
+ /** 尝试上报;返回是否成功(4xx 视为最终失败,不再重试) */
256
+ private tryUpload;
257
+ private readCache;
258
+ private writeCache;
259
+ private removeCache;
260
+ }
261
+
262
+ /**
263
+ * OTLP/HTTP JSON trace exporter(P1-2 标准协议,档位 B 起步)。
264
+ *
265
+ * 无外部依赖:直接把 EventBatch 映射为 OTLP JSON ExportTraceServiceRequest
266
+ * (protobuf JSON 编码),POST 到 OpenTelemetry Collector 的 /v1/traces。
267
+ *
268
+ * - traceId/spanId:OTLP 要求 16/8 字节二进制(base64),本 SDK 的 traceId 是任意
269
+ * 字符串,故用 md5 做确定性映射(同一字符串恒得同一字节序列);
270
+ * - 导出为 best-effort:失败仅 warn,不影响主上报链路(主链路仍走 HttpReporter);
271
+ * - 未配置 otlp 时零开销(不构造 exporter)。
272
+ */
273
+
274
+ interface OtlpExporterOptions {
275
+ /** OTLP/HTTP JSON 端点,如 http://otel-collector:4318(POST {endpoint}/v1/traces) */
276
+ endpoint: string;
277
+ /** resource.service.name,默认 = appId */
278
+ serviceName?: string;
279
+ /** 应用标识(写入 span attribute aipack.app,便于按应用过滤) */
280
+ appId?: string;
281
+ /** 额外请求头(如 x-otlp-token) */
282
+ headers?: Record<string, string>;
283
+ /** 超时(ms),默认 3000 */
284
+ timeoutMs?: number;
285
+ /** 便于测试注入;默认全局 fetch */
286
+ fetchImpl?: typeof fetch;
287
+ }
288
+ interface OtlpTraceExporter {
289
+ /** 导出批次;失败内部消化,恒不抛错,返回是否成功 */
290
+ export(batch: EventBatch): Promise<boolean>;
291
+ }
292
+ /** EventBatch → OTLP ExportTraceServiceRequest(JSON 对象,可直接 JSON.stringify) */
293
+ declare function toOtlpJsonTraces(batch: EventBatch, serviceName: string, appId?: string): object;
294
+ declare function createOtlpTraceExporter(opts: OtlpExporterOptions): OtlpTraceExporter;
295
+
296
+ interface CreateObservabilityOptions {
297
+ /** 应用标识(必填),收集端用它校验身份并隔离数据 */
298
+ appId: string;
299
+ /** 应用密钥(必填),与收集端 OBS_APPS 白名单匹配 */
300
+ appSecret: string;
301
+ /** 收集服务地址,默认 http://localhost:8787 */
302
+ endpoint?: string;
303
+ /** 上报失败本地缓存目录,默认 ./.aipack/observability */
304
+ cacheDir?: string;
305
+ /** 缓存条数上限,默认 2000 */
306
+ maxCacheSize?: number;
307
+ /** 上报周期(ms),默认 5000 */
308
+ flushIntervalMs?: number;
309
+ /** 积攒条数触发上报,默认 50 */
310
+ flushBatchSize?: number;
311
+ /** 可选:OTLP/HTTP JSON trace 导出(推 OpenTelemetry Collector),失败不影响主上报 */
312
+ otlp?: {
313
+ endpoint: string;
314
+ serviceName?: string;
315
+ headers?: Record<string, string>;
316
+ };
317
+ /** P2-1 明细采样率(0–1):只对 model/tool spans 与 toolCalls 采样,runs/permissions/events 全量。缺省 1 */
318
+ sampleRate?: number;
319
+ /** P2-1 脱敏钩子:send 前对整批改写(防 PII 明文上报),示例见 docs/observability-roadmap.md §P2-1 */
320
+ redact?: (batch: EventBatch) => EventBatch;
321
+ /** 发布版本(如 '1.3.0'),随每条 run 上报,供收集端按版本聚合对比。缺省不携带 */
322
+ version?: string;
323
+ }
324
+ interface Observability {
325
+ /** 注入 RuntimeOptions.telemetry */
326
+ telemetry: ObservabilityTelemetry;
327
+ /** 结构化 logger:自动注入当前 run 的 traceId(P1-1 日志关联) */
328
+ logger: Logger;
329
+ /** P2-1 自定义业务事件:run 内自动带 traceId,入 Trace 时间轴 */
330
+ emit(name: string, data?: unknown, opts?: {
331
+ traceId?: string;
332
+ sessionKey?: string;
333
+ }): void;
334
+ /** 立即上报队列中的残留数据 */
335
+ flush(): void;
336
+ /** 停止定时器并等残留上报完成(进程退出前调用) */
337
+ close(): Promise<void>;
338
+ }
339
+ declare function createObservability(opts: CreateObservabilityOptions): Observability;
340
+
341
+ export { type CreateObservabilityOptions, type EventBatch, type EventRecord, type FlushQueueOptions, HttpReporter, type LogFormat, type LogLevel, type Logger, type LoggerOptions, type Observability, ObservabilityTelemetry, type OtlpExporterOptions, type OtlpTraceExporter, type PermissionRecord, type ReporterOptions, type RetryRecord, type RunRecord, type SpanRecord, type ToolCallRecord, createLogger, createObservability, createOtlpTraceExporter, toOtlpJsonTraces };
package/dist/index.js ADDED
@@ -0,0 +1,621 @@
1
+ // src/reporter.ts
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+ var EMPTY_BATCH = () => ({
5
+ runs: [],
6
+ spans: [],
7
+ toolCalls: [],
8
+ permissions: [],
9
+ retries: [],
10
+ events: []
11
+ });
12
+ var HttpReporter = class {
13
+ endpoint;
14
+ appId;
15
+ appSecret;
16
+ cachePath;
17
+ maxCacheSize;
18
+ timeoutMs;
19
+ fetchImpl;
20
+ /** 串行发送锁:true 表示有发送在进行中 */
21
+ sending = false;
22
+ /** 发送期间新到的批次,发送完成后继续补发 */
23
+ pending = [];
24
+ constructor(opts) {
25
+ this.endpoint = opts.endpoint.replace(/\/+$/, "");
26
+ this.appId = opts.appId;
27
+ this.appSecret = opts.appSecret;
28
+ this.cachePath = path.join(
29
+ opts.cacheDir ?? path.join(process.cwd(), ".aipack", "observability"),
30
+ `${opts.appId}.json`
31
+ );
32
+ this.maxCacheSize = opts.maxCacheSize ?? 2e3;
33
+ this.timeoutMs = opts.timeoutMs ?? 5e3;
34
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
35
+ }
36
+ /** 上报批次;失败自动落缓存,返回是否成功(测试断言用) */
37
+ async send(batch) {
38
+ this.pending.push(batch);
39
+ if (this.sending) return false;
40
+ this.sending = true;
41
+ try {
42
+ return await this.drain();
43
+ } finally {
44
+ this.sending = false;
45
+ }
46
+ }
47
+ /** 排空所有批次(含缓存补报)。返回最后一轮是否成功 */
48
+ async drain() {
49
+ let lastOk = false;
50
+ for (; ; ) {
51
+ const cache = await this.readCache();
52
+ const batch = mergeBatches(cache, ...this.pending);
53
+ this.pending = [];
54
+ if (isEmpty(batch)) return lastOk;
55
+ lastOk = await this.tryUpload(batch);
56
+ if (!lastOk) {
57
+ await this.writeCache(batch);
58
+ return false;
59
+ }
60
+ await this.removeCache();
61
+ }
62
+ }
63
+ /** 尝试上报;返回是否成功(4xx 视为最终失败,不再重试) */
64
+ async tryUpload(batch) {
65
+ try {
66
+ const ac = new AbortController();
67
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
68
+ try {
69
+ const res = await this.fetchImpl(`${this.endpoint}/api/v1/ingest`, {
70
+ method: "POST",
71
+ headers: {
72
+ "content-type": "application/json",
73
+ "x-app-id": this.appId,
74
+ "x-app-secret": this.appSecret
75
+ },
76
+ body: JSON.stringify({ appId: this.appId, ...batch }),
77
+ signal: ac.signal
78
+ });
79
+ if (res.ok) return true;
80
+ if (res.status >= 400 && res.status < 500 && res.status !== 429) {
81
+ console.warn(
82
+ `[aipack/observability] \u4E0A\u62A5\u88AB\u62D2\u7EDD(${res.status})\uFF0C\u4E22\u5F03 ${count(batch)} \u6761\u3002\u68C0\u67E5 appId/appSecret \u4E0E endpoint\u3002`
83
+ );
84
+ return true;
85
+ }
86
+ console.warn(`[aipack/observability] \u4E0A\u62A5\u5931\u8D25(${res.status})\uFF0C\u5C06\u7F13\u5B58\u8865\u62A5\u3002`);
87
+ return false;
88
+ } finally {
89
+ clearTimeout(timer);
90
+ }
91
+ } catch (err) {
92
+ console.warn("[aipack/observability] \u4E0A\u62A5\u7F51\u7EDC\u9519\u8BEF\uFF0C\u5C06\u7F13\u5B58\u8865\u62A5:", err.message);
93
+ return false;
94
+ }
95
+ }
96
+ // ─── 本地缓存 ──────────────────────────────────────────────────
97
+ async readCache() {
98
+ try {
99
+ const raw = await fs.readFile(this.cachePath, "utf8");
100
+ const parsed = JSON.parse(raw);
101
+ return {
102
+ runs: Array.isArray(parsed.runs) ? parsed.runs : [],
103
+ spans: Array.isArray(parsed.spans) ? parsed.spans : [],
104
+ toolCalls: Array.isArray(parsed.toolCalls) ? parsed.toolCalls : [],
105
+ permissions: Array.isArray(parsed.permissions) ? parsed.permissions : [],
106
+ retries: Array.isArray(parsed.retries) ? parsed.retries : [],
107
+ events: Array.isArray(parsed.events) ? parsed.events : []
108
+ };
109
+ } catch {
110
+ return EMPTY_BATCH();
111
+ }
112
+ }
113
+ async writeCache(batch) {
114
+ try {
115
+ await fs.mkdir(path.dirname(this.cachePath), { recursive: true });
116
+ await fs.writeFile(this.cachePath, JSON.stringify(trimBatch(batch, this.maxCacheSize)), "utf8");
117
+ } catch (err) {
118
+ console.warn("[aipack/observability] \u7F13\u5B58\u5199\u5165\u5931\u8D25\uFF08\u6570\u636E\u4E22\u5F03\uFF09:", err.message);
119
+ }
120
+ }
121
+ async removeCache() {
122
+ try {
123
+ await fs.unlink(this.cachePath);
124
+ } catch {
125
+ }
126
+ }
127
+ };
128
+ function isEmpty(batch) {
129
+ return !batch.runs.length && !batch.spans.length && !batch.toolCalls.length && !batch.permissions.length && !batch.retries.length && !batch.events.length;
130
+ }
131
+ function count(batch) {
132
+ return batch.runs.length + batch.spans.length + batch.toolCalls.length + batch.permissions.length + batch.retries.length + batch.events.length;
133
+ }
134
+ function mergeBatches(...batches) {
135
+ const out = EMPTY_BATCH();
136
+ for (const b of batches) {
137
+ out.runs.push(...b.runs);
138
+ out.spans.push(...b.spans);
139
+ out.toolCalls.push(...b.toolCalls);
140
+ out.permissions.push(...b.permissions);
141
+ out.retries.push(...b.retries);
142
+ out.events.push(...b.events);
143
+ }
144
+ return out;
145
+ }
146
+ function trimBatch(batch, max) {
147
+ const total = count(batch);
148
+ if (total <= max) return batch;
149
+ let excess = total - max;
150
+ const out = {
151
+ ...batch,
152
+ runs: [...batch.runs],
153
+ spans: [...batch.spans],
154
+ toolCalls: [...batch.toolCalls],
155
+ permissions: [...batch.permissions],
156
+ retries: [...batch.retries],
157
+ events: [...batch.events]
158
+ };
159
+ for (const key of ["runs", "spans", "toolCalls", "permissions", "retries", "events"]) {
160
+ if (excess <= 0) break;
161
+ const removed = Math.min(out[key].length, excess);
162
+ out[key].splice(0, removed);
163
+ excess -= removed;
164
+ }
165
+ return out;
166
+ }
167
+
168
+ // src/telemetry.ts
169
+ var emptyBatch = () => ({
170
+ runs: [],
171
+ spans: [],
172
+ toolCalls: [],
173
+ permissions: [],
174
+ retries: [],
175
+ events: []
176
+ });
177
+ var ObservabilityTelemetry = class {
178
+ constructor(reporter, opts = {}) {
179
+ this.reporter = reporter;
180
+ this.batchSize = opts.batchSize ?? 50;
181
+ this.sampleRate = opts.sampleRate ?? 1;
182
+ this.redact = opts.redact;
183
+ this.appVersion = opts.appVersion;
184
+ const intervalMs = opts.intervalMs ?? 5e3;
185
+ this.timer = setInterval(() => void this.flush(), intervalMs);
186
+ this.timer.unref?.();
187
+ }
188
+ reporter;
189
+ queued = emptyBatch();
190
+ timer;
191
+ startedAt = /* @__PURE__ */ new Map();
192
+ // traceId -> queuedAt
193
+ /** in-flight run 顺序(最近开始的排最后),供 logger 关联当前 traceId */
194
+ inFlightOrder = [];
195
+ batchSize;
196
+ sampleRate;
197
+ redact;
198
+ appVersion;
199
+ onRunStart = (info) => {
200
+ this.startedAt.set(info.traceId, info.queuedAt);
201
+ this.inFlightOrder = this.inFlightOrder.filter((id) => id !== info.traceId);
202
+ this.inFlightOrder.push(info.traceId);
203
+ };
204
+ onRunEnd = (info) => {
205
+ const queuedAt = this.startedAt.get(info.traceId) ?? Date.now() - info.durationMs;
206
+ this.startedAt.delete(info.traceId);
207
+ this.inFlightOrder = this.inFlightOrder.filter((id) => id !== info.traceId);
208
+ this.queued.runs.push(this.runRecord(info, queuedAt));
209
+ this.queued.spans.push(this.runSpan(info, queuedAt));
210
+ this.maybeFlush();
211
+ };
212
+ onToolCall = (info) => {
213
+ if (!this.sample()) return;
214
+ const startedAt = Date.now() - info.durationMs;
215
+ this.queued.spans.push({
216
+ traceId: info.traceId,
217
+ spanId: info.spanId,
218
+ kind: "tool",
219
+ name: `tool:${info.toolName}`,
220
+ startedAt,
221
+ durationMs: info.durationMs,
222
+ status: info.status === "ok" ? "ok" : "error",
223
+ errorClass: info.errorClass,
224
+ sessionKey: info.sessionKey
225
+ });
226
+ this.queued.toolCalls.push({
227
+ traceId: info.traceId,
228
+ spanId: info.spanId,
229
+ toolName: info.toolName,
230
+ status: info.status,
231
+ durationMs: info.durationMs,
232
+ errorClass: info.errorClass
233
+ });
234
+ this.maybeFlush();
235
+ };
236
+ onModelCall = (info) => {
237
+ if (!this.sample()) return;
238
+ this.queued.spans.push({
239
+ traceId: info.traceId,
240
+ spanId: info.spanId,
241
+ kind: "model",
242
+ name: `model:${info.modelId}`,
243
+ startedAt: Date.now() - info.durationMs,
244
+ durationMs: info.durationMs,
245
+ status: info.errorClass ? "error" : "ok",
246
+ errorClass: info.errorClass,
247
+ attempts: info.attempts,
248
+ inputTokens: info.inputTokens,
249
+ outputTokens: info.outputTokens,
250
+ cacheRead: info.cacheRead,
251
+ cacheWrite: info.cacheWrite,
252
+ sessionKey: info.sessionKey
253
+ });
254
+ this.maybeFlush();
255
+ };
256
+ onRetry = (info) => {
257
+ this.queued.retries.push({
258
+ traceId: info.traceId,
259
+ spanId: info.spanId,
260
+ provider: info.provider,
261
+ modelId: info.modelId,
262
+ attempt: info.attempt,
263
+ errorClass: info.errorClass,
264
+ status: info.status,
265
+ delayMs: info.delayMs,
266
+ timestamp: Date.now()
267
+ });
268
+ this.maybeFlush();
269
+ };
270
+ onPermissionDenied = (info) => {
271
+ this.queued.permissions.push({
272
+ traceId: info.traceId,
273
+ sessionKey: info.sessionKey,
274
+ toolName: info.toolName,
275
+ reason: info.reason,
276
+ timestamp: Date.now()
277
+ });
278
+ this.maybeFlush();
279
+ };
280
+ /**
281
+ * 当前 run 上下文(供结构化 logger 注入 traceId)。
282
+ * 单并发时返回唯一 in-flight run;并发时返回最近开始的(约定语义,文档说明)。
283
+ */
284
+ currentContext() {
285
+ const traceId = this.inFlightOrder[this.inFlightOrder.length - 1];
286
+ return traceId ? { traceId } : {};
287
+ }
288
+ /**
289
+ * P2-1 自定义业务事件(通用埋点)。
290
+ * run 内调用自动注入当前 traceId;run 外可通过 opts 显式传 traceId/sessionKey。
291
+ */
292
+ emit(name, data, opts = {}) {
293
+ this.queued.events.push({
294
+ traceId: opts.traceId ?? this.currentContext().traceId,
295
+ sessionKey: opts.sessionKey,
296
+ name,
297
+ data,
298
+ timestamp: Date.now()
299
+ });
300
+ this.maybeFlush();
301
+ }
302
+ /** 立即上报队列中残留的数据(fire-and-forget,失败由 reporter 缓存补报) */
303
+ flush() {
304
+ if (this.isEmpty()) return;
305
+ const batch = this.queued;
306
+ this.queued = emptyBatch();
307
+ void this.reporter.send(this.redact ? this.redact(batch) : batch);
308
+ }
309
+ /** 停止定时器并等残留上报完成(进程退出前调用) */
310
+ async close() {
311
+ if (this.timer) {
312
+ clearInterval(this.timer);
313
+ this.timer = void 0;
314
+ }
315
+ if (this.isEmpty()) return;
316
+ const batch = this.queued;
317
+ this.queued = emptyBatch();
318
+ await this.reporter.send(this.redact ? this.redact(batch) : batch);
319
+ }
320
+ /** P2-1 明细采样判定:rate>=1 恒过、<=0 恒弃、否则按概率 */
321
+ sample() {
322
+ if (this.sampleRate >= 1) return true;
323
+ if (this.sampleRate <= 0) return false;
324
+ return Math.random() < this.sampleRate;
325
+ }
326
+ runRecord(info, queuedAt) {
327
+ return {
328
+ traceId: info.traceId,
329
+ startedAt: queuedAt,
330
+ endedAt: queuedAt + info.durationMs,
331
+ sessionKey: info.sessionKey,
332
+ channel: info.request.channel,
333
+ model: info.request.model,
334
+ status: info.success ? "success" : info.errorClass === "validation" ? "validation" : "error",
335
+ errorClass: info.errorClass,
336
+ turns: info.turnCount,
337
+ durationMs: info.durationMs,
338
+ activeMs: info.activeMs,
339
+ queuedMs: info.queuedMs,
340
+ ttftMs: info.ttftMs,
341
+ inputTokens: info.tokens.input,
342
+ outputTokens: info.tokens.output,
343
+ cacheRead: info.tokens.cacheRead,
344
+ cacheWrite: info.tokens.cacheWrite,
345
+ ...this.appVersion !== void 0 ? { appVersion: this.appVersion } : {}
346
+ };
347
+ }
348
+ runSpan(info, queuedAt) {
349
+ return {
350
+ traceId: info.traceId,
351
+ spanId: info.traceId,
352
+ kind: "run",
353
+ name: "run",
354
+ startedAt: queuedAt,
355
+ durationMs: info.durationMs,
356
+ status: info.success ? "ok" : "error",
357
+ errorClass: info.errorClass,
358
+ inputTokens: info.tokens.input,
359
+ outputTokens: info.tokens.output,
360
+ sessionKey: info.sessionKey
361
+ };
362
+ }
363
+ isEmpty() {
364
+ return !this.queued.runs.length && !this.queued.spans.length && !this.queued.toolCalls.length && !this.queued.permissions.length && !this.queued.retries.length && !this.queued.events.length;
365
+ }
366
+ maybeFlush() {
367
+ const total = this.queued.runs.length + this.queued.spans.length + this.queued.toolCalls.length + this.queued.permissions.length + this.queued.retries.length + this.queued.events.length;
368
+ if (total >= this.batchSize) this.flush();
369
+ }
370
+ };
371
+
372
+ // src/logger.ts
373
+ var LEVEL_ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
374
+ var REDACTED_KEYS = /(secret|token|password|key|authorization|apikey)/i;
375
+ function createLogger(opts = {}) {
376
+ const { format = "logfmt", dest = defaultDest } = opts;
377
+ const minLevel = LEVEL_ORDER[opts.level ?? "info"];
378
+ const timestamps = opts.timestamps ?? true;
379
+ const emit = (level, msg, fields) => {
380
+ if (LEVEL_ORDER[level] < minLevel) return;
381
+ const context = opts.context?.() ?? {};
382
+ dest(formatLine({ format, level, msg, fields, context, tags: opts.tags, timestamps }));
383
+ };
384
+ return {
385
+ debug: (msg, fields) => emit("debug", msg, fields),
386
+ info: (msg, fields) => emit("info", msg, fields),
387
+ warn: (msg, fields) => emit("warn", msg, fields),
388
+ error: (msg, fields) => emit("error", msg, fields),
389
+ child: (overrides) => createLogger({
390
+ format: overrides.format ?? opts.format,
391
+ level: overrides.level ?? opts.level,
392
+ tags: { ...opts.tags, ...overrides.tags },
393
+ context: overrides.context ?? opts.context,
394
+ dest: overrides.dest ?? opts.dest,
395
+ timestamps: overrides.timestamps ?? opts.timestamps
396
+ })
397
+ };
398
+ }
399
+ function formatLine(input) {
400
+ const data = {};
401
+ if (input.timestamps) data.time = (/* @__PURE__ */ new Date()).toISOString();
402
+ data.level = input.level;
403
+ data.msg = input.msg;
404
+ for (const src of [input.tags, input.context, input.fields]) {
405
+ if (!src) continue;
406
+ for (const [k, v] of Object.entries(src)) {
407
+ data[k] = REDACTED_KEYS.test(k) ? "***" : v;
408
+ }
409
+ }
410
+ return input.format === "json" ? JSON.stringify(data) : toLogfmt(data);
411
+ }
412
+ function toLogfmt(data) {
413
+ const parts = [];
414
+ for (const [k, v] of Object.entries(data)) {
415
+ if (v === void 0 || v === null) continue;
416
+ parts.push(`${k}=${logfmtValue(v)}`);
417
+ }
418
+ return parts.join(" ");
419
+ }
420
+ function logfmtValue(v) {
421
+ if (typeof v === "string") return quoteLogfmt(v);
422
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
423
+ if (typeof v === "bigint") return v.toString();
424
+ return quoteLogfmt(JSON.stringify(v));
425
+ }
426
+ function quoteLogfmt(s) {
427
+ if (!/[ \t"=\\]/u.test(s)) return s;
428
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
429
+ }
430
+ function defaultDest(line) {
431
+ console.log(line);
432
+ }
433
+
434
+ // src/otlp.ts
435
+ import { createHash } from "crypto";
436
+ function bytesOf(input, n) {
437
+ return createHash("md5").update(input).digest().subarray(0, n);
438
+ }
439
+ function str(value) {
440
+ return { stringValue: value == null ? "" : String(value) };
441
+ }
442
+ function int64(value) {
443
+ return { intValue: String(Math.round(value || 0)) };
444
+ }
445
+ function attr(key, value) {
446
+ return { key, value };
447
+ }
448
+ function tokenTotal(r) {
449
+ if (r.inputTokens === void 0 && r.outputTokens === void 0) return void 0;
450
+ return (r.inputTokens ?? 0) + (r.outputTokens ?? 0) + (r.cacheRead ?? 0) + (r.cacheWrite ?? 0);
451
+ }
452
+ function runToSpan(r, appId) {
453
+ const startNs = BigInt(r.startedAt) * 1000000n;
454
+ const endNs = BigInt(r.startedAt + r.durationMs) * 1000000n;
455
+ const ok = r.status === "success";
456
+ const attrs = [
457
+ attr("session.key", str(r.sessionKey)),
458
+ attr("aipack.status", str(r.status)),
459
+ attr("aipack.turns", int64(r.turns)),
460
+ attr("aipack.duration_ms", int64(r.durationMs)),
461
+ attr("tokens.input", int64(r.inputTokens)),
462
+ attr("tokens.output", int64(r.outputTokens))
463
+ ];
464
+ if (tokenTotal(r) !== void 0) attrs.push(attr("tokens.total", int64(tokenTotal(r))));
465
+ if (appId) attrs.push(attr("aipack.app", str(appId)));
466
+ if (r.model) attrs.push(attr("model", str(r.model)));
467
+ if (r.errorClass) attrs.push(attr("aipack.error_class", str(r.errorClass)));
468
+ return {
469
+ traceId: bytesOf(r.traceId, 16).toString("base64"),
470
+ spanId: bytesOf(`${r.traceId}:run`, 8).toString("base64"),
471
+ name: "agent.run",
472
+ kind: 1,
473
+ // INTERNAL
474
+ startTimeUnixNano: startNs.toString(),
475
+ endTimeUnixNano: endNs.toString(),
476
+ attributes: attrs,
477
+ status: ok ? { code: 1 } : { code: 2, message: r.errorClass || r.status }
478
+ };
479
+ }
480
+ function spanToSpan(s, appId) {
481
+ const startNs = BigInt(s.startedAt) * 1000000n;
482
+ const endNs = BigInt(s.startedAt + s.durationMs) * 1000000n;
483
+ const ok = s.status === "ok";
484
+ const attrs = [
485
+ attr("span.kind", str(s.kind)),
486
+ attr("session.key", str(s.sessionKey)),
487
+ attr("aipack.status", str(s.status)),
488
+ attr("aipack.duration_ms", int64(s.durationMs))
489
+ ];
490
+ if (appId) attrs.push(attr("aipack.app", str(appId)));
491
+ if (s.inputTokens !== void 0) attrs.push(attr("tokens.input", int64(s.inputTokens)));
492
+ if (s.outputTokens !== void 0) attrs.push(attr("tokens.output", int64(s.outputTokens)));
493
+ if (tokenTotal(s) !== void 0) attrs.push(attr("tokens.total", int64(tokenTotal(s))));
494
+ if (s.attempts !== void 0) attrs.push(attr("aipack.attempts", int64(s.attempts)));
495
+ if (s.errorClass) attrs.push(attr("aipack.error_class", str(s.errorClass)));
496
+ return {
497
+ traceId: bytesOf(s.traceId, 16).toString("base64"),
498
+ spanId: bytesOf(s.spanId, 8).toString("base64"),
499
+ name: s.name,
500
+ kind: 1,
501
+ // INTERNAL
502
+ startTimeUnixNano: startNs.toString(),
503
+ endTimeUnixNano: endNs.toString(),
504
+ attributes: attrs,
505
+ status: ok ? { code: 1 } : { code: 2, message: s.errorClass || s.status }
506
+ };
507
+ }
508
+ function toOtlpJsonTraces(batch, serviceName, appId) {
509
+ const spans = [];
510
+ for (const r of batch.runs) spans.push(runToSpan(r, appId));
511
+ for (const s of batch.spans) spans.push(spanToSpan(s, appId));
512
+ return {
513
+ resourceSpans: [
514
+ {
515
+ resource: {
516
+ attributes: [
517
+ { key: "service.name", value: str(serviceName) },
518
+ { key: "telemetry.sdk.name", value: str("aipack") },
519
+ { key: "telemetry.sdk.language", value: str("nodejs") }
520
+ ]
521
+ },
522
+ scopeSpans: [
523
+ {
524
+ scope: { name: "aipack", version: "0.1.0" },
525
+ spans
526
+ }
527
+ ]
528
+ }
529
+ ]
530
+ };
531
+ }
532
+ function createOtlpTraceExporter(opts) {
533
+ const endpoint = opts.endpoint.replace(/\/+$/, "");
534
+ const serviceName = opts.serviceName ?? "aipack-app";
535
+ const timeoutMs = opts.timeoutMs ?? 3e3;
536
+ const headers = { "content-type": "application/json", ...opts.headers };
537
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
538
+ return {
539
+ async export(batch) {
540
+ const runs = batch.runs.length;
541
+ const spans = batch.spans.length;
542
+ if (!runs && !spans) return true;
543
+ try {
544
+ const body = JSON.stringify(toOtlpJsonTraces(batch, serviceName, opts.appId));
545
+ const ac = new AbortController();
546
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
547
+ let res;
548
+ try {
549
+ res = await fetchImpl(`${endpoint}/v1/traces`, {
550
+ method: "POST",
551
+ headers,
552
+ body,
553
+ signal: ac.signal
554
+ });
555
+ } finally {
556
+ clearTimeout(timer);
557
+ }
558
+ if (res.ok) return true;
559
+ console.warn(`[aipack/observability] OTLP \u5BFC\u51FA\u88AB\u62D2\u7EDD(${res.status})\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u4E0D\u5F71\u54CD\u4E3B\u4E0A\u62A5\uFF09`);
560
+ return false;
561
+ } catch (err) {
562
+ console.warn("[aipack/observability] OTLP \u5BFC\u51FA\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u4E3B\u4E0A\u62A5\uFF09:", err.message);
563
+ return false;
564
+ }
565
+ }
566
+ };
567
+ }
568
+
569
+ // src/index.ts
570
+ function createObservability(opts) {
571
+ const reporter = new HttpReporter({
572
+ endpoint: opts.endpoint ?? "http://localhost:8787",
573
+ appId: opts.appId,
574
+ appSecret: opts.appSecret,
575
+ cacheDir: opts.cacheDir,
576
+ maxCacheSize: opts.maxCacheSize
577
+ });
578
+ const sink = reporter;
579
+ if (opts.otlp) {
580
+ const exporter = createOtlpTraceExporter({
581
+ endpoint: opts.otlp.endpoint,
582
+ serviceName: opts.otlp.serviceName ?? opts.appId,
583
+ appId: opts.appId,
584
+ headers: opts.otlp.headers
585
+ });
586
+ sink.send = async (batch) => {
587
+ const ok = await reporter.send(batch);
588
+ await exporter.export(batch);
589
+ return ok;
590
+ };
591
+ }
592
+ const telemetry = new ObservabilityTelemetry(sink, {
593
+ intervalMs: opts.flushIntervalMs,
594
+ batchSize: opts.flushBatchSize,
595
+ sampleRate: opts.sampleRate,
596
+ redact: opts.redact,
597
+ appVersion: opts.version
598
+ });
599
+ const logger = createLogger({
600
+ tags: { app: opts.appId },
601
+ context: () => telemetry.currentContext()
602
+ });
603
+ return {
604
+ telemetry,
605
+ logger,
606
+ emit: (name, data, emitOpts) => telemetry.emit(name, data, emitOpts),
607
+ flush: () => telemetry.flush(),
608
+ close: async () => {
609
+ await telemetry.close();
610
+ }
611
+ };
612
+ }
613
+ export {
614
+ HttpReporter,
615
+ ObservabilityTelemetry,
616
+ createLogger,
617
+ createObservability,
618
+ createOtlpTraceExporter,
619
+ toOtlpJsonTraces
620
+ };
621
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporter.ts","../src/telemetry.ts","../src/logger.ts","../src/otlp.ts","../src/index.ts"],"sourcesContent":["/**\n * HttpReporter — 把批量事件上报到收集服务(埋点模式)。\n *\n * - POST {endpoint}/api/v1/ingest,携带 x-app-id / x-app-secret 鉴权头\n * - 上报失败(网络错误 / 5xx / 429)→ 写入本地缓存文件({cacheDir}/{appId}.json),\n * 下次 send 前先补报缓存,全部成功才删除\n * - 4xx(鉴权失败、参数错误)判定为不可重试,直接丢弃并 warn,避免缓存无限重试\n * - 缓存条数上限 maxCacheSize,超出后丢弃最旧记录\n * - 并发保护:send 串行执行,重复调用合并为一次\n */\n\nimport { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { EventBatch } from './types';\n\nexport interface ReporterOptions {\n /** 收集服务地址,如 http://localhost:8787(必填) */\n endpoint: string;\n appId: string;\n appSecret: string;\n /** 缓存目录,默认 ./.aipack/observability */\n cacheDir?: string;\n /** 缓存条数上限(含 runs/spans/toolCalls/permissions),默认 2000 */\n maxCacheSize?: number;\n /** 上报超时(ms),默认 5000 */\n timeoutMs?: number;\n /** 便于测试注入;默认全局 fetch */\n fetchImpl?: typeof fetch;\n}\n\nconst EMPTY_BATCH = (): EventBatch => ({\n runs: [],\n spans: [],\n toolCalls: [],\n permissions: [],\n retries: [],\n events: [],\n});\n\nexport class HttpReporter {\n private endpoint: string;\n private appId: string;\n private appSecret: string;\n private cachePath: string;\n private maxCacheSize: number;\n private timeoutMs: number;\n private fetchImpl: typeof fetch;\n /** 串行发送锁:true 表示有发送在进行中 */\n private sending = false;\n /** 发送期间新到的批次,发送完成后继续补发 */\n private pending: EventBatch[] = [];\n\n constructor(opts: ReporterOptions) {\n this.endpoint = opts.endpoint.replace(/\\/+$/, '');\n this.appId = opts.appId;\n this.appSecret = opts.appSecret;\n this.cachePath = path.join(\n opts.cacheDir ?? path.join(process.cwd(), '.aipack', 'observability'),\n `${opts.appId}.json`,\n );\n this.maxCacheSize = opts.maxCacheSize ?? 2000;\n this.timeoutMs = opts.timeoutMs ?? 5000;\n this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n }\n\n /** 上报批次;失败自动落缓存,返回是否成功(测试断言用) */\n async send(batch: EventBatch): Promise<boolean> {\n this.pending.push(batch);\n if (this.sending) return false; // 已有一轮发送在跑,数据会被顺带带走\n this.sending = true;\n try {\n return await this.drain();\n } finally {\n this.sending = false;\n }\n }\n\n /** 排空所有批次(含缓存补报)。返回最后一轮是否成功 */\n private async drain(): Promise<boolean> {\n let lastOk = false;\n for (;;) {\n const cache = await this.readCache();\n const batch = mergeBatches(cache, ...this.pending);\n this.pending = [];\n if (isEmpty(batch)) return lastOk;\n\n lastOk = await this.tryUpload(batch);\n if (!lastOk) {\n // 失败 → 写回缓存,等待下次 flush\n await this.writeCache(batch);\n return false;\n }\n // 成功 → 清缓存\n await this.removeCache();\n }\n }\n\n /** 尝试上报;返回是否成功(4xx 视为最终失败,不再重试) */\n private async tryUpload(batch: EventBatch): Promise<boolean> {\n try {\n const ac = new AbortController();\n const timer = setTimeout(() => ac.abort(), this.timeoutMs);\n try {\n const res = await this.fetchImpl(`${this.endpoint}/api/v1/ingest`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-app-id': this.appId,\n 'x-app-secret': this.appSecret,\n },\n body: JSON.stringify({ appId: this.appId, ...batch }),\n signal: ac.signal,\n });\n if (res.ok) return true;\n if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n // 鉴权/参数错误:重试无意义,丢弃\n console.warn(\n `[aipack/observability] 上报被拒绝(${res.status}),丢弃 ${count(batch)} 条。检查 appId/appSecret 与 endpoint。`,\n );\n return true; // 视为已处理(丢弃),不再缓存\n }\n // 5xx / 429:可重试\n console.warn(`[aipack/observability] 上报失败(${res.status}),将缓存补报。`);\n return false;\n } finally {\n clearTimeout(timer);\n }\n } catch (err) {\n // 网络错误 / 超时:可重试\n console.warn('[aipack/observability] 上报网络错误,将缓存补报:', (err as Error).message);\n return false;\n }\n }\n\n // ─── 本地缓存 ──────────────────────────────────────────────────\n\n private async readCache(): Promise<EventBatch> {\n try {\n const raw = await fs.readFile(this.cachePath, 'utf8');\n const parsed = JSON.parse(raw) as EventBatch;\n return {\n runs: Array.isArray(parsed.runs) ? parsed.runs : [],\n spans: Array.isArray(parsed.spans) ? parsed.spans : [],\n toolCalls: Array.isArray(parsed.toolCalls) ? parsed.toolCalls : [],\n permissions: Array.isArray(parsed.permissions) ? parsed.permissions : [],\n retries: Array.isArray(parsed.retries) ? parsed.retries : [],\n events: Array.isArray(parsed.events) ? parsed.events : [],\n };\n } catch {\n return EMPTY_BATCH();\n }\n }\n\n private async writeCache(batch: EventBatch): Promise<void> {\n try {\n await fs.mkdir(path.dirname(this.cachePath), { recursive: true });\n await fs.writeFile(this.cachePath, JSON.stringify(trimBatch(batch, this.maxCacheSize)), 'utf8');\n } catch (err) {\n console.warn('[aipack/observability] 缓存写入失败(数据丢弃):', (err as Error).message);\n }\n }\n\n private async removeCache(): Promise<void> {\n try {\n await fs.unlink(this.cachePath);\n } catch {\n // 无缓存文件属正常\n }\n }\n}\n\n// ─── 工具 ─────────────────────────────────────────────────────────\n\nfunction isEmpty(batch: EventBatch): boolean {\n return (\n !batch.runs.length &&\n !batch.spans.length &&\n !batch.toolCalls.length &&\n !batch.permissions.length &&\n !batch.retries.length &&\n !batch.events.length\n );\n}\n\nfunction count(batch: EventBatch): number {\n return (\n batch.runs.length +\n batch.spans.length +\n batch.toolCalls.length +\n batch.permissions.length +\n batch.retries.length +\n batch.events.length\n );\n}\n\nfunction mergeBatches(...batches: EventBatch[]): EventBatch {\n const out = EMPTY_BATCH();\n for (const b of batches) {\n out.runs.push(...b.runs);\n out.spans.push(...b.spans);\n out.toolCalls.push(...b.toolCalls);\n out.permissions.push(...b.permissions);\n out.retries.push(...b.retries);\n out.events.push(...b.events);\n }\n return out;\n}\n\n/** 超过上限时按 runs → spans → toolCalls → permissions → retries → events 顺序从头部裁剪(保留最新) */\nfunction trimBatch(batch: EventBatch, max: number): EventBatch {\n const total = count(batch);\n if (total <= max) return batch;\n let excess = total - max;\n const out: EventBatch = {\n ...batch,\n runs: [...batch.runs],\n spans: [...batch.spans],\n toolCalls: [...batch.toolCalls],\n permissions: [...batch.permissions],\n retries: [...batch.retries],\n events: [...batch.events],\n };\n for (const key of ['runs', 'spans', 'toolCalls', 'permissions', 'retries', 'events'] as const) {\n if (excess <= 0) break;\n const removed = Math.min(out[key].length, excess);\n out[key].splice(0, removed);\n excess -= removed;\n }\n return out;\n}\n","/**\n * Telemetry 接口实现(客户端 SDK):事件 → 原始记录 → 上报队列 → HttpReporter。\n *\n * 埋点模式:不在本地聚合/落盘,事件同步转成 RunRecord / SpanRecord / ToolCallRecord /\n * PermissionRecord 后入队,由定时器批量 POST 到收集服务。上报失败由 reporter 缓存补报,\n * 事件路径零阻塞、失败不阻断 run()。\n *\n * 载荷无 startedAt 字段:用「到达时刻 - durationMs」推导;run 级用 onRunStart.queuedAt。\n *\n * 注意:runtime 的 emitTelemetry 会解构方法后调用(fn(info)),故所有回调必须为\n * 箭头函数属性以绑定 this,否则 this.queued / this.startedAt 为 undefined。\n */\n\nimport type {\n Telemetry,\n RunStartTelemetryInfo,\n RunTelemetryInfo,\n ToolTelemetryInfo,\n ModelTelemetryInfo,\n RetryTelemetryInfo,\n PermissionDeniedTelemetryInfo,\n} from '@aipack-ai/agent';\nimport type { RunRecord, SpanRecord, ToolCallRecord, PermissionRecord, EventBatch } from './types';\n\nexport interface FlushQueueOptions {\n /** 上报周期(ms),默认 5000 */\n intervalMs?: number;\n /** 积攒条数触发(含 runs/spans/toolCalls/permissions 总数),默认 50 */\n batchSize?: number;\n /** P2-1 明细采样率(0–1):只作用于 model/tool spans 与 toolCalls;runs/permissions/events 全量。缺省 1(全量) */\n sampleRate?: number;\n /** P2-1 脱敏钩子:send 前对整批改写(防 PII 明文上报) */\n redact?: (batch: EventBatch) => EventBatch;\n /** 发布版本:注入每条 run 记录(RunRecord.appVersion),供收集端按版本聚合。缺省不携带 */\n appVersion?: string;\n}\n\nconst emptyBatch = (): EventBatch => ({\n runs: [],\n spans: [],\n toolCalls: [],\n permissions: [],\n retries: [],\n events: [],\n});\n\nexport class ObservabilityTelemetry implements Telemetry {\n private queued: EventBatch = emptyBatch();\n private timer: NodeJS.Timeout | undefined;\n private startedAt = new Map<string, number>(); // traceId -> queuedAt\n /** in-flight run 顺序(最近开始的排最后),供 logger 关联当前 traceId */\n private inFlightOrder: string[] = [];\n private batchSize: number;\n private sampleRate: number;\n private redact?: (batch: EventBatch) => EventBatch;\n private appVersion?: string;\n\n constructor(private reporter: { send(batch: EventBatch): Promise<boolean> }, opts: FlushQueueOptions = {}) {\n this.batchSize = opts.batchSize ?? 50;\n this.sampleRate = opts.sampleRate ?? 1;\n this.redact = opts.redact;\n this.appVersion = opts.appVersion;\n const intervalMs = opts.intervalMs ?? 5000;\n this.timer = setInterval(() => void this.flush(), intervalMs);\n this.timer.unref?.();\n }\n\n onRunStart = (info: RunStartTelemetryInfo): void => {\n this.startedAt.set(info.traceId, info.queuedAt);\n this.inFlightOrder = this.inFlightOrder.filter((id) => id !== info.traceId);\n this.inFlightOrder.push(info.traceId);\n };\n\n onRunEnd = (info: RunTelemetryInfo): void => {\n const queuedAt = this.startedAt.get(info.traceId) ?? Date.now() - info.durationMs;\n this.startedAt.delete(info.traceId);\n this.inFlightOrder = this.inFlightOrder.filter((id) => id !== info.traceId);\n\n this.queued.runs.push(this.runRecord(info, queuedAt));\n this.queued.spans.push(this.runSpan(info, queuedAt));\n this.maybeFlush();\n };\n\n onToolCall = (info: ToolTelemetryInfo): void => {\n if (!this.sample()) return; // P2-1 明细采样\n const startedAt = Date.now() - info.durationMs;\n this.queued.spans.push({\n traceId: info.traceId,\n spanId: info.spanId,\n kind: 'tool',\n name: `tool:${info.toolName}`,\n startedAt,\n durationMs: info.durationMs,\n status: info.status === 'ok' ? 'ok' : 'error',\n errorClass: info.errorClass,\n sessionKey: info.sessionKey,\n });\n this.queued.toolCalls.push({\n traceId: info.traceId,\n spanId: info.spanId,\n toolName: info.toolName,\n status: info.status,\n durationMs: info.durationMs,\n errorClass: info.errorClass,\n });\n this.maybeFlush();\n };\n\n onModelCall = (info: ModelTelemetryInfo): void => {\n if (!this.sample()) return; // P2-1 明细采样\n this.queued.spans.push({\n traceId: info.traceId,\n spanId: info.spanId,\n kind: 'model',\n name: `model:${info.modelId}`,\n startedAt: Date.now() - info.durationMs,\n durationMs: info.durationMs,\n status: info.errorClass ? 'error' : 'ok',\n errorClass: info.errorClass,\n attempts: info.attempts,\n inputTokens: info.inputTokens,\n outputTokens: info.outputTokens,\n cacheRead: info.cacheRead,\n cacheWrite: info.cacheWrite,\n sessionKey: info.sessionKey,\n });\n this.maybeFlush();\n };\n\n onRetry = (info: RetryTelemetryInfo): void => {\n // P2-2:per-attempt 重试明细(关联 model span),server 落 retry_attempts 表\n this.queued.retries.push({\n traceId: info.traceId,\n spanId: info.spanId,\n provider: info.provider,\n modelId: info.modelId,\n attempt: info.attempt,\n errorClass: info.errorClass,\n status: info.status,\n delayMs: info.delayMs,\n timestamp: Date.now(),\n });\n this.maybeFlush();\n };\n\n onPermissionDenied = (info: PermissionDeniedTelemetryInfo): void => {\n this.queued.permissions.push({\n traceId: info.traceId,\n sessionKey: info.sessionKey,\n toolName: info.toolName,\n reason: info.reason,\n timestamp: Date.now(),\n });\n this.maybeFlush();\n };\n\n /**\n * 当前 run 上下文(供结构化 logger 注入 traceId)。\n * 单并发时返回唯一 in-flight run;并发时返回最近开始的(约定语义,文档说明)。\n */\n currentContext(): { traceId?: string } {\n const traceId = this.inFlightOrder[this.inFlightOrder.length - 1];\n return traceId ? { traceId } : {};\n }\n\n /**\n * P2-1 自定义业务事件(通用埋点)。\n * run 内调用自动注入当前 traceId;run 外可通过 opts 显式传 traceId/sessionKey。\n */\n emit(name: string, data?: unknown, opts: { traceId?: string; sessionKey?: string } = {}): void {\n this.queued.events.push({\n traceId: opts.traceId ?? this.currentContext().traceId,\n sessionKey: opts.sessionKey,\n name,\n data,\n timestamp: Date.now(),\n });\n this.maybeFlush();\n }\n\n /** 立即上报队列中残留的数据(fire-and-forget,失败由 reporter 缓存补报) */\n flush(): void {\n if (this.isEmpty()) return;\n const batch = this.queued;\n this.queued = emptyBatch();\n void this.reporter.send(this.redact ? this.redact(batch) : batch);\n }\n\n /** 停止定时器并等残留上报完成(进程退出前调用) */\n async close(): Promise<void> {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = undefined;\n }\n if (this.isEmpty()) return;\n const batch = this.queued;\n this.queued = emptyBatch();\n await this.reporter.send(this.redact ? this.redact(batch) : batch);\n }\n\n /** P2-1 明细采样判定:rate>=1 恒过、<=0 恒弃、否则按概率 */\n private sample(): boolean {\n if (this.sampleRate >= 1) return true;\n if (this.sampleRate <= 0) return false;\n return Math.random() < this.sampleRate;\n }\n\n private runRecord(info: RunTelemetryInfo, queuedAt: number): RunRecord {\n return {\n traceId: info.traceId,\n startedAt: queuedAt,\n endedAt: queuedAt + info.durationMs,\n sessionKey: info.sessionKey,\n channel: info.request.channel,\n model: info.request.model,\n status: info.success\n ? 'success'\n : info.errorClass === 'validation'\n ? 'validation'\n : 'error',\n errorClass: info.errorClass,\n turns: info.turnCount,\n durationMs: info.durationMs,\n activeMs: info.activeMs,\n queuedMs: info.queuedMs,\n ttftMs: info.ttftMs,\n inputTokens: info.tokens.input,\n outputTokens: info.tokens.output,\n cacheRead: info.tokens.cacheRead,\n cacheWrite: info.tokens.cacheWrite,\n ...(this.appVersion !== undefined ? { appVersion: this.appVersion } : {}),\n };\n }\n\n private runSpan(info: RunTelemetryInfo, queuedAt: number): SpanRecord {\n return {\n traceId: info.traceId,\n spanId: info.traceId,\n kind: 'run',\n name: 'run',\n startedAt: queuedAt,\n durationMs: info.durationMs,\n status: info.success ? 'ok' : 'error',\n errorClass: info.errorClass,\n inputTokens: info.tokens.input,\n outputTokens: info.tokens.output,\n sessionKey: info.sessionKey,\n };\n }\n\n private isEmpty(): boolean {\n return (\n !this.queued.runs.length &&\n !this.queued.spans.length &&\n !this.queued.toolCalls.length &&\n !this.queued.permissions.length &&\n !this.queued.retries.length &&\n !this.queued.events.length\n );\n }\n\n private maybeFlush(): void {\n const total =\n this.queued.runs.length +\n this.queued.spans.length +\n this.queued.toolCalls.length +\n this.queued.permissions.length +\n this.queued.retries.length +\n this.queued.events.length;\n if (total >= this.batchSize) this.flush();\n }\n}\n","/**\n * 结构化 logger(P1-1 日志关联)。\n *\n * 约定:logfmt 或 JSON 输出,可通过 context() 动态注入当前上下文(如 traceId),\n * 使应用日志与 Trace 关联(排障时按 traceId 检索日志流)。\n *\n * const logger = createLogger({ format: 'logfmt', context: () => telemetry.currentContext() });\n * logger.info('tool called', { tool: 'search_flights', durationMs: 120 });\n * // => time=2026-08-11T10:00:00.000Z level=info msg=\"tool called\" tool=search_flights durationMs=120\n *\n * 与收集服务 webhook/告警日志不同:本 logger 只负责\"应用日志\"输出约定,\n * 不参与上报(上报走 telemetry)。\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\nexport type LogFormat = 'logfmt' | 'json';\n\nexport interface LoggerOptions {\n /** 输出格式,默认 logfmt */\n format?: LogFormat;\n /** 最低输出级别,默认 info */\n level?: LogLevel;\n /** 每行日志的固定字段(如 service/app),会被动态 context / 单行 fields 覆盖 */\n tags?: Record<string, unknown>;\n /** 动态上下文(每行调用),例如 () => telemetry.currentContext() 注入 traceId */\n context?: () => Record<string, unknown> | undefined;\n /** 输出目标,默认 console(line 已含换行) */\n dest?: (line: string) => void;\n /** 是否注入 ISO 时间戳字段 time,默认 true */\n timestamps?: boolean;\n}\n\nexport interface Logger {\n debug(msg: string, fields?: Record<string, unknown>): void;\n info(msg: string, fields?: Record<string, unknown>): void;\n warn(msg: string, fields?: Record<string, unknown>): void;\n error(msg: string, fields?: Record<string, unknown>): void;\n /** 派生子 logger:合并部分配置(如固定业务标签) */\n child(overrides: Partial<LoggerOptions>): Logger;\n}\n\nconst LEVEL_ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };\n\n/** 隐藏敏感字段名(值以 *** 代替),防止 token/key 明文进日志 */\nconst REDACTED_KEYS = /(secret|token|password|key|authorization|apikey)/i;\n\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const { format = 'logfmt', dest = defaultDest } = opts;\n const minLevel = LEVEL_ORDER[opts.level ?? 'info'];\n const timestamps = opts.timestamps ?? true;\n\n const emit = (level: LogLevel, msg: string, fields?: Record<string, unknown>): void => {\n if (LEVEL_ORDER[level] < minLevel) return;\n const context = opts.context?.() ?? {};\n dest(formatLine({ format, level, msg, fields, context, tags: opts.tags, timestamps }));\n };\n\n return {\n debug: (msg, fields) => emit('debug', msg, fields),\n info: (msg, fields) => emit('info', msg, fields),\n warn: (msg, fields) => emit('warn', msg, fields),\n error: (msg, fields) => emit('error', msg, fields),\n child: (overrides) =>\n createLogger({\n format: overrides.format ?? opts.format,\n level: overrides.level ?? opts.level,\n tags: { ...opts.tags, ...overrides.tags },\n context: overrides.context ?? opts.context,\n dest: overrides.dest ?? opts.dest,\n timestamps: overrides.timestamps ?? opts.timestamps,\n }),\n };\n}\n\ninterface FormatInput {\n format: LogFormat;\n level: LogLevel;\n msg: string;\n fields?: Record<string, unknown>;\n context?: Record<string, unknown>;\n tags?: Record<string, unknown>;\n timestamps: boolean;\n}\n\nfunction formatLine(input: FormatInput): string {\n const data: Record<string, unknown> = {};\n if (input.timestamps) data.time = new Date().toISOString();\n data.level = input.level;\n data.msg = input.msg;\n // 顺序:tags → context → fields(后者覆盖前者)\n for (const src of [input.tags, input.context, input.fields]) {\n if (!src) continue;\n for (const [k, v] of Object.entries(src)) {\n data[k] = REDACTED_KEYS.test(k) ? '***' : v;\n }\n }\n return input.format === 'json' ? JSON.stringify(data) : toLogfmt(data);\n}\n\n/** logfmt 渲染:值含空白/引号/等号/反斜杠时按 RFC 引号包裹并转义 */\nfunction toLogfmt(data: Record<string, unknown>): string {\n const parts: string[] = [];\n for (const [k, v] of Object.entries(data)) {\n if (v === undefined || v === null) continue;\n parts.push(`${k}=${logfmtValue(v)}`);\n }\n return parts.join(' ');\n}\n\nfunction logfmtValue(v: unknown): string {\n if (typeof v === 'string') return quoteLogfmt(v);\n if (typeof v === 'number' || typeof v === 'boolean') return String(v);\n if (typeof v === 'bigint') return v.toString();\n // 对象/数组 → JSON 序列化后引号包裹\n return quoteLogfmt(JSON.stringify(v));\n}\n\nfunction quoteLogfmt(s: string): string {\n if (!/[ \\t\"=\\\\]/u.test(s)) return s;\n return '\"' + s.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"').replace(/\\n/g, '\\\\n') + '\"';\n}\n\nfunction defaultDest(line: string): void {\n // 走 console.log 而非 process.stdout.write,便于宿主拦截/重定向\n console.log(line);\n}\n","/**\n * OTLP/HTTP JSON trace exporter(P1-2 标准协议,档位 B 起步)。\n *\n * 无外部依赖:直接把 EventBatch 映射为 OTLP JSON ExportTraceServiceRequest\n * (protobuf JSON 编码),POST 到 OpenTelemetry Collector 的 /v1/traces。\n *\n * - traceId/spanId:OTLP 要求 16/8 字节二进制(base64),本 SDK 的 traceId 是任意\n * 字符串,故用 md5 做确定性映射(同一字符串恒得同一字节序列);\n * - 导出为 best-effort:失败仅 warn,不影响主上报链路(主链路仍走 HttpReporter);\n * - 未配置 otlp 时零开销(不构造 exporter)。\n */\n\nimport { createHash } from 'node:crypto';\nimport type { EventBatch, RunRecord, SpanRecord } from './types';\n\nexport interface OtlpExporterOptions {\n /** OTLP/HTTP JSON 端点,如 http://otel-collector:4318(POST {endpoint}/v1/traces) */\n endpoint: string;\n /** resource.service.name,默认 = appId */\n serviceName?: string;\n /** 应用标识(写入 span attribute aipack.app,便于按应用过滤) */\n appId?: string;\n /** 额外请求头(如 x-otlp-token) */\n headers?: Record<string, string>;\n /** 超时(ms),默认 3000 */\n timeoutMs?: number;\n /** 便于测试注入;默认全局 fetch */\n fetchImpl?: typeof fetch;\n}\n\nexport interface OtlpTraceExporter {\n /** 导出批次;失败内部消化,恒不抛错,返回是否成功 */\n export(batch: EventBatch): Promise<boolean>;\n}\n\n/** md5 确定性映射为 N 字节(traceId=16B / spanId=8B) */\nfunction bytesOf(input: string, n: number): Buffer {\n return createHash('md5').update(input).digest().subarray(0, n);\n}\n\nfunction str(value: unknown): { stringValue: string } {\n return { stringValue: value == null ? '' : String(value) };\n}\nfunction int64(value: number): { intValue: string } {\n return { intValue: String(Math.round(value || 0)) };\n}\n\nfunction attr(key: string, value: { stringValue?: string; intValue?: string }): object {\n return { key, value };\n}\n\n/** token 总消耗(input + output + cacheRead + cacheWrite);无 token 数据时返回 undefined */\nfunction tokenTotal(r: { inputTokens?: number; outputTokens?: number; cacheRead?: number; cacheWrite?: number }): number | undefined {\n if (r.inputTokens === undefined && r.outputTokens === undefined) return undefined;\n return (r.inputTokens ?? 0) + (r.outputTokens ?? 0) + (r.cacheRead ?? 0) + (r.cacheWrite ?? 0);\n}\n\n/** RunRecord → OTLP span(根 span,kind=INTERNAL) */\nfunction runToSpan(r: RunRecord, appId?: string): object {\n const startNs = BigInt(r.startedAt) * 1_000_000n;\n const endNs = BigInt(r.startedAt + r.durationMs) * 1_000_000n;\n const ok = r.status === 'success';\n const attrs: object[] = [\n attr('session.key', str(r.sessionKey)),\n attr('aipack.status', str(r.status)),\n attr('aipack.turns', int64(r.turns)),\n attr('aipack.duration_ms', int64(r.durationMs)),\n attr('tokens.input', int64(r.inputTokens)),\n attr('tokens.output', int64(r.outputTokens)),\n ];\n if (tokenTotal(r) !== undefined) attrs.push(attr('tokens.total', int64(tokenTotal(r)!)));\n if (appId) attrs.push(attr('aipack.app', str(appId)));\n if (r.model) attrs.push(attr('model', str(r.model)));\n if (r.errorClass) attrs.push(attr('aipack.error_class', str(r.errorClass)));\n return {\n traceId: bytesOf(r.traceId, 16).toString('base64'),\n spanId: bytesOf(`${r.traceId}:run`, 8).toString('base64'),\n name: 'agent.run',\n kind: 1, // INTERNAL\n startTimeUnixNano: startNs.toString(),\n endTimeUnixNano: endNs.toString(),\n attributes: attrs,\n status: ok ? { code: 1 } : { code: 2, message: r.errorClass || r.status },\n };\n}\n\n/** SpanRecord → OTLP span */\nfunction spanToSpan(s: SpanRecord, appId?: string): object {\n const startNs = BigInt(s.startedAt) * 1_000_000n;\n const endNs = BigInt(s.startedAt + s.durationMs) * 1_000_000n;\n const ok = s.status === 'ok';\n const attrs: object[] = [\n attr('span.kind', str(s.kind)),\n attr('session.key', str(s.sessionKey)),\n attr('aipack.status', str(s.status)),\n attr('aipack.duration_ms', int64(s.durationMs)),\n ];\n if (appId) attrs.push(attr('aipack.app', str(appId)));\n if (s.inputTokens !== undefined) attrs.push(attr('tokens.input', int64(s.inputTokens)));\n if (s.outputTokens !== undefined) attrs.push(attr('tokens.output', int64(s.outputTokens)));\n if (tokenTotal(s) !== undefined) attrs.push(attr('tokens.total', int64(tokenTotal(s)!)));\n if (s.attempts !== undefined) attrs.push(attr('aipack.attempts', int64(s.attempts)));\n if (s.errorClass) attrs.push(attr('aipack.error_class', str(s.errorClass)));\n return {\n traceId: bytesOf(s.traceId, 16).toString('base64'),\n spanId: bytesOf(s.spanId, 8).toString('base64'),\n name: s.name,\n kind: 1, // INTERNAL\n startTimeUnixNano: startNs.toString(),\n endTimeUnixNano: endNs.toString(),\n attributes: attrs,\n status: ok ? { code: 1 } : { code: 2, message: s.errorClass || s.status },\n };\n}\n\n/** EventBatch → OTLP ExportTraceServiceRequest(JSON 对象,可直接 JSON.stringify) */\nexport function toOtlpJsonTraces(batch: EventBatch, serviceName: string, appId?: string): object {\n const spans: object[] = [];\n for (const r of batch.runs) spans.push(runToSpan(r, appId));\n for (const s of batch.spans) spans.push(spanToSpan(s, appId));\n return {\n resourceSpans: [\n {\n resource: {\n attributes: [\n { key: 'service.name', value: str(serviceName) },\n { key: 'telemetry.sdk.name', value: str('aipack') },\n { key: 'telemetry.sdk.language', value: str('nodejs') },\n ],\n },\n scopeSpans: [\n {\n scope: { name: 'aipack', version: '0.1.0' },\n spans,\n },\n ],\n },\n ],\n };\n}\n\nexport function createOtlpTraceExporter(opts: OtlpExporterOptions): OtlpTraceExporter {\n const endpoint = opts.endpoint.replace(/\\/+$/, '');\n const serviceName = opts.serviceName ?? 'aipack-app';\n const timeoutMs = opts.timeoutMs ?? 3000;\n const headers = { 'content-type': 'application/json', ...opts.headers };\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n\n return {\n async export(batch) {\n const runs = batch.runs.length;\n const spans = batch.spans.length;\n if (!runs && !spans) return true; // 无 trace 数据(纯 tool/permission)→ 无操作\n try {\n const body = JSON.stringify(toOtlpJsonTraces(batch, serviceName, opts.appId));\n const ac = new AbortController();\n const timer = setTimeout(() => ac.abort(), timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(`${endpoint}/v1/traces`, {\n method: 'POST',\n headers,\n body,\n signal: ac.signal,\n });\n } finally {\n clearTimeout(timer);\n }\n if (res.ok) return true;\n console.warn(`[aipack/observability] OTLP 导出被拒绝(${res.status}),已跳过(不影响主上报)`);\n return false;\n } catch (err) {\n console.warn('[aipack/observability] OTLP 导出失败(不影响主上报):', (err as Error).message);\n return false;\n }\n },\n };\n}\n","/**\n * @aipack-ai/observability — aipack 可观测性上报 SDK。\n *\n * 埋点上报模式:客户端只需 appId + appSecret,6 类 Telemetry 事件自动批量\n * POST 到收集服务(默认 http://localhost:8787),失败本地缓存补报。\n *\n * const obs = createObservability({ appId: 'my-app', appSecret: 'xxx' });\n * createRuntime({ ..., telemetry: obs.telemetry });\n *\n * 收集服务见 @aipack-ai/observability-server(独立包,SQLite 落盘 + 内存聚合 + REST API)。\n */\n\nimport { HttpReporter } from './reporter';\nimport { ObservabilityTelemetry } from './telemetry';\nimport { createLogger, type Logger } from './logger';\nimport { createOtlpTraceExporter } from './otlp';\n\nexport interface CreateObservabilityOptions {\n /** 应用标识(必填),收集端用它校验身份并隔离数据 */\n appId: string;\n /** 应用密钥(必填),与收集端 OBS_APPS 白名单匹配 */\n appSecret: string;\n /** 收集服务地址,默认 http://localhost:8787 */\n endpoint?: string;\n /** 上报失败本地缓存目录,默认 ./.aipack/observability */\n cacheDir?: string;\n /** 缓存条数上限,默认 2000 */\n maxCacheSize?: number;\n /** 上报周期(ms),默认 5000 */\n flushIntervalMs?: number;\n /** 积攒条数触发上报,默认 50 */\n flushBatchSize?: number;\n /** 可选:OTLP/HTTP JSON trace 导出(推 OpenTelemetry Collector),失败不影响主上报 */\n otlp?: {\n endpoint: string;\n serviceName?: string;\n headers?: Record<string, string>;\n };\n /** P2-1 明细采样率(0–1):只对 model/tool spans 与 toolCalls 采样,runs/permissions/events 全量。缺省 1 */\n sampleRate?: number;\n /** P2-1 脱敏钩子:send 前对整批改写(防 PII 明文上报),示例见 docs/observability-roadmap.md §P2-1 */\n redact?: (batch: import('./types').EventBatch) => import('./types').EventBatch;\n /** 发布版本(如 '1.3.0'),随每条 run 上报,供收集端按版本聚合对比。缺省不携带 */\n version?: string;\n}\n\nexport interface Observability {\n /** 注入 RuntimeOptions.telemetry */\n telemetry: ObservabilityTelemetry;\n /** 结构化 logger:自动注入当前 run 的 traceId(P1-1 日志关联) */\n logger: Logger;\n /** P2-1 自定义业务事件:run 内自动带 traceId,入 Trace 时间轴 */\n emit(name: string, data?: unknown, opts?: { traceId?: string; sessionKey?: string }): void;\n /** 立即上报队列中的残留数据 */\n flush(): void;\n /** 停止定时器并等残留上报完成(进程退出前调用) */\n close(): Promise<void>;\n}\n\nexport function createObservability(opts: CreateObservabilityOptions): Observability {\n const reporter = new HttpReporter({\n endpoint: opts.endpoint ?? 'http://localhost:8787',\n appId: opts.appId,\n appSecret: opts.appSecret,\n cacheDir: opts.cacheDir,\n maxCacheSize: opts.maxCacheSize,\n });\n\n // OTLP 导出作为旁路:await 但不影响主上报结果;exporter 内部恒不抛错\n const sink: { send(batch: import('./types').EventBatch): Promise<boolean> } = reporter;\n if (opts.otlp) {\n const exporter = createOtlpTraceExporter({\n endpoint: opts.otlp.endpoint,\n serviceName: opts.otlp.serviceName ?? opts.appId,\n appId: opts.appId,\n headers: opts.otlp.headers,\n });\n sink.send = async (batch) => {\n const ok = await reporter.send(batch);\n await exporter.export(batch);\n return ok;\n };\n }\n\n const telemetry = new ObservabilityTelemetry(sink, {\n intervalMs: opts.flushIntervalMs,\n batchSize: opts.flushBatchSize,\n sampleRate: opts.sampleRate,\n redact: opts.redact,\n appVersion: opts.version,\n });\n const logger = createLogger({\n tags: { app: opts.appId },\n context: () => telemetry.currentContext(),\n });\n\n return {\n telemetry,\n logger,\n emit: (name, data, emitOpts) => telemetry.emit(name, data, emitOpts),\n flush: () => telemetry.flush(),\n close: async () => {\n await telemetry.close();\n },\n };\n}\n\n// ─── 类型导出 ─────────────────────────────────────────────────────\n\nexport { ObservabilityTelemetry } from './telemetry';\nexport type { FlushQueueOptions } from './telemetry';\nexport { HttpReporter } from './reporter';\nexport type { ReporterOptions } from './reporter';\nexport { createLogger } from './logger';\nexport type { Logger, LoggerOptions, LogLevel, LogFormat } from './logger';\nexport { createOtlpTraceExporter, toOtlpJsonTraces } from './otlp';\nexport type { OtlpTraceExporter, OtlpExporterOptions } from './otlp';\nexport type {\n RunRecord,\n SpanRecord,\n ToolCallRecord,\n PermissionRecord,\n EventRecord,\n RetryRecord,\n EventBatch,\n} from './types';\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@aipack-ai/observability",
3
+ "version": "0.0.1",
4
+ "description": "aipack 可观测性上报 SDK(S2):埋点上报模式,appId+appSecret 一行接入,失败本地缓存补报",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "keywords": [
20
+ "aipack",
21
+ "observability",
22
+ "telemetry",
23
+ "metrics",
24
+ "trace"
25
+ ],
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "peerDependencies": {
31
+ "@aipack-ai/agent": "0.0.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.12.0",
35
+ "tsup": "^8.5.1",
36
+ "tsx": "^4.16.0",
37
+ "typescript": "^5.5.0",
38
+ "@aipack-ai/agent": "0.0.1"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "prebuild": "pnpm --filter @aipack-ai/agent build || true",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "node --import tsx --test test/observability.test.ts"
45
+ }
46
+ }