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,1080 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { SigNozApiBackend } from './signoz-api.js';
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ const setupMock = (fn) => mock.fn(fn);
6
+ // Helper to create v5 API response format for traces
7
+ function createV5TraceResponse(spans) {
8
+ return {
9
+ data: {
10
+ data: {
11
+ results: [{
12
+ rows: spans.map(s => ({
13
+ timestamp: s.timestamp || new Date().toISOString(),
14
+ data: {
15
+ trace_id: s.traceID || s.trace_id,
16
+ span_id: s.spanID || s.span_id,
17
+ parent_span_id: s.parentSpanID || s.parent_span_id,
18
+ name: s.name,
19
+ kind: s.kind,
20
+ duration_nano: s.durationNano || s.duration_nano,
21
+ 'service.name': s.serviceName || s['service.name'],
22
+ response_status_code: s.statusCode,
23
+ },
24
+ })),
25
+ }],
26
+ },
27
+ },
28
+ };
29
+ }
30
+ describe('SigNozApiBackend', () => {
31
+ describe('constructor', () => {
32
+ it('should initialize with default URL and API key from environment', () => {
33
+ const backend = new SigNozApiBackend();
34
+ assert.strictEqual(backend.name, 'signoz-api');
35
+ });
36
+ it('should accept custom base URL and API key', () => {
37
+ const backend = new SigNozApiBackend('https://custom.example.com', 'custom-key');
38
+ assert.strictEqual(backend.name, 'signoz-api');
39
+ });
40
+ it('should strip trailing slashes from base URL', () => {
41
+ const backend = new SigNozApiBackend('https://example.com/', 'test-key');
42
+ const url = backend.getTraceUrl('trace-123');
43
+ assert.strictEqual(url.includes('//trace'), false);
44
+ });
45
+ it('should handle multiple trailing slashes', () => {
46
+ const backend = new SigNozApiBackend('https://example.com///', 'test-key');
47
+ const url = backend.getTraceUrl('trace-123');
48
+ assert.strictEqual(url.includes('//trace'), false);
49
+ });
50
+ });
51
+ describe('queryTraces', () => {
52
+ it('should query traces with basic options using v5 API', async () => {
53
+ globalThis.fetch = setupMock(async () => ({
54
+ ok: true,
55
+ json: async () => createV5TraceResponse([
56
+ {
57
+ traceID: 'trace-123',
58
+ spanID: 'span-456',
59
+ name: 'http-request',
60
+ timestamp: 1000000000,
61
+ durationNano: 500000000,
62
+ serviceName: 'api-service',
63
+ },
64
+ ]),
65
+ text: async () => '',
66
+ }));
67
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
68
+ const traces = await backend.queryTraces({
69
+ startDate: '2026-01-01',
70
+ endDate: '2026-01-02',
71
+ limit: 50,
72
+ });
73
+ assert.strictEqual(traces.length, 1);
74
+ assert.strictEqual(traces[0].traceId, 'trace-123');
75
+ assert.strictEqual(traces[0].spanId, 'span-456');
76
+ assert.strictEqual(traces[0].name, 'http-request');
77
+ assert.strictEqual(traces[0].durationMs, 500);
78
+ });
79
+ it('should build filter expression for traceId', async () => {
80
+ let capturedBody;
81
+ globalThis.fetch = setupMock(async (_url, options) => {
82
+ if (options?.body) {
83
+ capturedBody = JSON.parse(String(options.body));
84
+ }
85
+ return {
86
+ ok: true,
87
+ json: async () => createV5TraceResponse([]),
88
+ text: async () => '',
89
+ };
90
+ });
91
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
92
+ await backend.queryTraces({ traceId: 'trace-abc' });
93
+ const body = capturedBody;
94
+ const query = body.compositeQuery.queries;
95
+ const spec = query[0].spec;
96
+ const filter = spec.filter;
97
+ assert.ok(filter.expression.includes("traceID = 'trace-abc'"));
98
+ });
99
+ it('should build filter expression for serviceName', async () => {
100
+ let capturedBody;
101
+ globalThis.fetch = setupMock(async (_url, options) => {
102
+ if (options?.body) {
103
+ capturedBody = JSON.parse(String(options.body));
104
+ }
105
+ return {
106
+ ok: true,
107
+ json: async () => createV5TraceResponse([]),
108
+ text: async () => '',
109
+ };
110
+ });
111
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
112
+ await backend.queryTraces({ serviceName: 'my-service' });
113
+ const body = capturedBody;
114
+ const query = body.compositeQuery.queries;
115
+ const spec = query[0].spec;
116
+ const filter = spec.filter;
117
+ assert.ok(filter.expression.includes("serviceName = 'my-service'"));
118
+ });
119
+ it('should build filter expression for spanName with LIKE operator', async () => {
120
+ let capturedBody;
121
+ globalThis.fetch = setupMock(async (_url, options) => {
122
+ if (options?.body) {
123
+ capturedBody = JSON.parse(String(options.body));
124
+ }
125
+ return {
126
+ ok: true,
127
+ json: async () => createV5TraceResponse([]),
128
+ text: async () => '',
129
+ };
130
+ });
131
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
132
+ await backend.queryTraces({ spanName: 'request' });
133
+ const body = capturedBody;
134
+ const query = body.compositeQuery.queries;
135
+ const spec = query[0].spec;
136
+ const filter = spec.filter;
137
+ assert.ok(filter.expression.includes("name LIKE '%request%'"));
138
+ });
139
+ it('should build filter expression for minDurationMs', async () => {
140
+ let capturedBody;
141
+ globalThis.fetch = setupMock(async (_url, options) => {
142
+ if (options?.body) {
143
+ capturedBody = JSON.parse(String(options.body));
144
+ }
145
+ return {
146
+ ok: true,
147
+ json: async () => createV5TraceResponse([]),
148
+ text: async () => '',
149
+ };
150
+ });
151
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
152
+ await backend.queryTraces({ minDurationMs: 100 });
153
+ const body = capturedBody;
154
+ const query = body.compositeQuery.queries;
155
+ const spec = query[0].spec;
156
+ const filter = spec.filter;
157
+ assert.ok(filter.expression.includes('durationNano >= 100000000'));
158
+ });
159
+ it('should build filter expression for maxDurationMs', async () => {
160
+ let capturedBody;
161
+ globalThis.fetch = setupMock(async (_url, options) => {
162
+ if (options?.body) {
163
+ capturedBody = JSON.parse(String(options.body));
164
+ }
165
+ return {
166
+ ok: true,
167
+ json: async () => createV5TraceResponse([]),
168
+ text: async () => '',
169
+ };
170
+ });
171
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
172
+ await backend.queryTraces({ maxDurationMs: 500 });
173
+ const body = capturedBody;
174
+ const query = body.compositeQuery.queries;
175
+ const spec = query[0].spec;
176
+ const filter = spec.filter;
177
+ assert.ok(filter.expression.includes('durationNano <= 500000000'));
178
+ });
179
+ it('should combine multiple filters with AND', async () => {
180
+ let capturedBody;
181
+ globalThis.fetch = setupMock(async (_url, options) => {
182
+ if (options?.body) {
183
+ capturedBody = JSON.parse(String(options.body));
184
+ }
185
+ return {
186
+ ok: true,
187
+ json: async () => createV5TraceResponse([]),
188
+ text: async () => '',
189
+ };
190
+ });
191
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
192
+ await backend.queryTraces({
193
+ serviceName: 'api',
194
+ spanName: 'request',
195
+ minDurationMs: 50,
196
+ });
197
+ const body = capturedBody;
198
+ const query = body.compositeQuery.queries;
199
+ const spec = query[0].spec;
200
+ const filter = spec.filter;
201
+ // Should have 3 conditions joined by AND
202
+ const andCount = (filter.expression.match(/ AND /g) || []).length;
203
+ assert.strictEqual(andCount, 2);
204
+ });
205
+ it('should map trace span kinds correctly', async () => {
206
+ globalThis.fetch = setupMock(async () => ({
207
+ ok: true,
208
+ json: async () => createV5TraceResponse([
209
+ { traceID: 't1', spanID: 's1', name: 'op1', timestamp: 1000, durationNano: 500000, kind: 0 },
210
+ { traceID: 't2', spanID: 's2', name: 'op2', timestamp: 1000, durationNano: 500000, kind: 1 },
211
+ { traceID: 't3', spanID: 's3', name: 'op3', timestamp: 1000, durationNano: 500000, kind: 2 },
212
+ { traceID: 't4', spanID: 's4', name: 'op4', timestamp: 1000, durationNano: 500000, kind: 3 },
213
+ { traceID: 't5', spanID: 's5', name: 'op5', timestamp: 1000, durationNano: 500000, kind: 4 },
214
+ ]),
215
+ text: async () => '',
216
+ }));
217
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
218
+ const traces = await backend.queryTraces({});
219
+ assert.strictEqual(traces[0].kind, 'INTERNAL');
220
+ assert.strictEqual(traces[1].kind, 'SERVER');
221
+ assert.strictEqual(traces[2].kind, 'CLIENT');
222
+ assert.strictEqual(traces[3].kind, 'PRODUCER');
223
+ assert.strictEqual(traces[4].kind, 'CONSUMER');
224
+ });
225
+ it('should handle missing kind field', async () => {
226
+ globalThis.fetch = setupMock(async () => ({
227
+ ok: true,
228
+ json: async () => createV5TraceResponse([
229
+ { traceID: 't1', spanID: 's1', name: 'op1', timestamp: 1000, durationNano: 500000 },
230
+ ]),
231
+ text: async () => '',
232
+ }));
233
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
234
+ const traces = await backend.queryTraces({});
235
+ assert.strictEqual(traces[0].kind, undefined);
236
+ });
237
+ it('should set authentication header correctly', async () => {
238
+ let capturedHeaders;
239
+ globalThis.fetch = setupMock(async (_url, options) => {
240
+ capturedHeaders = options?.headers;
241
+ return {
242
+ ok: true,
243
+ json: async () => createV5TraceResponse([]),
244
+ text: async () => '',
245
+ };
246
+ });
247
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'my-secret-key');
248
+ await backend.queryTraces({});
249
+ assert.strictEqual(capturedHeaders?.['SIGNOZ-API-KEY'], 'my-secret-key');
250
+ });
251
+ it('should set Content-Type header', async () => {
252
+ let capturedHeaders;
253
+ globalThis.fetch = setupMock(async (_url, options) => {
254
+ capturedHeaders = options?.headers;
255
+ return {
256
+ ok: true,
257
+ json: async () => createV5TraceResponse([]),
258
+ text: async () => '',
259
+ };
260
+ });
261
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
262
+ await backend.queryTraces({});
263
+ assert.strictEqual(capturedHeaders?.['Content-Type'], 'application/json');
264
+ });
265
+ it('should handle API error responses', async () => {
266
+ globalThis.fetch = setupMock(async () => ({
267
+ ok: false,
268
+ status: 401,
269
+ text: async () => 'Unauthorized',
270
+ }));
271
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'invalid-key');
272
+ try {
273
+ await backend.queryTraces({});
274
+ assert.fail('Should have thrown an error');
275
+ }
276
+ catch (error) {
277
+ assert(error instanceof Error);
278
+ assert(error.message.includes('401'));
279
+ // Note: detailed error text is sanitized for security (not leaked to client)
280
+ assert(error.message.includes('Request failed'));
281
+ }
282
+ });
283
+ it('should return empty array on circuit breaker error', async () => {
284
+ globalThis.fetch = setupMock(async () => {
285
+ throw new Error('Circuit breaker open - SigNoz API unavailable');
286
+ });
287
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
288
+ const traces = await backend.queryTraces({});
289
+ assert.strictEqual(traces.length, 0);
290
+ });
291
+ it('should POST to v5 query_range endpoint', async () => {
292
+ let capturedUrl = '';
293
+ globalThis.fetch = setupMock(async (url) => {
294
+ capturedUrl = String(url);
295
+ return {
296
+ ok: true,
297
+ json: async () => createV5TraceResponse([]),
298
+ text: async () => '',
299
+ };
300
+ });
301
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
302
+ await backend.queryTraces({});
303
+ assert.ok(capturedUrl.includes('/api/v5/query_range'));
304
+ });
305
+ it('should use millisecond timestamps in request', async () => {
306
+ let capturedBody;
307
+ globalThis.fetch = setupMock(async (_url, options) => {
308
+ if (options?.body) {
309
+ capturedBody = JSON.parse(String(options.body));
310
+ }
311
+ return {
312
+ ok: true,
313
+ json: async () => createV5TraceResponse([]),
314
+ text: async () => '',
315
+ };
316
+ });
317
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
318
+ await backend.queryTraces({ startDate: '2026-01-01' });
319
+ const body = capturedBody;
320
+ // Check that start/end are numbers (milliseconds) not strings (nanoseconds)
321
+ assert.strictEqual(typeof body.start, 'number');
322
+ assert.strictEqual(typeof body.end, 'number');
323
+ });
324
+ it('should use compositeQuery with builder_query type', async () => {
325
+ let capturedBody;
326
+ globalThis.fetch = setupMock(async (_url, options) => {
327
+ if (options?.body) {
328
+ capturedBody = JSON.parse(String(options.body));
329
+ }
330
+ return {
331
+ ok: true,
332
+ json: async () => createV5TraceResponse([]),
333
+ text: async () => '',
334
+ };
335
+ });
336
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
337
+ await backend.queryTraces({});
338
+ const body = capturedBody;
339
+ const compositeQuery = body.compositeQuery;
340
+ const queries = compositeQuery.queries;
341
+ assert.strictEqual(queries[0].type, 'builder_query');
342
+ assert.strictEqual(queries[0].spec.signal, 'traces');
343
+ });
344
+ });
345
+ describe('queryLogs', () => {
346
+ // Helper to create v5 API log response format
347
+ function createV5LogResponse(logs) {
348
+ return {
349
+ data: {
350
+ data: {
351
+ results: [{
352
+ rows: logs.map(l => ({
353
+ timestamp: l.timestamp || new Date().toISOString(),
354
+ data: l,
355
+ })),
356
+ }],
357
+ },
358
+ },
359
+ };
360
+ }
361
+ it('should query logs with basic options', async () => {
362
+ globalThis.fetch = setupMock(async () => ({
363
+ ok: true,
364
+ json: async () => ({
365
+ data: {
366
+ data: {
367
+ results: [{
368
+ rows: [{
369
+ timestamp: '2026-01-01T12:00:00Z',
370
+ data: {
371
+ severity_text: 'ERROR',
372
+ body: 'An error occurred',
373
+ trace_id: 'trace-123',
374
+ span_id: 'span-456',
375
+ },
376
+ }],
377
+ }],
378
+ },
379
+ },
380
+ }),
381
+ text: async () => '',
382
+ }));
383
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
384
+ const logs = await backend.queryLogs({ startDate: '2026-01-01', limit: 50 });
385
+ assert.strictEqual(logs.length, 1);
386
+ assert.strictEqual(logs[0].timestamp, '2026-01-01T12:00:00Z');
387
+ assert.strictEqual(logs[0].severity, 'ERROR');
388
+ assert.strictEqual(logs[0].body, 'An error occurred');
389
+ assert.strictEqual(logs[0].traceId, 'trace-123');
390
+ });
391
+ it('should build severity filter expression', async () => {
392
+ let capturedBody;
393
+ globalThis.fetch = setupMock(async (_url, options) => {
394
+ if (options?.body) {
395
+ capturedBody = JSON.parse(String(options.body));
396
+ }
397
+ return {
398
+ ok: true,
399
+ json: async () => createV5LogResponse([]),
400
+ text: async () => '',
401
+ };
402
+ });
403
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
404
+ await backend.queryLogs({ severity: 'error' });
405
+ const body = capturedBody;
406
+ const query = body.compositeQuery.queries;
407
+ const spec = query[0].spec;
408
+ const filter = spec.filter;
409
+ assert.ok(filter.expression.includes("severity_text = 'ERROR'"));
410
+ });
411
+ it('should build traceId filter expression', async () => {
412
+ let capturedBody;
413
+ globalThis.fetch = setupMock(async (_url, options) => {
414
+ if (options?.body) {
415
+ capturedBody = JSON.parse(String(options.body));
416
+ }
417
+ return {
418
+ ok: true,
419
+ json: async () => createV5LogResponse([]),
420
+ text: async () => '',
421
+ };
422
+ });
423
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
424
+ await backend.queryLogs({ traceId: 'trace-xyz' });
425
+ const body = capturedBody;
426
+ const query = body.compositeQuery.queries;
427
+ const spec = query[0].spec;
428
+ const filter = spec.filter;
429
+ assert.ok(filter.expression.includes("trace_id = 'trace-xyz'"));
430
+ });
431
+ it('should build search filter with LIKE operator', async () => {
432
+ let capturedBody;
433
+ globalThis.fetch = setupMock(async (_url, options) => {
434
+ if (options?.body) {
435
+ capturedBody = JSON.parse(String(options.body));
436
+ }
437
+ return {
438
+ ok: true,
439
+ json: async () => createV5LogResponse([]),
440
+ text: async () => '',
441
+ };
442
+ });
443
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
444
+ await backend.queryLogs({ search: 'database error' });
445
+ const body = capturedBody;
446
+ const query = body.compositeQuery.queries;
447
+ const spec = query[0].spec;
448
+ const filter = spec.filter;
449
+ assert.ok(filter.expression.includes("body LIKE '%database error%'"));
450
+ });
451
+ it('should combine multiple log filters with AND', async () => {
452
+ let capturedBody;
453
+ globalThis.fetch = setupMock(async (_url, options) => {
454
+ if (options?.body) {
455
+ capturedBody = JSON.parse(String(options.body));
456
+ }
457
+ return {
458
+ ok: true,
459
+ json: async () => createV5LogResponse([]),
460
+ text: async () => '',
461
+ };
462
+ });
463
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
464
+ await backend.queryLogs({
465
+ severity: 'warn',
466
+ traceId: 'trace-abc',
467
+ search: 'timeout',
468
+ });
469
+ const body = capturedBody;
470
+ const query = body.compositeQuery.queries;
471
+ const spec = query[0].spec;
472
+ const filter = spec.filter;
473
+ // Should have 3 conditions joined by AND
474
+ const andCount = (filter.expression.match(/ AND /g) || []).length;
475
+ assert.strictEqual(andCount, 2);
476
+ });
477
+ it('should provide default values for missing log fields', async () => {
478
+ globalThis.fetch = setupMock(async () => ({
479
+ ok: true,
480
+ json: async () => ({
481
+ data: {
482
+ data: {
483
+ results: [{
484
+ rows: [{
485
+ timestamp: '2026-01-01T12:00:00Z',
486
+ data: {},
487
+ }],
488
+ }],
489
+ },
490
+ },
491
+ }),
492
+ text: async () => '',
493
+ }));
494
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
495
+ const logs = await backend.queryLogs({});
496
+ assert.strictEqual(logs[0].severity, 'INFO');
497
+ assert.strictEqual(logs[0].body, '');
498
+ });
499
+ it('should handle log API errors', async () => {
500
+ globalThis.fetch = setupMock(async () => ({
501
+ ok: false,
502
+ status: 500,
503
+ text: async () => 'Internal server error',
504
+ }));
505
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
506
+ try {
507
+ await backend.queryLogs({});
508
+ assert.fail('Should have thrown an error');
509
+ }
510
+ catch (error) {
511
+ assert(error instanceof Error);
512
+ assert(error.message.includes('500'));
513
+ }
514
+ });
515
+ it('should return empty array on log circuit breaker error', async () => {
516
+ globalThis.fetch = setupMock(async () => {
517
+ throw new Error('Circuit breaker open - SigNoz API unavailable');
518
+ });
519
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
520
+ const logs = await backend.queryLogs({});
521
+ assert.strictEqual(logs.length, 0);
522
+ });
523
+ });
524
+ describe('queryMetrics', () => {
525
+ // Helper to create v5 API metric response format
526
+ function createV5MetricResponse(series) {
527
+ return {
528
+ data: {
529
+ data: {
530
+ results: [{
531
+ aggregations: [{
532
+ series: series,
533
+ }],
534
+ }],
535
+ },
536
+ },
537
+ };
538
+ }
539
+ it('should query metrics with required metricName', async () => {
540
+ globalThis.fetch = setupMock(async () => ({
541
+ ok: true,
542
+ json: async () => createV5MetricResponse([
543
+ {
544
+ labels: { service: 'api' },
545
+ values: [
546
+ { timestamp: 1704110400000, value: 42.5 }, // milliseconds
547
+ { timestamp: 1704110460000, value: 43.2 },
548
+ ],
549
+ },
550
+ ]),
551
+ text: async () => '',
552
+ }));
553
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
554
+ const metrics = await backend.queryMetrics({
555
+ metricName: 'http_requests_total',
556
+ startDate: '2026-01-01',
557
+ });
558
+ assert.strictEqual(metrics.length, 2);
559
+ assert.strictEqual(metrics[0].name, 'http_requests_total');
560
+ assert.strictEqual(metrics[0].value, 42.5);
561
+ assert.strictEqual(metrics[0].attributes?.service, 'api');
562
+ });
563
+ it('should throw error when metricName is missing', async () => {
564
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
565
+ try {
566
+ await backend.queryMetrics({ startDate: '2026-01-01' });
567
+ assert.fail('Should have thrown an error');
568
+ }
569
+ catch (error) {
570
+ assert(error instanceof Error);
571
+ assert(error.message.includes('metricName is required'));
572
+ }
573
+ });
574
+ it('should use default aggregation when not specified', async () => {
575
+ let capturedBody;
576
+ globalThis.fetch = setupMock(async (_url, options) => {
577
+ if (options?.body) {
578
+ capturedBody = JSON.parse(String(options.body));
579
+ }
580
+ return {
581
+ ok: true,
582
+ json: async () => ({ data: { data: { results: [] } } }),
583
+ text: async () => '',
584
+ };
585
+ });
586
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
587
+ await backend.queryMetrics({ metricName: 'test_metric' });
588
+ const body = capturedBody;
589
+ const query = body.compositeQuery.queries;
590
+ const spec = query[0].spec;
591
+ const aggregations = spec.aggregations;
592
+ // Default uses 'sum' for spaceAggregation
593
+ assert.strictEqual(aggregations[0].spaceAggregation, 'sum');
594
+ });
595
+ it('should use custom aggregation operator', async () => {
596
+ let capturedBody;
597
+ globalThis.fetch = setupMock(async (_url, options) => {
598
+ if (options?.body) {
599
+ capturedBody = JSON.parse(String(options.body));
600
+ }
601
+ return {
602
+ ok: true,
603
+ json: async () => ({ data: { data: { results: [] } } }),
604
+ text: async () => '',
605
+ };
606
+ });
607
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
608
+ await backend.queryMetrics({ metricName: 'test_metric', aggregation: 'avg' });
609
+ const body = capturedBody;
610
+ const query = body.compositeQuery.queries;
611
+ const spec = query[0].spec;
612
+ const aggregations = spec.aggregations;
613
+ assert.strictEqual(aggregations[0].spaceAggregation, 'avg');
614
+ });
615
+ it('should pass groupBy to query', async () => {
616
+ let capturedBody;
617
+ globalThis.fetch = setupMock(async (_url, options) => {
618
+ if (options?.body) {
619
+ capturedBody = JSON.parse(String(options.body));
620
+ }
621
+ return {
622
+ ok: true,
623
+ json: async () => ({ data: { result: [] } }),
624
+ text: async () => '',
625
+ };
626
+ });
627
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
628
+ await backend.queryMetrics({
629
+ metricName: 'test_metric',
630
+ groupBy: ['service', 'region'],
631
+ });
632
+ const body = capturedBody;
633
+ const query = body.compositeQuery.queries;
634
+ const spec = query[0].spec;
635
+ const groupBy = spec.groupBy;
636
+ assert.strictEqual(groupBy.length, 2);
637
+ assert.strictEqual(groupBy[0].name, 'service');
638
+ assert.strictEqual(groupBy[1].name, 'region');
639
+ });
640
+ it('should convert timestamps correctly', async () => {
641
+ globalThis.fetch = setupMock(async () => ({
642
+ ok: true,
643
+ json: async () => createV5MetricResponse([
644
+ {
645
+ values: [{ timestamp: 1704110400000, value: 99 }], // milliseconds
646
+ },
647
+ ]),
648
+ text: async () => '',
649
+ }));
650
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
651
+ const metrics = await backend.queryMetrics({ metricName: 'test_metric' });
652
+ // Timestamp 1704110400000 ms = 2024-01-01T12:00:00.000Z
653
+ assert.strictEqual(metrics[0].timestamp, '2024-01-01T12:00:00.000Z');
654
+ });
655
+ it('should respect limit parameter', async () => {
656
+ globalThis.fetch = setupMock(async () => ({
657
+ ok: true,
658
+ json: async () => createV5MetricResponse([
659
+ {
660
+ values: Array(150)
661
+ .fill(0)
662
+ .map((_, i) => ({
663
+ timestamp: 1704110400000 + i * 60000, // milliseconds
664
+ value: i,
665
+ })),
666
+ },
667
+ ]),
668
+ text: async () => '',
669
+ }));
670
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
671
+ const metrics = await backend.queryMetrics({
672
+ metricName: 'test_metric',
673
+ limit: 50,
674
+ });
675
+ assert.strictEqual(metrics.length, 50);
676
+ });
677
+ it('should handle metric API errors', async () => {
678
+ globalThis.fetch = setupMock(async () => ({
679
+ ok: false,
680
+ status: 400,
681
+ text: async () => 'Bad request',
682
+ }));
683
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
684
+ try {
685
+ await backend.queryMetrics({ metricName: 'test_metric' });
686
+ assert.fail('Should have thrown an error');
687
+ }
688
+ catch (error) {
689
+ assert(error instanceof Error);
690
+ assert(error.message.includes('400'));
691
+ }
692
+ });
693
+ it('should return empty array on metric circuit breaker error', async () => {
694
+ globalThis.fetch = setupMock(async () => {
695
+ throw new Error('Circuit breaker open - SigNoz API unavailable');
696
+ });
697
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
698
+ const metrics = await backend.queryMetrics({ metricName: 'test_metric' });
699
+ assert.strictEqual(metrics.length, 0);
700
+ });
701
+ });
702
+ describe('healthCheck', () => {
703
+ it('should return ok status when healthy', async () => {
704
+ globalThis.fetch = setupMock(async () => ({
705
+ ok: true,
706
+ json: async () => ({ status: 'success' }),
707
+ text: async () => '',
708
+ }));
709
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
710
+ const health = await backend.healthCheck();
711
+ assert.strictEqual(health.status, 'ok');
712
+ assert(health.message?.includes('https://signoz.example.com'));
713
+ });
714
+ it('should return error status when URL not configured', async () => {
715
+ const backend = new SigNozApiBackend('', 'test-key');
716
+ const health = await backend.healthCheck();
717
+ // Empty URL should result in error status
718
+ if (health.status === 'error') {
719
+ assert.strictEqual(health.status, 'error');
720
+ assert(health.message?.includes('URL not configured'));
721
+ }
722
+ else {
723
+ // When no explicit URL is provided but env vars might be set
724
+ assert(health.status === 'ok' || health.status === 'error');
725
+ }
726
+ });
727
+ it('should return error status when API key not configured', async () => {
728
+ const backend = new SigNozApiBackend('https://signoz.example.com', '');
729
+ const health = await backend.healthCheck();
730
+ // Empty API key should result in error status
731
+ if (health.status === 'error') {
732
+ assert.strictEqual(health.status, 'error');
733
+ assert(health.message?.includes('API key not configured'));
734
+ }
735
+ else {
736
+ // When no explicit API key is provided but env vars might be set
737
+ assert(health.status === 'ok' || health.status === 'error');
738
+ }
739
+ });
740
+ it('should handle API errors during health check', async () => {
741
+ globalThis.fetch = setupMock(async () => ({
742
+ ok: false,
743
+ status: 503,
744
+ text: async () => 'Service unavailable',
745
+ }));
746
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
747
+ const health = await backend.healthCheck();
748
+ assert.strictEqual(health.status, 'error');
749
+ assert(health.message?.includes('503'));
750
+ });
751
+ });
752
+ describe('getTraceUrl', () => {
753
+ it('should construct trace viewer URL correctly', () => {
754
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
755
+ const url = backend.getTraceUrl('trace-abc-123');
756
+ assert.strictEqual(url, 'https://signoz.example.com/trace/trace-abc-123');
757
+ });
758
+ it('should append /trace/{traceId} to base URL', () => {
759
+ const backend = new SigNozApiBackend('https://signoz.example.com/v1/traces', 'test-key');
760
+ const url = backend.getTraceUrl('trace-123');
761
+ assert.ok(url.endsWith('/trace/trace-123'));
762
+ });
763
+ it('should use base URL as-is for trace URL', () => {
764
+ const backend = new SigNozApiBackend('https://ingest.signoz.example.com', 'test-key');
765
+ const url = backend.getTraceUrl('trace-123');
766
+ // The implementation uses baseUrl directly without transformations
767
+ assert.strictEqual(url, 'https://ingest.signoz.example.com/trace/trace-123');
768
+ });
769
+ it('should preserve port in trace URL', () => {
770
+ const backend = new SigNozApiBackend('https://signoz.example.com:4318', 'test-key');
771
+ const url = backend.getTraceUrl('trace-123');
772
+ assert.strictEqual(url, 'https://signoz.example.com:4318/trace/trace-123');
773
+ });
774
+ it('should handle complex base URL', () => {
775
+ const backend = new SigNozApiBackend('https://ingest.signoz.example.com:4318/v1/traces', 'test-key');
776
+ const url = backend.getTraceUrl('trace-123');
777
+ // Uses baseUrl directly
778
+ assert.strictEqual(url, 'https://ingest.signoz.example.com:4318/v1/traces/trace/trace-123');
779
+ });
780
+ });
781
+ describe('error response handling', () => {
782
+ it('should throw error with status code (response text sanitized)', async () => {
783
+ globalThis.fetch = setupMock(async () => ({
784
+ ok: false,
785
+ status: 404,
786
+ text: async () => 'Metric not found',
787
+ }));
788
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
789
+ try {
790
+ await backend.queryMetrics({ metricName: 'nonexistent' });
791
+ assert.fail('Should have thrown an error');
792
+ }
793
+ catch (error) {
794
+ assert(error instanceof Error);
795
+ assert(error.message.includes('404'));
796
+ // Note: detailed error text is sanitized for security (not leaked to client)
797
+ assert(error.message.includes('Request failed'));
798
+ }
799
+ });
800
+ it('should handle fetch network errors', async () => {
801
+ globalThis.fetch = setupMock(async () => {
802
+ throw new Error('Network timeout');
803
+ });
804
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
805
+ try {
806
+ await backend.queryTraces({});
807
+ assert.fail('Should have thrown an error');
808
+ }
809
+ catch (error) {
810
+ assert(error instanceof Error);
811
+ assert(error.message.includes('Network timeout'));
812
+ }
813
+ });
814
+ it('should not throw on circuit breaker error for traces', async () => {
815
+ globalThis.fetch = setupMock(async () => {
816
+ throw new Error('Circuit breaker open - SigNoz API unavailable');
817
+ });
818
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
819
+ const result = await backend.queryTraces({});
820
+ assert.deepStrictEqual(result, []);
821
+ });
822
+ it('should not throw on circuit breaker error for logs', async () => {
823
+ globalThis.fetch = setupMock(async () => {
824
+ throw new Error('Circuit breaker open - SigNoz API unavailable');
825
+ });
826
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
827
+ const result = await backend.queryLogs({});
828
+ assert.deepStrictEqual(result, []);
829
+ });
830
+ it('should not throw on circuit breaker error for metrics', async () => {
831
+ globalThis.fetch = setupMock(async () => {
832
+ throw new Error('Circuit breaker open - SigNoz API unavailable');
833
+ });
834
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
835
+ const result = await backend.queryMetrics({ metricName: 'test' });
836
+ assert.deepStrictEqual(result, []);
837
+ });
838
+ });
839
+ describe('circuit breaker', () => {
840
+ it('should open circuit after 3 consecutive failures', async () => {
841
+ let callCount = 0;
842
+ globalThis.fetch = setupMock(async () => {
843
+ callCount++;
844
+ throw new Error('API error');
845
+ });
846
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
847
+ // First 3 failures should hit the API (non-circuit-breaker errors are thrown)
848
+ for (let i = 0; i < 3; i++) {
849
+ try {
850
+ await backend.queryTraces({});
851
+ }
852
+ catch {
853
+ // expected - API errors are thrown
854
+ }
855
+ }
856
+ assert.strictEqual(callCount, 3);
857
+ // 4th call should be blocked by circuit breaker (returns [] instead of calling fetch)
858
+ const prevCount = callCount;
859
+ const result = await backend.queryTraces({});
860
+ assert.deepStrictEqual(result, [], 'Should return empty array when circuit open');
861
+ assert.strictEqual(callCount, prevCount, 'Should not have made another fetch call when circuit open');
862
+ });
863
+ it('should allow request in half-open state after reset time', async () => {
864
+ const originalDateNow = Date.now;
865
+ let currentTime = 1000000;
866
+ Date.now = () => currentTime;
867
+ let callCount = 0;
868
+ globalThis.fetch = setupMock(async () => {
869
+ callCount++;
870
+ throw new Error('API error');
871
+ });
872
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
873
+ // Trigger 3 failures to open circuit
874
+ for (let i = 0; i < 3; i++) {
875
+ try {
876
+ await backend.queryTraces({});
877
+ }
878
+ catch {
879
+ // expected
880
+ }
881
+ }
882
+ assert.strictEqual(callCount, 3);
883
+ // Verify circuit is open (returns [] without fetch call)
884
+ const result = await backend.queryTraces({});
885
+ assert.deepStrictEqual(result, []);
886
+ assert.strictEqual(callCount, 3, 'Should not have made fetch call when circuit open');
887
+ // Advance time past reset period (default 60000ms)
888
+ currentTime += 61000;
889
+ // Should allow one request in half-open state
890
+ try {
891
+ await backend.queryTraces({});
892
+ }
893
+ catch {
894
+ // expected to fail, but should have made the request
895
+ }
896
+ assert.strictEqual(callCount, 4, 'Should have made request in half-open state');
897
+ Date.now = originalDateNow;
898
+ });
899
+ it('should close circuit after successful request in half-open state', async () => {
900
+ const originalDateNow = Date.now;
901
+ let currentTime = 1000000;
902
+ Date.now = () => currentTime;
903
+ let shouldFail = true;
904
+ let callCount = 0;
905
+ globalThis.fetch = setupMock(async () => {
906
+ callCount++;
907
+ if (shouldFail) {
908
+ throw new Error('API error');
909
+ }
910
+ return {
911
+ ok: true,
912
+ json: async () => createV5TraceResponse([]),
913
+ text: async () => '',
914
+ };
915
+ });
916
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
917
+ // Trigger 3 failures to open circuit
918
+ for (let i = 0; i < 3; i++) {
919
+ try {
920
+ await backend.queryTraces({});
921
+ }
922
+ catch {
923
+ // expected
924
+ }
925
+ }
926
+ // Advance time past reset period
927
+ currentTime += 61000;
928
+ shouldFail = false;
929
+ // Successful request in half-open state should close circuit
930
+ await backend.queryTraces({});
931
+ assert.strictEqual(callCount, 4);
932
+ // Circuit should be closed, subsequent requests should work
933
+ await backend.queryTraces({});
934
+ assert.strictEqual(callCount, 5);
935
+ Date.now = originalDateNow;
936
+ });
937
+ it('should reopen circuit after failure in half-open state', async () => {
938
+ const originalDateNow = Date.now;
939
+ let currentTime = 1000000;
940
+ Date.now = () => currentTime;
941
+ let callCount = 0;
942
+ globalThis.fetch = setupMock(async () => {
943
+ callCount++;
944
+ throw new Error('API error');
945
+ });
946
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
947
+ // Trigger 3 failures to open circuit
948
+ for (let i = 0; i < 3; i++) {
949
+ try {
950
+ await backend.queryTraces({});
951
+ }
952
+ catch {
953
+ // expected
954
+ }
955
+ }
956
+ assert.strictEqual(callCount, 3);
957
+ // Advance time past reset period
958
+ currentTime += 61000;
959
+ // Request in half-open state fails
960
+ try {
961
+ await backend.queryTraces({});
962
+ }
963
+ catch {
964
+ // expected
965
+ }
966
+ assert.strictEqual(callCount, 4);
967
+ // Circuit should be open again (failure in half-open reopens it)
968
+ const result = await backend.queryTraces({});
969
+ assert.deepStrictEqual(result, []);
970
+ assert.strictEqual(callCount, 4, 'Should not have made fetch call when circuit reopened');
971
+ Date.now = originalDateNow;
972
+ });
973
+ it('should reset failure count after successful request', async () => {
974
+ let shouldFail = true;
975
+ let callCount = 0;
976
+ globalThis.fetch = setupMock(async () => {
977
+ callCount++;
978
+ if (shouldFail) {
979
+ throw new Error('API error');
980
+ }
981
+ return {
982
+ ok: true,
983
+ json: async () => createV5TraceResponse([]),
984
+ text: async () => '',
985
+ };
986
+ });
987
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
988
+ // 2 failures (not enough to open circuit)
989
+ for (let i = 0; i < 2; i++) {
990
+ try {
991
+ await backend.queryTraces({});
992
+ }
993
+ catch {
994
+ // expected
995
+ }
996
+ }
997
+ assert.strictEqual(callCount, 2);
998
+ // Successful request should reset failure count
999
+ shouldFail = false;
1000
+ await backend.queryTraces({});
1001
+ assert.strictEqual(callCount, 3);
1002
+ // 2 more failures should not open circuit (count was reset)
1003
+ shouldFail = true;
1004
+ for (let i = 0; i < 2; i++) {
1005
+ try {
1006
+ await backend.queryTraces({});
1007
+ }
1008
+ catch {
1009
+ // expected
1010
+ }
1011
+ }
1012
+ assert.strictEqual(callCount, 5);
1013
+ // Should still be able to make requests (circuit not open)
1014
+ try {
1015
+ await backend.queryTraces({});
1016
+ }
1017
+ catch {
1018
+ // expected to fail but should make the call
1019
+ }
1020
+ assert.strictEqual(callCount, 6, 'Circuit should still be closed');
1021
+ });
1022
+ it('should block all query methods when circuit is open', async () => {
1023
+ let callCount = 0;
1024
+ globalThis.fetch = setupMock(async () => {
1025
+ callCount++;
1026
+ throw new Error('API error');
1027
+ });
1028
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
1029
+ // Open the circuit with traces
1030
+ for (let i = 0; i < 3; i++) {
1031
+ try {
1032
+ await backend.queryTraces({});
1033
+ }
1034
+ catch {
1035
+ // expected
1036
+ }
1037
+ }
1038
+ assert.strictEqual(callCount, 3);
1039
+ // All methods should be blocked
1040
+ const methods = [
1041
+ () => backend.queryTraces({}),
1042
+ () => backend.queryLogs({}),
1043
+ () => backend.queryMetrics({ metricName: 'test' }),
1044
+ ];
1045
+ for (const method of methods) {
1046
+ const prevCount = callCount;
1047
+ try {
1048
+ await method();
1049
+ }
1050
+ catch {
1051
+ // queryTraces and queryLogs return [] on error, queryMetrics might too
1052
+ }
1053
+ // Verify no additional fetch calls were made
1054
+ assert.strictEqual(callCount, prevCount, 'Should not make fetch call when circuit open');
1055
+ }
1056
+ });
1057
+ it('should report circuit breaker state in health check', async () => {
1058
+ let callCount = 0;
1059
+ globalThis.fetch = setupMock(async () => {
1060
+ callCount++;
1061
+ throw new Error('API error');
1062
+ });
1063
+ const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
1064
+ // Open the circuit
1065
+ for (let i = 0; i < 3; i++) {
1066
+ try {
1067
+ await backend.queryTraces({});
1068
+ }
1069
+ catch {
1070
+ // expected
1071
+ }
1072
+ }
1073
+ // Health check should report circuit breaker status
1074
+ const health = await backend.healthCheck();
1075
+ assert.strictEqual(health.status, 'error');
1076
+ assert(health.message?.includes('Circuit breaker'));
1077
+ });
1078
+ });
1079
+ });
1080
+ //# sourceMappingURL=signoz-api.test.js.map