@geekmidas/telescope 9.0.2 → 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 (128) hide show
  1. package/README.md +14 -4
  2. package/dist/{http-D5kn3_Mt.d.mts → http-DULM1u2L.d.cts} +3 -3
  3. package/dist/{http-Dvca6fC3.d.cts.map → http-DULM1u2L.d.cts.map} +1 -1
  4. package/dist/{http-Dvca6fC3.d.cts → http-GB-Nlvwc.d.mts} +3 -3
  5. package/dist/{http-D5kn3_Mt.d.mts.map → http-GB-Nlvwc.d.mts.map} +1 -1
  6. package/dist/instrumentation/http.d.cts +1 -1
  7. package/dist/instrumentation/http.d.mts +1 -1
  8. package/dist/instrumentation/index.cjs +2 -2
  9. package/dist/instrumentation/index.d.cts +3 -3
  10. package/dist/instrumentation/index.d.mts +3 -3
  11. package/dist/instrumentation/index.mjs +2 -2
  12. package/dist/instrumentation/middleware.cjs +1 -1
  13. package/dist/instrumentation/middleware.d.cts +1 -1
  14. package/dist/instrumentation/middleware.d.mts +1 -1
  15. package/dist/instrumentation/middleware.mjs +1 -1
  16. package/dist/instrumentation/otel.cjs +1 -1
  17. package/dist/instrumentation/otel.d.cts +1 -1
  18. package/dist/instrumentation/otel.d.mts +1 -1
  19. package/dist/instrumentation/otel.mjs +1 -1
  20. package/dist/logger/console.d.cts.map +1 -1
  21. package/dist/logger/console.d.mts.map +1 -1
  22. package/dist/logger/pino.d.cts +2 -2
  23. package/dist/{middleware-DqPQpcup.cjs → middleware-Bcq6XXGG.cjs} +2 -2
  24. package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
  25. package/dist/{middleware-4-GrQlOT.mjs → middleware-D1XEHvyy.mjs} +2 -2
  26. package/dist/middleware-D1XEHvyy.mjs.map +1 -0
  27. package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
  28. package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
  29. package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
  30. package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
  31. package/dist/{otel-40udNP5g.mjs → otel-B9bNoeZ2.mjs} +3 -3
  32. package/dist/otel-B9bNoeZ2.mjs.map +1 -0
  33. package/dist/{otel-1bJPb2Z7.d.mts → otel-Bk89b1lX.d.cts} +3 -3
  34. package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
  35. package/dist/{otel-COrpz_GZ.cjs → otel-BzjOtVxZ.cjs} +3 -3
  36. package/dist/otel-BzjOtVxZ.cjs.map +1 -0
  37. package/dist/{otel-SBVoVEHr.d.cts → otel-lPMsM26e.d.mts} +3 -3
  38. package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
  39. package/dist/storage/kysely.cjs +48 -38
  40. package/dist/storage/kysely.cjs.map +1 -1
  41. package/dist/storage/kysely.d.cts +29 -9
  42. package/dist/storage/kysely.d.cts.map +1 -1
  43. package/dist/storage/kysely.d.mts +29 -9
  44. package/dist/storage/kysely.d.mts.map +1 -1
  45. package/dist/storage/kysely.mjs +48 -38
  46. package/dist/storage/kysely.mjs.map +1 -1
  47. package/dist/tracing-BX8uY2nR.d.cts.map +1 -1
  48. package/dist/types-B1EIa_cQ.d.mts.map +1 -1
  49. package/dist/types-l7Rlb5Mg.d.cts.map +1 -1
  50. package/package.json +6 -3
  51. package/CHANGELOG.md +0 -72
  52. package/dist/middleware-4-GrQlOT.mjs.map +0 -1
  53. package/dist/middleware-DqPQpcup.cjs.map +0 -1
  54. package/dist/otel-40udNP5g.mjs.map +0 -1
  55. package/dist/otel-COrpz_GZ.cjs.map +0 -1
  56. package/src/Telescope.ts +0 -561
  57. package/src/__tests__/Telescope.spec.ts +0 -438
  58. package/src/__tests__/pino-transport.spec.ts +0 -115
  59. package/src/__tests__/redact.spec.ts +0 -336
  60. package/src/adapters/__tests__/lambda.spec.ts +0 -318
  61. package/src/adapters/__tests__/middy.spec.ts +0 -741
  62. package/src/adapters/index.ts +0 -22
  63. package/src/adapters/lambda.ts +0 -432
  64. package/src/adapters/middy.ts +0 -402
  65. package/src/adapters/types.ts +0 -181
  66. package/src/core.ts +0 -51
  67. package/src/index.ts +0 -34
  68. package/src/instrumentation/__tests__/core.spec.ts +0 -248
  69. package/src/instrumentation/__tests__/hono.spec.ts +0 -348
  70. package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
  71. package/src/instrumentation/__tests__/otel.spec.ts +0 -291
  72. package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
  73. package/src/instrumentation/core.ts +0 -228
  74. package/src/instrumentation/hono.ts +0 -270
  75. package/src/instrumentation/http.ts +0 -306
  76. package/src/instrumentation/index.ts +0 -90
  77. package/src/instrumentation/middleware.ts +0 -297
  78. package/src/instrumentation/otel.ts +0 -271
  79. package/src/instrumentation/setup.ts +0 -246
  80. package/src/instrumentation/tracing.ts +0 -145
  81. package/src/logger/__tests__/console.spec.ts +0 -366
  82. package/src/logger/__tests__/pino.spec.ts +0 -217
  83. package/src/logger/console.ts +0 -230
  84. package/src/logger/pino.ts +0 -201
  85. package/src/metrics/MetricsAggregator.ts +0 -555
  86. package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
  87. package/src/metrics/index.ts +0 -2
  88. package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
  89. package/src/metrics/storage/MetricsStorage.ts +0 -74
  90. package/src/metrics/storage/index.ts +0 -3
  91. package/src/metrics/storage/types.ts +0 -243
  92. package/src/otlp/__tests__/receiver.spec.ts +0 -367
  93. package/src/otlp/__tests__/transformer.spec.ts +0 -455
  94. package/src/otlp/hono.ts +0 -120
  95. package/src/otlp/index.ts +0 -22
  96. package/src/otlp/receiver.ts +0 -162
  97. package/src/otlp/transformer.ts +0 -367
  98. package/src/otlp/types.ts +0 -364
  99. package/src/redact.ts +0 -116
  100. package/src/server/__tests__/hono.spec.ts +0 -814
  101. package/src/server/hono.ts +0 -442
  102. package/src/storage/__tests__/kysely.spec.ts +0 -715
  103. package/src/storage/__tests__/memory.spec.ts +0 -694
  104. package/src/storage/kysely.ts +0 -599
  105. package/src/storage/memory.ts +0 -204
  106. package/src/types.ts +0 -447
  107. package/src/ui-assets.ts +0 -40
  108. package/tsconfig.json +0 -9
  109. package/ui/CHANGELOG.md +0 -7
  110. package/ui/index.html +0 -12
  111. package/ui/node_modules/.bin/tsc +0 -21
  112. package/ui/node_modules/.bin/tsserver +0 -21
  113. package/ui/node_modules/.bin/vite +0 -21
  114. package/ui/package.json +0 -24
  115. package/ui/src/App.tsx +0 -595
  116. package/ui/src/api.ts +0 -88
  117. package/ui/src/components/ExceptionDetail.tsx +0 -100
  118. package/ui/src/components/FilterBar.tsx +0 -162
  119. package/ui/src/components/LogDetail.tsx +0 -91
  120. package/ui/src/components/RequestDetail.tsx +0 -143
  121. package/ui/src/main.tsx +0 -10
  122. package/ui/src/styles.css +0 -11
  123. package/ui/src/types.ts +0 -63
  124. package/ui/src/vite-env.d.ts +0 -1
  125. package/ui/src/vite-plugin-gkm-config.ts +0 -54
  126. package/ui/tsconfig.json +0 -20
  127. package/ui/tsconfig.tsbuildinfo +0 -1
  128. package/ui/vite.config.ts +0 -13
