@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,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
- }