observability-toolkit 1.6.0 → 1.8.2
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 +221 -91
- package/dist/backends/index.d.ts +146 -0
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +65 -1
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/local-jsonl-boolean-search.test.js +1 -23
- package/dist/backends/local-jsonl-boolean-search.test.js.map +1 -1
- package/dist/backends/local-jsonl.d.ts +4 -1
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +216 -6
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/local-jsonl.test.js +715 -26
- package/dist/backends/local-jsonl.test.js.map +1 -1
- package/dist/backends/signoz-api.d.ts +32 -0
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.js +237 -33
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.js +410 -63
- package/dist/backends/signoz-api.test.js.map +1 -1
- package/dist/lib/constants.d.ts +16 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +121 -5
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/constants.test.js +202 -15
- package/dist/lib/constants.test.js.map +1 -1
- package/dist/lib/error-sanitizer.d.ts +57 -0
- package/dist/lib/error-sanitizer.d.ts.map +1 -0
- package/dist/lib/error-sanitizer.js +197 -0
- package/dist/lib/error-sanitizer.js.map +1 -0
- package/dist/lib/error-sanitizer.test.d.ts +8 -0
- package/dist/lib/error-sanitizer.test.d.ts.map +1 -0
- package/dist/lib/error-sanitizer.test.js +342 -0
- package/dist/lib/error-sanitizer.test.js.map +1 -0
- package/dist/lib/file-utils.d.ts +210 -0
- package/dist/lib/file-utils.d.ts.map +1 -1
- package/dist/lib/file-utils.js +529 -14
- package/dist/lib/file-utils.js.map +1 -1
- package/dist/lib/file-utils.test.js +657 -3
- package/dist/lib/file-utils.test.js.map +1 -1
- package/dist/lib/indexer.d.ts +19 -1
- package/dist/lib/indexer.d.ts.map +1 -1
- package/dist/lib/indexer.js +84 -8
- package/dist/lib/indexer.js.map +1 -1
- package/dist/lib/indexer.test.js +187 -16
- package/dist/lib/indexer.test.js.map +1 -1
- package/dist/lib/input-validator.d.ts +98 -0
- package/dist/lib/input-validator.d.ts.map +1 -0
- package/dist/lib/input-validator.js +245 -0
- package/dist/lib/input-validator.js.map +1 -0
- package/dist/lib/input-validator.test.d.ts +2 -0
- package/dist/lib/input-validator.test.d.ts.map +1 -0
- package/dist/lib/input-validator.test.js +287 -0
- package/dist/lib/input-validator.test.js.map +1 -0
- package/dist/lib/query-sanitizer.d.ts +95 -0
- package/dist/lib/query-sanitizer.d.ts.map +1 -0
- package/dist/lib/query-sanitizer.js +187 -0
- package/dist/lib/query-sanitizer.js.map +1 -0
- package/dist/lib/query-sanitizer.test.d.ts +5 -0
- package/dist/lib/query-sanitizer.test.d.ts.map +1 -0
- package/dist/lib/query-sanitizer.test.js +299 -0
- package/dist/lib/query-sanitizer.test.js.map +1 -0
- package/dist/server.d.ts +49 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +97 -13
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +202 -0
- package/dist/server.test.js.map +1 -1
- package/dist/test-helpers/file-utils.d.ts +26 -0
- package/dist/test-helpers/file-utils.d.ts.map +1 -0
- package/dist/test-helpers/file-utils.js +43 -0
- package/dist/test-helpers/file-utils.js.map +1 -0
- package/dist/test-helpers/mock-backends.d.ts +28 -0
- package/dist/test-helpers/mock-backends.d.ts.map +1 -0
- package/dist/test-helpers/mock-backends.js +31 -0
- package/dist/test-helpers/mock-backends.js.map +1 -0
- package/dist/tools/health-check.js +1 -1
- package/dist/tools/health-check.js.map +1 -1
- 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-evaluations.d.ts +183 -0
- package/dist/tools/query-evaluations.d.ts.map +1 -0
- package/dist/tools/query-evaluations.js +351 -0
- package/dist/tools/query-evaluations.js.map +1 -0
- package/dist/tools/query-evaluations.test.d.ts +5 -0
- package/dist/tools/query-evaluations.test.d.ts.map +1 -0
- package/dist/tools/query-evaluations.test.js +743 -0
- package/dist/tools/query-evaluations.test.js.map +1 -0
- package/dist/tools/query-llm-events.d.ts +62 -11
- package/dist/tools/query-llm-events.d.ts.map +1 -1
- package/dist/tools/query-llm-events.js +97 -37
- package/dist/tools/query-llm-events.js.map +1 -1
- package/dist/tools/query-llm-events.test.js +253 -0
- package/dist/tools/query-llm-events.test.js.map +1 -1
- package/dist/tools/query-logs.d.ts +32 -18
- package/dist/tools/query-logs.d.ts.map +1 -1
- package/dist/tools/query-logs.js +77 -44
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-logs.test.js +226 -64
- package/dist/tools/query-logs.test.js.map +1 -1
- package/dist/tools/query-metrics.d.ts +24 -24
- package/dist/tools/query-metrics.d.ts.map +1 -1
- package/dist/tools/query-metrics.js +102 -54
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-metrics.test.js +35 -36
- package/dist/tools/query-metrics.test.js.map +1 -1
- package/dist/tools/query-traces.d.ts +66 -22
- package/dist/tools/query-traces.d.ts.map +1 -1
- package/dist/tools/query-traces.js +86 -42
- package/dist/tools/query-traces.js.map +1 -1
- package/dist/tools/query-traces.test.js +458 -36
- package/dist/tools/query-traces.test.js.map +1 -1
- package/package.json +1 -3
|
@@ -31,6 +31,35 @@ function createV5TraceResponse(spans) {
|
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
// Helper to create v5 API log response format
|
|
35
|
+
function createV5LogResponse(logs) {
|
|
36
|
+
return {
|
|
37
|
+
data: {
|
|
38
|
+
data: {
|
|
39
|
+
results: [{
|
|
40
|
+
rows: logs.map(l => ({
|
|
41
|
+
timestamp: l.timestamp || new Date().toISOString(),
|
|
42
|
+
data: l,
|
|
43
|
+
})),
|
|
44
|
+
}],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Helper to create v5 API metric response format
|
|
50
|
+
function createV5MetricResponse(series) {
|
|
51
|
+
return {
|
|
52
|
+
data: {
|
|
53
|
+
data: {
|
|
54
|
+
results: [{
|
|
55
|
+
aggregations: [{
|
|
56
|
+
series: series,
|
|
57
|
+
}],
|
|
58
|
+
}],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
34
63
|
describe('SigNozApiBackend', () => {
|
|
35
64
|
describe('constructor', () => {
|
|
36
65
|
it('should initialize with default URL and API key from environment', () => {
|
|
@@ -347,21 +376,6 @@ describe('SigNozApiBackend', () => {
|
|
|
347
376
|
});
|
|
348
377
|
});
|
|
349
378
|
describe('queryLogs', () => {
|
|
350
|
-
// Helper to create v5 API log response format
|
|
351
|
-
function createV5LogResponse(logs) {
|
|
352
|
-
return {
|
|
353
|
-
data: {
|
|
354
|
-
data: {
|
|
355
|
-
results: [{
|
|
356
|
-
rows: logs.map(l => ({
|
|
357
|
-
timestamp: l.timestamp || new Date().toISOString(),
|
|
358
|
-
data: l,
|
|
359
|
-
})),
|
|
360
|
-
}],
|
|
361
|
-
},
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
379
|
it('should query logs with basic options', async () => {
|
|
366
380
|
globalThis.fetch = setupMock(async () => ({
|
|
367
381
|
ok: true,
|
|
@@ -526,20 +540,6 @@ describe('SigNozApiBackend', () => {
|
|
|
526
540
|
});
|
|
527
541
|
});
|
|
528
542
|
describe('queryMetrics', () => {
|
|
529
|
-
// Helper to create v5 API metric response format
|
|
530
|
-
function createV5MetricResponse(series) {
|
|
531
|
-
return {
|
|
532
|
-
data: {
|
|
533
|
-
data: {
|
|
534
|
-
results: [{
|
|
535
|
-
aggregations: [{
|
|
536
|
-
series: series,
|
|
537
|
-
}],
|
|
538
|
-
}],
|
|
539
|
-
},
|
|
540
|
-
},
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
543
|
it('should query metrics with required metricName', async () => {
|
|
544
544
|
globalThis.fetch = setupMock(async () => ({
|
|
545
545
|
ok: true,
|
|
@@ -778,9 +778,42 @@ describe('SigNozApiBackend', () => {
|
|
|
778
778
|
it('should handle complex base URL', () => {
|
|
779
779
|
const backend = new SigNozApiBackend('https://ingest.signoz.example.com:4318/v1/traces', 'test-key');
|
|
780
780
|
const url = backend.getTraceUrl('trace-123');
|
|
781
|
-
//
|
|
781
|
+
// Whitelisted path prefix (/v1/) is preserved for backward compatibility
|
|
782
782
|
assert.strictEqual(url, 'https://ingest.signoz.example.com:4318/v1/traces/trace/trace-123');
|
|
783
783
|
});
|
|
784
|
+
it('should preserve /api/ path prefix', () => {
|
|
785
|
+
const backend = new SigNozApiBackend('https://signoz.example.com/api/v3', 'test-key');
|
|
786
|
+
const url = backend.getTraceUrl('trace-123');
|
|
787
|
+
assert.strictEqual(url, 'https://signoz.example.com/api/v3/trace/trace-123');
|
|
788
|
+
});
|
|
789
|
+
it('should preserve /signoz/ path prefix', () => {
|
|
790
|
+
const backend = new SigNozApiBackend('https://proxy.example.com/signoz/query', 'test-key');
|
|
791
|
+
const url = backend.getTraceUrl('trace-123');
|
|
792
|
+
assert.strictEqual(url, 'https://proxy.example.com/signoz/query/trace/trace-123');
|
|
793
|
+
});
|
|
794
|
+
it('should preserve /query/ path prefix', () => {
|
|
795
|
+
const backend = new SigNozApiBackend('https://signoz.example.com/query/service', 'test-key');
|
|
796
|
+
const url = backend.getTraceUrl('trace-123');
|
|
797
|
+
assert.strictEqual(url, 'https://signoz.example.com/query/service/trace/trace-123');
|
|
798
|
+
});
|
|
799
|
+
it('should strip non-whitelisted path prefixes', () => {
|
|
800
|
+
const backend = new SigNozApiBackend('https://signoz.example.com/custom/path', 'test-key');
|
|
801
|
+
const url = backend.getTraceUrl('trace-123');
|
|
802
|
+
// /custom/ is not whitelisted, so only origin is used
|
|
803
|
+
assert.strictEqual(url, 'https://signoz.example.com/trace/trace-123');
|
|
804
|
+
});
|
|
805
|
+
it('should strip query params from URL for security', () => {
|
|
806
|
+
const backend = new SigNozApiBackend('https://signoz.example.com/v1/traces?token=secret', 'test-key');
|
|
807
|
+
const url = backend.getTraceUrl('trace-123');
|
|
808
|
+
// Query params stripped, path preserved
|
|
809
|
+
assert.strictEqual(url, 'https://signoz.example.com/v1/traces/trace/trace-123');
|
|
810
|
+
assert.ok(!url.includes('token'));
|
|
811
|
+
});
|
|
812
|
+
it('should strip fragment from URL for security', () => {
|
|
813
|
+
const backend = new SigNozApiBackend('https://signoz.example.com/api/v3#section', 'test-key');
|
|
814
|
+
const url = backend.getTraceUrl('trace-123');
|
|
815
|
+
assert.ok(!url.includes('#'));
|
|
816
|
+
});
|
|
784
817
|
});
|
|
785
818
|
describe('error response handling', () => {
|
|
786
819
|
it('should throw error with status code (response text sanitized)', async () => {
|
|
@@ -1262,24 +1295,10 @@ describe('SigNozApiBackend', () => {
|
|
|
1262
1295
|
});
|
|
1263
1296
|
});
|
|
1264
1297
|
describe('queryLogsPaginated', () => {
|
|
1265
|
-
function createV5LogResponsePaginated(logs) {
|
|
1266
|
-
return {
|
|
1267
|
-
data: {
|
|
1268
|
-
data: {
|
|
1269
|
-
results: [{
|
|
1270
|
-
rows: logs.map(l => ({
|
|
1271
|
-
timestamp: l.timestamp || new Date().toISOString(),
|
|
1272
|
-
data: l,
|
|
1273
|
-
})),
|
|
1274
|
-
}],
|
|
1275
|
-
},
|
|
1276
|
-
},
|
|
1277
|
-
};
|
|
1278
|
-
}
|
|
1279
1298
|
it('should return paginated log results', async () => {
|
|
1280
1299
|
globalThis.fetch = setupMock(async () => ({
|
|
1281
1300
|
ok: true,
|
|
1282
|
-
json: async () =>
|
|
1301
|
+
json: async () => createV5LogResponse([
|
|
1283
1302
|
{ body: 'log1', severity_text: 'INFO', timestamp: '2026-01-01T12:00:00Z' },
|
|
1284
1303
|
{ body: 'log2', severity_text: 'ERROR', timestamp: '2026-01-01T12:01:00Z' },
|
|
1285
1304
|
]),
|
|
@@ -1293,7 +1312,7 @@ describe('SigNozApiBackend', () => {
|
|
|
1293
1312
|
it('should return hasMore=true when more logs available', async () => {
|
|
1294
1313
|
globalThis.fetch = setupMock(async () => ({
|
|
1295
1314
|
ok: true,
|
|
1296
|
-
json: async () =>
|
|
1315
|
+
json: async () => createV5LogResponse([
|
|
1297
1316
|
{ body: 'log1', severity_text: 'INFO', timestamp: '2026-01-01T12:00:00Z' },
|
|
1298
1317
|
{ body: 'log2', severity_text: 'ERROR', timestamp: '2026-01-01T12:01:00Z' },
|
|
1299
1318
|
{ body: 'log3', severity_text: 'WARN', timestamp: '2026-01-01T12:02:00Z' },
|
|
@@ -1314,7 +1333,7 @@ describe('SigNozApiBackend', () => {
|
|
|
1314
1333
|
}
|
|
1315
1334
|
return {
|
|
1316
1335
|
ok: true,
|
|
1317
|
-
json: async () =>
|
|
1336
|
+
json: async () => createV5LogResponse([]),
|
|
1318
1337
|
text: async () => '',
|
|
1319
1338
|
};
|
|
1320
1339
|
});
|
|
@@ -1338,23 +1357,10 @@ describe('SigNozApiBackend', () => {
|
|
|
1338
1357
|
});
|
|
1339
1358
|
});
|
|
1340
1359
|
describe('queryMetricsPaginated', () => {
|
|
1341
|
-
function createV5MetricResponsePaginated(series) {
|
|
1342
|
-
return {
|
|
1343
|
-
data: {
|
|
1344
|
-
data: {
|
|
1345
|
-
results: [{
|
|
1346
|
-
aggregations: [{
|
|
1347
|
-
series: series,
|
|
1348
|
-
}],
|
|
1349
|
-
}],
|
|
1350
|
-
},
|
|
1351
|
-
},
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
1360
|
it('should return paginated metric results', async () => {
|
|
1355
1361
|
globalThis.fetch = setupMock(async () => ({
|
|
1356
1362
|
ok: true,
|
|
1357
|
-
json: async () =>
|
|
1363
|
+
json: async () => createV5MetricResponse([{
|
|
1358
1364
|
values: [
|
|
1359
1365
|
{ timestamp: 1704110400000, value: 42.5 },
|
|
1360
1366
|
{ timestamp: 1704110460000, value: 43.2 },
|
|
@@ -1370,7 +1376,7 @@ describe('SigNozApiBackend', () => {
|
|
|
1370
1376
|
it('should return hasMore=true when more metrics available', async () => {
|
|
1371
1377
|
globalThis.fetch = setupMock(async () => ({
|
|
1372
1378
|
ok: true,
|
|
1373
|
-
json: async () =>
|
|
1379
|
+
json: async () => createV5MetricResponse([{
|
|
1374
1380
|
values: [
|
|
1375
1381
|
{ timestamp: 1704110400000, value: 1 },
|
|
1376
1382
|
{ timestamp: 1704110460000, value: 2 },
|
|
@@ -1393,7 +1399,7 @@ describe('SigNozApiBackend', () => {
|
|
|
1393
1399
|
}
|
|
1394
1400
|
return {
|
|
1395
1401
|
ok: true,
|
|
1396
|
-
json: async () =>
|
|
1402
|
+
json: async () => createV5MetricResponse([]),
|
|
1397
1403
|
text: async () => '',
|
|
1398
1404
|
};
|
|
1399
1405
|
});
|
|
@@ -1493,5 +1499,346 @@ describe('SigNozApiBackend', () => {
|
|
|
1493
1499
|
});
|
|
1494
1500
|
});
|
|
1495
1501
|
});
|
|
1502
|
+
describe('rate limiter', () => {
|
|
1503
|
+
it('should allow requests up to max tokens', async () => {
|
|
1504
|
+
let callCount = 0;
|
|
1505
|
+
globalThis.fetch = setupMock(async () => {
|
|
1506
|
+
callCount++;
|
|
1507
|
+
return {
|
|
1508
|
+
ok: true,
|
|
1509
|
+
json: async () => createV5TraceResponse([]),
|
|
1510
|
+
text: async () => '',
|
|
1511
|
+
};
|
|
1512
|
+
});
|
|
1513
|
+
// Create backend - default is 60 tokens
|
|
1514
|
+
const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
|
|
1515
|
+
// Make multiple rapid requests - should all succeed within token limit
|
|
1516
|
+
for (let i = 0; i < 10; i++) {
|
|
1517
|
+
await backend.queryTraces({});
|
|
1518
|
+
}
|
|
1519
|
+
assert.strictEqual(callCount, 10, 'All requests within limit should succeed');
|
|
1520
|
+
});
|
|
1521
|
+
it('should block requests when rate limit exceeded', async () => {
|
|
1522
|
+
const originalDateNow = Date.now;
|
|
1523
|
+
let currentTime = 1000000;
|
|
1524
|
+
Date.now = () => currentTime;
|
|
1525
|
+
let callCount = 0;
|
|
1526
|
+
globalThis.fetch = setupMock(async () => {
|
|
1527
|
+
callCount++;
|
|
1528
|
+
return {
|
|
1529
|
+
ok: true,
|
|
1530
|
+
json: async () => createV5TraceResponse([]),
|
|
1531
|
+
text: async () => '',
|
|
1532
|
+
};
|
|
1533
|
+
});
|
|
1534
|
+
// Create backend with very low rate limit (3 tokens) for testing
|
|
1535
|
+
// We need to test the TokenBucketRateLimiter directly since the backend
|
|
1536
|
+
// uses the default from constants
|
|
1537
|
+
const { TokenBucketRateLimiter } = await import('./signoz-api.js');
|
|
1538
|
+
const limiter = new TokenBucketRateLimiter(3, 1); // 3 tokens, 1/sec refill
|
|
1539
|
+
// Use all tokens
|
|
1540
|
+
assert.strictEqual(limiter.tryConsume(), true, '1st token should be available');
|
|
1541
|
+
assert.strictEqual(limiter.tryConsume(), true, '2nd token should be available');
|
|
1542
|
+
assert.strictEqual(limiter.tryConsume(), true, '3rd token should be available');
|
|
1543
|
+
assert.strictEqual(limiter.tryConsume(), false, '4th token should be blocked');
|
|
1544
|
+
Date.now = originalDateNow;
|
|
1545
|
+
});
|
|
1546
|
+
it('should refill tokens over time', async () => {
|
|
1547
|
+
const originalDateNow = Date.now;
|
|
1548
|
+
let currentTime = 1000000;
|
|
1549
|
+
Date.now = () => currentTime;
|
|
1550
|
+
const { TokenBucketRateLimiter } = await import('./signoz-api.js');
|
|
1551
|
+
const limiter = new TokenBucketRateLimiter(3, 1); // 3 tokens, 1/sec refill
|
|
1552
|
+
// Use all tokens
|
|
1553
|
+
limiter.tryConsume();
|
|
1554
|
+
limiter.tryConsume();
|
|
1555
|
+
limiter.tryConsume();
|
|
1556
|
+
assert.strictEqual(limiter.getAvailableTokens(), 0, 'All tokens should be used');
|
|
1557
|
+
// Advance time by 2 seconds
|
|
1558
|
+
currentTime += 2000;
|
|
1559
|
+
// Should have refilled 2 tokens
|
|
1560
|
+
assert.strictEqual(limiter.getAvailableTokens(), 2, 'Should have refilled 2 tokens');
|
|
1561
|
+
// Advance time by 5 more seconds (past max)
|
|
1562
|
+
currentTime += 5000;
|
|
1563
|
+
// Should cap at max tokens
|
|
1564
|
+
assert.strictEqual(limiter.getAvailableTokens(), 3, 'Should cap at max tokens');
|
|
1565
|
+
Date.now = originalDateNow;
|
|
1566
|
+
});
|
|
1567
|
+
it('should return empty array when rate limited', async () => {
|
|
1568
|
+
const originalDateNow = Date.now;
|
|
1569
|
+
let currentTime = 1000000;
|
|
1570
|
+
Date.now = () => currentTime;
|
|
1571
|
+
let callCount = 0;
|
|
1572
|
+
globalThis.fetch = setupMock(async () => {
|
|
1573
|
+
callCount++;
|
|
1574
|
+
return {
|
|
1575
|
+
ok: true,
|
|
1576
|
+
json: async () => createV5TraceResponse([]),
|
|
1577
|
+
text: async () => '',
|
|
1578
|
+
};
|
|
1579
|
+
});
|
|
1580
|
+
// We can't easily set custom rate limits on the backend, so we'll test
|
|
1581
|
+
// the behavior by making 60+ requests (default limit)
|
|
1582
|
+
const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
|
|
1583
|
+
// Make 60 requests (default token limit)
|
|
1584
|
+
for (let i = 0; i < 60; i++) {
|
|
1585
|
+
await backend.queryTraces({});
|
|
1586
|
+
}
|
|
1587
|
+
assert.strictEqual(callCount, 60);
|
|
1588
|
+
// 61st request should be rate limited
|
|
1589
|
+
const result = await backend.queryTraces({});
|
|
1590
|
+
assert.deepStrictEqual(result, [], 'Should return empty array when rate limited');
|
|
1591
|
+
assert.strictEqual(callCount, 60, 'Should not have made fetch call when rate limited');
|
|
1592
|
+
Date.now = originalDateNow;
|
|
1593
|
+
});
|
|
1594
|
+
it('should log warning when rate limit exceeded', async () => {
|
|
1595
|
+
const originalDateNow = Date.now;
|
|
1596
|
+
let currentTime = 1000000;
|
|
1597
|
+
Date.now = () => currentTime;
|
|
1598
|
+
const warnLogs = [];
|
|
1599
|
+
const originalWarn = console.warn;
|
|
1600
|
+
console.warn = (msg) => { warnLogs.push(msg); };
|
|
1601
|
+
const { TokenBucketRateLimiter } = await import('./signoz-api.js');
|
|
1602
|
+
const limiter = new TokenBucketRateLimiter(2, 1);
|
|
1603
|
+
// Use all tokens
|
|
1604
|
+
limiter.tryConsume();
|
|
1605
|
+
limiter.tryConsume();
|
|
1606
|
+
// This should trigger the warning
|
|
1607
|
+
limiter.tryConsume();
|
|
1608
|
+
console.warn = originalWarn;
|
|
1609
|
+
Date.now = originalDateNow;
|
|
1610
|
+
assert(warnLogs.some(log => log.includes('[obs-toolkit] Rate limit exceeded')));
|
|
1611
|
+
});
|
|
1612
|
+
it('should report rate limit status in health check', async () => {
|
|
1613
|
+
const originalDateNow = Date.now;
|
|
1614
|
+
let currentTime = 1000000;
|
|
1615
|
+
Date.now = () => currentTime;
|
|
1616
|
+
let callCount = 0;
|
|
1617
|
+
globalThis.fetch = setupMock(async () => {
|
|
1618
|
+
callCount++;
|
|
1619
|
+
return {
|
|
1620
|
+
ok: true,
|
|
1621
|
+
json: async () => ({ status: 'success' }),
|
|
1622
|
+
text: async () => '',
|
|
1623
|
+
};
|
|
1624
|
+
});
|
|
1625
|
+
const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
|
|
1626
|
+
// Use all 60 tokens
|
|
1627
|
+
for (let i = 0; i < 60; i++) {
|
|
1628
|
+
await backend.queryTraces({});
|
|
1629
|
+
}
|
|
1630
|
+
// Health check should report rate limit status
|
|
1631
|
+
const health = await backend.healthCheck();
|
|
1632
|
+
assert.strictEqual(health.status, 'error');
|
|
1633
|
+
assert(health.message?.includes('Rate limit'));
|
|
1634
|
+
Date.now = originalDateNow;
|
|
1635
|
+
});
|
|
1636
|
+
it('should reset tokens with reset method', async () => {
|
|
1637
|
+
const { TokenBucketRateLimiter } = await import('./signoz-api.js');
|
|
1638
|
+
const limiter = new TokenBucketRateLimiter(3, 1);
|
|
1639
|
+
// Use all tokens
|
|
1640
|
+
limiter.tryConsume();
|
|
1641
|
+
limiter.tryConsume();
|
|
1642
|
+
limiter.tryConsume();
|
|
1643
|
+
assert.strictEqual(limiter.getAvailableTokens(), 0);
|
|
1644
|
+
// Reset
|
|
1645
|
+
limiter.reset();
|
|
1646
|
+
assert.strictEqual(limiter.getAvailableTokens(), 3, 'Should have all tokens after reset');
|
|
1647
|
+
});
|
|
1648
|
+
it('should handle logs query when rate limited', async () => {
|
|
1649
|
+
const originalDateNow = Date.now;
|
|
1650
|
+
let currentTime = 1000000;
|
|
1651
|
+
Date.now = () => currentTime;
|
|
1652
|
+
let callCount = 0;
|
|
1653
|
+
globalThis.fetch = setupMock(async () => {
|
|
1654
|
+
callCount++;
|
|
1655
|
+
return {
|
|
1656
|
+
ok: true,
|
|
1657
|
+
json: async () => ({ data: { data: { results: [] } } }),
|
|
1658
|
+
text: async () => '',
|
|
1659
|
+
};
|
|
1660
|
+
});
|
|
1661
|
+
const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
|
|
1662
|
+
// Use all 60 tokens
|
|
1663
|
+
for (let i = 0; i < 60; i++) {
|
|
1664
|
+
await backend.queryTraces({});
|
|
1665
|
+
}
|
|
1666
|
+
assert.strictEqual(callCount, 60);
|
|
1667
|
+
// Logs query should also be rate limited
|
|
1668
|
+
const result = await backend.queryLogs({});
|
|
1669
|
+
assert.deepStrictEqual(result, []);
|
|
1670
|
+
assert.strictEqual(callCount, 60, 'Should not make fetch call for logs when rate limited');
|
|
1671
|
+
Date.now = originalDateNow;
|
|
1672
|
+
});
|
|
1673
|
+
it('should handle metrics query when rate limited', async () => {
|
|
1674
|
+
const originalDateNow = Date.now;
|
|
1675
|
+
let currentTime = 1000000;
|
|
1676
|
+
Date.now = () => currentTime;
|
|
1677
|
+
let callCount = 0;
|
|
1678
|
+
globalThis.fetch = setupMock(async () => {
|
|
1679
|
+
callCount++;
|
|
1680
|
+
return {
|
|
1681
|
+
ok: true,
|
|
1682
|
+
json: async () => ({ data: { data: { results: [] } } }),
|
|
1683
|
+
text: async () => '',
|
|
1684
|
+
};
|
|
1685
|
+
});
|
|
1686
|
+
const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
|
|
1687
|
+
// Use all 60 tokens
|
|
1688
|
+
for (let i = 0; i < 60; i++) {
|
|
1689
|
+
await backend.queryTraces({});
|
|
1690
|
+
}
|
|
1691
|
+
assert.strictEqual(callCount, 60);
|
|
1692
|
+
// Metrics query should also be rate limited
|
|
1693
|
+
const result = await backend.queryMetrics({ metricName: 'test' });
|
|
1694
|
+
assert.deepStrictEqual(result, []);
|
|
1695
|
+
assert.strictEqual(callCount, 60, 'Should not make fetch call for metrics when rate limited');
|
|
1696
|
+
Date.now = originalDateNow;
|
|
1697
|
+
});
|
|
1698
|
+
it('should refund single token correctly', async () => {
|
|
1699
|
+
const originalDateNow = Date.now;
|
|
1700
|
+
let currentTime = 1000000;
|
|
1701
|
+
Date.now = () => currentTime;
|
|
1702
|
+
const { TokenBucketRateLimiter } = await import('./signoz-api.js');
|
|
1703
|
+
const limiter = new TokenBucketRateLimiter(3, 1); // 3 tokens, 1/sec refill
|
|
1704
|
+
// Use all tokens
|
|
1705
|
+
limiter.tryConsume();
|
|
1706
|
+
limiter.tryConsume();
|
|
1707
|
+
limiter.tryConsume();
|
|
1708
|
+
assert.strictEqual(limiter.getAvailableTokens(), 0, 'All tokens should be used');
|
|
1709
|
+
// Refund one token
|
|
1710
|
+
limiter.refund();
|
|
1711
|
+
assert.strictEqual(limiter.getAvailableTokens(), 1, 'Should have 1 token after refund');
|
|
1712
|
+
// Refund again
|
|
1713
|
+
limiter.refund();
|
|
1714
|
+
assert.strictEqual(limiter.getAvailableTokens(), 2, 'Should have 2 tokens after second refund');
|
|
1715
|
+
// Refund at max should not exceed max
|
|
1716
|
+
limiter.refund();
|
|
1717
|
+
limiter.refund(); // This should cap at max
|
|
1718
|
+
assert.strictEqual(limiter.getAvailableTokens(), 3, 'Should cap at max tokens');
|
|
1719
|
+
Date.now = originalDateNow;
|
|
1720
|
+
});
|
|
1721
|
+
it('should refund token when circuit breaker rejects request', async () => {
|
|
1722
|
+
const originalDateNow = Date.now;
|
|
1723
|
+
let currentTime = 1000000;
|
|
1724
|
+
Date.now = () => currentTime;
|
|
1725
|
+
let callCount = 0;
|
|
1726
|
+
globalThis.fetch = setupMock(async () => {
|
|
1727
|
+
callCount++;
|
|
1728
|
+
// Simulate failures to open circuit breaker
|
|
1729
|
+
const response = {
|
|
1730
|
+
ok: false,
|
|
1731
|
+
status: 500,
|
|
1732
|
+
json: async () => ({}),
|
|
1733
|
+
text: async () => 'Internal Server Error',
|
|
1734
|
+
};
|
|
1735
|
+
return response;
|
|
1736
|
+
});
|
|
1737
|
+
const backend = new SigNozApiBackend('https://signoz.example.com', 'test-key');
|
|
1738
|
+
// Cause circuit breaker to open (default is 3 failures per constants.ts)
|
|
1739
|
+
for (let i = 0; i < 3; i++) {
|
|
1740
|
+
try {
|
|
1741
|
+
await backend.queryTraces({});
|
|
1742
|
+
}
|
|
1743
|
+
catch {
|
|
1744
|
+
// Expected to throw
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
assert.strictEqual(callCount, 3, 'Should have made 3 failing requests');
|
|
1748
|
+
// Get initial token count - 60 tokens - 3 consumed = 57
|
|
1749
|
+
// Now circuit breaker is open, subsequent requests should refund tokens
|
|
1750
|
+
const result1 = await backend.queryTraces({});
|
|
1751
|
+
assert.deepStrictEqual(result1, [], 'Should return empty when circuit breaker open');
|
|
1752
|
+
// This should not consume more tokens because the token was refunded
|
|
1753
|
+
const result2 = await backend.queryTraces({});
|
|
1754
|
+
assert.deepStrictEqual(result2, [], 'Should return empty when circuit breaker open');
|
|
1755
|
+
// No additional fetch calls should have been made
|
|
1756
|
+
assert.strictEqual(callCount, 3, 'Should not make more fetch calls when circuit breaker open');
|
|
1757
|
+
Date.now = originalDateNow;
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1760
|
+
describe('SSRF protection', () => {
|
|
1761
|
+
it('should block localhost URL', () => {
|
|
1762
|
+
const backend = new SigNozApiBackend('https://localhost/api', 'test-key');
|
|
1763
|
+
// Backend with blocked URL will have empty baseUrl
|
|
1764
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1765
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should have empty base for blocked localhost');
|
|
1766
|
+
});
|
|
1767
|
+
it('should block 127.0.0.1', () => {
|
|
1768
|
+
const backend = new SigNozApiBackend('https://127.0.0.1/api', 'test-key');
|
|
1769
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1770
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should have empty base for blocked 127.0.0.1');
|
|
1771
|
+
});
|
|
1772
|
+
it('should block IPv6 localhost ::1', () => {
|
|
1773
|
+
const backend = new SigNozApiBackend('https://[::1]/api', 'test-key');
|
|
1774
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1775
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block IPv6 localhost');
|
|
1776
|
+
});
|
|
1777
|
+
it('should block long-form IPv6 localhost', () => {
|
|
1778
|
+
const backend = new SigNozApiBackend('https://[0:0:0:0:0:0:0:1]/api', 'test-key');
|
|
1779
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1780
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block long-form IPv6 localhost');
|
|
1781
|
+
});
|
|
1782
|
+
it('should block IPv4-mapped IPv6 localhost', () => {
|
|
1783
|
+
const backend = new SigNozApiBackend('https://[::ffff:127.0.0.1]/api', 'test-key');
|
|
1784
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1785
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block IPv4-mapped localhost');
|
|
1786
|
+
});
|
|
1787
|
+
it('should block .localhost TLD', () => {
|
|
1788
|
+
const backend = new SigNozApiBackend('https://myapp.localhost/api', 'test-key');
|
|
1789
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1790
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block .localhost TLD');
|
|
1791
|
+
});
|
|
1792
|
+
it('should block private 192.168.x.x ranges', () => {
|
|
1793
|
+
const backend = new SigNozApiBackend('https://192.168.1.1/api', 'test-key');
|
|
1794
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1795
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block 192.168.x.x');
|
|
1796
|
+
});
|
|
1797
|
+
it('should block private 10.x.x.x ranges', () => {
|
|
1798
|
+
const backend = new SigNozApiBackend('https://10.0.0.1/api', 'test-key');
|
|
1799
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1800
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block 10.x.x.x');
|
|
1801
|
+
});
|
|
1802
|
+
it('should block private 172.16-31.x.x ranges', () => {
|
|
1803
|
+
const backend = new SigNozApiBackend('https://172.16.0.1/api', 'test-key');
|
|
1804
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1805
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block 172.16.x.x');
|
|
1806
|
+
});
|
|
1807
|
+
it('should block IPv6 unique local addresses (fc00::/7)', () => {
|
|
1808
|
+
const backend = new SigNozApiBackend('https://[fc00::1]/api', 'test-key');
|
|
1809
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1810
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block fc00:: ULA');
|
|
1811
|
+
});
|
|
1812
|
+
it('should block IPv6 link-local addresses (fe80::)', () => {
|
|
1813
|
+
const backend = new SigNozApiBackend('https://[fe80::1]/api', 'test-key');
|
|
1814
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1815
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block fe80:: link-local');
|
|
1816
|
+
});
|
|
1817
|
+
it('should block .local domain', () => {
|
|
1818
|
+
const backend = new SigNozApiBackend('https://myhost.local/api', 'test-key');
|
|
1819
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1820
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block .local domain');
|
|
1821
|
+
});
|
|
1822
|
+
it('should block .internal domain', () => {
|
|
1823
|
+
const backend = new SigNozApiBackend('https://signoz.internal/api', 'test-key');
|
|
1824
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1825
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block .internal domain');
|
|
1826
|
+
});
|
|
1827
|
+
it('should block .home.arpa domain', () => {
|
|
1828
|
+
const backend = new SigNozApiBackend('https://router.home.arpa/api', 'test-key');
|
|
1829
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1830
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block .home.arpa domain');
|
|
1831
|
+
});
|
|
1832
|
+
it('should allow valid external HTTPS URL', () => {
|
|
1833
|
+
const backend = new SigNozApiBackend('https://signoz.example.com/api/', 'test-key');
|
|
1834
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1835
|
+
assert.ok(url.includes('signoz.example.com'), 'Should allow valid external URL');
|
|
1836
|
+
});
|
|
1837
|
+
it('should block HTTP protocol', () => {
|
|
1838
|
+
const backend = new SigNozApiBackend('http://signoz.example.com/api', 'test-key');
|
|
1839
|
+
const url = backend.getTraceUrl('trace-123');
|
|
1840
|
+
assert.strictEqual(url, '/trace/trace-123', 'Should block HTTP protocol');
|
|
1841
|
+
});
|
|
1842
|
+
});
|
|
1496
1843
|
});
|
|
1497
1844
|
//# sourceMappingURL=signoz-api.test.js.map
|