@@ -1,555 +0,0 @@
1
- import type {
2
- EndpointBucket,
3
- EndpointDetails,
4
- EndpointMetrics,
5
- MetricsBucket,
6
- MetricsQueryOptions,
7
- RequestEntry,
8
- RequestMetrics,
9
- StatusDistribution,
10
- TimeRange,
11
- TimeSeriesPoint,
12
- } from '../types';
13
-
14
- /**
15
- * Configuration for MetricsAggregator
16
- */
17
- export interface MetricsAggregatorOptions {
18
- /** Bucket size in milliseconds (default: 60000 = 1 minute) */
19
- bucketSize?: number;
20
- /** Maximum number of buckets to retain (default: 1440 = 24 hours at 1 min buckets) */
21
- maxBuckets?: number;
22
- /** Maximum duration samples to keep per bucket for percentiles (default: 1000) */
23
- maxSamplesPerBucket?: number;
24
- }
25
-
26
- const DEFAULT_OPTIONS: Required<MetricsAggregatorOptions> = {
27
- bucketSize: 60_000, // 1 minute
28
- maxBuckets: 1440, // 24 hours
29
- maxSamplesPerBucket: 1000,
30
- };
31
-
32
- /**
33
- * In-memory metrics aggregator for Telescope.
34
- * Aggregates request data into time-bucketed metrics for efficient querying.
35
- */
36
- /**
37
- * Endpoint time bucket for per-endpoint time series
38
- */
39
- interface EndpointTimeBucket {
40
- timestamp: number;
41
- count: number;
42
- durationSum: number;
43
- errorCount: number;
44
- statusDistribution: StatusDistribution;
45
- }
46
-
47
- export class MetricsAggregator {
48
- private options: Required<MetricsAggregatorOptions>;
49
- private buckets: Map<number, MetricsBucket> = new Map();
50
- private endpoints: Map<string, EndpointBucket> = new Map();
51
- // Map of endpoint key -> Map of timestamp -> bucket
52
- private endpointTimeBuckets: Map<string, Map<number, EndpointTimeBucket>> =
53
- new Map();
54
-
55
- constructor(options: MetricsAggregatorOptions = {}) {
56
- this.options = { ...DEFAULT_OPTIONS, ...options };
57
- }
58
-
59
- /**
60
- * Record a request for metrics aggregation
61
- */
62
- record(entry: RequestEntry): void {
63
- const timestamp = entry.timestamp.getTime();
64
- const bucketTs = this.getBucketTimestamp(timestamp);
65
-
66
- // Update time bucket
67
- this.updateBucket(bucketTs, entry);
68
-
69
- // Update endpoint metrics
70
- this.updateEndpoint(entry);
71
-
72
- // Prune old buckets
73
- this.pruneBuckets();
74
- }
75
-
76
- /**
77
- * Get aggregated request metrics
78
- */
79
- getMetrics(options: MetricsQueryOptions = {}): RequestMetrics {
80
- const range = options.range ?? this.getDefaultRange();
81
- const bucketSize = options.bucketSize ?? this.options.bucketSize;
82
-
83
- // Get buckets in range
84
- const bucketsInRange = this.getBucketsInRange(range);
85
-
86
- if (bucketsInRange.length === 0) {
87
- return this.emptyMetrics();
88
- }
89
-
90
- // Aggregate all durations for percentile calculations
91
- const allDurations: number[] = [];
92
- let totalRequests = 0;
93
- let totalDuration = 0;
94
- let totalErrors = 0;
95
-
96
- for (const bucket of bucketsInRange) {
97
- totalRequests += bucket.count;
98
- totalDuration += bucket.durationSum;
99
- totalErrors += bucket.errorCount;
100
- allDurations.push(...bucket.durationSamples);
101
- }
102
-
103
- // Calculate percentiles
104
- allDurations.sort((a, b) => a - b);
105
- const p50 = this.percentile(allDurations, 50);
106
- const p95 = this.percentile(allDurations, 95);
107
- const p99 = this.percentile(allDurations, 99);
108
-
109
- // Calculate rates
110
- const rangeMs = range.end.getTime() - range.start.getTime();
111
- const rangeSeconds = rangeMs / 1000;
112
- const avgDuration = totalRequests > 0 ? totalDuration / totalRequests : 0;
113
- const errorRate =
114
- totalRequests > 0 ? (totalErrors / totalRequests) * 100 : 0;
115
-
116
- // Build time series (re-bucket if needed)
117
- const timeSeries = this.buildTimeSeries(bucketsInRange, range, bucketSize);
118
-
119
- return {
120
- totalRequests,
121
- avgDuration,
122
- p50Duration: p50,
123
- p95Duration: p95,
124
- p99Duration: p99,
125
- errorRate,
126
- successRate: 100 - errorRate,
127
- requestsPerSecond: rangeSeconds > 0 ? totalRequests / rangeSeconds : 0,
128
- timeSeries,
129
- };
130
- }
131
-
132
- /**
133
- * Get metrics for individual endpoints
134
- */
135
- getEndpointMetrics(options: MetricsQueryOptions = {}): EndpointMetrics[] {
136
- const limit = options.limit ?? 50;
137
-
138
- const results: EndpointMetrics[] = [];
139
-
140
- for (const [, bucket] of this.endpoints) {
141
- const avgDuration =
142
- bucket.count > 0 ? bucket.durationSum / bucket.count : 0;
143
- bucket.durationSamples.sort((a, b) => a - b);
144
- const p95 = this.percentile(bucket.durationSamples, 95);
145
- const errorRate =
146
- bucket.count > 0 ? (bucket.errorCount / bucket.count) * 100 : 0;
147
-
148
- results.push({
149
- method: bucket.method,
150
- path: bucket.path,
151
- count: bucket.count,
152
- avgDuration,
153
- p95Duration: p95,
154
- errorRate,
155
- lastSeen: bucket.lastSeen,
156
- });
157
- }
158
-
159
- // Sort by count descending
160
- results.sort((a, b) => b.count - a.count);
161
-
162
- return results.slice(0, limit);
163
- }
164
-
165
- /**
166
- * Get status code distribution
167
- */
168
- getStatusDistribution(options: MetricsQueryOptions = {}): StatusDistribution {
169
- const range = options.range ?? this.getDefaultRange();
170
- const bucketsInRange = this.getBucketsInRange(range);
171
-
172
- const distribution: StatusDistribution = {
173
- '2xx': 0,
174
- '3xx': 0,
175
- '4xx': 0,
176
- '5xx': 0,
177
- };
178
-
179
- for (const bucket of bucketsInRange) {
180
- distribution['2xx'] += bucket.statusDistribution['2xx'];
181
- distribution['3xx'] += bucket.statusDistribution['3xx'];
182
- distribution['4xx'] += bucket.statusDistribution['4xx'];
183
- distribution['5xx'] += bucket.statusDistribution['5xx'];
184
- }
185
-
186
- return distribution;
187
- }
188
-
189
- /**
190
- * Get detailed metrics for a specific endpoint
191
- */
192
- getEndpointDetails(
193
- method: string,
194
- path: string,
195
- options: MetricsQueryOptions = {},
196
- ): EndpointDetails | null {
197
- const key = `${method}:${path}`;
198
- const endpoint = this.endpoints.get(key);
199
-
200
- if (!endpoint) {
201
- return null;
202
- }
203
-
204
- // Calculate percentiles
205
- const samples = [...endpoint.durationSamples].sort((a, b) => a - b);
206
- const p50 = this.percentile(samples, 50);
207
- const p95 = this.percentile(samples, 95);
208
- const p99 = this.percentile(samples, 99);
209
-
210
- const avgDuration =
211
- endpoint.count > 0 ? endpoint.durationSum / endpoint.count : 0;
212
- const errorRate =
213
- endpoint.count > 0 ? (endpoint.errorCount / endpoint.count) * 100 : 0;
214
-
215
- // Get time series for this endpoint
216
- const endpointBuckets = this.endpointTimeBuckets.get(key);
217
- const timeSeries: TimeSeriesPoint[] = [];
218
- const statusDistribution: StatusDistribution = {
219
- '2xx': 0,
220
- '3xx': 0,
221
- '4xx': 0,
222
- '5xx': 0,
223
- };
224
-
225
- if (endpointBuckets) {
226
- const range = options.range ?? this.getDefaultRange();
227
- const startTs = range.start.getTime();
228
- const endTs = range.end.getTime();
229
-
230
- for (const [ts, bucket] of endpointBuckets) {
231
- if (ts >= startTs && ts <= endTs) {
232
- timeSeries.push({
233
- timestamp: bucket.timestamp,
234
- count: bucket.count,
235
- avgDuration:
236
- bucket.count > 0 ? bucket.durationSum / bucket.count : 0,
237
- errorCount: bucket.errorCount,
238
- });
239
-
240
- statusDistribution['2xx'] += bucket.statusDistribution['2xx'];
241
- statusDistribution['3xx'] += bucket.statusDistribution['3xx'];
242
- statusDistribution['4xx'] += bucket.statusDistribution['4xx'];
243
- statusDistribution['5xx'] += bucket.statusDistribution['5xx'];
244
- }
245
- }
246
-
247
- timeSeries.sort((a, b) => a.timestamp - b.timestamp);
248
- }
249
-
250
- return {
251
- method: endpoint.method,
252
- path: endpoint.path,
253
- count: endpoint.count,
254
- avgDuration,
255
- p50Duration: p50,
256
- p95Duration: p95,
257
- p99Duration: p99,
258
- errorRate,
259
- successRate: 100 - errorRate,
260
- lastSeen: endpoint.lastSeen,
261
- statusDistribution,
262
- timeSeries,
263
- };
264
- }
265
-
266
- /**
267
- * Reset all metrics
268
- */
269
- reset(): void {
270
- this.buckets.clear();
271
- this.endpoints.clear();
272
- this.endpointTimeBuckets.clear();
273
- }
274
-
275
- // ============================================
276
- // Private Methods
277
- // ============================================
278
-
279
- private getBucketTimestamp(timestamp: number): number {
280
- return (
281
- Math.floor(timestamp / this.options.bucketSize) * this.options.bucketSize
282
- );
283
- }
284
-
285
- private updateBucket(bucketTs: number, entry: RequestEntry): void {
286
- let bucket = this.buckets.get(bucketTs);
287
-
288
- if (!bucket) {
289
- bucket = this.createEmptyBucket(bucketTs);
290
- this.buckets.set(bucketTs, bucket);
291
- }
292
-
293
- bucket.count++;
294
- bucket.durationSum += entry.duration;
295
- bucket.durationMin = Math.min(bucket.durationMin, entry.duration);
296
- bucket.durationMax = Math.max(bucket.durationMax, entry.duration);
297
-
298
- // Keep samples for percentile calculations (with reservoir sampling if needed)
299
- if (bucket.durationSamples.length < this.options.maxSamplesPerBucket) {
300
- bucket.durationSamples.push(entry.duration);
301
- } else {
302
- // Reservoir sampling: replace random element
303
- const idx = Math.floor(Math.random() * bucket.count);
304
- if (idx < this.options.maxSamplesPerBucket) {
305
- bucket.durationSamples[idx] = entry.duration;
306
- }
307
- }
308
-
309
- // Update status distribution
310
- const isError = entry.status >= 400;
311
- if (isError) bucket.errorCount++;
312
-
313
- if (entry.status >= 200 && entry.status < 300) {
314
- bucket.statusDistribution['2xx']++;
315
- } else if (entry.status >= 300 && entry.status < 400) {
316
- bucket.statusDistribution['3xx']++;
317
- } else if (entry.status >= 400 && entry.status < 500) {
318
- bucket.statusDistribution['4xx']++;
319
- } else if (entry.status >= 500) {
320
- bucket.statusDistribution['5xx']++;
321
- }
322
- }
323
-
324
- private updateEndpoint(entry: RequestEntry): void {
325
- const key = `${entry.method}:${entry.path}`;
326
- let endpoint = this.endpoints.get(key);
327
-
328
- if (!endpoint) {
329
- endpoint = {
330
- method: entry.method,
331
- path: entry.path,
332
- count: 0,
333
- durationSum: 0,
334
- durationSamples: [],
335
- errorCount: 0,
336
- lastSeen: 0,
337
- };
338
- this.endpoints.set(key, endpoint);
339
- }
340
-
341
- endpoint.count++;
342
- endpoint.durationSum += entry.duration;
343
- endpoint.lastSeen = entry.timestamp.getTime();
344
-
345
- if (entry.status >= 400) {
346
- endpoint.errorCount++;
347
- }
348
-
349
- // Keep samples for percentiles
350
- if (endpoint.durationSamples.length < this.options.maxSamplesPerBucket) {
351
- endpoint.durationSamples.push(entry.duration);
352
- } else {
353
- const idx = Math.floor(Math.random() * endpoint.count);
354
- if (idx < this.options.maxSamplesPerBucket) {
355
- endpoint.durationSamples[idx] = entry.duration;
356
- }
357
- }
358
-
359
- // Update endpoint time bucket
360
- this.updateEndpointTimeBucket(key, entry);
361
- }
362
-
363
- private updateEndpointTimeBucket(key: string, entry: RequestEntry): void {
364
- const timestamp = entry.timestamp.getTime();
365
- const bucketTs = this.getBucketTimestamp(timestamp);
366
-
367
- let endpointBuckets = this.endpointTimeBuckets.get(key);
368
- if (!endpointBuckets) {
369
- endpointBuckets = new Map();
370
- this.endpointTimeBuckets.set(key, endpointBuckets);
371
- }
372
-
373
- let bucket = endpointBuckets.get(bucketTs);
374
- if (!bucket) {
375
- bucket = {
376
- timestamp: bucketTs,
377
- count: 0,
378
- durationSum: 0,
379
- errorCount: 0,
380
- statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
381
- };
382
- endpointBuckets.set(bucketTs, bucket);
383
- }
384
-
385
- bucket.count++;
386
- bucket.durationSum += entry.duration;
387
-
388
- if (entry.status >= 400) {
389
- bucket.errorCount++;
390
- }
391
-
392
- if (entry.status >= 200 && entry.status < 300) {
393
- bucket.statusDistribution['2xx']++;
394
- } else if (entry.status >= 300 && entry.status < 400) {
395
- bucket.statusDistribution['3xx']++;
396
- } else if (entry.status >= 400 && entry.status < 500) {
397
- bucket.statusDistribution['4xx']++;
398
- } else if (entry.status >= 500) {
399
- bucket.statusDistribution['5xx']++;
400
- }
401
-
402
- // Prune old endpoint buckets (keep max buckets per endpoint)
403
- if (endpointBuckets.size > this.options.maxBuckets) {
404
- const timestamps = Array.from(endpointBuckets.keys()).sort(
405
- (a, b) => a - b,
406
- );
407
- const toRemove = timestamps.slice(
408
- 0,
409
- timestamps.length - this.options.maxBuckets,
410
- );
411
- for (const ts of toRemove) {
412
- endpointBuckets.delete(ts);
413
- }
414
- }
415
- }
416
-
417
- private createEmptyBucket(timestamp: number): MetricsBucket {
418
- return {
419
- timestamp,
420
- bucketSize: this.options.bucketSize,
421
- count: 0,
422
- durationSum: 0,
423
- durationMin: Infinity,
424
- durationMax: 0,
425
- durationSamples: [],
426
- errorCount: 0,
427
- statusDistribution: {
428
- '2xx': 0,
429
- '3xx': 0,
430
- '4xx': 0,
431
- '5xx': 0,
432
- },
433
- };
434
- }
435
-
436
- private pruneBuckets(): void {
437
- if (this.buckets.size <= this.options.maxBuckets) return;
438
-
439
- // Get sorted timestamps
440
- const timestamps = Array.from(this.buckets.keys()).sort((a, b) => a - b);
441
-
442
- // Remove oldest buckets
443
- const toRemove = timestamps.slice(
444
- 0,
445
- timestamps.length - this.options.maxBuckets,
446
- );
447
- for (const ts of toRemove) {
448
- this.buckets.delete(ts);
449
- }
450
- }
451
-
452
- private getDefaultRange(): TimeRange {
453
- const now = new Date();
454
- const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
455
- return { start: oneHourAgo, end: now };
456
- }
457
-
458
- private getBucketsInRange(range: TimeRange): MetricsBucket[] {
459
- const startTs = range.start.getTime();
460
- const endTs = range.end.getTime();
461
-
462
- const results: MetricsBucket[] = [];
463
- for (const [ts, bucket] of this.buckets) {
464
- if (ts >= startTs && ts <= endTs) {
465
- results.push(bucket);
466
- }
467
- }
468
-
469
- return results.sort((a, b) => a.timestamp - b.timestamp);
470
- }
471
-
472
- private buildTimeSeries(
473
- buckets: MetricsBucket[],
474
- _range: TimeRange,
475
- targetBucketSize: number,
476
- ): TimeSeriesPoint[] {
477
- if (buckets.length === 0) return [];
478
-
479
- // If target bucket size matches, just convert
480
- if (targetBucketSize === this.options.bucketSize) {
481
- return buckets.map((b) => ({
482
- timestamp: b.timestamp,
483
- count: b.count,
484
- avgDuration: b.count > 0 ? b.durationSum / b.count : 0,
485
- errorCount: b.errorCount,
486
- }));
487
- }
488
-
489
- // Re-bucket into target size
490
- const points: Map<number, TimeSeriesPoint> = new Map();
491
-
492
- for (const bucket of buckets) {
493
- const targetTs =
494
- Math.floor(bucket.timestamp / targetBucketSize) * targetBucketSize;
495
- let point = points.get(targetTs);
496
-
497
- if (!point) {
498
- point = {
499
- timestamp: targetTs,
500
- count: 0,
501
- avgDuration: 0,
502
- errorCount: 0,
503
- };
504
- points.set(targetTs, point);
505
- }
506
-
507
- // Weighted average for duration
508
- const totalCount = point.count + bucket.count;
509
- if (totalCount > 0) {
510
- point.avgDuration =
511
- (point.avgDuration * point.count +
512
- (bucket.count > 0 ? bucket.durationSum / bucket.count : 0) *
513
- bucket.count) /
514
- totalCount;
515
- }
516
- point.count = totalCount;
517
- point.errorCount += bucket.errorCount;
518
- }
519
-
520
- return Array.from(points.values()).sort(
521
- (a, b) => a.timestamp - b.timestamp,
522
- );
523
- }
524
-
525
- private percentile(sortedValues: number[], p: number): number {
526
- if (sortedValues.length === 0) return 0;
527
- if (sortedValues.length === 1) return sortedValues[0]!;
528
-
529
- const index = (p / 100) * (sortedValues.length - 1);
530
- const lower = Math.floor(index);
531
- const upper = Math.ceil(index);
532
-
533
- if (lower === upper) return sortedValues[lower]!;
534
-
535
- // Linear interpolation
536
- const weight = index - lower;
537
- const lowerVal = sortedValues[lower]!;
538
- const upperVal = sortedValues[upper]!;
539
- return lowerVal * (1 - weight) + upperVal * weight;
540
- }
541
-
542
- private emptyMetrics(): RequestMetrics {
543
- return {
544
- totalRequests: 0,
545
- avgDuration: 0,
546
- p50Duration: 0,
547
- p95Duration: 0,
548
- p99Duration: 0,
549
- errorRate: 0,
550
- successRate: 100,
551
- requestsPerSecond: 0,
552
- timeSeries: [],
553
- };
554
- }
555
- }