observability-toolkit 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -3
- package/dist/backends/index.d.ts +28 -0
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/local-jsonl.d.ts +29 -1
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +259 -27
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/local-jsonl.test.d.ts +2 -0
- package/dist/backends/local-jsonl.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl.test.js +1638 -0
- package/dist/backends/local-jsonl.test.js.map +1 -0
- package/dist/backends/signoz-api.d.ts +9 -2
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.integration.test.d.ts +8 -0
- package/dist/backends/signoz-api.integration.test.d.ts.map +1 -0
- package/dist/backends/signoz-api.integration.test.js +137 -0
- package/dist/backends/signoz-api.integration.test.js.map +1 -0
- package/dist/backends/signoz-api.js +206 -115
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.d.ts +2 -0
- package/dist/backends/signoz-api.test.d.ts.map +1 -0
- package/dist/backends/signoz-api.test.js +1080 -0
- package/dist/backends/signoz-api.test.js.map +1 -0
- package/dist/lib/constants.d.ts +28 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +73 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/constants.test.d.ts +5 -0
- package/dist/lib/constants.test.d.ts.map +1 -0
- package/dist/lib/constants.test.js +381 -0
- package/dist/lib/constants.test.js.map +1 -0
- package/dist/lib/file-utils.d.ts +53 -1
- package/dist/lib/file-utils.d.ts.map +1 -1
- package/dist/lib/file-utils.js +142 -3
- package/dist/lib/file-utils.js.map +1 -1
- package/dist/lib/file-utils.test.d.ts +2 -0
- package/dist/lib/file-utils.test.d.ts.map +1 -0
- package/dist/lib/file-utils.test.js +649 -0
- package/dist/lib/file-utils.test.js.map +1 -0
- package/dist/server.js +50 -63
- package/dist/server.js.map +1 -1
- package/dist/server.test.d.ts +5 -0
- package/dist/server.test.d.ts.map +1 -0
- package/dist/server.test.js +547 -0
- package/dist/server.test.js.map +1 -0
- package/dist/tools/context-stats.d.ts +2 -2
- package/dist/tools/context-stats.d.ts.map +1 -1
- package/dist/tools/context-stats.js +2 -1
- package/dist/tools/context-stats.js.map +1 -1
- package/dist/tools/context-stats.test.d.ts +5 -0
- package/dist/tools/context-stats.test.d.ts.map +1 -0
- package/dist/tools/context-stats.test.js +465 -0
- package/dist/tools/context-stats.test.js.map +1 -0
- package/dist/tools/get-trace-url.d.ts.map +1 -1
- package/dist/tools/get-trace-url.js +5 -1
- package/dist/tools/get-trace-url.js.map +1 -1
- package/dist/tools/get-trace-url.test.d.ts +5 -0
- package/dist/tools/get-trace-url.test.d.ts.map +1 -0
- package/dist/tools/get-trace-url.test.js +429 -0
- package/dist/tools/get-trace-url.test.js.map +1 -0
- package/dist/tools/health-check.d.ts +9 -2
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +66 -27
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/health-check.test.d.ts +5 -0
- package/dist/tools/health-check.test.d.ts.map +1 -0
- package/dist/tools/health-check.test.js +386 -0
- package/dist/tools/health-check.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/query-llm-events.d.ts +82 -0
- package/dist/tools/query-llm-events.d.ts.map +1 -0
- package/dist/tools/query-llm-events.js +60 -0
- package/dist/tools/query-llm-events.js.map +1 -0
- package/dist/tools/query-llm-events.test.d.ts +5 -0
- package/dist/tools/query-llm-events.test.d.ts.map +1 -0
- package/dist/tools/query-llm-events.test.js +111 -0
- package/dist/tools/query-llm-events.test.js.map +1 -0
- package/dist/tools/query-logs.d.ts +15 -8
- package/dist/tools/query-logs.d.ts.map +1 -1
- package/dist/tools/query-logs.js +11 -10
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-logs.test.d.ts +5 -0
- package/dist/tools/query-logs.test.d.ts.map +1 -0
- package/dist/tools/query-logs.test.js +688 -0
- package/dist/tools/query-logs.test.js.map +1 -0
- package/dist/tools/query-metrics.d.ts +13 -15
- package/dist/tools/query-metrics.d.ts.map +1 -1
- package/dist/tools/query-metrics.js +12 -13
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-metrics.test.d.ts +5 -0
- package/dist/tools/query-metrics.test.d.ts.map +1 -0
- package/dist/tools/query-metrics.test.js +597 -0
- package/dist/tools/query-metrics.test.js.map +1 -0
- package/dist/tools/query-traces.d.ts +19 -14
- package/dist/tools/query-traces.d.ts.map +1 -1
- package/dist/tools/query-traces.js +14 -14
- package/dist/tools/query-traces.js.map +1 -1
- package/dist/tools/query-traces.test.d.ts +5 -0
- package/dist/tools/query-traces.test.d.ts.map +1 -0
- package/dist/tools/query-traces.test.js +643 -0
- package/dist/tools/query-traces.test.js.map +1 -0
- package/dist/tools/setup-claudeignore.d.ts +36 -10
- package/dist/tools/setup-claudeignore.d.ts.map +1 -1
- package/dist/tools/setup-claudeignore.js +193 -33
- package/dist/tools/setup-claudeignore.js.map +1 -1
- package/dist/tools/setup-claudeignore.test.d.ts +2 -0
- package/dist/tools/setup-claudeignore.test.d.ts.map +1 -0
- package/dist/tools/setup-claudeignore.test.js +481 -0
- package/dist/tools/setup-claudeignore.test.js.map +1 -0
- package/dist/tools/signoz.integration.test.d.ts +8 -0
- package/dist/tools/signoz.integration.test.d.ts.map +1 -0
- package/dist/tools/signoz.integration.test.js +141 -0
- package/dist/tools/signoz.integration.test.js.map +1 -0
- package/package.json +6 -3
|
@@ -0,0 +1,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
|