observability-toolkit 1.1.0 → 1.4.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/README.md +52 -3
- package/dist/backends/index.d.ts +28 -0
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/local-jsonl.d.ts +29 -1
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +259 -27
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/local-jsonl.test.d.ts +2 -0
- package/dist/backends/local-jsonl.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl.test.js +1638 -0
- package/dist/backends/local-jsonl.test.js.map +1 -0
- package/dist/backends/signoz-api.d.ts +9 -2
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.integration.test.d.ts +8 -0
- package/dist/backends/signoz-api.integration.test.d.ts.map +1 -0
- package/dist/backends/signoz-api.integration.test.js +137 -0
- package/dist/backends/signoz-api.integration.test.js.map +1 -0
- package/dist/backends/signoz-api.js +206 -115
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.d.ts +2 -0
- package/dist/backends/signoz-api.test.d.ts.map +1 -0
- package/dist/backends/signoz-api.test.js +1080 -0
- package/dist/backends/signoz-api.test.js.map +1 -0
- package/dist/lib/constants.d.ts +28 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +73 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/constants.test.d.ts +5 -0
- package/dist/lib/constants.test.d.ts.map +1 -0
- package/dist/lib/constants.test.js +381 -0
- package/dist/lib/constants.test.js.map +1 -0
- package/dist/lib/file-utils.d.ts +53 -1
- package/dist/lib/file-utils.d.ts.map +1 -1
- package/dist/lib/file-utils.js +142 -3
- package/dist/lib/file-utils.js.map +1 -1
- package/dist/lib/file-utils.test.d.ts +2 -0
- package/dist/lib/file-utils.test.d.ts.map +1 -0
- package/dist/lib/file-utils.test.js +649 -0
- package/dist/lib/file-utils.test.js.map +1 -0
- package/dist/server.js +50 -63
- package/dist/server.js.map +1 -1
- package/dist/server.test.d.ts +5 -0
- package/dist/server.test.d.ts.map +1 -0
- package/dist/server.test.js +547 -0
- package/dist/server.test.js.map +1 -0
- package/dist/tools/context-stats.d.ts +2 -2
- package/dist/tools/context-stats.d.ts.map +1 -1
- package/dist/tools/context-stats.js +2 -1
- package/dist/tools/context-stats.js.map +1 -1
- package/dist/tools/context-stats.test.d.ts +5 -0
- package/dist/tools/context-stats.test.d.ts.map +1 -0
- package/dist/tools/context-stats.test.js +465 -0
- package/dist/tools/context-stats.test.js.map +1 -0
- package/dist/tools/get-trace-url.d.ts.map +1 -1
- package/dist/tools/get-trace-url.js +5 -1
- package/dist/tools/get-trace-url.js.map +1 -1
- package/dist/tools/get-trace-url.test.d.ts +5 -0
- package/dist/tools/get-trace-url.test.d.ts.map +1 -0
- package/dist/tools/get-trace-url.test.js +429 -0
- package/dist/tools/get-trace-url.test.js.map +1 -0
- package/dist/tools/health-check.d.ts +9 -2
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +66 -27
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/health-check.test.d.ts +5 -0
- package/dist/tools/health-check.test.d.ts.map +1 -0
- package/dist/tools/health-check.test.js +386 -0
- package/dist/tools/health-check.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/query-llm-events.d.ts +82 -0
- package/dist/tools/query-llm-events.d.ts.map +1 -0
- package/dist/tools/query-llm-events.js +60 -0
- package/dist/tools/query-llm-events.js.map +1 -0
- package/dist/tools/query-llm-events.test.d.ts +5 -0
- package/dist/tools/query-llm-events.test.d.ts.map +1 -0
- package/dist/tools/query-llm-events.test.js +111 -0
- package/dist/tools/query-llm-events.test.js.map +1 -0
- package/dist/tools/query-logs.d.ts +15 -8
- package/dist/tools/query-logs.d.ts.map +1 -1
- package/dist/tools/query-logs.js +11 -10
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-logs.test.d.ts +5 -0
- package/dist/tools/query-logs.test.d.ts.map +1 -0
- package/dist/tools/query-logs.test.js +688 -0
- package/dist/tools/query-logs.test.js.map +1 -0
- package/dist/tools/query-metrics.d.ts +13 -15
- package/dist/tools/query-metrics.d.ts.map +1 -1
- package/dist/tools/query-metrics.js +12 -13
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-metrics.test.d.ts +5 -0
- package/dist/tools/query-metrics.test.d.ts.map +1 -0
- package/dist/tools/query-metrics.test.js +597 -0
- package/dist/tools/query-metrics.test.js.map +1 -0
- package/dist/tools/query-traces.d.ts +19 -14
- package/dist/tools/query-traces.d.ts.map +1 -1
- package/dist/tools/query-traces.js +14 -14
- package/dist/tools/query-traces.js.map +1 -1
- package/dist/tools/query-traces.test.d.ts +5 -0
- package/dist/tools/query-traces.test.d.ts.map +1 -0
- package/dist/tools/query-traces.test.js +643 -0
- package/dist/tools/query-traces.test.js.map +1 -0
- package/dist/tools/setup-claudeignore.d.ts +36 -10
- package/dist/tools/setup-claudeignore.d.ts.map +1 -1
- package/dist/tools/setup-claudeignore.js +193 -33
- package/dist/tools/setup-claudeignore.js.map +1 -1
- package/dist/tools/setup-claudeignore.test.d.ts +2 -0
- package/dist/tools/setup-claudeignore.test.d.ts.map +1 -0
- package/dist/tools/setup-claudeignore.test.js +481 -0
- package/dist/tools/setup-claudeignore.test.js.map +1 -0
- package/dist/tools/signoz.integration.test.d.ts +8 -0
- package/dist/tools/signoz.integration.test.d.ts.map +1 -0
- package/dist/tools/signoz.integration.test.js +141 -0
- package/dist/tools/signoz.integration.test.js.map +1 -0
- package/package.json +6 -3
|
@@ -0,0 +1,1638 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import * as assert from 'node:assert';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import { LocalJsonlBackend, MultiDirectoryBackend } from './local-jsonl.js';
|
|
7
|
+
/**
|
|
8
|
+
* Test utilities for creating temp test fixtures
|
|
9
|
+
*/
|
|
10
|
+
function createTempDir() {
|
|
11
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'local-jsonl-test-'));
|
|
12
|
+
}
|
|
13
|
+
function removeTempDir(dir) {
|
|
14
|
+
try {
|
|
15
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// Ignore cleanup errors
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeJsonlFile(filePath, data) {
|
|
22
|
+
const content = data.map(item => JSON.stringify(item)).join('\n');
|
|
23
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
24
|
+
}
|
|
25
|
+
function getTestDate() {
|
|
26
|
+
return new Date().toISOString().split('T')[0];
|
|
27
|
+
}
|
|
28
|
+
describe('LocalJsonlBackend', () => {
|
|
29
|
+
let tempDir;
|
|
30
|
+
let backend;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
tempDir = createTempDir();
|
|
33
|
+
backend = new LocalJsonlBackend(tempDir);
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
removeTempDir(tempDir);
|
|
37
|
+
});
|
|
38
|
+
describe('queryTraces', () => {
|
|
39
|
+
it('should read and normalize trace spans from JSONL files', async () => {
|
|
40
|
+
const today = getTestDate();
|
|
41
|
+
const mockSpans = [
|
|
42
|
+
{
|
|
43
|
+
traceId: 'trace1',
|
|
44
|
+
spanId: 'span1',
|
|
45
|
+
name: 'test-operation',
|
|
46
|
+
startTime: [1700000000, 0],
|
|
47
|
+
endTime: [1700000001, 500000000],
|
|
48
|
+
resource: { serviceName: 'test-service', serviceVersion: '1.0.0' },
|
|
49
|
+
attributes: { 'custom.attr': 'value1' },
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
53
|
+
const results = await backend.queryTraces({});
|
|
54
|
+
assert.strictEqual(results.length, 1);
|
|
55
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
56
|
+
assert.strictEqual(results[0].spanId, 'span1');
|
|
57
|
+
assert.strictEqual(results[0].name, 'test-operation');
|
|
58
|
+
assert.strictEqual(results[0].attributes?.['service.name'], 'test-service');
|
|
59
|
+
assert.strictEqual(results[0].attributes?.['service.version'], '1.0.0');
|
|
60
|
+
assert.strictEqual(results[0].attributes?.['custom.attr'], 'value1');
|
|
61
|
+
});
|
|
62
|
+
it('should filter spans by traceId', async () => {
|
|
63
|
+
const today = getTestDate();
|
|
64
|
+
const mockSpans = [
|
|
65
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
66
|
+
{ traceId: 'trace2', spanId: 'span2', name: 'op2', startTime: [1700000000, 0] },
|
|
67
|
+
{ traceId: 'trace1', spanId: 'span3', name: 'op3', startTime: [1700000000, 0] },
|
|
68
|
+
];
|
|
69
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
70
|
+
const results = await backend.queryTraces({ traceId: 'trace1' });
|
|
71
|
+
assert.strictEqual(results.length, 2);
|
|
72
|
+
assert.ok(results.every(s => s.traceId === 'trace1'));
|
|
73
|
+
});
|
|
74
|
+
it('should filter spans by spanName substring', async () => {
|
|
75
|
+
const today = getTestDate();
|
|
76
|
+
const mockSpans = [
|
|
77
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'user-create', startTime: [1700000000, 0] },
|
|
78
|
+
{ traceId: 'trace1', spanId: 'span2', name: 'user-update', startTime: [1700000000, 0] },
|
|
79
|
+
{ traceId: 'trace1', spanId: 'span3', name: 'db-query', startTime: [1700000000, 0] },
|
|
80
|
+
];
|
|
81
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
82
|
+
const results = await backend.queryTraces({ spanName: 'user' });
|
|
83
|
+
assert.strictEqual(results.length, 2);
|
|
84
|
+
assert.ok(results.every(s => s.name.includes('user')));
|
|
85
|
+
});
|
|
86
|
+
it('should filter spans by duration range', async () => {
|
|
87
|
+
const today = getTestDate();
|
|
88
|
+
const mockSpans = [
|
|
89
|
+
{
|
|
90
|
+
traceId: 'trace1',
|
|
91
|
+
spanId: 'span1',
|
|
92
|
+
name: 'fast-op',
|
|
93
|
+
startTime: [1700000000, 0],
|
|
94
|
+
endTime: [1700000000, 500000000], // 0.5s
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
traceId: 'trace1',
|
|
98
|
+
spanId: 'span2',
|
|
99
|
+
name: 'medium-op',
|
|
100
|
+
startTime: [1700000000, 0],
|
|
101
|
+
endTime: [1700000002, 0], // 2s
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
traceId: 'trace1',
|
|
105
|
+
spanId: 'span3',
|
|
106
|
+
name: 'slow-op',
|
|
107
|
+
startTime: [1700000000, 0],
|
|
108
|
+
endTime: [1700000010, 0], // 10s
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
112
|
+
const results = await backend.queryTraces({ minDurationMs: 1000, maxDurationMs: 5000 });
|
|
113
|
+
assert.strictEqual(results.length, 1);
|
|
114
|
+
assert.strictEqual(results[0].name, 'medium-op');
|
|
115
|
+
});
|
|
116
|
+
it('should filter spans by serviceName', async () => {
|
|
117
|
+
const today = getTestDate();
|
|
118
|
+
const mockSpans = [
|
|
119
|
+
{
|
|
120
|
+
traceId: 'trace1',
|
|
121
|
+
spanId: 'span1',
|
|
122
|
+
name: 'op1',
|
|
123
|
+
startTime: [1700000000, 0],
|
|
124
|
+
resource: { serviceName: 'service-a' },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
traceId: 'trace1',
|
|
128
|
+
spanId: 'span2',
|
|
129
|
+
name: 'op2',
|
|
130
|
+
startTime: [1700000000, 0],
|
|
131
|
+
resource: { serviceName: 'service-b' },
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
135
|
+
const results = await backend.queryTraces({ serviceName: 'service-a' });
|
|
136
|
+
assert.strictEqual(results.length, 1);
|
|
137
|
+
assert.strictEqual(results[0].attributes?.['service.name'], 'service-a');
|
|
138
|
+
});
|
|
139
|
+
it('should apply limit and offset to results', async () => {
|
|
140
|
+
const today = getTestDate();
|
|
141
|
+
const mockSpans = Array.from({ length: 150 }, (_, i) => ({
|
|
142
|
+
traceId: `trace${i}`,
|
|
143
|
+
spanId: `span${i}`,
|
|
144
|
+
name: `op${i}`,
|
|
145
|
+
startTime: [1700000000, 0],
|
|
146
|
+
}));
|
|
147
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
148
|
+
const results = await backend.queryTraces({ limit: 50, offset: 25 });
|
|
149
|
+
assert.strictEqual(results.length, 50);
|
|
150
|
+
assert.strictEqual(results[0].traceId, 'trace25');
|
|
151
|
+
});
|
|
152
|
+
it('should skip invalid spans (missing required fields)', async () => {
|
|
153
|
+
const today = getTestDate();
|
|
154
|
+
const mockSpans = [
|
|
155
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
156
|
+
{ traceId: 'trace2', spanId: 'span2', startTime: [1700000000, 0] }, // missing name
|
|
157
|
+
{ spanId: 'span3', name: 'op3', startTime: [1700000000, 0] }, // missing traceId
|
|
158
|
+
{ traceId: 'trace4', name: 'op4', startTime: [1700000000, 0] }, // missing spanId
|
|
159
|
+
];
|
|
160
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
161
|
+
const results = await backend.queryTraces({});
|
|
162
|
+
assert.strictEqual(results.length, 1);
|
|
163
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
164
|
+
});
|
|
165
|
+
it('should convert duration from [seconds, nanoseconds] array', async () => {
|
|
166
|
+
const today = getTestDate();
|
|
167
|
+
const mockSpans = [
|
|
168
|
+
{
|
|
169
|
+
traceId: 'trace1',
|
|
170
|
+
spanId: 'span1',
|
|
171
|
+
name: 'op1',
|
|
172
|
+
startTime: [1700000000, 0],
|
|
173
|
+
duration: [2, 500000000], // 2.5 seconds
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
177
|
+
const results = await backend.queryTraces({});
|
|
178
|
+
assert.strictEqual(results.length, 1);
|
|
179
|
+
assert.strictEqual(results[0].durationMs, 2500);
|
|
180
|
+
});
|
|
181
|
+
it('should convert span kind number to string', async () => {
|
|
182
|
+
const today = getTestDate();
|
|
183
|
+
const mockSpans = [
|
|
184
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'op1', kind: 0, startTime: [1700000000, 0] },
|
|
185
|
+
{ traceId: 'trace2', spanId: 'span2', name: 'op2', kind: 1, startTime: [1700000000, 0] },
|
|
186
|
+
{ traceId: 'trace3', spanId: 'span3', name: 'op3', kind: 2, startTime: [1700000000, 0] },
|
|
187
|
+
];
|
|
188
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
189
|
+
const results = await backend.queryTraces({});
|
|
190
|
+
assert.strictEqual(results[0].kind, 'INTERNAL');
|
|
191
|
+
assert.strictEqual(results[1].kind, 'SERVER');
|
|
192
|
+
assert.strictEqual(results[2].kind, 'CLIENT');
|
|
193
|
+
});
|
|
194
|
+
it('should convert status code number to string', async () => {
|
|
195
|
+
const today = getTestDate();
|
|
196
|
+
const mockSpans = [
|
|
197
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0], status: { code: 0 } },
|
|
198
|
+
{ traceId: 'trace2', spanId: 'span2', name: 'op2', startTime: [1700000000, 0], status: { code: 1 } },
|
|
199
|
+
{ traceId: 'trace3', spanId: 'span3', name: 'op3', startTime: [1700000000, 0], status: { code: 2, message: 'Test error' } },
|
|
200
|
+
];
|
|
201
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
202
|
+
const results = await backend.queryTraces({});
|
|
203
|
+
assert.strictEqual(results[0].statusCode, 'UNSET');
|
|
204
|
+
assert.strictEqual(results[0].status?.code, 0);
|
|
205
|
+
assert.strictEqual(results[1].statusCode, 'OK');
|
|
206
|
+
assert.strictEqual(results[1].status?.code, 1);
|
|
207
|
+
assert.strictEqual(results[2].statusCode, 'ERROR');
|
|
208
|
+
assert.strictEqual(results[2].status?.code, 2);
|
|
209
|
+
assert.strictEqual(results[2].status?.message, 'Test error');
|
|
210
|
+
});
|
|
211
|
+
it('should handle spans without status', async () => {
|
|
212
|
+
const today = getTestDate();
|
|
213
|
+
const mockSpans = [
|
|
214
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
215
|
+
];
|
|
216
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
217
|
+
const results = await backend.queryTraces({});
|
|
218
|
+
assert.strictEqual(results[0].statusCode, undefined);
|
|
219
|
+
assert.strictEqual(results[0].status, undefined);
|
|
220
|
+
});
|
|
221
|
+
it('should return empty array when no files found', async () => {
|
|
222
|
+
// No files created - tempDir is empty
|
|
223
|
+
const results = await backend.queryTraces({});
|
|
224
|
+
assert.strictEqual(results.length, 0);
|
|
225
|
+
});
|
|
226
|
+
it('should filter spans by attributeFilter with string value', async () => {
|
|
227
|
+
const today = getTestDate();
|
|
228
|
+
const mockSpans = [
|
|
229
|
+
{
|
|
230
|
+
traceId: 'trace1',
|
|
231
|
+
spanId: 'span1',
|
|
232
|
+
name: 'hook:session-start',
|
|
233
|
+
startTime: [1700000000, 0],
|
|
234
|
+
attributes: { 'hook.name': 'session-start', 'hook.type': 'session' },
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
traceId: 'trace2',
|
|
238
|
+
spanId: 'span2',
|
|
239
|
+
name: 'hook:mcp-pre-tool',
|
|
240
|
+
startTime: [1700000000, 0],
|
|
241
|
+
attributes: { 'hook.name': 'mcp-pre-tool', 'mcp.server': 'signoz' },
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
traceId: 'trace3',
|
|
245
|
+
spanId: 'span3',
|
|
246
|
+
name: 'hook:post-tool',
|
|
247
|
+
startTime: [1700000000, 0],
|
|
248
|
+
attributes: { 'hook.name': 'post-tool', 'mcp.server': 'webresearch' },
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
252
|
+
const results = await backend.queryTraces({
|
|
253
|
+
attributeFilter: { 'hook.name': 'session-start' },
|
|
254
|
+
});
|
|
255
|
+
assert.strictEqual(results.length, 1);
|
|
256
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
257
|
+
});
|
|
258
|
+
it('should filter spans by attributeFilter with multiple attributes', async () => {
|
|
259
|
+
const today = getTestDate();
|
|
260
|
+
const mockSpans = [
|
|
261
|
+
{
|
|
262
|
+
traceId: 'trace1',
|
|
263
|
+
spanId: 'span1',
|
|
264
|
+
name: 'mcp-call',
|
|
265
|
+
startTime: [1700000000, 0],
|
|
266
|
+
attributes: { 'mcp.server': 'signoz', 'mcp.success': true },
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
traceId: 'trace2',
|
|
270
|
+
spanId: 'span2',
|
|
271
|
+
name: 'mcp-call',
|
|
272
|
+
startTime: [1700000000, 0],
|
|
273
|
+
attributes: { 'mcp.server': 'signoz', 'mcp.success': false },
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
traceId: 'trace3',
|
|
277
|
+
spanId: 'span3',
|
|
278
|
+
name: 'mcp-call',
|
|
279
|
+
startTime: [1700000000, 0],
|
|
280
|
+
attributes: { 'mcp.server': 'webresearch', 'mcp.success': true },
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
284
|
+
const results = await backend.queryTraces({
|
|
285
|
+
attributeFilter: { 'mcp.server': 'signoz', 'mcp.success': true },
|
|
286
|
+
});
|
|
287
|
+
assert.strictEqual(results.length, 1);
|
|
288
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
289
|
+
});
|
|
290
|
+
it('should filter spans by attributeFilter with number value', async () => {
|
|
291
|
+
const today = getTestDate();
|
|
292
|
+
const mockSpans = [
|
|
293
|
+
{
|
|
294
|
+
traceId: 'trace1',
|
|
295
|
+
spanId: 'span1',
|
|
296
|
+
name: 'http-request',
|
|
297
|
+
startTime: [1700000000, 0],
|
|
298
|
+
attributes: { 'http.status_code': 200 },
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
traceId: 'trace2',
|
|
302
|
+
spanId: 'span2',
|
|
303
|
+
name: 'http-request',
|
|
304
|
+
startTime: [1700000000, 0],
|
|
305
|
+
attributes: { 'http.status_code': 500 },
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
309
|
+
const results = await backend.queryTraces({
|
|
310
|
+
attributeFilter: { 'http.status_code': 200 },
|
|
311
|
+
});
|
|
312
|
+
assert.strictEqual(results.length, 1);
|
|
313
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
314
|
+
});
|
|
315
|
+
it('should filter spans by attributeFilter with boolean value', async () => {
|
|
316
|
+
const today = getTestDate();
|
|
317
|
+
const mockSpans = [
|
|
318
|
+
{
|
|
319
|
+
traceId: 'trace1',
|
|
320
|
+
spanId: 'span1',
|
|
321
|
+
name: 'agent-call',
|
|
322
|
+
startTime: [1700000000, 0],
|
|
323
|
+
attributes: { 'agent.is_background': true },
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
traceId: 'trace2',
|
|
327
|
+
spanId: 'span2',
|
|
328
|
+
name: 'agent-call',
|
|
329
|
+
startTime: [1700000000, 0],
|
|
330
|
+
attributes: { 'agent.is_background': false },
|
|
331
|
+
},
|
|
332
|
+
];
|
|
333
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
334
|
+
const results = await backend.queryTraces({
|
|
335
|
+
attributeFilter: { 'agent.is_background': false },
|
|
336
|
+
});
|
|
337
|
+
assert.strictEqual(results.length, 1);
|
|
338
|
+
assert.strictEqual(results[0].traceId, 'trace2');
|
|
339
|
+
});
|
|
340
|
+
it('should return empty array when attributeFilter matches nothing', async () => {
|
|
341
|
+
const today = getTestDate();
|
|
342
|
+
const mockSpans = [
|
|
343
|
+
{
|
|
344
|
+
traceId: 'trace1',
|
|
345
|
+
spanId: 'span1',
|
|
346
|
+
name: 'op1',
|
|
347
|
+
startTime: [1700000000, 0],
|
|
348
|
+
attributes: { 'hook.name': 'session-start' },
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
352
|
+
const results = await backend.queryTraces({
|
|
353
|
+
attributeFilter: { 'hook.name': 'nonexistent' },
|
|
354
|
+
});
|
|
355
|
+
assert.strictEqual(results.length, 0);
|
|
356
|
+
});
|
|
357
|
+
it('should combine attributeFilter with other filters', async () => {
|
|
358
|
+
const today = getTestDate();
|
|
359
|
+
const mockSpans = [
|
|
360
|
+
{
|
|
361
|
+
traceId: 'trace1',
|
|
362
|
+
spanId: 'span1',
|
|
363
|
+
name: 'hook:mcp-pre-tool',
|
|
364
|
+
startTime: [1700000000, 0],
|
|
365
|
+
endTime: [1700000000, 500000000], // 500ms
|
|
366
|
+
attributes: { 'mcp.server': 'signoz' },
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
traceId: 'trace2',
|
|
370
|
+
spanId: 'span2',
|
|
371
|
+
name: 'hook:mcp-pre-tool',
|
|
372
|
+
startTime: [1700000000, 0],
|
|
373
|
+
endTime: [1700000002, 0], // 2000ms
|
|
374
|
+
attributes: { 'mcp.server': 'signoz' },
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
traceId: 'trace3',
|
|
378
|
+
spanId: 'span3',
|
|
379
|
+
name: 'hook:mcp-pre-tool',
|
|
380
|
+
startTime: [1700000000, 0],
|
|
381
|
+
endTime: [1700000000, 500000000], // 500ms
|
|
382
|
+
attributes: { 'mcp.server': 'webresearch' },
|
|
383
|
+
},
|
|
384
|
+
];
|
|
385
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
386
|
+
const results = await backend.queryTraces({
|
|
387
|
+
spanName: 'mcp',
|
|
388
|
+
minDurationMs: 1000,
|
|
389
|
+
attributeFilter: { 'mcp.server': 'signoz' },
|
|
390
|
+
});
|
|
391
|
+
assert.strictEqual(results.length, 1);
|
|
392
|
+
assert.strictEqual(results[0].traceId, 'trace2');
|
|
393
|
+
});
|
|
394
|
+
it('should exclude spans matching excludeSpanName', async () => {
|
|
395
|
+
const today = getTestDate();
|
|
396
|
+
const mockSpans = [
|
|
397
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'http-request', startTime: [1700000000, 0] },
|
|
398
|
+
{ traceId: 'trace2', spanId: 'span2', name: 'db-query', startTime: [1700000000, 0] },
|
|
399
|
+
{ traceId: 'trace3', spanId: 'span3', name: 'http-response', startTime: [1700000000, 0] },
|
|
400
|
+
];
|
|
401
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
402
|
+
const results = await backend.queryTraces({ excludeSpanName: 'http' });
|
|
403
|
+
assert.strictEqual(results.length, 1);
|
|
404
|
+
assert.strictEqual(results[0].name, 'db-query');
|
|
405
|
+
});
|
|
406
|
+
it('should filter spans by attributeExists - all must exist', async () => {
|
|
407
|
+
const today = getTestDate();
|
|
408
|
+
const mockSpans = [
|
|
409
|
+
{
|
|
410
|
+
traceId: 'trace1',
|
|
411
|
+
spanId: 'span1',
|
|
412
|
+
name: 'op1',
|
|
413
|
+
startTime: [1700000000, 0],
|
|
414
|
+
attributes: { 'http.method': 'GET', 'http.status_code': 200 },
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
traceId: 'trace2',
|
|
418
|
+
spanId: 'span2',
|
|
419
|
+
name: 'op2',
|
|
420
|
+
startTime: [1700000000, 0],
|
|
421
|
+
attributes: { 'http.method': 'POST' }, // missing http.status_code
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
traceId: 'trace3',
|
|
425
|
+
spanId: 'span3',
|
|
426
|
+
name: 'op3',
|
|
427
|
+
startTime: [1700000000, 0],
|
|
428
|
+
attributes: { 'db.system': 'postgres' }, // missing both
|
|
429
|
+
},
|
|
430
|
+
];
|
|
431
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
432
|
+
const results = await backend.queryTraces({
|
|
433
|
+
attributeExists: ['http.method', 'http.status_code'],
|
|
434
|
+
});
|
|
435
|
+
assert.strictEqual(results.length, 1);
|
|
436
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
437
|
+
});
|
|
438
|
+
it('should filter spans by attributeNotExists - exclude if any exist', async () => {
|
|
439
|
+
const today = getTestDate();
|
|
440
|
+
const mockSpans = [
|
|
441
|
+
{
|
|
442
|
+
traceId: 'trace1',
|
|
443
|
+
spanId: 'span1',
|
|
444
|
+
name: 'op1',
|
|
445
|
+
startTime: [1700000000, 0],
|
|
446
|
+
attributes: { 'http.method': 'GET', 'error.message': 'timeout' },
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
traceId: 'trace2',
|
|
450
|
+
spanId: 'span2',
|
|
451
|
+
name: 'op2',
|
|
452
|
+
startTime: [1700000000, 0],
|
|
453
|
+
attributes: { 'http.method': 'POST' }, // no error attributes
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
traceId: 'trace3',
|
|
457
|
+
spanId: 'span3',
|
|
458
|
+
name: 'op3',
|
|
459
|
+
startTime: [1700000000, 0],
|
|
460
|
+
attributes: { 'http.method': 'GET', 'error.type': 'network' },
|
|
461
|
+
},
|
|
462
|
+
];
|
|
463
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
464
|
+
const results = await backend.queryTraces({
|
|
465
|
+
attributeNotExists: ['error.message', 'error.type'],
|
|
466
|
+
});
|
|
467
|
+
assert.strictEqual(results.length, 1);
|
|
468
|
+
assert.strictEqual(results[0].traceId, 'trace2');
|
|
469
|
+
});
|
|
470
|
+
it('should combine spanName with excludeSpanName', async () => {
|
|
471
|
+
const today = getTestDate();
|
|
472
|
+
const mockSpans = [
|
|
473
|
+
{ traceId: 'trace1', spanId: 'span1', name: 'http-request-external', startTime: [1700000000, 0] },
|
|
474
|
+
{ traceId: 'trace2', spanId: 'span2', name: 'http-request-internal', startTime: [1700000000, 0] },
|
|
475
|
+
{ traceId: 'trace3', spanId: 'span3', name: 'db-query', startTime: [1700000000, 0] },
|
|
476
|
+
];
|
|
477
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
478
|
+
const results = await backend.queryTraces({
|
|
479
|
+
spanName: 'http',
|
|
480
|
+
excludeSpanName: 'internal',
|
|
481
|
+
});
|
|
482
|
+
assert.strictEqual(results.length, 1);
|
|
483
|
+
assert.strictEqual(results[0].name, 'http-request-external');
|
|
484
|
+
});
|
|
485
|
+
it('should combine attributeExists with attributeFilter', async () => {
|
|
486
|
+
const today = getTestDate();
|
|
487
|
+
const mockSpans = [
|
|
488
|
+
{
|
|
489
|
+
traceId: 'trace1',
|
|
490
|
+
spanId: 'span1',
|
|
491
|
+
name: 'op1',
|
|
492
|
+
startTime: [1700000000, 0],
|
|
493
|
+
attributes: { 'http.method': 'GET', 'http.status_code': 200 },
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
traceId: 'trace2',
|
|
497
|
+
spanId: 'span2',
|
|
498
|
+
name: 'op2',
|
|
499
|
+
startTime: [1700000000, 0],
|
|
500
|
+
attributes: { 'http.method': 'POST', 'http.status_code': 500 },
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
traceId: 'trace3',
|
|
504
|
+
spanId: 'span3',
|
|
505
|
+
name: 'op3',
|
|
506
|
+
startTime: [1700000000, 0],
|
|
507
|
+
attributes: { 'http.method': 'GET' }, // missing status_code
|
|
508
|
+
},
|
|
509
|
+
];
|
|
510
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
511
|
+
const results = await backend.queryTraces({
|
|
512
|
+
attributeFilter: { 'http.method': 'GET' },
|
|
513
|
+
attributeExists: ['http.status_code'],
|
|
514
|
+
});
|
|
515
|
+
assert.strictEqual(results.length, 1);
|
|
516
|
+
assert.strictEqual(results[0].traceId, 'trace1');
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
describe('queryLogs', () => {
|
|
520
|
+
it('should read and normalize log records from JSONL files', async () => {
|
|
521
|
+
const today = getTestDate();
|
|
522
|
+
const mockLogs = [
|
|
523
|
+
{
|
|
524
|
+
timestamp: '2026-01-28T10:00:00.000Z',
|
|
525
|
+
severityText: 'ERROR',
|
|
526
|
+
body: 'Connection failed',
|
|
527
|
+
traceId: 'trace1',
|
|
528
|
+
spanId: 'span1',
|
|
529
|
+
resource: { serviceName: 'api-service' },
|
|
530
|
+
attributes: { 'error.type': 'timeout' },
|
|
531
|
+
},
|
|
532
|
+
];
|
|
533
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
534
|
+
const results = await backend.queryLogs({});
|
|
535
|
+
assert.strictEqual(results.length, 1);
|
|
536
|
+
assert.strictEqual(results[0].severity, 'ERROR');
|
|
537
|
+
assert.strictEqual(results[0].body, 'Connection failed');
|
|
538
|
+
assert.strictEqual(results[0].attributes?.['service.name'], 'api-service');
|
|
539
|
+
assert.strictEqual(results[0].attributes?.['error.type'], 'timeout');
|
|
540
|
+
});
|
|
541
|
+
it('should handle timestamp as ISO string', async () => {
|
|
542
|
+
const today = getTestDate();
|
|
543
|
+
const mockLogs = [
|
|
544
|
+
{
|
|
545
|
+
timestamp: '2026-01-28T10:00:00.123Z',
|
|
546
|
+
body: 'Test log',
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
550
|
+
const results = await backend.queryLogs({});
|
|
551
|
+
assert.strictEqual(results[0].timestamp, '2026-01-28T10:00:00.123Z');
|
|
552
|
+
});
|
|
553
|
+
it('should convert timestamp from [seconds, nanoseconds] array', async () => {
|
|
554
|
+
const today = getTestDate();
|
|
555
|
+
const mockLogs = [
|
|
556
|
+
{
|
|
557
|
+
timestamp: [1700000000, 123456789], // seconds + nanoseconds
|
|
558
|
+
body: 'Test log',
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
562
|
+
const results = await backend.queryLogs({});
|
|
563
|
+
// Verify it's a valid ISO string
|
|
564
|
+
assert.match(results[0].timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
|
565
|
+
});
|
|
566
|
+
it('should filter logs by severity (case-insensitive)', async () => {
|
|
567
|
+
const today = getTestDate();
|
|
568
|
+
const mockLogs = [
|
|
569
|
+
{ timestamp: '2026-01-28T10:00:00Z', severityText: 'ERROR', body: 'Error 1' },
|
|
570
|
+
{ timestamp: '2026-01-28T10:01:00Z', severity: 'WARN', body: 'Warning 1' },
|
|
571
|
+
{ timestamp: '2026-01-28T10:02:00Z', severity: 'error', body: 'Error 2' },
|
|
572
|
+
{ timestamp: '2026-01-28T10:03:00Z', severity: 'INFO', body: 'Info 1' },
|
|
573
|
+
];
|
|
574
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
575
|
+
const results = await backend.queryLogs({ severity: 'ERROR' });
|
|
576
|
+
assert.strictEqual(results.length, 2);
|
|
577
|
+
assert.ok(results.every(l => l.severity.toUpperCase() === 'ERROR'));
|
|
578
|
+
});
|
|
579
|
+
it('should filter logs by traceId', async () => {
|
|
580
|
+
const today = getTestDate();
|
|
581
|
+
const mockLogs = [
|
|
582
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'Log 1', traceId: 'trace1' },
|
|
583
|
+
{ timestamp: '2026-01-28T10:01:00Z', body: 'Log 2', traceId: 'trace2' },
|
|
584
|
+
{ timestamp: '2026-01-28T10:02:00Z', body: 'Log 3', traceId: 'trace1' },
|
|
585
|
+
];
|
|
586
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
587
|
+
const results = await backend.queryLogs({ traceId: 'trace1' });
|
|
588
|
+
assert.strictEqual(results.length, 2);
|
|
589
|
+
assert.ok(results.every(l => l.traceId === 'trace1'));
|
|
590
|
+
});
|
|
591
|
+
it('should filter logs by search text (case-insensitive substring)', async () => {
|
|
592
|
+
const today = getTestDate();
|
|
593
|
+
const mockLogs = [
|
|
594
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'Connection timeout' },
|
|
595
|
+
{ timestamp: '2026-01-28T10:01:00Z', body: 'Database query failed' },
|
|
596
|
+
{ timestamp: '2026-01-28T10:02:00Z', body: 'Connection reset by peer' },
|
|
597
|
+
];
|
|
598
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
599
|
+
const results = await backend.queryLogs({ search: 'CONNECTION' });
|
|
600
|
+
assert.strictEqual(results.length, 2);
|
|
601
|
+
assert.ok(results.every(l => l.body.toLowerCase().includes('connection')));
|
|
602
|
+
});
|
|
603
|
+
it('should use severityText if available, fallback to severity', async () => {
|
|
604
|
+
const today = getTestDate();
|
|
605
|
+
const mockLogs = [
|
|
606
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'Log 1', severityText: 'CUSTOM' },
|
|
607
|
+
{ timestamp: '2026-01-28T10:01:00Z', body: 'Log 2', severity: 'WARN' },
|
|
608
|
+
{ timestamp: '2026-01-28T10:02:00Z', body: 'Log 3' }, // no severity
|
|
609
|
+
];
|
|
610
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
611
|
+
const results = await backend.queryLogs({});
|
|
612
|
+
assert.strictEqual(results[0].severity, 'CUSTOM');
|
|
613
|
+
assert.strictEqual(results[1].severity, 'WARN');
|
|
614
|
+
assert.strictEqual(results[2].severity, 'INFO'); // default
|
|
615
|
+
});
|
|
616
|
+
it('should apply limit and offset to log results', async () => {
|
|
617
|
+
const today = getTestDate();
|
|
618
|
+
const mockLogs = Array.from({ length: 200 }, (_, i) => ({
|
|
619
|
+
timestamp: new Date(Date.now() + i * 1000).toISOString(),
|
|
620
|
+
body: `Log ${i}`,
|
|
621
|
+
}));
|
|
622
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
623
|
+
const results = await backend.queryLogs({ limit: 50, offset: 75 });
|
|
624
|
+
assert.strictEqual(results.length, 50);
|
|
625
|
+
assert.strictEqual(results[0].body, 'Log 75');
|
|
626
|
+
});
|
|
627
|
+
it('should handle empty body field', async () => {
|
|
628
|
+
const today = getTestDate();
|
|
629
|
+
const mockLogs = [
|
|
630
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'Normal log' },
|
|
631
|
+
{ timestamp: '2026-01-28T10:01:00Z' }, // missing body
|
|
632
|
+
];
|
|
633
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
634
|
+
const results = await backend.queryLogs({});
|
|
635
|
+
assert.strictEqual(results.length, 2);
|
|
636
|
+
assert.strictEqual(results[1].body, '');
|
|
637
|
+
});
|
|
638
|
+
it('should set severityNumber based on severity text', async () => {
|
|
639
|
+
const today = getTestDate();
|
|
640
|
+
const mockLogs = [
|
|
641
|
+
{ timestamp: '2026-01-28T10:00:00Z', severityText: 'TRACE', body: 'Trace log' },
|
|
642
|
+
{ timestamp: '2026-01-28T10:01:00Z', severityText: 'DEBUG', body: 'Debug log' },
|
|
643
|
+
{ timestamp: '2026-01-28T10:02:00Z', severityText: 'INFO', body: 'Info log' },
|
|
644
|
+
{ timestamp: '2026-01-28T10:03:00Z', severityText: 'WARN', body: 'Warn log' },
|
|
645
|
+
{ timestamp: '2026-01-28T10:04:00Z', severityText: 'ERROR', body: 'Error log' },
|
|
646
|
+
{ timestamp: '2026-01-28T10:05:00Z', severityText: 'FATAL', body: 'Fatal log' },
|
|
647
|
+
];
|
|
648
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
649
|
+
const results = await backend.queryLogs({});
|
|
650
|
+
assert.strictEqual(results.length, 6);
|
|
651
|
+
assert.strictEqual(results[0].severityNumber, 1); // TRACE
|
|
652
|
+
assert.strictEqual(results[1].severityNumber, 5); // DEBUG
|
|
653
|
+
assert.strictEqual(results[2].severityNumber, 9); // INFO
|
|
654
|
+
assert.strictEqual(results[3].severityNumber, 13); // WARN
|
|
655
|
+
assert.strictEqual(results[4].severityNumber, 17); // ERROR
|
|
656
|
+
assert.strictEqual(results[5].severityNumber, 21); // FATAL
|
|
657
|
+
});
|
|
658
|
+
it('should handle lowercase severity when setting severityNumber', async () => {
|
|
659
|
+
const today = getTestDate();
|
|
660
|
+
const mockLogs = [
|
|
661
|
+
{ timestamp: '2026-01-28T10:00:00Z', severity: 'error', body: 'Lowercase error' },
|
|
662
|
+
{ timestamp: '2026-01-28T10:01:00Z', severity: 'warn', body: 'Lowercase warn' },
|
|
663
|
+
{ timestamp: '2026-01-28T10:02:00Z', severity: 'info', body: 'Lowercase info' },
|
|
664
|
+
];
|
|
665
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
666
|
+
const results = await backend.queryLogs({});
|
|
667
|
+
assert.strictEqual(results.length, 3);
|
|
668
|
+
assert.strictEqual(results[0].severityNumber, 17); // error -> ERROR -> 17
|
|
669
|
+
assert.strictEqual(results[1].severityNumber, 13); // warn -> WARN -> 13
|
|
670
|
+
assert.strictEqual(results[2].severityNumber, 9); // info -> INFO -> 9
|
|
671
|
+
});
|
|
672
|
+
it('should set severityNumber to undefined for unknown severity levels', async () => {
|
|
673
|
+
const today = getTestDate();
|
|
674
|
+
const mockLogs = [
|
|
675
|
+
{ timestamp: '2026-01-28T10:00:00Z', severityText: 'CUSTOM', body: 'Custom severity' },
|
|
676
|
+
{ timestamp: '2026-01-28T10:01:00Z', severityText: 'VERBOSE', body: 'Verbose severity' },
|
|
677
|
+
];
|
|
678
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
679
|
+
const results = await backend.queryLogs({});
|
|
680
|
+
assert.strictEqual(results.length, 2);
|
|
681
|
+
assert.strictEqual(results[0].severityNumber, undefined);
|
|
682
|
+
assert.strictEqual(results[1].severityNumber, undefined);
|
|
683
|
+
});
|
|
684
|
+
it('should set severityNumber to 9 (INFO) when severity defaults', async () => {
|
|
685
|
+
const today = getTestDate();
|
|
686
|
+
const mockLogs = [
|
|
687
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'No severity specified' },
|
|
688
|
+
];
|
|
689
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
690
|
+
const results = await backend.queryLogs({});
|
|
691
|
+
assert.strictEqual(results.length, 1);
|
|
692
|
+
assert.strictEqual(results[0].severity, 'INFO');
|
|
693
|
+
assert.strictEqual(results[0].severityNumber, 9);
|
|
694
|
+
});
|
|
695
|
+
it('should exclude logs matching excludeSearch', async () => {
|
|
696
|
+
const today = getTestDate();
|
|
697
|
+
const mockLogs = [
|
|
698
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'Connection failed' },
|
|
699
|
+
{ timestamp: '2026-01-28T10:01:00Z', body: 'Request successful' },
|
|
700
|
+
{ timestamp: '2026-01-28T10:02:00Z', body: 'Connection timeout' },
|
|
701
|
+
];
|
|
702
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
703
|
+
const results = await backend.queryLogs({ excludeSearch: 'connection' });
|
|
704
|
+
assert.strictEqual(results.length, 1);
|
|
705
|
+
assert.strictEqual(results[0].body, 'Request successful');
|
|
706
|
+
});
|
|
707
|
+
it('should combine search with excludeSearch', async () => {
|
|
708
|
+
const today = getTestDate();
|
|
709
|
+
const mockLogs = [
|
|
710
|
+
{ timestamp: '2026-01-28T10:00:00Z', body: 'User login successful' },
|
|
711
|
+
{ timestamp: '2026-01-28T10:01:00Z', body: 'User login failed' },
|
|
712
|
+
{ timestamp: '2026-01-28T10:02:00Z', body: 'System startup' },
|
|
713
|
+
{ timestamp: '2026-01-28T10:03:00Z', body: 'User logout' },
|
|
714
|
+
];
|
|
715
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
716
|
+
const results = await backend.queryLogs({
|
|
717
|
+
search: 'user',
|
|
718
|
+
excludeSearch: 'failed',
|
|
719
|
+
});
|
|
720
|
+
assert.strictEqual(results.length, 2);
|
|
721
|
+
assert.ok(results.some(l => l.body === 'User login successful'));
|
|
722
|
+
assert.ok(results.some(l => l.body === 'User logout'));
|
|
723
|
+
});
|
|
724
|
+
it('should filter logs by attributeExists - all must exist', async () => {
|
|
725
|
+
const today = getTestDate();
|
|
726
|
+
const mockLogs = [
|
|
727
|
+
{
|
|
728
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
729
|
+
body: 'Log 1',
|
|
730
|
+
attributes: { 'request.id': 'abc', 'user.id': '123' },
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
734
|
+
body: 'Log 2',
|
|
735
|
+
attributes: { 'request.id': 'def' }, // missing user.id
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
739
|
+
body: 'Log 3',
|
|
740
|
+
attributes: { 'other.attr': 'value' }, // missing both
|
|
741
|
+
},
|
|
742
|
+
];
|
|
743
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
744
|
+
const results = await backend.queryLogs({
|
|
745
|
+
attributeExists: ['request.id', 'user.id'],
|
|
746
|
+
});
|
|
747
|
+
assert.strictEqual(results.length, 1);
|
|
748
|
+
assert.strictEqual(results[0].body, 'Log 1');
|
|
749
|
+
});
|
|
750
|
+
it('should filter logs by attributeNotExists - exclude if any exist', async () => {
|
|
751
|
+
const today = getTestDate();
|
|
752
|
+
const mockLogs = [
|
|
753
|
+
{
|
|
754
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
755
|
+
body: 'Log with error',
|
|
756
|
+
attributes: { 'request.id': 'abc', 'error.message': 'timeout' },
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
760
|
+
body: 'Clean log',
|
|
761
|
+
attributes: { 'request.id': 'def' },
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
765
|
+
body: 'Log with exception',
|
|
766
|
+
attributes: { 'request.id': 'ghi', 'exception.type': 'NullPointer' },
|
|
767
|
+
},
|
|
768
|
+
];
|
|
769
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
770
|
+
const results = await backend.queryLogs({
|
|
771
|
+
attributeNotExists: ['error.message', 'exception.type'],
|
|
772
|
+
});
|
|
773
|
+
assert.strictEqual(results.length, 1);
|
|
774
|
+
assert.strictEqual(results[0].body, 'Clean log');
|
|
775
|
+
});
|
|
776
|
+
it('should combine search with attribute filters', async () => {
|
|
777
|
+
const today = getTestDate();
|
|
778
|
+
const mockLogs = [
|
|
779
|
+
{
|
|
780
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
781
|
+
body: 'API request completed',
|
|
782
|
+
attributes: { 'request.id': 'abc', 'http.status_code': 200 },
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
786
|
+
body: 'API request failed',
|
|
787
|
+
attributes: { 'request.id': 'def' }, // missing status_code
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
791
|
+
body: 'Database query completed',
|
|
792
|
+
attributes: { 'request.id': 'ghi', 'http.status_code': 200 },
|
|
793
|
+
},
|
|
794
|
+
];
|
|
795
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), mockLogs);
|
|
796
|
+
const results = await backend.queryLogs({
|
|
797
|
+
search: 'API',
|
|
798
|
+
attributeExists: ['http.status_code'],
|
|
799
|
+
});
|
|
800
|
+
assert.strictEqual(results.length, 1);
|
|
801
|
+
assert.strictEqual(results[0].body, 'API request completed');
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
describe('queryMetrics', () => {
|
|
805
|
+
it('should read and normalize metric data points from JSONL files', async () => {
|
|
806
|
+
const today = getTestDate();
|
|
807
|
+
const mockMetrics = [
|
|
808
|
+
{
|
|
809
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
810
|
+
name: 'http.requests.total',
|
|
811
|
+
value: 100,
|
|
812
|
+
type: 'counter',
|
|
813
|
+
unit: 'requests',
|
|
814
|
+
resource: { serviceName: 'api-gateway' },
|
|
815
|
+
attributes: { 'http.method': 'GET', 'http.status_code': 200 },
|
|
816
|
+
},
|
|
817
|
+
];
|
|
818
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
819
|
+
const results = await backend.queryMetrics({});
|
|
820
|
+
assert.strictEqual(results.length, 1);
|
|
821
|
+
assert.strictEqual(results[0].name, 'http.requests.total');
|
|
822
|
+
assert.strictEqual(results[0].value, 100);
|
|
823
|
+
assert.strictEqual(results[0].unit, 'requests');
|
|
824
|
+
assert.strictEqual(results[0].attributes?.['service.name'], 'api-gateway');
|
|
825
|
+
assert.strictEqual(results[0].attributes?.['http.method'], 'GET');
|
|
826
|
+
});
|
|
827
|
+
it('should filter metrics by name substring', async () => {
|
|
828
|
+
const today = getTestDate();
|
|
829
|
+
const mockMetrics = [
|
|
830
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'http.requests.total', value: 100, type: 'counter' },
|
|
831
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'http.request.duration', value: 150, type: 'histogram' },
|
|
832
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'memory.usage', value: 512, type: 'gauge' },
|
|
833
|
+
];
|
|
834
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
835
|
+
const results = await backend.queryMetrics({ metricName: 'http' });
|
|
836
|
+
assert.strictEqual(results.length, 2);
|
|
837
|
+
assert.ok(results.every(m => m.name.includes('http')));
|
|
838
|
+
});
|
|
839
|
+
it('should apply limit and offset to metric results', async () => {
|
|
840
|
+
const today = getTestDate();
|
|
841
|
+
const mockMetrics = Array.from({ length: 150 }, (_, i) => ({
|
|
842
|
+
timestamp: new Date(Date.now() + i * 1000).toISOString(),
|
|
843
|
+
name: `metric.${i}`,
|
|
844
|
+
value: i * 10,
|
|
845
|
+
type: 'gauge',
|
|
846
|
+
}));
|
|
847
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
848
|
+
const results = await backend.queryMetrics({ limit: 50, offset: 30 });
|
|
849
|
+
assert.strictEqual(results.length, 50);
|
|
850
|
+
assert.strictEqual(results[0].name, 'metric.30');
|
|
851
|
+
});
|
|
852
|
+
it('should aggregate metrics with sum function', async () => {
|
|
853
|
+
const today = getTestDate();
|
|
854
|
+
const mockMetrics = [
|
|
855
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'requests', value: 100, type: 'counter' },
|
|
856
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'requests', value: 150, type: 'counter' },
|
|
857
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'requests', value: 200, type: 'counter' },
|
|
858
|
+
];
|
|
859
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
860
|
+
const results = await backend.queryMetrics({ aggregation: 'sum' });
|
|
861
|
+
assert.strictEqual(results.length, 1);
|
|
862
|
+
assert.strictEqual(results[0].value, 450);
|
|
863
|
+
});
|
|
864
|
+
it('should aggregate metrics with avg function', async () => {
|
|
865
|
+
const today = getTestDate();
|
|
866
|
+
const mockMetrics = [
|
|
867
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'latency', value: 100, type: 'histogram' },
|
|
868
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'latency', value: 200, type: 'histogram' },
|
|
869
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'latency', value: 300, type: 'histogram' },
|
|
870
|
+
];
|
|
871
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
872
|
+
const results = await backend.queryMetrics({ aggregation: 'avg' });
|
|
873
|
+
assert.strictEqual(results.length, 1);
|
|
874
|
+
assert.strictEqual(results[0].value, 200);
|
|
875
|
+
});
|
|
876
|
+
it('should aggregate metrics with min function', async () => {
|
|
877
|
+
const today = getTestDate();
|
|
878
|
+
const mockMetrics = [
|
|
879
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'response_time', value: 150, type: 'gauge' },
|
|
880
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'response_time', value: 50, type: 'gauge' },
|
|
881
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'response_time', value: 200, type: 'gauge' },
|
|
882
|
+
];
|
|
883
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
884
|
+
const results = await backend.queryMetrics({ aggregation: 'min' });
|
|
885
|
+
assert.strictEqual(results.length, 1);
|
|
886
|
+
assert.strictEqual(results[0].value, 50);
|
|
887
|
+
});
|
|
888
|
+
it('should aggregate metrics with max function', async () => {
|
|
889
|
+
const today = getTestDate();
|
|
890
|
+
const mockMetrics = [
|
|
891
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'memory', value: 512, type: 'gauge' },
|
|
892
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'memory', value: 256, type: 'gauge' },
|
|
893
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'memory', value: 1024, type: 'gauge' },
|
|
894
|
+
];
|
|
895
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
896
|
+
const results = await backend.queryMetrics({ aggregation: 'max' });
|
|
897
|
+
assert.strictEqual(results.length, 1);
|
|
898
|
+
assert.strictEqual(results[0].value, 1024);
|
|
899
|
+
});
|
|
900
|
+
it('should aggregate metrics with count function', async () => {
|
|
901
|
+
const today = getTestDate();
|
|
902
|
+
const mockMetrics = [
|
|
903
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'events', value: 10, type: 'counter' },
|
|
904
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'events', value: 20, type: 'counter' },
|
|
905
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'events', value: 30, type: 'counter' },
|
|
906
|
+
];
|
|
907
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
908
|
+
const results = await backend.queryMetrics({ aggregation: 'count' });
|
|
909
|
+
assert.strictEqual(results.length, 1);
|
|
910
|
+
assert.strictEqual(results[0].value, 3);
|
|
911
|
+
});
|
|
912
|
+
it('should aggregate metrics grouped by attributes', async () => {
|
|
913
|
+
const today = getTestDate();
|
|
914
|
+
const mockMetrics = [
|
|
915
|
+
{
|
|
916
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
917
|
+
name: 'http.requests',
|
|
918
|
+
value: 100,
|
|
919
|
+
type: 'counter',
|
|
920
|
+
attributes: { method: 'GET' },
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
924
|
+
name: 'http.requests',
|
|
925
|
+
value: 50,
|
|
926
|
+
type: 'counter',
|
|
927
|
+
attributes: { method: 'POST' },
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
931
|
+
name: 'http.requests',
|
|
932
|
+
value: 200,
|
|
933
|
+
type: 'counter',
|
|
934
|
+
attributes: { method: 'GET' },
|
|
935
|
+
},
|
|
936
|
+
];
|
|
937
|
+
writeJsonlFile(path.join(tempDir, `metrics-${today}.jsonl`), mockMetrics);
|
|
938
|
+
const results = await backend.queryMetrics({ aggregation: 'sum', groupBy: ['method'] });
|
|
939
|
+
assert.strictEqual(results.length, 2);
|
|
940
|
+
const getMetric = results.find(m => m.attributes?.method === 'GET');
|
|
941
|
+
const postMetric = results.find(m => m.attributes?.method === 'POST');
|
|
942
|
+
assert.strictEqual(getMetric?.value, 300);
|
|
943
|
+
assert.strictEqual(postMetric?.value, 50);
|
|
944
|
+
});
|
|
945
|
+
it('should return empty array when no metrics found', async () => {
|
|
946
|
+
// No files created
|
|
947
|
+
const results = await backend.queryMetrics({});
|
|
948
|
+
assert.strictEqual(results.length, 0);
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
describe('queryLLMEvents', () => {
|
|
952
|
+
it('should read and normalize LLM events from JSONL files', async () => {
|
|
953
|
+
const today = getTestDate();
|
|
954
|
+
const mockEvents = [
|
|
955
|
+
{
|
|
956
|
+
timestamp: '2026-01-28T10:00:00.000Z',
|
|
957
|
+
name: 'llm.completion',
|
|
958
|
+
attributes: {
|
|
959
|
+
'gen_ai.request.model': 'claude-3-opus',
|
|
960
|
+
'gen_ai.system': 'anthropic',
|
|
961
|
+
'gen_ai.usage.input_tokens': 100,
|
|
962
|
+
'gen_ai.usage.output_tokens': 50,
|
|
963
|
+
'duration_ms': 1500,
|
|
964
|
+
'success': true,
|
|
965
|
+
},
|
|
966
|
+
},
|
|
967
|
+
];
|
|
968
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
969
|
+
const results = await backend.queryLLMEvents({});
|
|
970
|
+
assert.strictEqual(results.length, 1);
|
|
971
|
+
assert.strictEqual(results[0].name, 'llm.completion');
|
|
972
|
+
assert.strictEqual(results[0].attributes['gen_ai.request.model'], 'claude-3-opus');
|
|
973
|
+
assert.strictEqual(results[0].attributes['gen_ai.system'], 'anthropic');
|
|
974
|
+
assert.strictEqual(results[0].attributes['gen_ai.usage.input_tokens'], 100);
|
|
975
|
+
});
|
|
976
|
+
it('should filter events by eventName substring', async () => {
|
|
977
|
+
const today = getTestDate();
|
|
978
|
+
const mockEvents = [
|
|
979
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'llm.completion', attributes: {} },
|
|
980
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'llm.embedding', attributes: {} },
|
|
981
|
+
{ timestamp: '2026-01-28T10:02:00Z', name: 'tool.execution', attributes: {} },
|
|
982
|
+
];
|
|
983
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
984
|
+
const results = await backend.queryLLMEvents({ eventName: 'llm' });
|
|
985
|
+
assert.strictEqual(results.length, 2);
|
|
986
|
+
assert.ok(results.every(e => e.name.includes('llm')));
|
|
987
|
+
});
|
|
988
|
+
it('should filter events by model', async () => {
|
|
989
|
+
const today = getTestDate();
|
|
990
|
+
const mockEvents = [
|
|
991
|
+
{
|
|
992
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
993
|
+
name: 'llm.completion',
|
|
994
|
+
attributes: { 'gen_ai.request.model': 'claude-3-opus' },
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
998
|
+
name: 'llm.completion',
|
|
999
|
+
attributes: { 'gen_ai.request.model': 'gpt-4' },
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
1003
|
+
name: 'llm.completion',
|
|
1004
|
+
attributes: { model: 'claude-3-opus' }, // alternate attribute name
|
|
1005
|
+
},
|
|
1006
|
+
];
|
|
1007
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1008
|
+
const results = await backend.queryLLMEvents({ model: 'claude-3-opus' });
|
|
1009
|
+
assert.strictEqual(results.length, 2);
|
|
1010
|
+
});
|
|
1011
|
+
it('should filter events by provider', async () => {
|
|
1012
|
+
const today = getTestDate();
|
|
1013
|
+
const mockEvents = [
|
|
1014
|
+
{
|
|
1015
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
1016
|
+
name: 'llm.completion',
|
|
1017
|
+
attributes: { 'gen_ai.system': 'anthropic' },
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
1021
|
+
name: 'llm.completion',
|
|
1022
|
+
attributes: { 'gen_ai.system': 'openai' },
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
1026
|
+
name: 'llm.completion',
|
|
1027
|
+
attributes: { provider: 'anthropic' }, // alternate attribute name
|
|
1028
|
+
},
|
|
1029
|
+
];
|
|
1030
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1031
|
+
const results = await backend.queryLLMEvents({ provider: 'anthropic' });
|
|
1032
|
+
assert.strictEqual(results.length, 2);
|
|
1033
|
+
});
|
|
1034
|
+
it('should filter events by search text in attributes', async () => {
|
|
1035
|
+
const today = getTestDate();
|
|
1036
|
+
const mockEvents = [
|
|
1037
|
+
{
|
|
1038
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
1039
|
+
name: 'llm.completion',
|
|
1040
|
+
attributes: { prompt: 'Write a function to calculate fibonacci' },
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
1044
|
+
name: 'llm.completion',
|
|
1045
|
+
attributes: { prompt: 'Explain quantum computing' },
|
|
1046
|
+
},
|
|
1047
|
+
];
|
|
1048
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1049
|
+
const results = await backend.queryLLMEvents({ search: 'fibonacci' });
|
|
1050
|
+
assert.strictEqual(results.length, 1);
|
|
1051
|
+
assert.strictEqual(results[0].attributes.prompt, 'Write a function to calculate fibonacci');
|
|
1052
|
+
});
|
|
1053
|
+
it('should filter events by search text in event name', async () => {
|
|
1054
|
+
const today = getTestDate();
|
|
1055
|
+
const mockEvents = [
|
|
1056
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'llm.completion.streaming', attributes: {} },
|
|
1057
|
+
{ timestamp: '2026-01-28T10:01:00Z', name: 'llm.completion', attributes: {} },
|
|
1058
|
+
];
|
|
1059
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1060
|
+
const results = await backend.queryLLMEvents({ search: 'streaming' });
|
|
1061
|
+
assert.strictEqual(results.length, 1);
|
|
1062
|
+
assert.strictEqual(results[0].name, 'llm.completion.streaming');
|
|
1063
|
+
});
|
|
1064
|
+
it('should apply limit and offset to LLM event results', async () => {
|
|
1065
|
+
const today = getTestDate();
|
|
1066
|
+
const mockEvents = Array.from({ length: 100 }, (_, i) => ({
|
|
1067
|
+
timestamp: new Date(Date.now() + i * 1000).toISOString(),
|
|
1068
|
+
name: `event-${i}`,
|
|
1069
|
+
attributes: { index: i },
|
|
1070
|
+
}));
|
|
1071
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1072
|
+
const results = await backend.queryLLMEvents({ limit: 20, offset: 50 });
|
|
1073
|
+
assert.strictEqual(results.length, 20);
|
|
1074
|
+
assert.strictEqual(results[0].name, 'event-50');
|
|
1075
|
+
});
|
|
1076
|
+
it('should filter events by date range', async () => {
|
|
1077
|
+
// Create files for multiple dates
|
|
1078
|
+
writeJsonlFile(path.join(tempDir, 'llm-events-2026-01-26.jsonl'), [
|
|
1079
|
+
{ timestamp: '2026-01-26T10:00:00Z', name: 'event-26', attributes: {} },
|
|
1080
|
+
]);
|
|
1081
|
+
writeJsonlFile(path.join(tempDir, 'llm-events-2026-01-27.jsonl'), [
|
|
1082
|
+
{ timestamp: '2026-01-27T10:00:00Z', name: 'event-27', attributes: {} },
|
|
1083
|
+
]);
|
|
1084
|
+
writeJsonlFile(path.join(tempDir, 'llm-events-2026-01-28.jsonl'), [
|
|
1085
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'event-28', attributes: {} },
|
|
1086
|
+
]);
|
|
1087
|
+
const results = await backend.queryLLMEvents({
|
|
1088
|
+
startDate: '2026-01-27',
|
|
1089
|
+
endDate: '2026-01-27',
|
|
1090
|
+
});
|
|
1091
|
+
assert.strictEqual(results.length, 1);
|
|
1092
|
+
assert.strictEqual(results[0].name, 'event-27');
|
|
1093
|
+
});
|
|
1094
|
+
it('should skip events with missing required fields', async () => {
|
|
1095
|
+
const today = getTestDate();
|
|
1096
|
+
const mockEvents = [
|
|
1097
|
+
{ timestamp: '2026-01-28T10:00:00Z', name: 'valid-event', attributes: {} },
|
|
1098
|
+
{ timestamp: '2026-01-28T10:01:00Z', attributes: {} }, // missing name
|
|
1099
|
+
{ name: 'no-timestamp', attributes: {} }, // missing timestamp
|
|
1100
|
+
];
|
|
1101
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1102
|
+
const results = await backend.queryLLMEvents({});
|
|
1103
|
+
assert.strictEqual(results.length, 1);
|
|
1104
|
+
assert.strictEqual(results[0].name, 'valid-event');
|
|
1105
|
+
});
|
|
1106
|
+
it('should return empty array when no LLM event files found', async () => {
|
|
1107
|
+
const results = await backend.queryLLMEvents({});
|
|
1108
|
+
assert.strictEqual(results.length, 0);
|
|
1109
|
+
});
|
|
1110
|
+
it('should combine multiple filters', async () => {
|
|
1111
|
+
const today = getTestDate();
|
|
1112
|
+
const mockEvents = [
|
|
1113
|
+
{
|
|
1114
|
+
timestamp: '2026-01-28T10:00:00Z',
|
|
1115
|
+
name: 'llm.completion',
|
|
1116
|
+
attributes: { 'gen_ai.request.model': 'claude-3-opus', 'gen_ai.system': 'anthropic' },
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
timestamp: '2026-01-28T10:01:00Z',
|
|
1120
|
+
name: 'llm.completion',
|
|
1121
|
+
attributes: { 'gen_ai.request.model': 'gpt-4', 'gen_ai.system': 'openai' },
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
timestamp: '2026-01-28T10:02:00Z',
|
|
1125
|
+
name: 'llm.embedding',
|
|
1126
|
+
attributes: { 'gen_ai.request.model': 'claude-3-opus', 'gen_ai.system': 'anthropic' },
|
|
1127
|
+
},
|
|
1128
|
+
];
|
|
1129
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
|
|
1130
|
+
const results = await backend.queryLLMEvents({
|
|
1131
|
+
eventName: 'completion',
|
|
1132
|
+
model: 'claude-3-opus',
|
|
1133
|
+
provider: 'anthropic',
|
|
1134
|
+
});
|
|
1135
|
+
assert.strictEqual(results.length, 1);
|
|
1136
|
+
assert.strictEqual(results[0].name, 'llm.completion');
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
describe('healthCheck', () => {
|
|
1140
|
+
it('should return error when telemetry directory does not exist', async () => {
|
|
1141
|
+
const nonExistentBackend = new LocalJsonlBackend('/nonexistent/telemetry');
|
|
1142
|
+
const result = await nonExistentBackend.healthCheck();
|
|
1143
|
+
assert.strictEqual(result.status, 'error');
|
|
1144
|
+
assert.match(result.message || '', /not found/);
|
|
1145
|
+
});
|
|
1146
|
+
it('should return ok when directory exists with no files', async () => {
|
|
1147
|
+
// tempDir exists but has no files
|
|
1148
|
+
const result = await backend.healthCheck();
|
|
1149
|
+
assert.strictEqual(result.status, 'ok');
|
|
1150
|
+
assert.match(result.message || '', /No telemetry files/);
|
|
1151
|
+
});
|
|
1152
|
+
it('should return ok with found files message', async () => {
|
|
1153
|
+
const today = getTestDate();
|
|
1154
|
+
// Create both traces and logs files
|
|
1155
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), []);
|
|
1156
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), []);
|
|
1157
|
+
const result = await backend.healthCheck();
|
|
1158
|
+
assert.strictEqual(result.status, 'ok');
|
|
1159
|
+
assert.match(result.message || '', /traces.*logs/);
|
|
1160
|
+
});
|
|
1161
|
+
it('should include llm-events in health check message', async () => {
|
|
1162
|
+
const today = getTestDate();
|
|
1163
|
+
// Create all three file types
|
|
1164
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), []);
|
|
1165
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), []);
|
|
1166
|
+
writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), []);
|
|
1167
|
+
const result = await backend.healthCheck();
|
|
1168
|
+
assert.strictEqual(result.status, 'ok');
|
|
1169
|
+
assert.match(result.message || '', /llm-events/);
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
describe('date range filtering', () => {
|
|
1173
|
+
it('should filter files by startDate and endDate', async () => {
|
|
1174
|
+
// Create files for multiple dates
|
|
1175
|
+
writeJsonlFile(path.join(tempDir, 'traces-2026-01-26.jsonl'), [
|
|
1176
|
+
{ traceId: 'trace-26', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
1177
|
+
]);
|
|
1178
|
+
writeJsonlFile(path.join(tempDir, 'traces-2026-01-27.jsonl'), [
|
|
1179
|
+
{ traceId: 'trace-27', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
1180
|
+
]);
|
|
1181
|
+
writeJsonlFile(path.join(tempDir, 'traces-2026-01-28.jsonl'), [
|
|
1182
|
+
{ traceId: 'trace-28', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
1183
|
+
]);
|
|
1184
|
+
writeJsonlFile(path.join(tempDir, 'traces-2026-01-29.jsonl'), [
|
|
1185
|
+
{ traceId: 'trace-29', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
1186
|
+
]);
|
|
1187
|
+
const results = await backend.queryTraces({ startDate: '2026-01-27', endDate: '2026-01-28' });
|
|
1188
|
+
assert.strictEqual(results.length, 2);
|
|
1189
|
+
const traceIds = results.map(r => r.traceId);
|
|
1190
|
+
assert.ok(traceIds.includes('trace-27'));
|
|
1191
|
+
assert.ok(traceIds.includes('trace-28'));
|
|
1192
|
+
assert.ok(!traceIds.includes('trace-26'));
|
|
1193
|
+
assert.ok(!traceIds.includes('trace-29'));
|
|
1194
|
+
});
|
|
1195
|
+
it('should use today as default when no date range specified', async () => {
|
|
1196
|
+
const today = getTestDate();
|
|
1197
|
+
const yesterday = new Date(Date.now() - 86400000).toISOString().split('T')[0];
|
|
1198
|
+
// Create file for today and yesterday
|
|
1199
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), [
|
|
1200
|
+
{ traceId: 'today-trace', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
1201
|
+
]);
|
|
1202
|
+
writeJsonlFile(path.join(tempDir, `traces-${yesterday}.jsonl`), [
|
|
1203
|
+
{ traceId: 'yesterday-trace', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
|
|
1204
|
+
]);
|
|
1205
|
+
// Query with no date range should only get today's data
|
|
1206
|
+
const results = await backend.queryTraces({});
|
|
1207
|
+
assert.strictEqual(results.length, 1);
|
|
1208
|
+
assert.strictEqual(results[0].traceId, 'today-trace');
|
|
1209
|
+
});
|
|
1210
|
+
});
|
|
1211
|
+
describe('error handling', () => {
|
|
1212
|
+
it('should handle JSONL parsing errors gracefully', async () => {
|
|
1213
|
+
const today = getTestDate();
|
|
1214
|
+
const filePath = path.join(tempDir, `traces-${today}.jsonl`);
|
|
1215
|
+
// Write malformed JSON
|
|
1216
|
+
fs.writeFileSync(filePath, 'not valid json\n{"traceId":"t1","spanId":"s1","name":"op"}\n', 'utf-8');
|
|
1217
|
+
const results = await backend.queryTraces({});
|
|
1218
|
+
// Should skip the malformed line and parse the valid one
|
|
1219
|
+
assert.strictEqual(results.length, 1);
|
|
1220
|
+
assert.strictEqual(results[0].traceId, 't1');
|
|
1221
|
+
});
|
|
1222
|
+
it('should skip spans with invalid time calculations', async () => {
|
|
1223
|
+
const today = getTestDate();
|
|
1224
|
+
const mockSpans = [
|
|
1225
|
+
{
|
|
1226
|
+
traceId: 'trace1',
|
|
1227
|
+
spanId: 'span1',
|
|
1228
|
+
name: 'op1',
|
|
1229
|
+
startTime: [1700000000, 0],
|
|
1230
|
+
endTime: [1700000000, 0],
|
|
1231
|
+
},
|
|
1232
|
+
];
|
|
1233
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
|
|
1234
|
+
const results = await backend.queryTraces({});
|
|
1235
|
+
assert.strictEqual(results.length, 1);
|
|
1236
|
+
assert.strictEqual(results[0].durationMs, 0);
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
describe('insertSortedBounded helper', () => {
|
|
1241
|
+
// Test the insertSortedBounded function by observing its behavior
|
|
1242
|
+
// through MultiDirectoryBackend's query methods
|
|
1243
|
+
it('should maintain sorted order by timestamp in traces', async () => {
|
|
1244
|
+
const projectDir = createTempDir();
|
|
1245
|
+
try {
|
|
1246
|
+
const today = getTestDate();
|
|
1247
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1248
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1249
|
+
// Create traces with various timestamps
|
|
1250
|
+
writeJsonlFile(path.join(localTelemetry, `traces-${today}.jsonl`), [
|
|
1251
|
+
{ traceId: 'sorted-test-1', spanId: 's1', name: 'SortedTestOp', startTime: [1700000005, 0] },
|
|
1252
|
+
{ traceId: 'sorted-test-2', spanId: 's2', name: 'SortedTestOp', startTime: [1700000001, 0] },
|
|
1253
|
+
{ traceId: 'sorted-test-3', spanId: 's3', name: 'SortedTestOp', startTime: [1700000009, 0] },
|
|
1254
|
+
{ traceId: 'sorted-test-4', spanId: 's4', name: 'SortedTestOp', startTime: [1700000003, 0] },
|
|
1255
|
+
{ traceId: 'sorted-test-5', spanId: 's5', name: 'SortedTestOp', startTime: [1700000007, 0] },
|
|
1256
|
+
]);
|
|
1257
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1258
|
+
const results = await backend.queryTraces({ spanName: 'SortedTestOp' });
|
|
1259
|
+
// Should be sorted by timestamp descending (most recent first)
|
|
1260
|
+
assert.strictEqual(results.length, 5);
|
|
1261
|
+
assert.strictEqual(results[0].traceId, 'sorted-test-3'); // 1700000009
|
|
1262
|
+
assert.strictEqual(results[1].traceId, 'sorted-test-5'); // 1700000007
|
|
1263
|
+
assert.strictEqual(results[2].traceId, 'sorted-test-1'); // 1700000005
|
|
1264
|
+
assert.strictEqual(results[3].traceId, 'sorted-test-4'); // 1700000003
|
|
1265
|
+
assert.strictEqual(results[4].traceId, 'sorted-test-2'); // 1700000001
|
|
1266
|
+
}
|
|
1267
|
+
finally {
|
|
1268
|
+
removeTempDir(projectDir);
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
it('should limit results to maxSize efficiently', async () => {
|
|
1272
|
+
const projectDir = createTempDir();
|
|
1273
|
+
try {
|
|
1274
|
+
const today = getTestDate();
|
|
1275
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1276
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1277
|
+
// Create 20 traces
|
|
1278
|
+
writeJsonlFile(path.join(localTelemetry, `traces-${today}.jsonl`), Array.from({ length: 20 }, (_, i) => ({
|
|
1279
|
+
traceId: `bounded-test-${i}`,
|
|
1280
|
+
spanId: `s${i}`,
|
|
1281
|
+
name: 'BoundedTestOp',
|
|
1282
|
+
startTime: [1700000000 + i, 0],
|
|
1283
|
+
})));
|
|
1284
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1285
|
+
const results = await backend.queryTraces({ spanName: 'BoundedTestOp', limit: 5 });
|
|
1286
|
+
// Should return only 5 results
|
|
1287
|
+
assert.strictEqual(results.length, 5);
|
|
1288
|
+
// Results should be sorted by timestamp descending
|
|
1289
|
+
// LocalJsonlBackend returns first 5 found (0-4), then sorted
|
|
1290
|
+
assert.strictEqual(results[0].traceId, 'bounded-test-4'); // highest of 0-4
|
|
1291
|
+
assert.strictEqual(results[4].traceId, 'bounded-test-0'); // lowest of 0-4
|
|
1292
|
+
}
|
|
1293
|
+
finally {
|
|
1294
|
+
removeTempDir(projectDir);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
it('should maintain sorted order in logs', async () => {
|
|
1298
|
+
const projectDir = createTempDir();
|
|
1299
|
+
try {
|
|
1300
|
+
const today = getTestDate();
|
|
1301
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1302
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1303
|
+
writeJsonlFile(path.join(localTelemetry, `logs-${today}.jsonl`), [
|
|
1304
|
+
{ timestamp: `${today}T10:00:00Z`, body: 'SortedLogTest_A' },
|
|
1305
|
+
{ timestamp: `${today}T12:00:00Z`, body: 'SortedLogTest_B' },
|
|
1306
|
+
{ timestamp: `${today}T08:00:00Z`, body: 'SortedLogTest_C' },
|
|
1307
|
+
{ timestamp: `${today}T14:00:00Z`, body: 'SortedLogTest_D' },
|
|
1308
|
+
]);
|
|
1309
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1310
|
+
const results = await backend.queryLogs({ search: 'SortedLogTest' });
|
|
1311
|
+
// Should be sorted by timestamp descending
|
|
1312
|
+
assert.strictEqual(results.length, 4);
|
|
1313
|
+
assert.strictEqual(results[0].body, 'SortedLogTest_D'); // 14:00
|
|
1314
|
+
assert.strictEqual(results[1].body, 'SortedLogTest_B'); // 12:00
|
|
1315
|
+
assert.strictEqual(results[2].body, 'SortedLogTest_A'); // 10:00
|
|
1316
|
+
assert.strictEqual(results[3].body, 'SortedLogTest_C'); // 08:00
|
|
1317
|
+
}
|
|
1318
|
+
finally {
|
|
1319
|
+
removeTempDir(projectDir);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
it('should maintain sorted order in LLM events', async () => {
|
|
1323
|
+
const projectDir = createTempDir();
|
|
1324
|
+
try {
|
|
1325
|
+
const today = getTestDate();
|
|
1326
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1327
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1328
|
+
writeJsonlFile(path.join(localTelemetry, `llm-events-${today}.jsonl`), [
|
|
1329
|
+
{ timestamp: `${today}T09:00:00Z`, name: 'SortedLLMTest.A', attributes: {} },
|
|
1330
|
+
{ timestamp: `${today}T15:00:00Z`, name: 'SortedLLMTest.B', attributes: {} },
|
|
1331
|
+
{ timestamp: `${today}T11:00:00Z`, name: 'SortedLLMTest.C', attributes: {} },
|
|
1332
|
+
]);
|
|
1333
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1334
|
+
const results = await backend.queryLLMEvents({ eventName: 'SortedLLMTest' });
|
|
1335
|
+
// Should be sorted by timestamp descending
|
|
1336
|
+
assert.strictEqual(results.length, 3);
|
|
1337
|
+
assert.strictEqual(results[0].name, 'SortedLLMTest.B'); // 15:00
|
|
1338
|
+
assert.strictEqual(results[1].name, 'SortedLLMTest.C'); // 11:00
|
|
1339
|
+
assert.strictEqual(results[2].name, 'SortedLLMTest.A'); // 09:00
|
|
1340
|
+
}
|
|
1341
|
+
finally {
|
|
1342
|
+
removeTempDir(projectDir);
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
describe('streaming JSONL optimization', () => {
|
|
1347
|
+
it('should handle large files with streaming', async () => {
|
|
1348
|
+
const tempDir = createTempDir();
|
|
1349
|
+
try {
|
|
1350
|
+
const today = getTestDate();
|
|
1351
|
+
// Create a file with many records
|
|
1352
|
+
const spans = Array.from({ length: 500 }, (_, i) => ({
|
|
1353
|
+
traceId: `stream-test-${i}`,
|
|
1354
|
+
spanId: `span-${i}`,
|
|
1355
|
+
name: 'StreamTestOp',
|
|
1356
|
+
startTime: [1700000000 + i, 0],
|
|
1357
|
+
}));
|
|
1358
|
+
writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), spans);
|
|
1359
|
+
const backend = new LocalJsonlBackend(tempDir);
|
|
1360
|
+
const results = await backend.queryTraces({ spanName: 'StreamTestOp', limit: 50 });
|
|
1361
|
+
// Should return limited results without loading all into memory
|
|
1362
|
+
assert.strictEqual(results.length, 50);
|
|
1363
|
+
}
|
|
1364
|
+
finally {
|
|
1365
|
+
removeTempDir(tempDir);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
it('should terminate early when limit is reached', async () => {
|
|
1369
|
+
const tempDir = createTempDir();
|
|
1370
|
+
try {
|
|
1371
|
+
const today = getTestDate();
|
|
1372
|
+
// Create file with many records
|
|
1373
|
+
const logs = Array.from({ length: 1000 }, (_, i) => ({
|
|
1374
|
+
timestamp: new Date(Date.now() - i * 1000).toISOString(),
|
|
1375
|
+
body: `StreamingLog_${i}`,
|
|
1376
|
+
severity: 'INFO',
|
|
1377
|
+
}));
|
|
1378
|
+
writeJsonlFile(path.join(tempDir, `logs-${today}.jsonl`), logs);
|
|
1379
|
+
const backend = new LocalJsonlBackend(tempDir);
|
|
1380
|
+
const start = Date.now();
|
|
1381
|
+
const results = await backend.queryLogs({ search: 'StreamingLog', limit: 10 });
|
|
1382
|
+
const elapsed = Date.now() - start;
|
|
1383
|
+
// Should return quickly with early termination
|
|
1384
|
+
assert.strictEqual(results.length, 10);
|
|
1385
|
+
// Processing should be fast due to early termination
|
|
1386
|
+
assert.ok(elapsed < 1000, `Query took too long: ${elapsed}ms`);
|
|
1387
|
+
}
|
|
1388
|
+
finally {
|
|
1389
|
+
removeTempDir(tempDir);
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
describe('MultiDirectoryBackend', () => {
|
|
1394
|
+
let projectDir;
|
|
1395
|
+
beforeEach(() => {
|
|
1396
|
+
// Create a project directory that will have local telemetry subdirectories
|
|
1397
|
+
projectDir = createTempDir();
|
|
1398
|
+
});
|
|
1399
|
+
afterEach(() => {
|
|
1400
|
+
removeTempDir(projectDir);
|
|
1401
|
+
});
|
|
1402
|
+
describe('constructor and getDirectories', () => {
|
|
1403
|
+
it('should return directories when local telemetry dirs exist', () => {
|
|
1404
|
+
// Create local telemetry directory
|
|
1405
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1406
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1407
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1408
|
+
const dirs = backend.getDirectories();
|
|
1409
|
+
assert.ok(Array.isArray(dirs));
|
|
1410
|
+
// Should include the local telemetry directory
|
|
1411
|
+
const localDir = dirs.find(d => d.source === 'local' && d.path === localTelemetry);
|
|
1412
|
+
assert.ok(localDir);
|
|
1413
|
+
});
|
|
1414
|
+
it('should have name property set to multi-directory', () => {
|
|
1415
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1416
|
+
assert.strictEqual(backend.name, 'multi-directory');
|
|
1417
|
+
});
|
|
1418
|
+
it('should detect .telemetry local directory', () => {
|
|
1419
|
+
const localTelemetry = path.join(projectDir, '.telemetry');
|
|
1420
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1421
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1422
|
+
const dirs = backend.getDirectories();
|
|
1423
|
+
const localDir = dirs.find(d => d.path === localTelemetry);
|
|
1424
|
+
assert.ok(localDir);
|
|
1425
|
+
assert.strictEqual(localDir?.source, 'local');
|
|
1426
|
+
});
|
|
1427
|
+
it('should detect .claude/telemetry local directory', () => {
|
|
1428
|
+
const localTelemetry = path.join(projectDir, '.claude', 'telemetry');
|
|
1429
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1430
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1431
|
+
const dirs = backend.getDirectories();
|
|
1432
|
+
const localDir = dirs.find(d => d.path === localTelemetry);
|
|
1433
|
+
assert.ok(localDir);
|
|
1434
|
+
assert.strictEqual(localDir?.source, 'local');
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
describe('queryTraces', () => {
|
|
1438
|
+
it('should query traces from local telemetry directory', async () => {
|
|
1439
|
+
const today = getTestDate();
|
|
1440
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1441
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1442
|
+
// Create traces in local directory with unique IDs
|
|
1443
|
+
writeJsonlFile(path.join(localTelemetry, `traces-${today}.jsonl`), [
|
|
1444
|
+
{ traceId: 'multidir-unique-test-abc123-1', spanId: 'span1', name: 'MultiDirUniqueOp_XYZ', startTime: [1700000000, 0] },
|
|
1445
|
+
{ traceId: 'multidir-unique-test-abc123-2', spanId: 'span2', name: 'MultiDirUniqueOp_XYZ', startTime: [1700000002, 0] },
|
|
1446
|
+
]);
|
|
1447
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1448
|
+
// Use spanName filter to find our specific test traces
|
|
1449
|
+
const results = await backend.queryTraces({ spanName: 'MultiDirUniqueOp_XYZ' });
|
|
1450
|
+
assert.strictEqual(results.length, 2);
|
|
1451
|
+
assert.ok(results.some(t => t.traceId === 'multidir-unique-test-abc123-1'), 'Should find test trace 1');
|
|
1452
|
+
assert.ok(results.some(t => t.traceId === 'multidir-unique-test-abc123-2'), 'Should find test trace 2');
|
|
1453
|
+
});
|
|
1454
|
+
it('should merge and sort traces by timestamp', async () => {
|
|
1455
|
+
const today = getTestDate();
|
|
1456
|
+
// Create two local telemetry directories
|
|
1457
|
+
const localTelemetry1 = path.join(projectDir, 'telemetry');
|
|
1458
|
+
const localTelemetry2 = path.join(projectDir, '.telemetry');
|
|
1459
|
+
fs.mkdirSync(localTelemetry1, { recursive: true });
|
|
1460
|
+
fs.mkdirSync(localTelemetry2, { recursive: true });
|
|
1461
|
+
// Create traces with different timestamps and unique operation name
|
|
1462
|
+
writeJsonlFile(path.join(localTelemetry1, `traces-${today}.jsonl`), [
|
|
1463
|
+
{ traceId: 'multidir-sort-old-xyz789', spanId: 'span1', name: 'MultiDirSortOp_ABC', startTime: [1700000000, 0] },
|
|
1464
|
+
]);
|
|
1465
|
+
writeJsonlFile(path.join(localTelemetry2, `traces-${today}.jsonl`), [
|
|
1466
|
+
{ traceId: 'multidir-sort-new-xyz789', spanId: 'span2', name: 'MultiDirSortOp_ABC', startTime: [1700000010, 0] },
|
|
1467
|
+
]);
|
|
1468
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1469
|
+
const results = await backend.queryTraces({ spanName: 'MultiDirSortOp_ABC' });
|
|
1470
|
+
assert.strictEqual(results.length, 2);
|
|
1471
|
+
// Newer trace should be first (sorted by startTimeUnixNano descending)
|
|
1472
|
+
assert.strictEqual(results[0].traceId, 'multidir-sort-new-xyz789');
|
|
1473
|
+
assert.strictEqual(results[1].traceId, 'multidir-sort-old-xyz789');
|
|
1474
|
+
});
|
|
1475
|
+
it('should respect limit parameter', async () => {
|
|
1476
|
+
const today = getTestDate();
|
|
1477
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1478
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1479
|
+
// Create many traces with unique prefix
|
|
1480
|
+
writeJsonlFile(path.join(localTelemetry, `traces-${today}.jsonl`), Array.from({ length: 150 }, (_, i) => ({
|
|
1481
|
+
traceId: `multidir-limit-trace-${i}`,
|
|
1482
|
+
spanId: `span${i}`,
|
|
1483
|
+
name: `MultiDirLimitOp_${i}`,
|
|
1484
|
+
startTime: [1700000000 + i, 0],
|
|
1485
|
+
})));
|
|
1486
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1487
|
+
const results = await backend.queryTraces({ spanName: 'MultiDirLimitOp', limit: 50 });
|
|
1488
|
+
assert.ok(results.length <= 50);
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1491
|
+
describe('queryLogs', () => {
|
|
1492
|
+
it('should query logs from local telemetry directory', async () => {
|
|
1493
|
+
const today = getTestDate();
|
|
1494
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1495
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1496
|
+
writeJsonlFile(path.join(localTelemetry, `logs-${today}.jsonl`), [
|
|
1497
|
+
{ timestamp: `${today}T10:00:00Z`, body: 'MultiDirUniqueTestLog_ABC123_1', severity: 'INFO' },
|
|
1498
|
+
{ timestamp: `${today}T11:00:00Z`, body: 'MultiDirUniqueTestLog_ABC123_2', severity: 'ERROR' },
|
|
1499
|
+
]);
|
|
1500
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1501
|
+
// Use search filter to find our specific test logs
|
|
1502
|
+
const results = await backend.queryLogs({ search: 'MultiDirUniqueTestLog_ABC123' });
|
|
1503
|
+
assert.strictEqual(results.length, 2);
|
|
1504
|
+
assert.ok(results.some(l => l.body === 'MultiDirUniqueTestLog_ABC123_1'), 'Should find test log 1');
|
|
1505
|
+
assert.ok(results.some(l => l.body === 'MultiDirUniqueTestLog_ABC123_2'), 'Should find test log 2');
|
|
1506
|
+
});
|
|
1507
|
+
it('should sort logs by timestamp descending', async () => {
|
|
1508
|
+
const today = getTestDate();
|
|
1509
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1510
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1511
|
+
writeJsonlFile(path.join(localTelemetry, `logs-${today}.jsonl`), [
|
|
1512
|
+
{ timestamp: `${today}T08:00:00Z`, body: 'MultiDirSortTest_Early', severity: 'INFO' },
|
|
1513
|
+
{ timestamp: `${today}T12:00:00Z`, body: 'MultiDirSortTest_Late', severity: 'INFO' },
|
|
1514
|
+
]);
|
|
1515
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1516
|
+
const results = await backend.queryLogs({ search: 'MultiDirSortTest' });
|
|
1517
|
+
assert.strictEqual(results.length, 2);
|
|
1518
|
+
// Later log should come first (sorted descending)
|
|
1519
|
+
assert.strictEqual(results[0].body, 'MultiDirSortTest_Late');
|
|
1520
|
+
assert.strictEqual(results[1].body, 'MultiDirSortTest_Early');
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
describe('queryMetrics', () => {
|
|
1524
|
+
it('should query metrics from local telemetry directory', async () => {
|
|
1525
|
+
const today = getTestDate();
|
|
1526
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1527
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1528
|
+
writeJsonlFile(path.join(localTelemetry, `metrics-${today}.jsonl`), [
|
|
1529
|
+
{ timestamp: `${today}T10:00:00Z`, name: 'multidir.unique.xyz789.metric1', value: 100, type: 'gauge' },
|
|
1530
|
+
{ timestamp: `${today}T11:00:00Z`, name: 'multidir.unique.xyz789.metric2', value: 200, type: 'gauge' },
|
|
1531
|
+
]);
|
|
1532
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1533
|
+
// Use metricName filter to find our specific test metrics
|
|
1534
|
+
const results = await backend.queryMetrics({ metricName: 'multidir.unique.xyz789' });
|
|
1535
|
+
assert.strictEqual(results.length, 2);
|
|
1536
|
+
assert.ok(results.some(m => m.name === 'multidir.unique.xyz789.metric1'), 'Should find test metric 1');
|
|
1537
|
+
assert.ok(results.some(m => m.name === 'multidir.unique.xyz789.metric2'), 'Should find test metric 2');
|
|
1538
|
+
});
|
|
1539
|
+
it('should respect limit parameter', async () => {
|
|
1540
|
+
const today = getTestDate();
|
|
1541
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1542
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1543
|
+
writeJsonlFile(path.join(localTelemetry, `metrics-${today}.jsonl`), Array.from({ length: 150 }, (_, i) => ({
|
|
1544
|
+
timestamp: `${today}T10:${String(Math.floor(i / 60) % 60).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}Z`,
|
|
1545
|
+
name: `multidir.limit.abc123.metric-${i}`,
|
|
1546
|
+
value: i * 10,
|
|
1547
|
+
type: 'gauge',
|
|
1548
|
+
})));
|
|
1549
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1550
|
+
const results = await backend.queryMetrics({ metricName: 'multidir.limit.abc123', limit: 50 });
|
|
1551
|
+
assert.ok(results.length <= 50);
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
describe('queryLLMEvents', () => {
|
|
1555
|
+
it('should query LLM events from local telemetry directory', async () => {
|
|
1556
|
+
const today = getTestDate();
|
|
1557
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1558
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1559
|
+
writeJsonlFile(path.join(localTelemetry, `llm-events-${today}.jsonl`), [
|
|
1560
|
+
{ timestamp: `${today}T10:00:00Z`, name: 'multidir.unique.xyz456.llm.event1', attributes: { model: 'claude' } },
|
|
1561
|
+
{ timestamp: `${today}T11:00:00Z`, name: 'multidir.unique.xyz456.llm.event2', attributes: { model: 'gpt' } },
|
|
1562
|
+
]);
|
|
1563
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1564
|
+
// Use eventName filter to find our specific test events
|
|
1565
|
+
const results = await backend.queryLLMEvents({ eventName: 'multidir.unique.xyz456' });
|
|
1566
|
+
assert.strictEqual(results.length, 2);
|
|
1567
|
+
assert.ok(results.some(e => e.name === 'multidir.unique.xyz456.llm.event1'), 'Should find test event 1');
|
|
1568
|
+
assert.ok(results.some(e => e.name === 'multidir.unique.xyz456.llm.event2'), 'Should find test event 2');
|
|
1569
|
+
});
|
|
1570
|
+
it('should sort LLM events by timestamp descending', async () => {
|
|
1571
|
+
const today = getTestDate();
|
|
1572
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1573
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1574
|
+
writeJsonlFile(path.join(localTelemetry, `llm-events-${today}.jsonl`), [
|
|
1575
|
+
{ timestamp: `${today}T08:00:00Z`, name: 'multidir.sort.abc789.early', attributes: {} },
|
|
1576
|
+
{ timestamp: `${today}T12:00:00Z`, name: 'multidir.sort.abc789.late', attributes: {} },
|
|
1577
|
+
]);
|
|
1578
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1579
|
+
const results = await backend.queryLLMEvents({ eventName: 'multidir.sort.abc789' });
|
|
1580
|
+
assert.strictEqual(results.length, 2);
|
|
1581
|
+
// Later event should be first (sorted descending)
|
|
1582
|
+
assert.strictEqual(results[0].name, 'multidir.sort.abc789.late');
|
|
1583
|
+
assert.strictEqual(results[1].name, 'multidir.sort.abc789.early');
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
describe('healthCheck', () => {
|
|
1587
|
+
it('should return error when no directories found', async () => {
|
|
1588
|
+
// Create a backend with cwd that has no telemetry directories
|
|
1589
|
+
// AND ensure global telemetry doesn't exist for this test
|
|
1590
|
+
const emptyProject = createTempDir();
|
|
1591
|
+
try {
|
|
1592
|
+
const backend = new MultiDirectoryBackend(emptyProject);
|
|
1593
|
+
// Only test error condition if we actually have no directories
|
|
1594
|
+
if (backend.getDirectories().length === 0) {
|
|
1595
|
+
const health = await backend.healthCheck();
|
|
1596
|
+
assert.strictEqual(health.status, 'error');
|
|
1597
|
+
assert.ok(health.message?.includes('No telemetry directories'));
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
finally {
|
|
1601
|
+
removeTempDir(emptyProject);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
it('should return ok when local telemetry directory exists', async () => {
|
|
1605
|
+
const today = getTestDate();
|
|
1606
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1607
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1608
|
+
// Create some telemetry files
|
|
1609
|
+
writeJsonlFile(path.join(localTelemetry, `traces-${today}.jsonl`), []);
|
|
1610
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1611
|
+
const health = await backend.healthCheck();
|
|
1612
|
+
assert.strictEqual(health.status, 'ok');
|
|
1613
|
+
assert.ok(health.directories);
|
|
1614
|
+
assert.ok(health.directories.length > 0);
|
|
1615
|
+
});
|
|
1616
|
+
it('should include directory statuses in health response', async () => {
|
|
1617
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1618
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1619
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1620
|
+
const health = await backend.healthCheck();
|
|
1621
|
+
if (health.directories) {
|
|
1622
|
+
for (const dir of health.directories) {
|
|
1623
|
+
assert.ok(dir.path);
|
|
1624
|
+
assert.ok(dir.source);
|
|
1625
|
+
assert.ok(dir.status);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
it('should report correct directory count in message', async () => {
|
|
1630
|
+
const localTelemetry = path.join(projectDir, 'telemetry');
|
|
1631
|
+
fs.mkdirSync(localTelemetry, { recursive: true });
|
|
1632
|
+
const backend = new MultiDirectoryBackend(projectDir);
|
|
1633
|
+
const health = await backend.healthCheck();
|
|
1634
|
+
assert.ok(health.message?.includes('telemetry director'));
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
//# sourceMappingURL=local-jsonl.test.js.map
|