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.
Files changed (117) hide show
  1. package/README.md +52 -3
  2. package/dist/backends/index.d.ts +28 -0
  3. package/dist/backends/index.d.ts.map +1 -1
  4. package/dist/backends/local-jsonl.d.ts +29 -1
  5. package/dist/backends/local-jsonl.d.ts.map +1 -1
  6. package/dist/backends/local-jsonl.js +259 -27
  7. package/dist/backends/local-jsonl.js.map +1 -1
  8. package/dist/backends/local-jsonl.test.d.ts +2 -0
  9. package/dist/backends/local-jsonl.test.d.ts.map +1 -0
  10. package/dist/backends/local-jsonl.test.js +1638 -0
  11. package/dist/backends/local-jsonl.test.js.map +1 -0
  12. package/dist/backends/signoz-api.d.ts +9 -2
  13. package/dist/backends/signoz-api.d.ts.map +1 -1
  14. package/dist/backends/signoz-api.integration.test.d.ts +8 -0
  15. package/dist/backends/signoz-api.integration.test.d.ts.map +1 -0
  16. package/dist/backends/signoz-api.integration.test.js +137 -0
  17. package/dist/backends/signoz-api.integration.test.js.map +1 -0
  18. package/dist/backends/signoz-api.js +206 -115
  19. package/dist/backends/signoz-api.js.map +1 -1
  20. package/dist/backends/signoz-api.test.d.ts +2 -0
  21. package/dist/backends/signoz-api.test.d.ts.map +1 -0
  22. package/dist/backends/signoz-api.test.js +1080 -0
  23. package/dist/backends/signoz-api.test.js.map +1 -0
  24. package/dist/lib/constants.d.ts +28 -0
  25. package/dist/lib/constants.d.ts.map +1 -1
  26. package/dist/lib/constants.js +73 -0
  27. package/dist/lib/constants.js.map +1 -1
  28. package/dist/lib/constants.test.d.ts +5 -0
  29. package/dist/lib/constants.test.d.ts.map +1 -0
  30. package/dist/lib/constants.test.js +381 -0
  31. package/dist/lib/constants.test.js.map +1 -0
  32. package/dist/lib/file-utils.d.ts +53 -1
  33. package/dist/lib/file-utils.d.ts.map +1 -1
  34. package/dist/lib/file-utils.js +142 -3
  35. package/dist/lib/file-utils.js.map +1 -1
  36. package/dist/lib/file-utils.test.d.ts +2 -0
  37. package/dist/lib/file-utils.test.d.ts.map +1 -0
  38. package/dist/lib/file-utils.test.js +649 -0
  39. package/dist/lib/file-utils.test.js.map +1 -0
  40. package/dist/server.js +50 -63
  41. package/dist/server.js.map +1 -1
  42. package/dist/server.test.d.ts +5 -0
  43. package/dist/server.test.d.ts.map +1 -0
  44. package/dist/server.test.js +547 -0
  45. package/dist/server.test.js.map +1 -0
  46. package/dist/tools/context-stats.d.ts +2 -2
  47. package/dist/tools/context-stats.d.ts.map +1 -1
  48. package/dist/tools/context-stats.js +2 -1
  49. package/dist/tools/context-stats.js.map +1 -1
  50. package/dist/tools/context-stats.test.d.ts +5 -0
  51. package/dist/tools/context-stats.test.d.ts.map +1 -0
  52. package/dist/tools/context-stats.test.js +465 -0
  53. package/dist/tools/context-stats.test.js.map +1 -0
  54. package/dist/tools/get-trace-url.d.ts.map +1 -1
  55. package/dist/tools/get-trace-url.js +5 -1
  56. package/dist/tools/get-trace-url.js.map +1 -1
  57. package/dist/tools/get-trace-url.test.d.ts +5 -0
  58. package/dist/tools/get-trace-url.test.d.ts.map +1 -0
  59. package/dist/tools/get-trace-url.test.js +429 -0
  60. package/dist/tools/get-trace-url.test.js.map +1 -0
  61. package/dist/tools/health-check.d.ts +9 -2
  62. package/dist/tools/health-check.d.ts.map +1 -1
  63. package/dist/tools/health-check.js +66 -27
  64. package/dist/tools/health-check.js.map +1 -1
  65. package/dist/tools/health-check.test.d.ts +5 -0
  66. package/dist/tools/health-check.test.d.ts.map +1 -0
  67. package/dist/tools/health-check.test.js +386 -0
  68. package/dist/tools/health-check.test.js.map +1 -0
  69. package/dist/tools/index.d.ts +1 -0
  70. package/dist/tools/index.d.ts.map +1 -1
  71. package/dist/tools/index.js +1 -0
  72. package/dist/tools/index.js.map +1 -1
  73. package/dist/tools/query-llm-events.d.ts +82 -0
  74. package/dist/tools/query-llm-events.d.ts.map +1 -0
  75. package/dist/tools/query-llm-events.js +60 -0
  76. package/dist/tools/query-llm-events.js.map +1 -0
  77. package/dist/tools/query-llm-events.test.d.ts +5 -0
  78. package/dist/tools/query-llm-events.test.d.ts.map +1 -0
  79. package/dist/tools/query-llm-events.test.js +111 -0
  80. package/dist/tools/query-llm-events.test.js.map +1 -0
  81. package/dist/tools/query-logs.d.ts +15 -8
  82. package/dist/tools/query-logs.d.ts.map +1 -1
  83. package/dist/tools/query-logs.js +11 -10
  84. package/dist/tools/query-logs.js.map +1 -1
  85. package/dist/tools/query-logs.test.d.ts +5 -0
  86. package/dist/tools/query-logs.test.d.ts.map +1 -0
  87. package/dist/tools/query-logs.test.js +688 -0
  88. package/dist/tools/query-logs.test.js.map +1 -0
  89. package/dist/tools/query-metrics.d.ts +13 -15
  90. package/dist/tools/query-metrics.d.ts.map +1 -1
  91. package/dist/tools/query-metrics.js +12 -13
  92. package/dist/tools/query-metrics.js.map +1 -1
  93. package/dist/tools/query-metrics.test.d.ts +5 -0
  94. package/dist/tools/query-metrics.test.d.ts.map +1 -0
  95. package/dist/tools/query-metrics.test.js +597 -0
  96. package/dist/tools/query-metrics.test.js.map +1 -0
  97. package/dist/tools/query-traces.d.ts +19 -14
  98. package/dist/tools/query-traces.d.ts.map +1 -1
  99. package/dist/tools/query-traces.js +14 -14
  100. package/dist/tools/query-traces.js.map +1 -1
  101. package/dist/tools/query-traces.test.d.ts +5 -0
  102. package/dist/tools/query-traces.test.d.ts.map +1 -0
  103. package/dist/tools/query-traces.test.js +643 -0
  104. package/dist/tools/query-traces.test.js.map +1 -0
  105. package/dist/tools/setup-claudeignore.d.ts +36 -10
  106. package/dist/tools/setup-claudeignore.d.ts.map +1 -1
  107. package/dist/tools/setup-claudeignore.js +193 -33
  108. package/dist/tools/setup-claudeignore.js.map +1 -1
  109. package/dist/tools/setup-claudeignore.test.d.ts +2 -0
  110. package/dist/tools/setup-claudeignore.test.d.ts.map +1 -0
  111. package/dist/tools/setup-claudeignore.test.js +481 -0
  112. package/dist/tools/setup-claudeignore.test.js.map +1 -0
  113. package/dist/tools/signoz.integration.test.d.ts +8 -0
  114. package/dist/tools/signoz.integration.test.d.ts.map +1 -0
  115. package/dist/tools/signoz.integration.test.js +141 -0
  116. package/dist/tools/signoz.integration.test.js.map +1 -0
  117. 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