@geekmidas/telescope 1.1.1 → 10.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/{hono-Bz9PQQzj.mjs → hono-Bw-ey6CT.mjs} +2 -2
- package/dist/{hono-Bz9PQQzj.mjs.map → hono-Bw-ey6CT.mjs.map} +1 -1
- package/dist/{hono-Z_JIICmt.cjs → hono-CPHX7FAm.cjs} +2 -2
- package/dist/{hono-Z_JIICmt.cjs.map → hono-CPHX7FAm.cjs.map} +1 -1
- package/dist/{http-Crsriv32.mjs → http-BIvr0YdI.mjs} +1 -1
- package/dist/{http-Crsriv32.mjs.map → http-BIvr0YdI.mjs.map} +1 -1
- package/dist/{http-JycFRfSe.cjs → http-BsqmhurZ.cjs} +1 -1
- package/dist/{http-JycFRfSe.cjs.map → http-BsqmhurZ.cjs.map} +1 -1
- package/dist/{http-D5kn3_Mt.d.mts → http-DULM1u2L.d.cts} +3 -3
- package/dist/{http-D5kn3_Mt.d.mts.map → http-DULM1u2L.d.cts.map} +1 -1
- package/dist/{http-Dvca6fC3.d.cts → http-GB-Nlvwc.d.mts} +3 -3
- package/dist/{http-Dvca6fC3.d.cts.map → http-GB-Nlvwc.d.mts.map} +1 -1
- package/dist/instrumentation/hono.cjs +2 -2
- package/dist/instrumentation/hono.mjs +2 -2
- package/dist/instrumentation/http.cjs +1 -1
- package/dist/instrumentation/http.d.cts +1 -1
- package/dist/instrumentation/http.d.mts +1 -1
- package/dist/instrumentation/http.mjs +1 -1
- package/dist/instrumentation/index.cjs +5 -5
- package/dist/instrumentation/index.d.cts +3 -3
- package/dist/instrumentation/index.d.mts +3 -3
- package/dist/instrumentation/index.mjs +5 -5
- package/dist/instrumentation/middleware.cjs +2 -2
- package/dist/instrumentation/middleware.d.cts +1 -1
- package/dist/instrumentation/middleware.d.mts +1 -1
- package/dist/instrumentation/middleware.mjs +2 -2
- package/dist/instrumentation/otel.cjs +2 -2
- package/dist/instrumentation/otel.d.cts +1 -1
- package/dist/instrumentation/otel.d.mts +1 -1
- package/dist/instrumentation/otel.mjs +2 -2
- package/dist/instrumentation/setup.cjs +1 -1
- package/dist/instrumentation/setup.mjs +1 -1
- package/dist/logger/console.d.cts.map +1 -1
- package/dist/{middleware-D3v5SnSs.cjs → middleware-Bcq6XXGG.cjs} +3 -3
- package/dist/middleware-Bcq6XXGG.cjs.map +1 -0
- package/dist/{middleware-Dy1ctr2N.mjs → middleware-D1XEHvyy.mjs} +3 -3
- package/dist/middleware-D1XEHvyy.mjs.map +1 -0
- package/dist/{middleware-CA84nsNN.d.cts → middleware-PeyS-9Ny.d.cts} +2 -2
- package/dist/{middleware-CA84nsNN.d.cts.map → middleware-PeyS-9Ny.d.cts.map} +1 -1
- package/dist/{middleware-DAceVpfw.d.mts → middleware-oHvo_ZgU.d.mts} +2 -2
- package/dist/{middleware-DAceVpfw.d.mts.map → middleware-oHvo_ZgU.d.mts.map} +1 -1
- package/dist/{otel-DPKNyJ0t.mjs → otel-B9bNoeZ2.mjs} +4 -4
- package/dist/otel-B9bNoeZ2.mjs.map +1 -0
- package/dist/{otel-1bJPb2Z7.d.mts → otel-Bk89b1lX.d.cts} +3 -3
- package/dist/{otel-1bJPb2Z7.d.mts.map → otel-Bk89b1lX.d.cts.map} +1 -1
- package/dist/{otel-DuiBeBPU.cjs → otel-BzjOtVxZ.cjs} +4 -4
- package/dist/otel-BzjOtVxZ.cjs.map +1 -0
- package/dist/{otel-SBVoVEHr.d.cts → otel-lPMsM26e.d.mts} +3 -3
- package/dist/{otel-SBVoVEHr.d.cts.map → otel-lPMsM26e.d.mts.map} +1 -1
- package/dist/otlp/hono.cjs +3 -3
- package/dist/otlp/hono.mjs +3 -3
- package/dist/otlp/index.cjs +3 -3
- package/dist/otlp/index.mjs +3 -3
- package/dist/otlp/receiver.cjs +3 -3
- package/dist/otlp/receiver.mjs +3 -3
- package/dist/otlp/transformer.cjs +2 -2
- package/dist/otlp/transformer.mjs +2 -2
- package/dist/otlp/types.cjs +1 -1
- package/dist/otlp/types.mjs +1 -1
- package/dist/{receiver-q4f7eirs.mjs → receiver-D46dfkYI.mjs} +2 -2
- package/dist/{receiver-q4f7eirs.mjs.map → receiver-D46dfkYI.mjs.map} +1 -1
- package/dist/{receiver-BW_4go_X.cjs → receiver-slCoAFat.cjs} +2 -2
- package/dist/{receiver-BW_4go_X.cjs.map → receiver-slCoAFat.cjs.map} +1 -1
- package/dist/{setup-BhCp2LUp.mjs → setup-DC9JMEYZ.mjs} +1 -1
- package/dist/{setup-BhCp2LUp.mjs.map → setup-DC9JMEYZ.mjs.map} +1 -1
- package/dist/{setup-_uT4L_x5.cjs → setup-DufxQpi9.cjs} +1 -1
- package/dist/{setup-_uT4L_x5.cjs.map → setup-DufxQpi9.cjs.map} +1 -1
- package/dist/storage/kysely.cjs +48 -38
- package/dist/storage/kysely.cjs.map +1 -1
- package/dist/storage/kysely.d.cts +29 -9
- package/dist/storage/kysely.d.cts.map +1 -1
- package/dist/storage/kysely.d.mts +29 -9
- package/dist/storage/kysely.d.mts.map +1 -1
- package/dist/storage/kysely.mjs +48 -38
- package/dist/storage/kysely.mjs.map +1 -1
- package/dist/tracing-BX8uY2nR.d.cts.map +1 -1
- package/dist/tracing-PnECEbEu.d.mts.map +1 -1
- package/dist/{transformer-CFrJQkUg.mjs → transformer-CFQ4WFk6.mjs} +2 -2
- package/dist/{transformer-CFrJQkUg.mjs.map → transformer-CFQ4WFk6.mjs.map} +1 -1
- package/dist/{transformer-DvDYyFRQ.cjs → transformer-Csl6fbgv.cjs} +2 -2
- package/dist/{transformer-DvDYyFRQ.cjs.map → transformer-Csl6fbgv.cjs.map} +1 -1
- package/dist/types-B1EIa_cQ.d.mts.map +1 -1
- package/dist/{types-BBeTDpHr.cjs → types-BMiHgsi4.cjs} +1 -1
- package/dist/{types-BBeTDpHr.cjs.map → types-BMiHgsi4.cjs.map} +1 -1
- package/dist/{types-CXYLhG_G.mjs → types-Cj59nqy7.mjs} +1 -1
- package/dist/{types-CXYLhG_G.mjs.map → types-Cj59nqy7.mjs.map} +1 -1
- package/dist/types-l7Rlb5Mg.d.cts.map +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -51
- package/dist/middleware-D3v5SnSs.cjs.map +0 -1
- package/dist/middleware-Dy1ctr2N.mjs.map +0 -1
- package/dist/otel-DPKNyJ0t.mjs.map +0 -1
- package/dist/otel-DuiBeBPU.cjs.map +0 -1
- package/src/Telescope.ts +0 -561
- package/src/__tests__/Telescope.spec.ts +0 -438
- package/src/__tests__/pino-transport.spec.ts +0 -115
- package/src/__tests__/redact.spec.ts +0 -336
- package/src/adapters/__tests__/lambda.spec.ts +0 -318
- package/src/adapters/__tests__/middy.spec.ts +0 -741
- package/src/adapters/index.ts +0 -22
- package/src/adapters/lambda.ts +0 -432
- package/src/adapters/middy.ts +0 -402
- package/src/adapters/types.ts +0 -181
- package/src/core.ts +0 -51
- package/src/index.ts +0 -34
- package/src/instrumentation/__tests__/core.spec.ts +0 -248
- package/src/instrumentation/__tests__/hono.spec.ts +0 -348
- package/src/instrumentation/__tests__/middleware.spec.ts +0 -528
- package/src/instrumentation/__tests__/otel.spec.ts +0 -291
- package/src/instrumentation/__tests__/tracing.spec.ts +0 -208
- package/src/instrumentation/core.ts +0 -228
- package/src/instrumentation/hono.ts +0 -270
- package/src/instrumentation/http.ts +0 -306
- package/src/instrumentation/index.ts +0 -90
- package/src/instrumentation/middleware.ts +0 -297
- package/src/instrumentation/otel.ts +0 -271
- package/src/instrumentation/setup.ts +0 -246
- package/src/instrumentation/tracing.ts +0 -145
- package/src/logger/__tests__/console.spec.ts +0 -366
- package/src/logger/__tests__/pino.spec.ts +0 -217
- package/src/logger/console.ts +0 -230
- package/src/logger/pino.ts +0 -201
- package/src/metrics/MetricsAggregator.ts +0 -555
- package/src/metrics/__tests__/MetricsAggregator.spec.ts +0 -298
- package/src/metrics/index.ts +0 -2
- package/src/metrics/storage/InMemoryMetricsStorage.ts +0 -380
- package/src/metrics/storage/MetricsStorage.ts +0 -74
- package/src/metrics/storage/index.ts +0 -3
- package/src/metrics/storage/types.ts +0 -243
- package/src/otlp/__tests__/receiver.spec.ts +0 -367
- package/src/otlp/__tests__/transformer.spec.ts +0 -455
- package/src/otlp/hono.ts +0 -120
- package/src/otlp/index.ts +0 -22
- package/src/otlp/receiver.ts +0 -162
- package/src/otlp/transformer.ts +0 -367
- package/src/otlp/types.ts +0 -364
- package/src/redact.ts +0 -116
- package/src/server/__tests__/hono.spec.ts +0 -814
- package/src/server/hono.ts +0 -442
- package/src/storage/__tests__/kysely.spec.ts +0 -715
- package/src/storage/__tests__/memory.spec.ts +0 -694
- package/src/storage/kysely.ts +0 -599
- package/src/storage/memory.ts +0 -204
- package/src/types.ts +0 -447
- package/src/ui-assets.ts +0 -40
- package/tsconfig.json +0 -9
- package/ui/CHANGELOG.md +0 -7
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -24
- package/ui/src/App.tsx +0 -595
- package/ui/src/api.ts +0 -88
- package/ui/src/components/ExceptionDetail.tsx +0 -100
- package/ui/src/components/FilterBar.tsx +0 -162
- package/ui/src/components/LogDetail.tsx +0 -91
- package/ui/src/components/RequestDetail.tsx +0 -143
- package/ui/src/main.tsx +0 -10
- package/ui/src/styles.css +0 -11
- package/ui/src/types.ts +0 -63
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/src/vite-plugin-gkm-config.ts +0 -54
- package/ui/tsconfig.json +0 -20
- package/ui/tsconfig.tsbuildinfo +0 -1
- package/ui/vite.config.ts +0 -13
|
@@ -1,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
|
-
}
|