@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.
Files changed (167) hide show
  1. package/README.md +14 -4
  2. package/dist/{hono-Bz9PQQzj.mjs → hono-Bw-ey6CT.mjs} +2 -2
  3. package/dist/{hono-Bz9PQQzj.mjs.map → hono-Bw-ey6CT.mjs.map} +1 -1
  4. package/dist/{hono-Z_JIICmt.cjs → hono-CPHX7FAm.cjs} +2 -2
  5. package/dist/{hono-Z_JIICmt.cjs.map → hono-CPHX7FAm.cjs.map} +1 -1
  6. package/dist/{http-Crsriv32.mjs → http-BIvr0YdI.mjs} +1 -1
  7. package/dist/{http-Crsriv32.mjs.map → http-BIvr0YdI.mjs.map} +1 -1
  8. package/dist/{http-JycFRfSe.cjs → http-BsqmhurZ.cjs} +1 -1
  9. package/dist/{http-JycFRfSe.cjs.map → http-BsqmhurZ.cjs.map} +1 -1
  10. package/dist/{http-D5kn3_Mt.d.mts → http-DULM1u2L.d.cts} +3 -3
  11. package/dist/{http-D5kn3_Mt.d.mts.map → http-DULM1u2L.d.cts.map} +1 -1
  12. package/dist/{http-Dvca6fC3.d.cts → http-GB-Nlvwc.d.mts} +3 -3
  13. package/dist/{http-Dvca6fC3.d.cts.map → http-GB-Nlvwc.d.mts.map} +1 -1
  14. package/dist/instrumentation/hono.cjs +2 -2
  15. package/dist/instrumentation/hono.mjs +2 -2
  16. package/dist/instrumentation/http.cjs +1 -1
  17. package/dist/instrumentation/http.d.cts +1 -1
  18. package/dist/instrumentation/http.d.mts +1 -1
  19. package/dist/instrumentation/http.mjs +1 -1
  20. package/dist/instrumentation/index.cjs +5 -5
  21. package/dist/instrumentation/index.d.cts +3 -3
  22. package/dist/instrumentation/index.d.mts +3 -3
  23. package/dist/instrumentation/index.mjs +5 -5
  24. package/dist/instrumentation/middleware.cjs +2 -2
  25. package/dist/instrumentation/middleware.d.cts +1 -1
  26. package/dist/instrumentation/middleware.d.mts +1 -1
  27. package/dist/instrumentation/middleware.mjs +2 -2
  28. package/dist/instrumentation/otel.cjs +2 -2
  29. package/dist/instrumentation/otel.d.cts +1 -1
  30. package/dist/instrumentation/otel.d.mts +1 -1
  31. package/dist/instrumentation/otel.mjs +2 -2
  32. package/dist/instrumentation/setup.cjs +1 -1
  33. package/dist/instrumentation/setup.mjs +1 -1
  34. package/dist/logger/console.d.cts.map +1 -1
  35. package/dist/{middleware-D3v5SnSs.cjs → middleware-Bcq6XXGG.cjs} +3 -3
  36. package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
  37. package/dist/{middleware-Dy1ctr2N.mjs → middleware-D1XEHvyy.mjs} +3 -3
  38. package/dist/middleware-D1XEHvyy.mjs.map +1 -0
  39. package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
  40. package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
  41. package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
  42. package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
  43. package/dist/{otel-DPKNyJ0t.mjs → otel-B9bNoeZ2.mjs} +4 -4
  44. package/dist/otel-B9bNoeZ2.mjs.map +1 -0
  45. package/dist/{otel-1bJPb2Z7.d.mts → otel-Bk89b1lX.d.cts} +3 -3
  46. package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
  47. package/dist/{otel-DuiBeBPU.cjs → otel-BzjOtVxZ.cjs} +4 -4
  48. package/dist/otel-BzjOtVxZ.cjs.map +1 -0
  49. package/dist/{otel-SBVoVEHr.d.cts → otel-lPMsM26e.d.mts} +3 -3
  50. package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
  51. package/dist/otlp/hono.cjs +3 -3
  52. package/dist/otlp/hono.mjs +3 -3
  53. package/dist/otlp/index.cjs +3 -3
  54. package/dist/otlp/index.mjs +3 -3
  55. package/dist/otlp/receiver.cjs +3 -3
  56. package/dist/otlp/receiver.mjs +3 -3
  57. package/dist/otlp/transformer.cjs +2 -2
  58. package/dist/otlp/transformer.mjs +2 -2
  59. package/dist/otlp/types.cjs +1 -1
  60. package/dist/otlp/types.mjs +1 -1
  61. package/dist/{receiver-q4f7eirs.mjs → receiver-D46dfkYI.mjs} +2 -2
  62. package/dist/{receiver-q4f7eirs.mjs.map → receiver-D46dfkYI.mjs.map} +1 -1
  63. package/dist/{receiver-BW_4go_X.cjs → receiver-slCoAFat.cjs} +2 -2
  64. package/dist/{receiver-BW_4go_X.cjs.map → receiver-slCoAFat.cjs.map} +1 -1
  65. package/dist/{setup-BhCp2LUp.mjs → setup-DC9JMEYZ.mjs} +1 -1
  66. package/dist/{setup-BhCp2LUp.mjs.map → setup-DC9JMEYZ.mjs.map} +1 -1
  67. package/dist/{setup-_uT4L_x5.cjs → setup-DufxQpi9.cjs} +1 -1
  68. package/dist/{setup-_uT4L_x5.cjs.map → setup-DufxQpi9.cjs.map} +1 -1
  69. package/dist/storage/kysely.cjs +48 -38
  70. package/dist/storage/kysely.cjs.map +1 -1
  71. package/dist/storage/kysely.d.cts +29 -9
  72. package/dist/storage/kysely.d.cts.map +1 -1
  73. package/dist/storage/kysely.d.mts +29 -9
  74. package/dist/storage/kysely.d.mts.map +1 -1
  75. package/dist/storage/kysely.mjs +48 -38
  76. package/dist/storage/kysely.mjs.map +1 -1
  77. package/dist/tracing-BX8uY2nR.d.cts.map +1 -1
  78. package/dist/tracing-PnECEbEu.d.mts.map +1 -1
  79. package/dist/{transformer-CFrJQkUg.mjs → transformer-CFQ4WFk6.mjs} +2 -2
  80. package/dist/{transformer-CFrJQkUg.mjs.map → transformer-CFQ4WFk6.mjs.map} +1 -1
  81. package/dist/{transformer-DvDYyFRQ.cjs → transformer-Csl6fbgv.cjs} +2 -2
  82. package/dist/{transformer-DvDYyFRQ.cjs.map → transformer-Csl6fbgv.cjs.map} +1 -1
  83. package/dist/types-B1EIa_cQ.d.mts.map +1 -1
  84. package/dist/{types-BBeTDpHr.cjs → types-BMiHgsi4.cjs} +1 -1
  85. package/dist/{types-BBeTDpHr.cjs.map → types-BMiHgsi4.cjs.map} +1 -1
  86. package/dist/{types-CXYLhG_G.mjs → types-Cj59nqy7.mjs} +1 -1
  87. package/dist/{types-CXYLhG_G.mjs.map → types-Cj59nqy7.mjs.map} +1 -1
  88. package/dist/types-l7Rlb5Mg.d.cts.map +1 -1
  89. package/package.json +6 -3
  90. package/CHANGELOG.md +0 -51
  91. package/dist/middleware-D3v5SnSs.cjs.map +0 -1
  92. package/dist/middleware-Dy1ctr2N.mjs.map +0 -1
  93. package/dist/otel-DPKNyJ0t.mjs.map +0 -1
  94. package/dist/otel-DuiBeBPU.cjs.map +0 -1
  95. package/src/Telescope.ts +0 -561
  96. package/src/__tests__/Telescope.spec.ts +0 -438
  97. package/src/__tests__/pino-transport.spec.ts +0 -115
  98. package/src/__tests__/redact.spec.ts +0 -336
  99. package/src/adapters/__tests__/lambda.spec.ts +0 -318
  100. package/src/adapters/__tests__/middy.spec.ts +0 -741
  101. package/src/adapters/index.ts +0 -22
  102. package/src/adapters/lambda.ts +0 -432
  103. package/src/adapters/middy.ts +0 -402
  104. package/src/adapters/types.ts +0 -181
  105. package/src/core.ts +0 -51
  106. package/src/index.ts +0 -34
  107. package/src/instrumentation/__tests__/core.spec.ts +0 -248
  108. package/src/instrumentation/__tests__/hono.spec.ts +0 -348
  109. package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
  110. package/src/instrumentation/__tests__/otel.spec.ts +0 -291
  111. package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
  112. package/src/instrumentation/core.ts +0 -228
  113. package/src/instrumentation/hono.ts +0 -270
  114. package/src/instrumentation/http.ts +0 -306
  115. package/src/instrumentation/index.ts +0 -90
  116. package/src/instrumentation/middleware.ts +0 -297
  117. package/src/instrumentation/otel.ts +0 -271
  118. package/src/instrumentation/setup.ts +0 -246
  119. package/src/instrumentation/tracing.ts +0 -145
  120. package/src/logger/__tests__/console.spec.ts +0 -366
  121. package/src/logger/__tests__/pino.spec.ts +0 -217
  122. package/src/logger/console.ts +0 -230
  123. package/src/logger/pino.ts +0 -201
  124. package/src/metrics/MetricsAggregator.ts +0 -555
  125. package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
  126. package/src/metrics/index.ts +0 -2
  127. package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
  128. package/src/metrics/storage/MetricsStorage.ts +0 -74
  129. package/src/metrics/storage/index.ts +0 -3
  130. package/src/metrics/storage/types.ts +0 -243
  131. package/src/otlp/__tests__/receiver.spec.ts +0 -367
  132. package/src/otlp/__tests__/transformer.spec.ts +0 -455
  133. package/src/otlp/hono.ts +0 -120
  134. package/src/otlp/index.ts +0 -22
  135. package/src/otlp/receiver.ts +0 -162
  136. package/src/otlp/transformer.ts +0 -367
  137. package/src/otlp/types.ts +0 -364
  138. package/src/redact.ts +0 -116
  139. package/src/server/__tests__/hono.spec.ts +0 -814
  140. package/src/server/hono.ts +0 -442
  141. package/src/storage/__tests__/kysely.spec.ts +0 -715
  142. package/src/storage/__tests__/memory.spec.ts +0 -694
  143. package/src/storage/kysely.ts +0 -599
  144. package/src/storage/memory.ts +0 -204
  145. package/src/types.ts +0 -447
  146. package/src/ui-assets.ts +0 -40
  147. package/tsconfig.json +0 -9
  148. package/ui/CHANGELOG.md +0 -7
  149. package/ui/index.html +0 -12
  150. package/ui/node_modules/.bin/tsc +0 -21
  151. package/ui/node_modules/.bin/tsserver +0 -21
  152. package/ui/node_modules/.bin/vite +0 -21
  153. package/ui/package.json +0 -24
  154. package/ui/src/App.tsx +0 -595
  155. package/ui/src/api.ts +0 -88
  156. package/ui/src/components/ExceptionDetail.tsx +0 -100
  157. package/ui/src/components/FilterBar.tsx +0 -162
  158. package/ui/src/components/LogDetail.tsx +0 -91
  159. package/ui/src/components/RequestDetail.tsx +0 -143
  160. package/ui/src/main.tsx +0 -10
  161. package/ui/src/styles.css +0 -11
  162. package/ui/src/types.ts +0 -63
  163. package/ui/src/vite-env.d.ts +0 -1
  164. package/ui/src/vite-plugin-gkm-config.ts +0 -54
  165. package/ui/tsconfig.json +0 -20
  166. package/ui/tsconfig.tsbuildinfo +0 -1
  167. 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
- });
@@ -1,2 +0,0 @@
1
- export type { MetricsAggregatorOptions } from './MetricsAggregator';
2
- export { MetricsAggregator } from './MetricsAggregator';
@@ -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
- }