@geekmidas/telescope 1.1.1 → 10.0.0-alpha.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 +14 -4
- package/dist/{hono-Bz9PQQzj.mjs → hono-Bw-ey6CT.mjs} +2 -2
- package/dist/{hono-Bz9PQQzj.mjs.map → hono-Bw-ey6CT.mjs.map} +1 -1
- package/dist/{hono-Z_JIICmt.cjs → hono-CPHX7FAm.cjs} +2 -2
- package/dist/{hono-Z_JIICmt.cjs.map → hono-CPHX7FAm.cjs.map} +1 -1
- package/dist/{http-Crsriv32.mjs → http-BIvr0YdI.mjs} +1 -1
- package/dist/{http-Crsriv32.mjs.map → http-BIvr0YdI.mjs.map} +1 -1
- package/dist/{http-JycFRfSe.cjs → http-BsqmhurZ.cjs} +1 -1
- package/dist/{http-JycFRfSe.cjs.map → http-BsqmhurZ.cjs.map} +1 -1
- package/dist/{http-D5kn3_Mt.d.mts → http-DULM1u2L.d.cts} +3 -3
- package/dist/{http-D5kn3_Mt.d.mts.map → http-DULM1u2L.d.cts.map} +1 -1
- package/dist/{http-Dvca6fC3.d.cts → http-GB-Nlvwc.d.mts} +3 -3
- package/dist/{http-Dvca6fC3.d.cts.map → http-GB-Nlvwc.d.mts.map} +1 -1
- package/dist/instrumentation/hono.cjs +2 -2
- package/dist/instrumentation/hono.mjs +2 -2
- package/dist/instrumentation/http.cjs +1 -1
- package/dist/instrumentation/http.d.cts +1 -1
- package/dist/instrumentation/http.d.mts +1 -1
- package/dist/instrumentation/http.mjs +1 -1
- package/dist/instrumentation/index.cjs +5 -5
- package/dist/instrumentation/index.d.cts +3 -3
- package/dist/instrumentation/index.d.mts +3 -3
- package/dist/instrumentation/index.mjs +5 -5
- package/dist/instrumentation/middleware.cjs +2 -2
- package/dist/instrumentation/middleware.d.cts +1 -1
- package/dist/instrumentation/middleware.d.mts +1 -1
- package/dist/instrumentation/middleware.mjs +2 -2
- package/dist/instrumentation/otel.cjs +2 -2
- package/dist/instrumentation/otel.d.cts +1 -1
- package/dist/instrumentation/otel.d.mts +1 -1
- package/dist/instrumentation/otel.mjs +2 -2
- package/dist/instrumentation/setup.cjs +1 -1
- package/dist/instrumentation/setup.mjs +1 -1
- package/dist/logger/console.d.cts.map +1 -1
- package/dist/{middleware-D3v5SnSs.cjs → middleware-Bcq6XXGG.cjs} +3 -3
- package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
- package/dist/{middleware-Dy1ctr2N.mjs → middleware-D1XEHvyy.mjs} +3 -3
- package/dist/middleware-D1XEHvyy.mjs.map +1 -0
- package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
- package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
- package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
- package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
- package/dist/{otel-DPKNyJ0t.mjs → otel-B9bNoeZ2.mjs} +4 -4
- package/dist/otel-B9bNoeZ2.mjs.map +1 -0
- package/dist/{otel-1bJPb2Z7.d.mts → otel-Bk89b1lX.d.cts} +3 -3
- package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
- package/dist/{otel-DuiBeBPU.cjs → otel-BzjOtVxZ.cjs} +4 -4
- package/dist/otel-BzjOtVxZ.cjs.map +1 -0
- package/dist/{otel-SBVoVEHr.d.cts → otel-lPMsM26e.d.mts} +3 -3
- package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
- package/dist/otlp/hono.cjs +3 -3
- package/dist/otlp/hono.mjs +3 -3
- package/dist/otlp/index.cjs +3 -3
- package/dist/otlp/index.mjs +3 -3
- package/dist/otlp/receiver.cjs +3 -3
- package/dist/otlp/receiver.mjs +3 -3
- package/dist/otlp/transformer.cjs +2 -2
- package/dist/otlp/transformer.mjs +2 -2
- package/dist/otlp/types.cjs +1 -1
- package/dist/otlp/types.mjs +1 -1
- package/dist/{receiver-q4f7eirs.mjs → receiver-D46dfkYI.mjs} +2 -2
- package/dist/{receiver-q4f7eirs.mjs.map → receiver-D46dfkYI.mjs.map} +1 -1
- package/dist/{receiver-BW_4go_X.cjs → receiver-slCoAFat.cjs} +2 -2
- package/dist/{receiver-BW_4go_X.cjs.map → receiver-slCoAFat.cjs.map} +1 -1
- package/dist/{setup-BhCp2LUp.mjs → setup-DC9JMEYZ.mjs} +1 -1
- package/dist/{setup-BhCp2LUp.mjs.map → setup-DC9JMEYZ.mjs.map} +1 -1
- package/dist/{setup-_uT4L_x5.cjs → setup-DufxQpi9.cjs} +1 -1
- package/dist/{setup-_uT4L_x5.cjs.map → setup-DufxQpi9.cjs.map} +1 -1
- package/dist/storage/kysely.cjs +48 -38
- package/dist/storage/kysely.cjs.map +1 -1
- package/dist/storage/kysely.d.cts +29 -9
- package/dist/storage/kysely.d.cts.map +1 -1
- package/dist/storage/kysely.d.mts +29 -9
- package/dist/storage/kysely.d.mts.map +1 -1
- package/dist/storage/kysely.mjs +48 -38
- package/dist/storage/kysely.mjs.map +1 -1
- package/dist/tracing-BX8uY2nR.d.cts.map +1 -1
- package/dist/tracing-PnECEbEu.d.mts.map +1 -1
- package/dist/{transformer-CFrJQkUg.mjs → transformer-CFQ4WFk6.mjs} +2 -2
- package/dist/{transformer-CFrJQkUg.mjs.map → transformer-CFQ4WFk6.mjs.map} +1 -1
- package/dist/{transformer-DvDYyFRQ.cjs → transformer-Csl6fbgv.cjs} +2 -2
- package/dist/{transformer-DvDYyFRQ.cjs.map → transformer-Csl6fbgv.cjs.map} +1 -1
- package/dist/types-B1EIa_cQ.d.mts.map +1 -1
- package/dist/{types-BBeTDpHr.cjs → types-BMiHgsi4.cjs} +1 -1
- package/dist/{types-BBeTDpHr.cjs.map → types-BMiHgsi4.cjs.map} +1 -1
- package/dist/{types-CXYLhG_G.mjs → types-Cj59nqy7.mjs} +1 -1
- package/dist/{types-CXYLhG_G.mjs.map → types-Cj59nqy7.mjs.map} +1 -1
- package/dist/types-l7Rlb5Mg.d.cts.map +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -51
- package/dist/middleware-D3v5SnSs.cjs.map +0 -1
- package/dist/middleware-Dy1ctr2N.mjs.map +0 -1
- package/dist/otel-DPKNyJ0t.mjs.map +0 -1
- package/dist/otel-DuiBeBPU.cjs.map +0 -1
- package/src/Telescope.ts +0 -561
- package/src/__tests__/Telescope.spec.ts +0 -438
- package/src/__tests__/pino-transport.spec.ts +0 -115
- package/src/__tests__/redact.spec.ts +0 -336
- package/src/adapters/__tests__/lambda.spec.ts +0 -318
- package/src/adapters/__tests__/middy.spec.ts +0 -741
- package/src/adapters/index.ts +0 -22
- package/src/adapters/lambda.ts +0 -432
- package/src/adapters/middy.ts +0 -402
- package/src/adapters/types.ts +0 -181
- package/src/core.ts +0 -51
- package/src/index.ts +0 -34
- package/src/instrumentation/__tests__/core.spec.ts +0 -248
- package/src/instrumentation/__tests__/hono.spec.ts +0 -348
- package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
- package/src/instrumentation/__tests__/otel.spec.ts +0 -291
- package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
- package/src/instrumentation/core.ts +0 -228
- package/src/instrumentation/hono.ts +0 -270
- package/src/instrumentation/http.ts +0 -306
- package/src/instrumentation/index.ts +0 -90
- package/src/instrumentation/middleware.ts +0 -297
- package/src/instrumentation/otel.ts +0 -271
- package/src/instrumentation/setup.ts +0 -246
- package/src/instrumentation/tracing.ts +0 -145
- package/src/logger/__tests__/console.spec.ts +0 -366
- package/src/logger/__tests__/pino.spec.ts +0 -217
- package/src/logger/console.ts +0 -230
- package/src/logger/pino.ts +0 -201
- package/src/metrics/MetricsAggregator.ts +0 -555
- package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
- package/src/metrics/index.ts +0 -2
- package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
- package/src/metrics/storage/MetricsStorage.ts +0 -74
- package/src/metrics/storage/index.ts +0 -3
- package/src/metrics/storage/types.ts +0 -243
- package/src/otlp/__tests__/receiver.spec.ts +0 -367
- package/src/otlp/__tests__/transformer.spec.ts +0 -455
- package/src/otlp/hono.ts +0 -120
- package/src/otlp/index.ts +0 -22
- package/src/otlp/receiver.ts +0 -162
- package/src/otlp/transformer.ts +0 -367
- package/src/otlp/types.ts +0 -364
- package/src/redact.ts +0 -116
- package/src/server/__tests__/hono.spec.ts +0 -814
- package/src/server/hono.ts +0 -442
- package/src/storage/__tests__/kysely.spec.ts +0 -715
- package/src/storage/__tests__/memory.spec.ts +0 -694
- package/src/storage/kysely.ts +0 -599
- package/src/storage/memory.ts +0 -204
- package/src/types.ts +0 -447
- package/src/ui-assets.ts +0 -40
- package/tsconfig.json +0 -9
- package/ui/CHANGELOG.md +0 -7
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -24
- package/ui/src/App.tsx +0 -595
- package/ui/src/api.ts +0 -88
- package/ui/src/components/ExceptionDetail.tsx +0 -100
- package/ui/src/components/FilterBar.tsx +0 -162
- package/ui/src/components/LogDetail.tsx +0 -91
- package/ui/src/components/RequestDetail.tsx +0 -143
- package/ui/src/main.tsx +0 -10
- package/ui/src/styles.css +0 -11
- package/ui/src/types.ts +0 -63
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/src/vite-plugin-gkm-config.ts +0 -54
- package/ui/tsconfig.json +0 -20
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -13
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import type { RequestEntry } from '../../types';
|
|
3
|
-
import { MetricsAggregator } from '../MetricsAggregator';
|
|
4
|
-
|
|
5
|
-
function createRequest(overrides: Partial<RequestEntry> = {}): RequestEntry {
|
|
6
|
-
return {
|
|
7
|
-
id: `req-${Math.random().toString(36).slice(2)}`,
|
|
8
|
-
method: 'GET',
|
|
9
|
-
path: '/api/test',
|
|
10
|
-
url: 'http://localhost/api/test',
|
|
11
|
-
headers: {},
|
|
12
|
-
query: {},
|
|
13
|
-
status: 200,
|
|
14
|
-
responseHeaders: {},
|
|
15
|
-
duration: 50,
|
|
16
|
-
timestamp: new Date(),
|
|
17
|
-
...overrides,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('MetricsAggregator', () => {
|
|
22
|
-
let aggregator: MetricsAggregator;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
aggregator = new MetricsAggregator();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('record', () => {
|
|
29
|
-
it('should record a request', () => {
|
|
30
|
-
const request = createRequest();
|
|
31
|
-
aggregator.record(request);
|
|
32
|
-
|
|
33
|
-
const metrics = aggregator.getMetrics();
|
|
34
|
-
expect(metrics.totalRequests).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should aggregate multiple requests', () => {
|
|
38
|
-
aggregator.record(createRequest({ duration: 100 }));
|
|
39
|
-
aggregator.record(createRequest({ duration: 200 }));
|
|
40
|
-
aggregator.record(createRequest({ duration: 300 }));
|
|
41
|
-
|
|
42
|
-
const metrics = aggregator.getMetrics();
|
|
43
|
-
expect(metrics.totalRequests).toBe(3);
|
|
44
|
-
expect(metrics.avgDuration).toBe(200);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should track error requests', () => {
|
|
48
|
-
aggregator.record(createRequest({ status: 200 }));
|
|
49
|
-
aggregator.record(createRequest({ status: 404 }));
|
|
50
|
-
aggregator.record(createRequest({ status: 500 }));
|
|
51
|
-
aggregator.record(createRequest({ status: 200 }));
|
|
52
|
-
|
|
53
|
-
const metrics = aggregator.getMetrics();
|
|
54
|
-
expect(metrics.totalRequests).toBe(4);
|
|
55
|
-
expect(metrics.errorRate).toBe(50); // 2 out of 4
|
|
56
|
-
expect(metrics.successRate).toBe(50);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('getMetrics', () => {
|
|
61
|
-
it('should return empty metrics when no requests', () => {
|
|
62
|
-
const metrics = aggregator.getMetrics();
|
|
63
|
-
|
|
64
|
-
expect(metrics.totalRequests).toBe(0);
|
|
65
|
-
expect(metrics.avgDuration).toBe(0);
|
|
66
|
-
expect(metrics.p50Duration).toBe(0);
|
|
67
|
-
expect(metrics.p95Duration).toBe(0);
|
|
68
|
-
expect(metrics.p99Duration).toBe(0);
|
|
69
|
-
expect(metrics.errorRate).toBe(0);
|
|
70
|
-
expect(metrics.successRate).toBe(100);
|
|
71
|
-
expect(metrics.requestsPerSecond).toBe(0);
|
|
72
|
-
expect(metrics.timeSeries).toEqual([]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should calculate percentiles correctly', () => {
|
|
76
|
-
// Add 100 requests with durations 1-100ms
|
|
77
|
-
for (let i = 1; i <= 100; i++) {
|
|
78
|
-
aggregator.record(createRequest({ duration: i }));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const metrics = aggregator.getMetrics();
|
|
82
|
-
|
|
83
|
-
// p50 should be around 50 (median of 1-100 is 50.5)
|
|
84
|
-
expect(metrics.p50Duration).toBeGreaterThanOrEqual(49);
|
|
85
|
-
expect(metrics.p50Duration).toBeLessThanOrEqual(52);
|
|
86
|
-
// p95 should be around 95
|
|
87
|
-
expect(metrics.p95Duration).toBeGreaterThanOrEqual(94);
|
|
88
|
-
expect(metrics.p95Duration).toBeLessThanOrEqual(96);
|
|
89
|
-
// p99 should be around 99
|
|
90
|
-
expect(metrics.p99Duration).toBeGreaterThanOrEqual(98);
|
|
91
|
-
expect(metrics.p99Duration).toBeLessThanOrEqual(100);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should calculate requests per second', () => {
|
|
95
|
-
// Record 10 requests
|
|
96
|
-
for (let i = 0; i < 10; i++) {
|
|
97
|
-
aggregator.record(createRequest());
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Use default range (last hour) - just verify it calculates something reasonable
|
|
101
|
-
const metrics = aggregator.getMetrics();
|
|
102
|
-
|
|
103
|
-
expect(metrics.totalRequests).toBe(10);
|
|
104
|
-
// With 10 requests in the default 1 hour range, rps should be small but positive
|
|
105
|
-
expect(metrics.requestsPerSecond).toBeGreaterThan(0);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should generate time series data', () => {
|
|
109
|
-
const now = new Date();
|
|
110
|
-
const bucketSize = 60000; // 1 minute
|
|
111
|
-
|
|
112
|
-
// Record requests in different buckets
|
|
113
|
-
aggregator.record(
|
|
114
|
-
createRequest({
|
|
115
|
-
timestamp: new Date(now.getTime() - 2 * bucketSize),
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
aggregator.record(
|
|
119
|
-
createRequest({
|
|
120
|
-
timestamp: new Date(now.getTime() - bucketSize),
|
|
121
|
-
}),
|
|
122
|
-
);
|
|
123
|
-
aggregator.record(createRequest({ timestamp: now }));
|
|
124
|
-
|
|
125
|
-
const metrics = aggregator.getMetrics({
|
|
126
|
-
range: {
|
|
127
|
-
start: new Date(now.getTime() - 3 * bucketSize),
|
|
128
|
-
end: now,
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(metrics.timeSeries.length).toBeGreaterThan(0);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('getEndpointMetrics', () => {
|
|
137
|
-
it('should track metrics per endpoint', () => {
|
|
138
|
-
aggregator.record(createRequest({ method: 'GET', path: '/users' }));
|
|
139
|
-
aggregator.record(createRequest({ method: 'GET', path: '/users' }));
|
|
140
|
-
aggregator.record(createRequest({ method: 'POST', path: '/users' }));
|
|
141
|
-
aggregator.record(createRequest({ method: 'GET', path: '/posts' }));
|
|
142
|
-
|
|
143
|
-
const endpoints = aggregator.getEndpointMetrics();
|
|
144
|
-
|
|
145
|
-
expect(endpoints.length).toBe(3);
|
|
146
|
-
|
|
147
|
-
const getUsersEndpoint = endpoints.find(
|
|
148
|
-
(e) => e.method === 'GET' && e.path === '/users',
|
|
149
|
-
);
|
|
150
|
-
expect(getUsersEndpoint?.count).toBe(2);
|
|
151
|
-
|
|
152
|
-
const postUsersEndpoint = endpoints.find(
|
|
153
|
-
(e) => e.method === 'POST' && e.path === '/users',
|
|
154
|
-
);
|
|
155
|
-
expect(postUsersEndpoint?.count).toBe(1);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should sort by request count descending', () => {
|
|
159
|
-
aggregator.record(createRequest({ path: '/a' }));
|
|
160
|
-
aggregator.record(createRequest({ path: '/b' }));
|
|
161
|
-
aggregator.record(createRequest({ path: '/b' }));
|
|
162
|
-
aggregator.record(createRequest({ path: '/c' }));
|
|
163
|
-
aggregator.record(createRequest({ path: '/c' }));
|
|
164
|
-
aggregator.record(createRequest({ path: '/c' }));
|
|
165
|
-
|
|
166
|
-
const endpoints = aggregator.getEndpointMetrics();
|
|
167
|
-
|
|
168
|
-
expect(endpoints[0].path).toBe('/c');
|
|
169
|
-
expect(endpoints[0].count).toBe(3);
|
|
170
|
-
expect(endpoints[1].path).toBe('/b');
|
|
171
|
-
expect(endpoints[1].count).toBe(2);
|
|
172
|
-
expect(endpoints[2].path).toBe('/a');
|
|
173
|
-
expect(endpoints[2].count).toBe(1);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should respect limit option', () => {
|
|
177
|
-
for (let i = 0; i < 10; i++) {
|
|
178
|
-
aggregator.record(createRequest({ path: `/path-${i}` }));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const endpoints = aggregator.getEndpointMetrics({ limit: 5 });
|
|
182
|
-
expect(endpoints.length).toBe(5);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('should calculate endpoint error rates', () => {
|
|
186
|
-
aggregator.record(createRequest({ path: '/error', status: 500 }));
|
|
187
|
-
aggregator.record(createRequest({ path: '/error', status: 500 }));
|
|
188
|
-
aggregator.record(createRequest({ path: '/error', status: 200 }));
|
|
189
|
-
aggregator.record(createRequest({ path: '/error', status: 200 }));
|
|
190
|
-
|
|
191
|
-
const endpoints = aggregator.getEndpointMetrics();
|
|
192
|
-
const errorEndpoint = endpoints.find((e) => e.path === '/error');
|
|
193
|
-
|
|
194
|
-
expect(errorEndpoint?.errorRate).toBe(50);
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
describe('getStatusDistribution', () => {
|
|
199
|
-
it('should count status codes by category', () => {
|
|
200
|
-
aggregator.record(createRequest({ status: 200 }));
|
|
201
|
-
aggregator.record(createRequest({ status: 201 }));
|
|
202
|
-
aggregator.record(createRequest({ status: 301 }));
|
|
203
|
-
aggregator.record(createRequest({ status: 400 }));
|
|
204
|
-
aggregator.record(createRequest({ status: 404 }));
|
|
205
|
-
aggregator.record(createRequest({ status: 500 }));
|
|
206
|
-
|
|
207
|
-
const dist = aggregator.getStatusDistribution();
|
|
208
|
-
|
|
209
|
-
expect(dist['2xx']).toBe(2);
|
|
210
|
-
expect(dist['3xx']).toBe(1);
|
|
211
|
-
expect(dist['4xx']).toBe(2);
|
|
212
|
-
expect(dist['5xx']).toBe(1);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should return zeros when no requests', () => {
|
|
216
|
-
const dist = aggregator.getStatusDistribution();
|
|
217
|
-
|
|
218
|
-
expect(dist['2xx']).toBe(0);
|
|
219
|
-
expect(dist['3xx']).toBe(0);
|
|
220
|
-
expect(dist['4xx']).toBe(0);
|
|
221
|
-
expect(dist['5xx']).toBe(0);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('reset', () => {
|
|
226
|
-
it('should clear all metrics', () => {
|
|
227
|
-
aggregator.record(createRequest());
|
|
228
|
-
aggregator.record(createRequest());
|
|
229
|
-
aggregator.record(createRequest());
|
|
230
|
-
|
|
231
|
-
expect(aggregator.getMetrics().totalRequests).toBe(3);
|
|
232
|
-
|
|
233
|
-
aggregator.reset();
|
|
234
|
-
|
|
235
|
-
expect(aggregator.getMetrics().totalRequests).toBe(0);
|
|
236
|
-
expect(aggregator.getEndpointMetrics().length).toBe(0);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
describe('bucket pruning', () => {
|
|
241
|
-
it('should prune old buckets when maxBuckets exceeded', () => {
|
|
242
|
-
const aggregator = new MetricsAggregator({
|
|
243
|
-
bucketSize: 1000, // 1 second buckets
|
|
244
|
-
maxBuckets: 5,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Add requests across 10 different seconds
|
|
248
|
-
const baseTime = Date.now();
|
|
249
|
-
for (let i = 0; i < 10; i++) {
|
|
250
|
-
aggregator.record(
|
|
251
|
-
createRequest({
|
|
252
|
-
timestamp: new Date(baseTime + i * 1000),
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Query should only return data from the last 5 buckets
|
|
258
|
-
const metrics = aggregator.getMetrics({
|
|
259
|
-
range: {
|
|
260
|
-
start: new Date(baseTime),
|
|
261
|
-
end: new Date(baseTime + 10000),
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Should have at most 5 buckets worth of data
|
|
266
|
-
expect(metrics.totalRequests).toBeLessThanOrEqual(5);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
describe('custom bucket size', () => {
|
|
271
|
-
it('should use custom bucket size', () => {
|
|
272
|
-
const aggregator = new MetricsAggregator({
|
|
273
|
-
bucketSize: 5000, // 5 second buckets
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Use a fixed timestamp to ensure all requests fall in the same bucket
|
|
277
|
-
const baseTime = Math.floor(Date.now() / 5000) * 5000; // Align to bucket boundary
|
|
278
|
-
const timestamp = new Date(baseTime);
|
|
279
|
-
|
|
280
|
-
// Add 3 requests within the same 5-second bucket
|
|
281
|
-
aggregator.record(createRequest({ timestamp }));
|
|
282
|
-
aggregator.record(createRequest({ timestamp }));
|
|
283
|
-
aggregator.record(createRequest({ timestamp }));
|
|
284
|
-
|
|
285
|
-
const metrics = aggregator.getMetrics({
|
|
286
|
-
range: {
|
|
287
|
-
start: new Date(baseTime - 1000),
|
|
288
|
-
end: new Date(baseTime + 6000),
|
|
289
|
-
},
|
|
290
|
-
bucketSize: 5000,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// Should have at least 1 time series point with 3 requests total
|
|
294
|
-
expect(metrics.totalRequests).toBe(3);
|
|
295
|
-
expect(metrics.timeSeries.length).toBeGreaterThanOrEqual(1);
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
});
|
package/src/metrics/index.ts
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
import type { MetricsStorage } from './MetricsStorage';
|
|
2
|
-
import {
|
|
3
|
-
type AggregationInterval,
|
|
4
|
-
DEFAULT_METRICS_CONFIG,
|
|
5
|
-
type MetricBucket,
|
|
6
|
-
type MetricLabels,
|
|
7
|
-
type MetricPoint,
|
|
8
|
-
type MetricQuery,
|
|
9
|
-
type MetricResult,
|
|
10
|
-
type MetricsStorageConfig,
|
|
11
|
-
type PartitionInfo,
|
|
12
|
-
type RollupOptions,
|
|
13
|
-
} from './types';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* In-memory metrics storage for development and testing.
|
|
17
|
-
*
|
|
18
|
-
* Features:
|
|
19
|
-
* - Buffered writes with configurable flush interval
|
|
20
|
-
* - Time-bucketed aggregation
|
|
21
|
-
* - Basic percentile calculation (reservoir sampling)
|
|
22
|
-
* - Automatic pruning based on retention
|
|
23
|
-
*
|
|
24
|
-
* Not suitable for production - use a proper time-series database.
|
|
25
|
-
*/
|
|
26
|
-
export class InMemoryMetricsStorage implements MetricsStorage {
|
|
27
|
-
private config: MetricsStorageConfig;
|
|
28
|
-
private buffer: MetricPoint[] = [];
|
|
29
|
-
private buckets: Map<string, MetricBucket> = new Map();
|
|
30
|
-
private samples: Map<string, number[]> = new Map(); // For percentile calculations
|
|
31
|
-
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
32
|
-
|
|
33
|
-
constructor(config: Partial<MetricsStorageConfig> = {}) {
|
|
34
|
-
this.config = { ...DEFAULT_METRICS_CONFIG, ...config };
|
|
35
|
-
|
|
36
|
-
if (this.config.flushInterval > 0) {
|
|
37
|
-
this.flushTimer = setInterval(() => {
|
|
38
|
-
this.flush().catch(console.error);
|
|
39
|
-
}, this.config.flushInterval);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async write(points: MetricPoint[]): Promise<void> {
|
|
44
|
-
this.buffer.push(...points);
|
|
45
|
-
|
|
46
|
-
if (this.buffer.length >= this.config.maxBatchSize) {
|
|
47
|
-
await this.flush();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async flush(): Promise<void> {
|
|
52
|
-
if (this.buffer.length === 0) return;
|
|
53
|
-
|
|
54
|
-
const points = this.buffer.splice(0);
|
|
55
|
-
|
|
56
|
-
for (const point of points) {
|
|
57
|
-
this.aggregatePoint(point);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Prune if auto-rollup is enabled
|
|
61
|
-
if (this.config.autoRollup) {
|
|
62
|
-
await this.prune();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private aggregatePoint(point: MetricPoint): void {
|
|
67
|
-
const bucketTime = this.getBucketTime(point.timestamp, '1m');
|
|
68
|
-
const key = this.getBucketKey(point, bucketTime);
|
|
69
|
-
|
|
70
|
-
const existing = this.buckets.get(key);
|
|
71
|
-
|
|
72
|
-
if (existing) {
|
|
73
|
-
existing.count += 1;
|
|
74
|
-
existing.sum += point.value;
|
|
75
|
-
existing.min = Math.min(existing.min, point.value);
|
|
76
|
-
existing.max = Math.max(existing.max, point.value);
|
|
77
|
-
} else {
|
|
78
|
-
this.buckets.set(key, {
|
|
79
|
-
bucket: bucketTime,
|
|
80
|
-
projectId: point.projectId,
|
|
81
|
-
service: point.service,
|
|
82
|
-
environment: point.environment,
|
|
83
|
-
name: point.name,
|
|
84
|
-
labelsHash: this.hashLabels(point.labels),
|
|
85
|
-
labels: { ...point.labels },
|
|
86
|
-
count: 1,
|
|
87
|
-
sum: point.value,
|
|
88
|
-
min: point.value,
|
|
89
|
-
max: point.value,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Store samples for percentile calculations (reservoir sampling)
|
|
94
|
-
const sampleKey = `${point.projectId}:${point.name}:${bucketTime.getTime()}`;
|
|
95
|
-
const samples = this.samples.get(sampleKey) ?? [];
|
|
96
|
-
if (samples.length < 1000) {
|
|
97
|
-
samples.push(point.value);
|
|
98
|
-
} else {
|
|
99
|
-
// Reservoir sampling
|
|
100
|
-
const idx = Math.floor(Math.random() * (samples.length + 1));
|
|
101
|
-
if (idx < samples.length) {
|
|
102
|
-
samples[idx] = point.value;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
this.samples.set(sampleKey, samples);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async query(query: MetricQuery): Promise<MetricResult[]> {
|
|
109
|
-
await this.flush();
|
|
110
|
-
|
|
111
|
-
const results = new Map<string, MetricResult>();
|
|
112
|
-
const startTime = query.range.start.getTime();
|
|
113
|
-
const endTime = query.range.end.getTime();
|
|
114
|
-
const interval = query.interval ?? '1m';
|
|
115
|
-
const intervalMs = this.getIntervalMs(interval);
|
|
116
|
-
|
|
117
|
-
for (const [, bucket] of this.buckets) {
|
|
118
|
-
if (bucket.projectId !== query.projectId) continue;
|
|
119
|
-
if (query.service && bucket.service !== query.service) continue;
|
|
120
|
-
if (query.environment && bucket.environment !== query.environment)
|
|
121
|
-
continue;
|
|
122
|
-
if (query.name && !this.matchesPattern(bucket.name, query.name)) continue;
|
|
123
|
-
if (query.labels && !this.matchesLabels(bucket.labels, query.labels))
|
|
124
|
-
continue;
|
|
125
|
-
|
|
126
|
-
const bucketTime = bucket.bucket.getTime();
|
|
127
|
-
if (bucketTime < startTime || bucketTime > endTime) continue;
|
|
128
|
-
|
|
129
|
-
// Group by metric name + labels
|
|
130
|
-
const groupKey = query.groupBy
|
|
131
|
-
? this.getGroupKey(bucket, query.groupBy)
|
|
132
|
-
: bucket.name;
|
|
133
|
-
|
|
134
|
-
let result = results.get(groupKey);
|
|
135
|
-
if (!result) {
|
|
136
|
-
result = {
|
|
137
|
-
name: bucket.name,
|
|
138
|
-
labels: query.groupBy
|
|
139
|
-
? this.filterLabels(bucket.labels, query.groupBy)
|
|
140
|
-
: bucket.labels,
|
|
141
|
-
timeSeries: [],
|
|
142
|
-
stats: {
|
|
143
|
-
count: 0,
|
|
144
|
-
sum: 0,
|
|
145
|
-
avg: 0,
|
|
146
|
-
min: Infinity,
|
|
147
|
-
max: -Infinity,
|
|
148
|
-
p50: 0,
|
|
149
|
-
p95: 0,
|
|
150
|
-
p99: 0,
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
results.set(groupKey, result);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Aggregate into time series
|
|
157
|
-
const tsIndex = Math.floor((bucketTime - startTime) / intervalMs);
|
|
158
|
-
while (result.timeSeries.length <= tsIndex) {
|
|
159
|
-
result.timeSeries.push({
|
|
160
|
-
timestamp: new Date(
|
|
161
|
-
startTime + result.timeSeries.length * intervalMs,
|
|
162
|
-
),
|
|
163
|
-
count: 0,
|
|
164
|
-
sum: 0,
|
|
165
|
-
avg: 0,
|
|
166
|
-
min: Infinity,
|
|
167
|
-
max: -Infinity,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const tsPoint = result.timeSeries[tsIndex];
|
|
172
|
-
if (tsPoint) {
|
|
173
|
-
tsPoint.count += bucket.count;
|
|
174
|
-
tsPoint.sum += bucket.sum;
|
|
175
|
-
tsPoint.min = Math.min(tsPoint.min, bucket.min);
|
|
176
|
-
tsPoint.max = Math.max(tsPoint.max, bucket.max);
|
|
177
|
-
tsPoint.avg = tsPoint.sum / tsPoint.count;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Update overall stats
|
|
181
|
-
result.stats.count += bucket.count;
|
|
182
|
-
result.stats.sum += bucket.sum;
|
|
183
|
-
result.stats.min = Math.min(result.stats.min, bucket.min);
|
|
184
|
-
result.stats.max = Math.max(result.stats.max, bucket.max);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Calculate percentiles and finalize
|
|
188
|
-
for (const result of results.values()) {
|
|
189
|
-
if (result.stats.count > 0) {
|
|
190
|
-
result.stats.avg = result.stats.sum / result.stats.count;
|
|
191
|
-
|
|
192
|
-
// Gather all samples for this result
|
|
193
|
-
const allSamples: number[] = [];
|
|
194
|
-
for (const [key, samples] of this.samples) {
|
|
195
|
-
if (key.startsWith(`${query.projectId}:${result.name}:`)) {
|
|
196
|
-
allSamples.push(...samples);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (allSamples.length > 0) {
|
|
201
|
-
allSamples.sort((a, b) => a - b);
|
|
202
|
-
result.stats.p50 = this.percentile(allSamples, 50);
|
|
203
|
-
result.stats.p95 = this.percentile(allSamples, 95);
|
|
204
|
-
result.stats.p99 = this.percentile(allSamples, 99);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Fix infinity values
|
|
209
|
-
if (result.stats.min === Infinity) result.stats.min = 0;
|
|
210
|
-
if (result.stats.max === -Infinity) result.stats.max = 0;
|
|
211
|
-
|
|
212
|
-
for (const point of result.timeSeries) {
|
|
213
|
-
if (point.min === Infinity) point.min = 0;
|
|
214
|
-
if (point.max === -Infinity) point.max = 0;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const resultArray = Array.from(results.values());
|
|
219
|
-
|
|
220
|
-
if (query.limit) {
|
|
221
|
-
return resultArray.slice(0, query.limit);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return resultArray;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async getBucket(
|
|
228
|
-
projectId: string,
|
|
229
|
-
name: string,
|
|
230
|
-
bucket: Date,
|
|
231
|
-
): Promise<MetricBucket | null> {
|
|
232
|
-
await this.flush();
|
|
233
|
-
|
|
234
|
-
for (const [, b] of this.buckets) {
|
|
235
|
-
if (
|
|
236
|
-
b.projectId === projectId &&
|
|
237
|
-
b.name === name &&
|
|
238
|
-
b.bucket.getTime() === bucket.getTime()
|
|
239
|
-
) {
|
|
240
|
-
return b;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async rollup(_options: RollupOptions): Promise<number> {
|
|
248
|
-
// In-memory storage doesn't need rollup - data is already aggregated
|
|
249
|
-
return 0;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async prune(): Promise<number> {
|
|
253
|
-
const now = Date.now();
|
|
254
|
-
let deleted = 0;
|
|
255
|
-
|
|
256
|
-
// Use the shortest retention from tiers
|
|
257
|
-
const retentionMs = Math.min(
|
|
258
|
-
...this.config.tiers
|
|
259
|
-
.filter((t) => t.retention > 0)
|
|
260
|
-
.map((t) => t.retention),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
if (retentionMs === Infinity) return 0;
|
|
264
|
-
|
|
265
|
-
const cutoff = now - retentionMs;
|
|
266
|
-
|
|
267
|
-
for (const [key, bucket] of this.buckets) {
|
|
268
|
-
if (bucket.bucket.getTime() < cutoff) {
|
|
269
|
-
this.buckets.delete(key);
|
|
270
|
-
deleted++;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Also prune samples
|
|
275
|
-
for (const [key] of this.samples) {
|
|
276
|
-
const timestamp = parseInt(key.split(':').pop() ?? '0', 10);
|
|
277
|
-
if (timestamp < cutoff) {
|
|
278
|
-
this.samples.delete(key);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return deleted;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async listPartitions(_tier?: string): Promise<PartitionInfo[]> {
|
|
286
|
-
// In-memory storage doesn't use partitions
|
|
287
|
-
return [];
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
getConfig(): MetricsStorageConfig {
|
|
291
|
-
return this.config;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async close(): Promise<void> {
|
|
295
|
-
if (this.flushTimer) {
|
|
296
|
-
clearInterval(this.flushTimer);
|
|
297
|
-
this.flushTimer = null;
|
|
298
|
-
}
|
|
299
|
-
await this.flush();
|
|
300
|
-
this.buckets.clear();
|
|
301
|
-
this.samples.clear();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Helper methods
|
|
305
|
-
|
|
306
|
-
private getBucketTime(timestamp: Date, interval: AggregationInterval): Date {
|
|
307
|
-
const ms = this.getIntervalMs(interval);
|
|
308
|
-
return new Date(Math.floor(timestamp.getTime() / ms) * ms);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private getBucketKey(point: MetricPoint, bucketTime: Date): string {
|
|
312
|
-
return [
|
|
313
|
-
point.projectId,
|
|
314
|
-
point.service,
|
|
315
|
-
point.environment,
|
|
316
|
-
point.name,
|
|
317
|
-
this.hashLabels(point.labels),
|
|
318
|
-
bucketTime.getTime(),
|
|
319
|
-
].join(':');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private hashLabels(labels: MetricLabels): string {
|
|
323
|
-
const sorted = Object.keys(labels)
|
|
324
|
-
.sort()
|
|
325
|
-
.map((k) => `${k}=${labels[k]}`)
|
|
326
|
-
.join(',');
|
|
327
|
-
return sorted || '_empty_';
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
private getIntervalMs(interval: AggregationInterval): number {
|
|
331
|
-
const map: Record<AggregationInterval, number> = {
|
|
332
|
-
'1m': 60_000,
|
|
333
|
-
'5m': 300_000,
|
|
334
|
-
'15m': 900_000,
|
|
335
|
-
'1h': 3_600_000,
|
|
336
|
-
'6h': 21_600_000,
|
|
337
|
-
'1d': 86_400_000,
|
|
338
|
-
};
|
|
339
|
-
return map[interval];
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private matchesPattern(name: string, pattern: string): boolean {
|
|
343
|
-
if (!pattern.includes('*')) return name === pattern;
|
|
344
|
-
const regex = new RegExp(
|
|
345
|
-
`^${pattern.replace(/\./g, '\\.').replace(/\*/g, '.*')}$`,
|
|
346
|
-
);
|
|
347
|
-
return regex.test(name);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
private matchesLabels(labels: MetricLabels, filter: MetricLabels): boolean {
|
|
351
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
352
|
-
if (labels[key] !== value) return false;
|
|
353
|
-
}
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private getGroupKey(bucket: MetricBucket, groupBy: string[]): string {
|
|
358
|
-
const parts = [bucket.name];
|
|
359
|
-
for (const key of groupBy) {
|
|
360
|
-
parts.push(`${key}=${bucket.labels[key] ?? ''}`);
|
|
361
|
-
}
|
|
362
|
-
return parts.join(':');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private filterLabels(labels: MetricLabels, keys: string[]): MetricLabels {
|
|
366
|
-
const result: MetricLabels = {};
|
|
367
|
-
for (const key of keys) {
|
|
368
|
-
if (labels[key] !== undefined) {
|
|
369
|
-
result[key] = labels[key];
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return result;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
private percentile(sorted: number[], p: number): number {
|
|
376
|
-
if (sorted.length === 0) return 0;
|
|
377
|
-
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
378
|
-
return sorted[Math.max(0, index)] ?? 0;
|
|
379
|
-
}
|
|
380
|
-
}
|