dsh-local-telemetry 0.1.0
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.
- package/CHANGELOG.md +82 -0
- package/DSH-TELEMETRY-/345/274/200/345/217/221/350/256/241/345/210/222.md +954 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +42 -0
- package/README.md +156 -0
- package/bin/telemetry.mjs +295 -0
- package/cordis.patch.yml +11 -0
- package/docs/configuration.md +148 -0
- package/docs/schema.md +165 -0
- package/examples/prices.json +16 -0
- package/examples/telemetry.json +17 -0
- package/package.json +62 -0
- package/plugin/index.js +82 -0
- package/skills/telemetry-runbook/SKILL.md +88 -0
- package/src/adapter.mjs +79 -0
- package/src/aggregate.mjs +677 -0
- package/src/config.mjs +199 -0
- package/src/cost.mjs +118 -0
- package/src/index.mjs +20 -0
- package/src/privacy.mjs +208 -0
- package/src/recorder.mjs +215 -0
- package/src/report.mjs +220 -0
- package/src/sampling.mjs +50 -0
- package/src/schema.mjs +246 -0
- package/src/server.mjs +163 -0
- package/src/sink-jsonl.mjs +368 -0
- package/src/sink-sqlite.mjs +382 -0
- package/src/store.mjs +346 -0
- package/web/app.js +337 -0
- package/web/index.html +87 -0
- package/web/style.css +158 -0
package/docs/schema.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# dsh-telemetry 事件契约(schema version 1.0)
|
|
2
|
+
|
|
3
|
+
本文档定义 dsh-telemetry 事件的 JSON 结构、字段含义与使用约束。所有事件必须通过 `validateEvent` 校验,未知指标为 `null` 而非 0。
|
|
4
|
+
|
|
5
|
+
## 事件列表(计划 §2)
|
|
6
|
+
|
|
7
|
+
| 事件名 | 含义 |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `request.started` | 用户请求开始(根 span) |
|
|
10
|
+
| `request.context` | 请求上下文(可选 metadata,仅 capture_metadata="safe" 时脱敏附加) |
|
|
11
|
+
| `model.requested` | 模型请求开始(attempt span) |
|
|
12
|
+
| `model.first_token` | 模型首 Token 到达 |
|
|
13
|
+
| `model.completed` | 模型请求成功完成 |
|
|
14
|
+
| `model.failed` | 模型请求失败 |
|
|
15
|
+
| `tool.started` | 工具调用开始 |
|
|
16
|
+
| `tool.completed` | 工具调用完成 |
|
|
17
|
+
| `plugin.started` | 插件 hook 开始 |
|
|
18
|
+
| `plugin.completed` | 插件 hook 完成 |
|
|
19
|
+
| `request.completed` | 用户请求成功完成 |
|
|
20
|
+
| `request.cancelled` | 用户请求被取消 |
|
|
21
|
+
|
|
22
|
+
**重要**:失败/超时/取消通过 `completed` 事件的 `result.status` 表达,无独立的 `tool.failed` 事件名。
|
|
23
|
+
|
|
24
|
+
## 最小事件结构
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"schema_version": "1.0",
|
|
29
|
+
"event": "model.completed",
|
|
30
|
+
"event_id": "evt-001",
|
|
31
|
+
"trace_id": "trace-001",
|
|
32
|
+
"span_id": "span-003",
|
|
33
|
+
"parent_id": "span-001",
|
|
34
|
+
"timestamp": "2026-08-23T12:00:00.000Z",
|
|
35
|
+
"duration_ms": 8420,
|
|
36
|
+
"session": { "profile": "web", "environment": "local" },
|
|
37
|
+
"model": {
|
|
38
|
+
"provider": "deepseek",
|
|
39
|
+
"name": "deepseek-chat",
|
|
40
|
+
"request_type": "chat"
|
|
41
|
+
},
|
|
42
|
+
"usage": {
|
|
43
|
+
"input_tokens": 4200,
|
|
44
|
+
"output_tokens": 860,
|
|
45
|
+
"cached_input_tokens": 0,
|
|
46
|
+
"reasoning_tokens": null
|
|
47
|
+
},
|
|
48
|
+
"result": {
|
|
49
|
+
"status": "success",
|
|
50
|
+
"finish_reason": "stop"
|
|
51
|
+
},
|
|
52
|
+
"privacy": {
|
|
53
|
+
"content_captured": false,
|
|
54
|
+
"redactions": 0
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 字段说明
|
|
60
|
+
|
|
61
|
+
### 顶层字段
|
|
62
|
+
|
|
63
|
+
| 字段 | 类型 | 必填 | 说明 |
|
|
64
|
+
| --- | --- | --- | --- |
|
|
65
|
+
| `schema_version` | string | 是 | 当前为 "1.0" |
|
|
66
|
+
| `event` | string | 是 | 事件名(必须为 12 种之一) |
|
|
67
|
+
| `event_id` | string | 是 | 事件唯一 ID(推荐 `evt-` + 12 hex) |
|
|
68
|
+
| `trace_id` | string | 是 | 请求追踪 ID(推荐 `trace-` + 12 hex) |
|
|
69
|
+
| `span_id` | string | 是 | 跨时间线的操作 ID(用于配对 started/completed) |
|
|
70
|
+
| `parent_id` | string | 否 | 父 span ID(构建树) |
|
|
71
|
+
| `request_id` | string | 否 | 用户请求 ID(可选,request 事件常用) |
|
|
72
|
+
| `timestamp` | string | 是 | ISO 8601 UTC(如 `2026-08-23T12:00:00.000Z`) |
|
|
73
|
+
| `duration_ms` | number | 否 | 毫秒整数(非负整数,未知则 null) |
|
|
74
|
+
| `session` | object | 否 | 请求会话信息 |
|
|
75
|
+
| `model` | object | 否 | 模型信息(model 事件) |
|
|
76
|
+
| `tool` | object | 否 | 工具信息(tool 事件) |
|
|
77
|
+
| `plugin` | object | 否 | 插件信息(plugin 事件) |
|
|
78
|
+
| `usage` | object | 否 | Token 使用量(model.completed 事件) |
|
|
79
|
+
| `result` | object | 否 | 结果状态(终止型事件) |
|
|
80
|
+
| `error` | object | 否 | 错误信息(失败/超时事件) |
|
|
81
|
+
| `sampling` | object | 否 | 采样元数据(写入事件,用于解释聚合结果) |
|
|
82
|
+
| `privacy` | object | 否 | 隐私元数据(content_captured/redactions/rules) |
|
|
83
|
+
| `metadata` | object | 否 | 可选脱敏后的安全元数据(仅 safe 模式) |
|
|
84
|
+
|
|
85
|
+
### session
|
|
86
|
+
|
|
87
|
+
| 字段 | 类型 | 说明 |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `profile` | string | Harness profile 名称(可配置哈希) |
|
|
90
|
+
| `environment` | string | 环境(如 local / prod) |
|
|
91
|
+
|
|
92
|
+
### model
|
|
93
|
+
|
|
94
|
+
| 字段 | 类型 | 说明 |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| `provider` | string | 提供商(如 deepseek / openai) |
|
|
97
|
+
| `name` | string | 模型名(如 deepseek-chat) |
|
|
98
|
+
| `request_type` | string | 请求类型(如 chat / completion) |
|
|
99
|
+
|
|
100
|
+
### tool
|
|
101
|
+
|
|
102
|
+
| 字段 | 类型 | 说明 |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `name` | string | 工具名 |
|
|
105
|
+
|
|
106
|
+
### plugin
|
|
107
|
+
|
|
108
|
+
| 字段 | 类型 | 说明 |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `name` | string | 插件名 |
|
|
111
|
+
| `hook` | string | Hook 名称(如 onRequest / onResponse) |
|
|
112
|
+
|
|
113
|
+
### usage
|
|
114
|
+
|
|
115
|
+
| 字段 | 类型 | 说明 |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `input_tokens` | number | 输入 Token 数(整数,无则为 null) |
|
|
118
|
+
| `output_tokens` | number | 输出 Token 数(整数,无则为 null) |
|
|
119
|
+
| `cached_input_tokens` | number | 缓存命中输入 Token 数(整数,无则为 null) |
|
|
120
|
+
| `reasoning_tokens` | number | 推理 Token 数(整数,无则为 null) |
|
|
121
|
+
|
|
122
|
+
### result
|
|
123
|
+
|
|
124
|
+
| 字段 | 类型 | 允许值 | 说明 |
|
|
125
|
+
| --- | --- | --- | --- |
|
|
126
|
+
| `status` | string | success / failed / timeout / cancelled | 操作结果 |
|
|
127
|
+
| `finish_reason` | string | - | 完成原因(仅 model.completed) |
|
|
128
|
+
|
|
129
|
+
### error
|
|
130
|
+
|
|
131
|
+
| 字段 | 类型 | 说明 |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `kind` | string | 错误分类(如 rate_limit / auth / network / server / invalid_request / timeout / unknown) |
|
|
134
|
+
|
|
135
|
+
### sampling
|
|
136
|
+
|
|
137
|
+
| 字段 | 类型 | 说明 |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| `rate` | number | 采样率(0..1) |
|
|
140
|
+
| `strategy` | string | 策略标识(per-trace-hash-v1) |
|
|
141
|
+
| `errors_always_sample` | boolean | 是否强制保留错误/取消 |
|
|
142
|
+
|
|
143
|
+
### privacy
|
|
144
|
+
|
|
145
|
+
| 字段 | 类型 | 说明 |
|
|
146
|
+
| --- | --- | --- |
|
|
147
|
+
| `content_captured` | boolean | 是否采集了内容字段(默认 false) |
|
|
148
|
+
| `redactions` | number | 脱敏命中次数 |
|
|
149
|
+
| `rules` | string[] | 命中的脱敏规则名列表 |
|
|
150
|
+
|
|
151
|
+
### metadata
|
|
152
|
+
|
|
153
|
+
- 仅当 `capture_metadata="safe"` 时附加。
|
|
154
|
+
- 脱敏后仅保留安全原语(数字/布尔/null/字符串)。
|
|
155
|
+
- 敏感键(Authorization/Cookie/token/password/secret/api_key 等)整键丢弃。
|
|
156
|
+
- 字符串值经过内置 + 自定义规则脱敏(URL 凭据、绝对路径、正则匹配)。
|
|
157
|
+
|
|
158
|
+
## 约束
|
|
159
|
+
|
|
160
|
+
- 时间戳必须为 ISO 8601 UTC(`YYYY-MM-DDThh:mm:ss.sssZ`)。
|
|
161
|
+
- `duration_ms` 为非负整数;未知时为 null(不补 0)。
|
|
162
|
+
- Token 字段为整数或 null;不可用 0 替代未知。
|
|
163
|
+
- 成本计算仅在 模型名 + usage + 价格目录 三者齐备时进行,否则 `cost: null` 并记录缺失原因。
|
|
164
|
+
- 采样按 trace 整体决策:同一 trace_id 的所有事件同决策,避免 trace 断裂。
|
|
165
|
+
- 默认不采集内容字段(prompt/response/文件正文/命令参数/环境变量/密钥)。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"currency": "USD",
|
|
3
|
+
"effective_at": "2026-08-23T00:00:00Z",
|
|
4
|
+
"models": {
|
|
5
|
+
"deepseek-chat": {
|
|
6
|
+
"input_per_million": 0.27,
|
|
7
|
+
"cached_input_per_million": 0.07,
|
|
8
|
+
"output_per_million": 1.1
|
|
9
|
+
},
|
|
10
|
+
"deepseek-reasoner": {
|
|
11
|
+
"input_per_million": 0.55,
|
|
12
|
+
"cached_input_per_million": 0.14,
|
|
13
|
+
"output_per_million": 2.19
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"enabled": true,
|
|
3
|
+
"store": "jsonl",
|
|
4
|
+
"path": "~/.dsh/telemetry",
|
|
5
|
+
"sample_rate": 1,
|
|
6
|
+
"errors_always_sample": true,
|
|
7
|
+
"slow_request_ms": 10000,
|
|
8
|
+
"capture_metadata": "none",
|
|
9
|
+
"hash_names": false,
|
|
10
|
+
"retention_days": 7,
|
|
11
|
+
"max_file_mb": 100,
|
|
12
|
+
"flush_interval_ms": 1000,
|
|
13
|
+
"batch_size": 100,
|
|
14
|
+
"max_queue_events": 1000,
|
|
15
|
+
"price_catalog": null,
|
|
16
|
+
"redact_rules": []
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-local-telemetry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "本地优先的 Harness 运行遥测插件:记录请求、模型、工具与插件生命周期指标(延迟、Token、成本、错误、缓存),默认不采集内容。Local-first Harness telemetry plugin for DeepSeek Harness. npm 包 dsh-local-telemetry(dsh-telemetry 名已被第三方占用)。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./plugin/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./plugin/index.js",
|
|
9
|
+
"./telemetry": "./src/index.mjs",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"telemetry": "./bin/telemetry.mjs",
|
|
14
|
+
"dsh-local-telemetry": "./bin/telemetry.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"plugin/index.js",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"skills/",
|
|
20
|
+
"src",
|
|
21
|
+
"bin",
|
|
22
|
+
"web",
|
|
23
|
+
"docs",
|
|
24
|
+
"examples",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"PUBLISHING.md",
|
|
29
|
+
"DSH-TELEMETRY-开发计划.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test test/schema.test.mjs test/config.test.mjs test/privacy.test.mjs test/cost.test.mjs test/sink.test.mjs test/recorder.test.mjs test/store.test.mjs test/aggregate.test.mjs test/cli.test.mjs test/server.test.mjs test/manifest.test.mjs",
|
|
33
|
+
"prepublishOnly": "npm test",
|
|
34
|
+
"check": "node --check bin/telemetry.mjs && node --check src/index.mjs && node --check src/schema.mjs && node --check src/config.mjs && node --check src/privacy.mjs && node --check src/sampling.mjs && node --check src/cost.mjs && node --check src/sink-jsonl.mjs && node --check src/sink-sqlite.mjs && node --check src/adapter.mjs && node --check src/recorder.mjs && node --check src/store.mjs && node --check src/aggregate.mjs && node --check src/report.mjs && node --check src/server.mjs && node --check plugin/index.js"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh-skill-filesystem": "*"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@deepseek-ai/dsh-skill-filesystem": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"dsh",
|
|
54
|
+
"dsh-plugin",
|
|
55
|
+
"deepseek-harness",
|
|
56
|
+
"telemetry",
|
|
57
|
+
"observability",
|
|
58
|
+
"local-first",
|
|
59
|
+
"privacy"
|
|
60
|
+
],
|
|
61
|
+
"license": "MIT"
|
|
62
|
+
}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-local-telemetry — DSH (DeepSeek Harness) 插件入口(GitHub 仓库 dsh-telemetry;npm 名 dsh-telemetry 已被第三方占用)。
|
|
3
|
+
*
|
|
4
|
+
* 职责(计划 §10):读取配置、注册技能根、惰性创建 sink/记录器、
|
|
5
|
+
* 注册关闭时 flush 的资源清理逻辑、暴露查询 CLI 与本地服务。
|
|
6
|
+
*
|
|
7
|
+
* 遥测是旁路能力(fail-open):
|
|
8
|
+
* - 初始化失败不阻止 Harness 启动(只留一次性 stderr 提示);
|
|
9
|
+
* - 不注册任何改变请求语义的人设、工具或参数;
|
|
10
|
+
* - 宿主生命周期 Hook 尚未确认(计划 §2),事件经显式适配器接入:
|
|
11
|
+
* 宿主集成代码 `import { createRecorder } from "dsh-local-telemetry/telemetry"`。
|
|
12
|
+
*/
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { createRecorder } from "../src/recorder.mjs";
|
|
17
|
+
|
|
18
|
+
export const name = "dsh-local-telemetry";
|
|
19
|
+
|
|
20
|
+
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
21
|
+
const skillsDir = join(rootDir, "skills");
|
|
22
|
+
const requireOptional = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
let sharedRecorder = null;
|
|
25
|
+
|
|
26
|
+
/** 供宿主集成代码获取共享记录器(惰性创建;配置 enabled=false 时仍可创建,由 record() 自行短路)。 */
|
|
27
|
+
export function getTelemetry(config) {
|
|
28
|
+
if (!sharedRecorder) {
|
|
29
|
+
sharedRecorder = createRecorder(config ?? {});
|
|
30
|
+
}
|
|
31
|
+
return sharedRecorder;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function apply(ctx, config = {}) {
|
|
35
|
+
let provider;
|
|
36
|
+
let skillsRegistered = false;
|
|
37
|
+
|
|
38
|
+
// 技能注册:复用官方 FileSystemSkillProvider;不可用时静默降级(遥测核心不依赖技能)
|
|
39
|
+
try {
|
|
40
|
+
const { FileSystemSkillProvider } = requireOptional("@deepseek-ai/dsh-skill-filesystem");
|
|
41
|
+
ctx.skills.registerProvider((control) => {
|
|
42
|
+
provider = new FileSystemSkillProvider(ctx, control, {
|
|
43
|
+
providerName: "dsh-local-telemetry",
|
|
44
|
+
includeDefaultRoots: false,
|
|
45
|
+
customSkillDirs: [skillsDir],
|
|
46
|
+
});
|
|
47
|
+
skillsRegistered = true;
|
|
48
|
+
return provider;
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
console.warn?.("[dsh-local-telemetry] skill provider unavailable; CLI and library interfaces remain usable");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 记录器预热(惰性 sink:首个事件落盘前不创建文件)
|
|
55
|
+
let recorder = null;
|
|
56
|
+
try {
|
|
57
|
+
recorder = getTelemetry(config?.telemetry);
|
|
58
|
+
void recorder.start?.();
|
|
59
|
+
} catch {
|
|
60
|
+
recorder = null; // fail-open:遥测初始化失败不影响 Harness
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ctx.effect(
|
|
64
|
+
function* () {
|
|
65
|
+
yield async () => {
|
|
66
|
+
try {
|
|
67
|
+
if (provider && typeof provider.dispose === "function") await provider.dispose();
|
|
68
|
+
} catch {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (recorder && typeof recorder.close === "function") await recorder.close();
|
|
73
|
+
} catch {
|
|
74
|
+
/* 关闭 flush 失败不阻塞退出(§6.3) */
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
"dsh-local-telemetry skill provider + telemetry flush"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return { name, skillsRegistered };
|
|
82
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: telemetry-runbook
|
|
3
|
+
description: 本地遥测查询与诊断:查询 Harness 请求、模型、工具、插件生命周期指标(延迟分位数、Token、估算成本、错误分类、重试与回退),查看 trace/span 树,导出 Markdown 报告,清理保留期数据,或启动本地只读 Web UI。需要分析请求耗时、Token 成本、工具失败或验证遥测采集状态时加载本技能。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# telemetry-runbook
|
|
7
|
+
|
|
8
|
+
dsh-local-telemetry runbook(GitHub 仓库 dsh-telemetry;npm 名 dsh-telemetry 已被第三方占用)。遥测是旁路能力:查询只读本地存储,不联网;默认不采集 prompt / response / 文件内容 / 密钥。CLI 路径以仓库根为基准(插件安装后为 `<bundle-dir>/bin/telemetry.mjs`)。
|
|
9
|
+
|
|
10
|
+
## 调用方式
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 采集状态与累计计数(written / dropped / sampled_out)
|
|
14
|
+
node bin/telemetry.mjs --status
|
|
15
|
+
node bin/telemetry.mjs --status --json
|
|
16
|
+
|
|
17
|
+
# 摘要(默认窗口全量;百分比与分位数为 nearest-rank,可由原始事件重算)
|
|
18
|
+
node bin/telemetry.mjs --summary --since 1h
|
|
19
|
+
node bin/telemetry.mjs --summary --since 24h --group-by model # model|plugin|tool|profile|day
|
|
20
|
+
node bin/telemetry.mjs --summary --errors-only --slow-over-ms 10000
|
|
21
|
+
|
|
22
|
+
# trace/span 树
|
|
23
|
+
node bin/telemetry.mjs --trace <trace_id>
|
|
24
|
+
|
|
25
|
+
# 导出(json 或 markdown;markdown 生成 TELEMETRY-REPORT.md 结构)
|
|
26
|
+
node bin/telemetry.mjs --export summary.json --since 7d
|
|
27
|
+
node bin/telemetry.mjs --export TELEMETRY-REPORT.md --since 7d --format markdown
|
|
28
|
+
|
|
29
|
+
# 保留期清理(删除 30 天前的日期文件/行)
|
|
30
|
+
node bin/telemetry.mjs --purge --before 30d
|
|
31
|
+
|
|
32
|
+
# 本地只读 Web UI(仅绑定 127.0.0.1,禁止暴露局域网)
|
|
33
|
+
node bin/telemetry.mjs --ui --port 47610
|
|
34
|
+
|
|
35
|
+
# 配置文件
|
|
36
|
+
node bin/telemetry.mjs --config telemetry.json --summary --since 24h
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 常用参数
|
|
40
|
+
|
|
41
|
+
- `--store jsonl|sqlite`:存储后端;sqlite 需要 Node ≥ 22.5(内置 node:sqlite),低版本会明确报错并建议回退 jsonl。
|
|
42
|
+
- `--path <dir>`:数据目录(默认 `~/.dsh/telemetry`;JSONL 按日期命名 `<YYYY-MM-DD>.jsonl`,超过 100MB 轮转 `.part-NNN`)。
|
|
43
|
+
- `--since / --until`:`500ms|10s|30m|1h|7d|30d` 或 ISO 时间戳。
|
|
44
|
+
- `--profile / --model / --plugin / --event <name|prefix.*>`:维度过滤。
|
|
45
|
+
- `--format text|json|markdown`;`--json` 等价 `--format json`。
|
|
46
|
+
|
|
47
|
+
## 库接口(宿主集成 / 上层插件)
|
|
48
|
+
|
|
49
|
+
宿主生命周期 Hook 尚未确认,事件经显式适配器接入:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { createRecorder, createEventBusAdapter } from 'dsh-local-telemetry/telemetry';
|
|
53
|
+
|
|
54
|
+
const recorder = createRecorder({ config: { path: '~/.dsh/telemetry', capture_metadata: 'safe' } });
|
|
55
|
+
await recorder.start();
|
|
56
|
+
recorder.record({
|
|
57
|
+
event: 'model.completed',
|
|
58
|
+
trace_id: 'trace-001', span_id: 'span-003', parent_id: 'span-001',
|
|
59
|
+
timestamp: '2026-08-23T12:00:00.000Z',
|
|
60
|
+
model: { provider: 'deepseek', name: 'deepseek-chat', request_type: 'chat' },
|
|
61
|
+
usage: { input_tokens: 4200, output_tokens: 860, cached_input_tokens: 0, reasoning_tokens: null },
|
|
62
|
+
result: { status: 'success', finish_reason: 'stop' },
|
|
63
|
+
});
|
|
64
|
+
await recorder.close(); // 退出前 flush;失败不阻塞
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
聚合读取(供 dsh-test-insight 等按 §15 边界通过公开聚合接口使用):
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { openStore, aggregateEvents, buildTraceView } from 'dsh-local-telemetry/telemetry';
|
|
71
|
+
const store = await openStore({ store: 'jsonl', path: '~/.dsh/telemetry' });
|
|
72
|
+
const { events } = await store.readEvents({ fromMs: Date.now() - 3600e3 });
|
|
73
|
+
const summary = aggregateEvents(events, { catalog: null });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 事件契约要点
|
|
77
|
+
|
|
78
|
+
- 12 种生命周期事件(schema_version 1.0);失败/超时用 completed 事件的 `result.status` 表达,无独立 `tool.failed` 事件名。
|
|
79
|
+
- 时长不在事件里臆造:聚合器按 `span_id` 配对 started/completed 推导;两端缺失则为 null。
|
|
80
|
+
- 未知指标为 null,不补 0;Token 只记录宿主/模型返回的真实 usage。
|
|
81
|
+
- 采样按 trace 整体决策(salted hash),错误/取消/慢请求默认绕过;`sampling` 块写入事件元数据。
|
|
82
|
+
- 成本仅在模型、usage、价格目录齐备时计算,否则 `cost: null` 并给出缺失原因。
|
|
83
|
+
|
|
84
|
+
## 解读提示
|
|
85
|
+
|
|
86
|
+
- 摘要样本数 n 与分位数方法随输出标注;窗口不同结果不可直接比较。
|
|
87
|
+
- `dropped` 计数为存储级累计值(write/queue/oversize/invalid/sampled),与时间窗口无关。
|
|
88
|
+
- 估算成本 ≠ 实际账单;价格目录版本与生效时间在摘要与 Markdown 报告附录中标注。
|
package/src/adapter.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-local-telemetry — 宿主适配器(计划 §2)。
|
|
3
|
+
*
|
|
4
|
+
* DSH Harness 的生命周期 Hook(request.started / model.completed / …)目前
|
|
5
|
+
* 无法确认其真实暴露面。按计划 §2 的降级路径:
|
|
6
|
+
* 1. 只实现独立事件记录器与显式适配器接口;
|
|
7
|
+
* 2. 只接入已确认存在的生命周期事件(当前:无,全部经显式 emit 接入);
|
|
8
|
+
* 3. 缺失指标返回 null / unavailable,不从时间戳臆造;
|
|
9
|
+
* 4. 用适配器隔离具体 DSH 版本,宿主私有 API 不散落到各模块。
|
|
10
|
+
*
|
|
11
|
+
* 因此 getCapabilities() 如实声明:lifecycle_hooks: "unconfirmed"。
|
|
12
|
+
* 宿主未来确认 Hook 后,仅需提供一个新的 adapter 实现,其余模块零改动。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { SCHEMA_VERSION } from "./schema.mjs";
|
|
16
|
+
|
|
17
|
+
/** 能力声明:未确认的能力一律 "unconfirmed",不得伪造成可用。 */
|
|
18
|
+
export function createCapabilities(overrides = {}) {
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
schema_version: SCHEMA_VERSION,
|
|
21
|
+
lifecycle_hooks: "unconfirmed", // "full" | "partial" | "none" | "unconfirmed"
|
|
22
|
+
events: [], // 宿主确认可提供的事件名;显式接入模式下由 emit 方声明
|
|
23
|
+
usage_tokens: "unconfirmed",
|
|
24
|
+
cache_tokens: "unconfirmed",
|
|
25
|
+
cost_model: "catalog", // 成本来自版本化价格目录,非宿主
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 显式事件总线适配器 — 当前唯一受支持的接入方式。
|
|
32
|
+
* 嵌入方(宿主集成代码、其他插件、测试)通过 emit() 送入事件,
|
|
33
|
+
* 录制器经 on() 消费。on() 返回解绑函数。
|
|
34
|
+
*/
|
|
35
|
+
export function createEventBusAdapter({ capabilities } = {}) {
|
|
36
|
+
const handlers = new Map(); // eventName | "*" -> Set<handler>
|
|
37
|
+
const caps = createCapabilities({
|
|
38
|
+
lifecycle_hooks: "none",
|
|
39
|
+
...capabilities,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function on(eventName, handler) {
|
|
43
|
+
if (typeof handler !== "function") return () => {};
|
|
44
|
+
if (!handlers.has(eventName)) handlers.set(eventName, new Set());
|
|
45
|
+
handlers.get(eventName).add(handler);
|
|
46
|
+
return () => handlers.get(eventName)?.delete(handler);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function emit(event) {
|
|
50
|
+
let delivered = 0;
|
|
51
|
+
const sets = [handlers.get("*"), handlers.get(event?.event)].filter(Boolean);
|
|
52
|
+
for (const set of sets) {
|
|
53
|
+
for (const handler of set) {
|
|
54
|
+
try {
|
|
55
|
+
handler(event);
|
|
56
|
+
delivered += 1;
|
|
57
|
+
} catch {
|
|
58
|
+
/* 单个 handler 异常不阻断其余订阅者(fail-open) */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return delivered;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCapabilities() {
|
|
66
|
+
return caps;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { on, emit, getCapabilities };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 能力探测:以最保守结果响应。绝不因为探测代码「没有抛错」就声明宿主
|
|
74
|
+
* 具备 Hook(缺失宿主能力不会被伪造成可用指标 —— Phase 0 验收)。
|
|
75
|
+
*/
|
|
76
|
+
export function detectCapabilities(host) {
|
|
77
|
+
if (!host || typeof host !== "object") return createCapabilities();
|
|
78
|
+
return createCapabilities();
|
|
79
|
+
}
|