@onebun/metrics 0.1.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 +245 -0
- package/package.json +41 -0
- package/src/decorators.test.ts +531 -0
- package/src/decorators.ts +249 -0
- package/src/index.test.ts +357 -0
- package/src/index.ts +90 -0
- package/src/metrics.service.test.ts +374 -0
- package/src/metrics.service.ts +371 -0
- package/src/middleware.test.ts +295 -0
- package/src/middleware.ts +186 -0
- package/src/types.ts +133 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
|
|
3
|
+
import type { HttpMetricsData } from './types';
|
|
4
|
+
|
|
5
|
+
import { HttpStatusCode } from '@onebun/requests';
|
|
6
|
+
|
|
7
|
+
import { MetricsService } from './metrics.service';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Metrics middleware for automatic HTTP metrics collection
|
|
11
|
+
*/
|
|
12
|
+
export class MetricsMiddleware {
|
|
13
|
+
constructor(private metricsService: MetricsService) {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create middleware function for HTTP request metrics
|
|
17
|
+
*/
|
|
18
|
+
createHttpMetricsMiddleware() {
|
|
19
|
+
return async (
|
|
20
|
+
req: Request,
|
|
21
|
+
context: {
|
|
22
|
+
controller?: string;
|
|
23
|
+
action?: string;
|
|
24
|
+
route?: string;
|
|
25
|
+
} = {},
|
|
26
|
+
): Promise<(response: Response, startTime: number) => void> => {
|
|
27
|
+
return (response: Response, requestStartTime: number) => {
|
|
28
|
+
const duration = (Date.now() - requestStartTime) / 1000; // Convert to seconds
|
|
29
|
+
const url = new URL(req.url);
|
|
30
|
+
|
|
31
|
+
const metricsData: HttpMetricsData = {
|
|
32
|
+
method: req.method,
|
|
33
|
+
route: context.route || url.pathname,
|
|
34
|
+
statusCode: response.status,
|
|
35
|
+
duration,
|
|
36
|
+
controller: context.controller,
|
|
37
|
+
action: context.action,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.metricsService.recordHttpRequest(metricsData);
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrap controller method with metrics collection
|
|
47
|
+
*/
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
wrapControllerMethod<T extends (...args: any[]) => any>(
|
|
50
|
+
originalMethod: T,
|
|
51
|
+
controllerName: string,
|
|
52
|
+
methodName: string,
|
|
53
|
+
route: string,
|
|
54
|
+
): T {
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
return (async (...args: any[]) => {
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
let statusCode = HttpStatusCode.OK;
|
|
59
|
+
try {
|
|
60
|
+
const result = await originalMethod.apply(this, args);
|
|
61
|
+
|
|
62
|
+
// If result is a Response, extract status code
|
|
63
|
+
if (result instanceof Response) {
|
|
64
|
+
statusCode = result.status;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
statusCode = HttpStatusCode.INTERNAL_SERVER_ERROR; // Default error status
|
|
70
|
+
throw err;
|
|
71
|
+
} finally {
|
|
72
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
73
|
+
|
|
74
|
+
this.metricsService.recordHttpRequest({
|
|
75
|
+
method: 'UNKNOWN', // Will be overridden by actual request
|
|
76
|
+
route,
|
|
77
|
+
statusCode,
|
|
78
|
+
duration,
|
|
79
|
+
controller: controllerName,
|
|
80
|
+
action: methodName,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}) as T;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decorator for automatic metrics collection on controller methods
|
|
89
|
+
*/
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-explicit-any
|
|
91
|
+
export function WithMetrics(route?: string): any {
|
|
92
|
+
return (
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
target: any,
|
|
95
|
+
propertyKey: string,
|
|
96
|
+
descriptor: PropertyDescriptor,
|
|
97
|
+
) => {
|
|
98
|
+
const originalMethod = descriptor.value;
|
|
99
|
+
const controllerName = target.constructor.name;
|
|
100
|
+
const routePath = route || `/${propertyKey}`;
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
descriptor.value = function (...args: any[]) {
|
|
104
|
+
const startTime = Date.now();
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = originalMethod.apply(this, args);
|
|
108
|
+
|
|
109
|
+
// Handle both sync and async methods
|
|
110
|
+
if (result instanceof Promise) {
|
|
111
|
+
return result
|
|
112
|
+
.then((res) => {
|
|
113
|
+
recordMetrics(controllerName, propertyKey, routePath, startTime, HttpStatusCode.OK);
|
|
114
|
+
|
|
115
|
+
return res;
|
|
116
|
+
})
|
|
117
|
+
.catch((err) => {
|
|
118
|
+
recordMetrics(
|
|
119
|
+
controllerName,
|
|
120
|
+
propertyKey,
|
|
121
|
+
routePath,
|
|
122
|
+
startTime,
|
|
123
|
+
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
124
|
+
);
|
|
125
|
+
throw err;
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
recordMetrics(controllerName, propertyKey, routePath, startTime, HttpStatusCode.OK);
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
recordMetrics(
|
|
134
|
+
controllerName,
|
|
135
|
+
propertyKey,
|
|
136
|
+
routePath,
|
|
137
|
+
startTime,
|
|
138
|
+
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
139
|
+
);
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return descriptor;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Helper function to record metrics
|
|
150
|
+
*/
|
|
151
|
+
function recordMetrics(
|
|
152
|
+
controller: string,
|
|
153
|
+
action: string,
|
|
154
|
+
route: string,
|
|
155
|
+
startTime: number,
|
|
156
|
+
statusCode: number,
|
|
157
|
+
): void {
|
|
158
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
159
|
+
|
|
160
|
+
// This would ideally get the metrics service from the current context
|
|
161
|
+
// For now, we'll store this information and let the application handle it
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).__onebunMetricsService) {
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
const metricsService = (globalThis as any).__onebunMetricsService;
|
|
166
|
+
metricsService.recordHttpRequest({
|
|
167
|
+
method: 'UNKNOWN',
|
|
168
|
+
route,
|
|
169
|
+
statusCode,
|
|
170
|
+
duration,
|
|
171
|
+
controller,
|
|
172
|
+
action,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Effect-based metrics collection
|
|
179
|
+
*/
|
|
180
|
+
export const recordHttpMetrics = (
|
|
181
|
+
data: HttpMetricsData,
|
|
182
|
+
): Effect.Effect<void, never, MetricsService> =>
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
const metricsService = yield* MetricsService;
|
|
185
|
+
metricsService.recordHttpRequest(data);
|
|
186
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { register } from 'prom-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default system metrics collection interval (5 seconds)
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_SYSTEM_METRICS_INTERVAL = 5000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default HTTP duration buckets for histogram metrics (in seconds)
|
|
10
|
+
*/
|
|
11
|
+
/* eslint-disable no-magic-numbers -- Standard Prometheus histogram buckets defined in one place */
|
|
12
|
+
export const DEFAULT_HTTP_DURATION_BUCKETS = [
|
|
13
|
+
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
|
|
14
|
+
];
|
|
15
|
+
/* eslint-enable no-magic-numbers */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default metrics max age in seconds (10 minutes)
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_METRICS_MAX_AGE_SECONDS = 600;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Metrics module configuration options
|
|
24
|
+
*/
|
|
25
|
+
export interface MetricsOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Enable/disable metrics collection
|
|
28
|
+
* @defaultValue true
|
|
29
|
+
*/
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* HTTP path for exposing metrics endpoint
|
|
34
|
+
* @defaultValue '/metrics'
|
|
35
|
+
*/
|
|
36
|
+
path?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default labels to add to all metrics
|
|
40
|
+
*/
|
|
41
|
+
defaultLabels?: Record<string, string>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Enable automatic HTTP metrics collection
|
|
45
|
+
* @defaultValue true
|
|
46
|
+
*/
|
|
47
|
+
collectHttpMetrics?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enable automatic system metrics collection
|
|
51
|
+
* @defaultValue true
|
|
52
|
+
*/
|
|
53
|
+
collectSystemMetrics?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Enable GC metrics collection
|
|
57
|
+
* @defaultValue true
|
|
58
|
+
*/
|
|
59
|
+
collectGcMetrics?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Collection interval for system metrics in milliseconds
|
|
63
|
+
* @defaultValue 5000
|
|
64
|
+
*/
|
|
65
|
+
systemMetricsInterval?: number;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Custom prefix for all metrics
|
|
69
|
+
* @defaultValue 'onebun_'
|
|
70
|
+
*/
|
|
71
|
+
prefix?: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Buckets for HTTP request duration histogram
|
|
75
|
+
* @defaultValue [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
|
76
|
+
*/
|
|
77
|
+
httpDurationBuckets?: number[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* HTTP metrics data
|
|
82
|
+
*/
|
|
83
|
+
export interface HttpMetricsData {
|
|
84
|
+
method: string;
|
|
85
|
+
route: string;
|
|
86
|
+
statusCode: number;
|
|
87
|
+
duration: number;
|
|
88
|
+
controller?: string;
|
|
89
|
+
action?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* System metrics data
|
|
94
|
+
*/
|
|
95
|
+
export interface SystemMetricsData {
|
|
96
|
+
memoryUsage: NodeJS.MemoryUsage;
|
|
97
|
+
cpuUsage: NodeJS.CpuUsage;
|
|
98
|
+
uptime: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Metrics registry interface
|
|
103
|
+
*/
|
|
104
|
+
export interface MetricsRegistry {
|
|
105
|
+
getMetrics(): Promise<string>;
|
|
106
|
+
getContentType(): string;
|
|
107
|
+
clear(): void;
|
|
108
|
+
register: typeof register;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Custom metric types
|
|
113
|
+
*/
|
|
114
|
+
export enum MetricType {
|
|
115
|
+
COUNTER = 'counter',
|
|
116
|
+
GAUGE = 'gauge',
|
|
117
|
+
HISTOGRAM = 'histogram',
|
|
118
|
+
SUMMARY = 'summary',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Custom metric configuration
|
|
123
|
+
*/
|
|
124
|
+
export interface CustomMetricConfig {
|
|
125
|
+
name: string;
|
|
126
|
+
help: string;
|
|
127
|
+
type: MetricType;
|
|
128
|
+
labelNames?: string[];
|
|
129
|
+
buckets?: number[]; // for histogram
|
|
130
|
+
percentiles?: number[]; // for summary
|
|
131
|
+
maxAgeSeconds?: number; // for summary
|
|
132
|
+
ageBuckets?: number; // for summary
|
|
133
|
+
}
|