@geekmidas/telescope 9.0.2 → 10.0.0-alpha.1

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 (122) hide show
  1. package/README.md +14 -4
  2. package/dist/{http-D5kn3_Mt.d.mts → http-BiNtg85L.d.mts} +3 -3
  3. package/dist/{http-D5kn3_Mt.d.mts.map → http-BiNtg85L.d.mts.map} +1 -1
  4. package/dist/instrumentation/http.d.mts +1 -1
  5. package/dist/instrumentation/index.cjs +2 -2
  6. package/dist/instrumentation/index.d.cts +2 -2
  7. package/dist/instrumentation/index.d.mts +3 -3
  8. package/dist/instrumentation/index.mjs +2 -2
  9. package/dist/instrumentation/middleware.cjs +1 -1
  10. package/dist/instrumentation/middleware.d.cts +1 -1
  11. package/dist/instrumentation/middleware.d.mts +1 -1
  12. package/dist/instrumentation/middleware.mjs +1 -1
  13. package/dist/instrumentation/otel.cjs +1 -1
  14. package/dist/instrumentation/otel.d.cts +1 -1
  15. package/dist/instrumentation/otel.d.mts +1 -1
  16. package/dist/instrumentation/otel.mjs +1 -1
  17. package/dist/logger/console.d.cts.map +1 -1
  18. package/dist/logger/pino.d.mts +2 -2
  19. package/dist/{middleware-DqPQpcup.cjs → middleware-Bcq6XXGG.cjs} +2 -2
  20. package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
  21. package/dist/{middleware-4-GrQlOT.mjs → middleware-D1XEHvyy.mjs} +2 -2
  22. package/dist/middleware-D1XEHvyy.mjs.map +1 -0
  23. package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
  24. package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
  25. package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
  26. package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
  27. package/dist/{otel-40udNP5g.mjs → otel-B9bNoeZ2.mjs} +3 -3
  28. package/dist/otel-B9bNoeZ2.mjs.map +1 -0
  29. package/dist/{otel-SBVoVEHr.d.cts → otel-Bk89b1lX.d.cts} +3 -3
  30. package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
  31. package/dist/{otel-COrpz_GZ.cjs → otel-BzjOtVxZ.cjs} +3 -3
  32. package/dist/otel-BzjOtVxZ.cjs.map +1 -0
  33. package/dist/{otel-1bJPb2Z7.d.mts → otel-lPMsM26e.d.mts} +3 -3
  34. package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
  35. package/dist/storage/kysely.cjs +48 -38
  36. package/dist/storage/kysely.cjs.map +1 -1
  37. package/dist/storage/kysely.d.cts +29 -9
  38. package/dist/storage/kysely.d.cts.map +1 -1
  39. package/dist/storage/kysely.d.mts +29 -9
  40. package/dist/storage/kysely.d.mts.map +1 -1
  41. package/dist/storage/kysely.mjs +48 -38
  42. package/dist/storage/kysely.mjs.map +1 -1
  43. package/dist/tracing-PnECEbEu.d.mts.map +1 -1
  44. package/package.json +6 -3
  45. package/CHANGELOG.md +0 -72
  46. package/dist/middleware-4-GrQlOT.mjs.map +0 -1
  47. package/dist/middleware-DqPQpcup.cjs.map +0 -1
  48. package/dist/otel-40udNP5g.mjs.map +0 -1
  49. package/dist/otel-COrpz_GZ.cjs.map +0 -1
  50. package/src/Telescope.ts +0 -561
  51. package/src/__tests__/Telescope.spec.ts +0 -438
  52. package/src/__tests__/pino-transport.spec.ts +0 -115
  53. package/src/__tests__/redact.spec.ts +0 -336
  54. package/src/adapters/__tests__/lambda.spec.ts +0 -318
  55. package/src/adapters/__tests__/middy.spec.ts +0 -741
  56. package/src/adapters/index.ts +0 -22
  57. package/src/adapters/lambda.ts +0 -432
  58. package/src/adapters/middy.ts +0 -402
  59. package/src/adapters/types.ts +0 -181
  60. package/src/core.ts +0 -51
  61. package/src/index.ts +0 -34
  62. package/src/instrumentation/__tests__/core.spec.ts +0 -248
  63. package/src/instrumentation/__tests__/hono.spec.ts +0 -348
  64. package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
  65. package/src/instrumentation/__tests__/otel.spec.ts +0 -291
  66. package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
  67. package/src/instrumentation/core.ts +0 -228
  68. package/src/instrumentation/hono.ts +0 -270
  69. package/src/instrumentation/http.ts +0 -306
  70. package/src/instrumentation/index.ts +0 -90
  71. package/src/instrumentation/middleware.ts +0 -297
  72. package/src/instrumentation/otel.ts +0 -271
  73. package/src/instrumentation/setup.ts +0 -246
  74. package/src/instrumentation/tracing.ts +0 -145
  75. package/src/logger/__tests__/console.spec.ts +0 -366
  76. package/src/logger/__tests__/pino.spec.ts +0 -217
  77. package/src/logger/console.ts +0 -230
  78. package/src/logger/pino.ts +0 -201
  79. package/src/metrics/MetricsAggregator.ts +0 -555
  80. package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
  81. package/src/metrics/index.ts +0 -2
  82. package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
  83. package/src/metrics/storage/MetricsStorage.ts +0 -74
  84. package/src/metrics/storage/index.ts +0 -3
  85. package/src/metrics/storage/types.ts +0 -243
  86. package/src/otlp/__tests__/receiver.spec.ts +0 -367
  87. package/src/otlp/__tests__/transformer.spec.ts +0 -455
  88. package/src/otlp/hono.ts +0 -120
  89. package/src/otlp/index.ts +0 -22
  90. package/src/otlp/receiver.ts +0 -162
  91. package/src/otlp/transformer.ts +0 -367
  92. package/src/otlp/types.ts +0 -364
  93. package/src/redact.ts +0 -116
  94. package/src/server/__tests__/hono.spec.ts +0 -814
  95. package/src/server/hono.ts +0 -442
  96. package/src/storage/__tests__/kysely.spec.ts +0 -715
  97. package/src/storage/__tests__/memory.spec.ts +0 -694
  98. package/src/storage/kysely.ts +0 -599
  99. package/src/storage/memory.ts +0 -204
  100. package/src/types.ts +0 -447
  101. package/src/ui-assets.ts +0 -40
  102. package/tsconfig.json +0 -9
  103. package/ui/CHANGELOG.md +0 -7
  104. package/ui/index.html +0 -12
  105. package/ui/node_modules/.bin/tsc +0 -21
  106. package/ui/node_modules/.bin/tsserver +0 -21
  107. package/ui/node_modules/.bin/vite +0 -21
  108. package/ui/package.json +0 -24
  109. package/ui/src/App.tsx +0 -595
  110. package/ui/src/api.ts +0 -88
  111. package/ui/src/components/ExceptionDetail.tsx +0 -100
  112. package/ui/src/components/FilterBar.tsx +0 -162
  113. package/ui/src/components/LogDetail.tsx +0 -91
  114. package/ui/src/components/RequestDetail.tsx +0 -143
  115. package/ui/src/main.tsx +0 -10
  116. package/ui/src/styles.css +0 -11
  117. package/ui/src/types.ts +0 -63
  118. package/ui/src/vite-env.d.ts +0 -1
  119. package/ui/src/vite-plugin-gkm-config.ts +0 -54
  120. package/ui/tsconfig.json +0 -20
  121. package/ui/tsconfig.tsbuildinfo +0 -1
  122. 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
- }