@kodelyth/diagnostics-prometheus 2026.5.39 → 2026.5.42
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/api.ts +12 -0
- package/dist/api.js +3 -0
- package/dist/index.js +488 -0
- package/index.ts +20 -0
- package/package.json +2 -2
- package/src/service.test.ts +319 -0
- package/src/service.ts +751 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/index.js +0 -7
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { DiagnosticEventMetadata, DiagnosticEventPayload } from "../api.js";
|
|
3
|
+
import { createDiagnosticsPrometheusExporter, testApi } from "./service.js";
|
|
4
|
+
|
|
5
|
+
const trusted: DiagnosticEventMetadata = Object.freeze({ trusted: true });
|
|
6
|
+
const untrusted: DiagnosticEventMetadata = Object.freeze({ trusted: false });
|
|
7
|
+
|
|
8
|
+
function baseEvent(): Pick<DiagnosticEventPayload, "seq" | "ts"> {
|
|
9
|
+
return { seq: 1, ts: 1700000000000 };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("diagnostics-prometheus service", () => {
|
|
13
|
+
it("records trusted run metrics without raw diagnostic identifiers", () => {
|
|
14
|
+
const store = testApi.createPrometheusMetricStore();
|
|
15
|
+
|
|
16
|
+
testApi.recordDiagnosticEvent(
|
|
17
|
+
store,
|
|
18
|
+
{
|
|
19
|
+
...baseEvent(),
|
|
20
|
+
type: "run.completed",
|
|
21
|
+
runId: "run-should-not-export",
|
|
22
|
+
sessionKey: "session-should-not-export",
|
|
23
|
+
provider: "openai",
|
|
24
|
+
model: "gpt-5.4",
|
|
25
|
+
channel: "discord",
|
|
26
|
+
trigger: "message",
|
|
27
|
+
durationMs: 1500,
|
|
28
|
+
outcome: "completed",
|
|
29
|
+
},
|
|
30
|
+
trusted,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const rendered = testApi.renderPrometheusMetrics(store);
|
|
34
|
+
|
|
35
|
+
expect(rendered).toContain("# TYPE klaw_run_completed_total counter");
|
|
36
|
+
expect(rendered).toContain(
|
|
37
|
+
'klaw_run_completed_total{channel="discord",model="gpt-5.4",outcome="completed",provider="openai",trigger="message"} 1',
|
|
38
|
+
);
|
|
39
|
+
expect(rendered).toContain(
|
|
40
|
+
'klaw_run_duration_seconds_sum{channel="discord",model="gpt-5.4",outcome="completed",provider="openai",trigger="message"} 1.5',
|
|
41
|
+
);
|
|
42
|
+
expect(rendered).not.toContain("run-should-not-export");
|
|
43
|
+
expect(rendered).not.toContain("session-should-not-export");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("records hook-blocked run metrics with safe blocker originator only", () => {
|
|
47
|
+
const store = testApi.createPrometheusMetricStore();
|
|
48
|
+
|
|
49
|
+
testApi.recordDiagnosticEvent(
|
|
50
|
+
store,
|
|
51
|
+
{
|
|
52
|
+
...baseEvent(),
|
|
53
|
+
type: "run.completed",
|
|
54
|
+
runId: "run-should-not-export",
|
|
55
|
+
sessionKey: "session-should-not-export",
|
|
56
|
+
provider: "openai",
|
|
57
|
+
model: "gpt-5.4",
|
|
58
|
+
channel: "slack",
|
|
59
|
+
trigger: "message",
|
|
60
|
+
durationMs: 250,
|
|
61
|
+
outcome: "blocked",
|
|
62
|
+
blockedBy: "policy-plugin",
|
|
63
|
+
},
|
|
64
|
+
trusted,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const rendered = testApi.renderPrometheusMetrics(store);
|
|
68
|
+
|
|
69
|
+
expect(rendered).toContain(
|
|
70
|
+
'klaw_run_completed_total{blocked_by="policy-plugin",channel="slack",model="gpt-5.4",outcome="blocked",provider="openai",trigger="message"} 1',
|
|
71
|
+
);
|
|
72
|
+
expect(rendered).not.toContain("run-should-not-export");
|
|
73
|
+
expect(rendered).not.toContain("session-should-not-export");
|
|
74
|
+
expect(rendered).not.toContain("matched secret prompt");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("drops untrusted plugin-emitted diagnostic events", () => {
|
|
78
|
+
const store = testApi.createPrometheusMetricStore();
|
|
79
|
+
|
|
80
|
+
testApi.recordDiagnosticEvent(
|
|
81
|
+
store,
|
|
82
|
+
{
|
|
83
|
+
...baseEvent(),
|
|
84
|
+
type: "model.call.completed",
|
|
85
|
+
runId: "run-1",
|
|
86
|
+
callId: "call-1",
|
|
87
|
+
provider: "openai",
|
|
88
|
+
model: "gpt-5.4",
|
|
89
|
+
durationMs: 10,
|
|
90
|
+
},
|
|
91
|
+
untrusted,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(testApi.renderPrometheusMetrics(store)).toBe("");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("redacts and bounds label values", () => {
|
|
98
|
+
const store = testApi.createPrometheusMetricStore();
|
|
99
|
+
|
|
100
|
+
testApi.recordDiagnosticEvent(
|
|
101
|
+
store,
|
|
102
|
+
{
|
|
103
|
+
...baseEvent(),
|
|
104
|
+
type: "tool.execution.error",
|
|
105
|
+
toolName: "shell\nbad",
|
|
106
|
+
durationMs: 25,
|
|
107
|
+
errorCategory: "Bearer sk-secret-token-value",
|
|
108
|
+
},
|
|
109
|
+
trusted,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const rendered = testApi.renderPrometheusMetrics(store);
|
|
113
|
+
|
|
114
|
+
expect(rendered).toContain(
|
|
115
|
+
'klaw_tool_execution_total{error_category="other",outcome="error",params_kind="unknown",tool="tool"} 1',
|
|
116
|
+
);
|
|
117
|
+
expect(rendered).not.toContain("Bearer");
|
|
118
|
+
expect(rendered).not.toContain("sk-secret");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("bounds messaging labels without exporting raw chat identifiers", () => {
|
|
122
|
+
const store = testApi.createPrometheusMetricStore();
|
|
123
|
+
|
|
124
|
+
testApi.recordDiagnosticEvent(
|
|
125
|
+
store,
|
|
126
|
+
{
|
|
127
|
+
...baseEvent(),
|
|
128
|
+
type: "message.delivery.started",
|
|
129
|
+
channel: "matrix",
|
|
130
|
+
deliveryKind: "text",
|
|
131
|
+
sessionKey: "session-should-not-export",
|
|
132
|
+
},
|
|
133
|
+
trusted,
|
|
134
|
+
);
|
|
135
|
+
testApi.recordDiagnosticEvent(
|
|
136
|
+
store,
|
|
137
|
+
{
|
|
138
|
+
...baseEvent(),
|
|
139
|
+
type: "message.processed",
|
|
140
|
+
channel: "telegram/custom",
|
|
141
|
+
chatId: "chat-should-not-export",
|
|
142
|
+
messageId: "message-should-not-export",
|
|
143
|
+
outcome: "completed",
|
|
144
|
+
reason: "progress draft / message tool 123",
|
|
145
|
+
durationMs: 25,
|
|
146
|
+
},
|
|
147
|
+
trusted,
|
|
148
|
+
);
|
|
149
|
+
testApi.recordDiagnosticEvent(
|
|
150
|
+
store,
|
|
151
|
+
{
|
|
152
|
+
...baseEvent(),
|
|
153
|
+
type: "message.delivery.error",
|
|
154
|
+
channel: "discord/custom",
|
|
155
|
+
deliveryKind: "progress draft" as never,
|
|
156
|
+
durationMs: 50,
|
|
157
|
+
errorCategory: "TimeoutError",
|
|
158
|
+
},
|
|
159
|
+
trusted,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const rendered = testApi.renderPrometheusMetrics(store);
|
|
163
|
+
|
|
164
|
+
expect(rendered).toContain(
|
|
165
|
+
'klaw_message_delivery_started_total{channel="matrix",delivery_kind="text"} 1',
|
|
166
|
+
);
|
|
167
|
+
expect(rendered).toContain(
|
|
168
|
+
'klaw_message_processed_total{channel="unknown",outcome="completed",reason="none"} 1',
|
|
169
|
+
);
|
|
170
|
+
expect(rendered).toContain(
|
|
171
|
+
'klaw_message_delivery_total{channel="unknown",delivery_kind="other",error_category="TimeoutError",outcome="error"} 1',
|
|
172
|
+
);
|
|
173
|
+
expect(rendered).not.toContain("chat-should-not-export");
|
|
174
|
+
expect(rendered).not.toContain("message-should-not-export");
|
|
175
|
+
expect(rendered).not.toContain("session-should-not-export");
|
|
176
|
+
expect(rendered).not.toContain("progress draft");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("records session recovery and talk metrics without exporting raw ids or content", () => {
|
|
180
|
+
const store = testApi.createPrometheusMetricStore();
|
|
181
|
+
|
|
182
|
+
testApi.recordDiagnosticEvent(
|
|
183
|
+
store,
|
|
184
|
+
{
|
|
185
|
+
...baseEvent(),
|
|
186
|
+
type: "session.recovery.completed",
|
|
187
|
+
sessionId: "session-should-not-export",
|
|
188
|
+
sessionKey: "key-should-not-export",
|
|
189
|
+
state: "processing",
|
|
190
|
+
stateGeneration: 2,
|
|
191
|
+
ageMs: 12_000,
|
|
192
|
+
queueDepth: 1,
|
|
193
|
+
reason: "startup-sweep",
|
|
194
|
+
activeWorkKind: "tool_call",
|
|
195
|
+
allowActiveAbort: true,
|
|
196
|
+
status: "released",
|
|
197
|
+
action: "abort-active-run",
|
|
198
|
+
},
|
|
199
|
+
trusted,
|
|
200
|
+
);
|
|
201
|
+
testApi.recordDiagnosticEvent(
|
|
202
|
+
store,
|
|
203
|
+
{
|
|
204
|
+
...baseEvent(),
|
|
205
|
+
type: "talk.event",
|
|
206
|
+
sessionId: "talk-session-should-not-export",
|
|
207
|
+
turnId: "turn-should-not-export",
|
|
208
|
+
talkEventType: "input.audio.delta",
|
|
209
|
+
mode: "realtime",
|
|
210
|
+
transport: "gateway-relay",
|
|
211
|
+
brain: "agent-consult",
|
|
212
|
+
provider: "openai",
|
|
213
|
+
byteLength: 320,
|
|
214
|
+
},
|
|
215
|
+
trusted,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const rendered = testApi.renderPrometheusMetrics(store);
|
|
219
|
+
|
|
220
|
+
expect(rendered).toContain(
|
|
221
|
+
'klaw_session_recovery_total{action="abort-active-run",active_work_kind="tool_call",state="processing",status="released"} 1',
|
|
222
|
+
);
|
|
223
|
+
expect(rendered).toContain(
|
|
224
|
+
'klaw_session_recovery_age_seconds_sum{action="abort-active-run",active_work_kind="tool_call",state="processing",status="released"} 12',
|
|
225
|
+
);
|
|
226
|
+
expect(rendered).toContain(
|
|
227
|
+
'klaw_talk_event_total{brain="agent-consult",event_type="input.audio.delta",mode="realtime",provider="openai",transport="gateway-relay"} 1',
|
|
228
|
+
);
|
|
229
|
+
expect(rendered).toContain(
|
|
230
|
+
'klaw_talk_audio_bytes_sum{brain="agent-consult",event_type="input.audio.delta",mode="realtime",provider="openai",transport="gateway-relay"} 320',
|
|
231
|
+
);
|
|
232
|
+
expect(rendered).not.toContain("session-should-not-export");
|
|
233
|
+
expect(rendered).not.toContain("key-should-not-export");
|
|
234
|
+
expect(rendered).not.toContain("talk-session-should-not-export");
|
|
235
|
+
expect(rendered).not.toContain("turn-should-not-export");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("caps metric series growth and reports dropped series", () => {
|
|
239
|
+
const store = testApi.createPrometheusMetricStore();
|
|
240
|
+
|
|
241
|
+
for (let index = 0; index < 2100; index += 1) {
|
|
242
|
+
testApi.recordDiagnosticEvent(
|
|
243
|
+
store,
|
|
244
|
+
{
|
|
245
|
+
...baseEvent(),
|
|
246
|
+
type: "model.call.completed",
|
|
247
|
+
runId: `run-${index}`,
|
|
248
|
+
callId: `call-${index}`,
|
|
249
|
+
provider: "openai",
|
|
250
|
+
model: `model.${index}`,
|
|
251
|
+
durationMs: 10,
|
|
252
|
+
},
|
|
253
|
+
trusted,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const rendered = testApi.renderPrometheusMetrics(store);
|
|
258
|
+
|
|
259
|
+
expect(rendered).toContain("# TYPE klaw_prometheus_series_dropped_total counter");
|
|
260
|
+
expect(rendered).toContain("klaw_prometheus_series_dropped_total ");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("subscribes to internal diagnostics and renders scrape text", () => {
|
|
264
|
+
const listeners: Array<
|
|
265
|
+
(event: DiagnosticEventPayload, metadata: DiagnosticEventMetadata) => void
|
|
266
|
+
> = [];
|
|
267
|
+
const emitted: unknown[] = [];
|
|
268
|
+
const exporter = createDiagnosticsPrometheusExporter();
|
|
269
|
+
const unsubscribe = vi.fn();
|
|
270
|
+
|
|
271
|
+
exporter.service.start({
|
|
272
|
+
config: {} as never,
|
|
273
|
+
stateDir: "/tmp/klaw-prometheus-test",
|
|
274
|
+
logger: {
|
|
275
|
+
info: vi.fn(),
|
|
276
|
+
warn: vi.fn(),
|
|
277
|
+
error: vi.fn(),
|
|
278
|
+
debug: vi.fn(),
|
|
279
|
+
},
|
|
280
|
+
internalDiagnostics: {
|
|
281
|
+
emit: (event) => emitted.push(event),
|
|
282
|
+
onEvent: (listener) => {
|
|
283
|
+
listeners.push(listener);
|
|
284
|
+
return unsubscribe;
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(listeners).toHaveLength(1);
|
|
290
|
+
listeners[0](
|
|
291
|
+
{
|
|
292
|
+
...baseEvent(),
|
|
293
|
+
type: "model.usage",
|
|
294
|
+
provider: "openai",
|
|
295
|
+
model: "gpt-5.4",
|
|
296
|
+
usage: { input: 12, output: 3, total: 15 },
|
|
297
|
+
},
|
|
298
|
+
trusted,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
expect(emitted).toStrictEqual([
|
|
302
|
+
{
|
|
303
|
+
type: "telemetry.exporter",
|
|
304
|
+
exporter: "diagnostics-prometheus",
|
|
305
|
+
signal: "metrics",
|
|
306
|
+
status: "started",
|
|
307
|
+
reason: "configured",
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
expect(exporter.render()).toContain(
|
|
311
|
+
'klaw_model_tokens_total{agent="unknown",channel="unknown",model="gpt-5.4",provider="openai",token_type="input"} 12',
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
exporter.service.stop?.();
|
|
315
|
+
|
|
316
|
+
expect(unsubscribe).toHaveBeenCalledOnce();
|
|
317
|
+
expect(exporter.render()).toBe("");
|
|
318
|
+
});
|
|
319
|
+
});
|