@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.
@@ -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
+ }