@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,249 @@
|
|
|
1
|
+
/* eslint-disable
|
|
2
|
+
@typescript-eslint/no-explicit-any,
|
|
3
|
+
@typescript-eslint/naming-convention */
|
|
4
|
+
// Decorators work with any class/method types - this is standard for TypeScript decorators
|
|
5
|
+
// PascalCase naming is standard for decorator functions (e.g., @Component, @Injectable)
|
|
6
|
+
|
|
7
|
+
import { Effect } from 'effect';
|
|
8
|
+
|
|
9
|
+
import type { CustomMetricConfig } from './types';
|
|
10
|
+
|
|
11
|
+
import { MetricsService } from './metrics.service';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Decorator for measuring method execution time
|
|
15
|
+
*/
|
|
16
|
+
export function MeasureTime(metricName?: string, labels?: string[]): MethodDecorator {
|
|
17
|
+
return (
|
|
18
|
+
target: any,
|
|
19
|
+
propertyKey: string | symbol,
|
|
20
|
+
descriptor: PropertyDescriptor,
|
|
21
|
+
): PropertyDescriptor => {
|
|
22
|
+
const originalMethod = descriptor.value;
|
|
23
|
+
const methodName = metricName || `${target.constructor.name}_${String(propertyKey)}_duration`;
|
|
24
|
+
|
|
25
|
+
descriptor.value = function (...args: any[]): any {
|
|
26
|
+
const startTime = Date.now();
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const result = originalMethod.apply(this, args);
|
|
30
|
+
|
|
31
|
+
if (result instanceof Promise) {
|
|
32
|
+
return result
|
|
33
|
+
.then((res) => {
|
|
34
|
+
recordDuration(methodName, startTime, labels);
|
|
35
|
+
|
|
36
|
+
return res;
|
|
37
|
+
})
|
|
38
|
+
.catch((err) => {
|
|
39
|
+
recordDuration(methodName, startTime, labels);
|
|
40
|
+
throw err;
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
recordDuration(methodName, startTime, labels);
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
recordDuration(methodName, startTime, labels);
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return descriptor;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decorator for counting method calls
|
|
59
|
+
*/
|
|
60
|
+
export function CountCalls(metricName?: string, labels?: string[]): MethodDecorator {
|
|
61
|
+
return (
|
|
62
|
+
target: any,
|
|
63
|
+
propertyKey: string | symbol,
|
|
64
|
+
descriptor: PropertyDescriptor,
|
|
65
|
+
): PropertyDescriptor => {
|
|
66
|
+
const originalMethod = descriptor.value;
|
|
67
|
+
const counterName =
|
|
68
|
+
metricName || `${target.constructor.name}_${String(propertyKey)}_calls_total`;
|
|
69
|
+
|
|
70
|
+
descriptor.value = function (...args: any[]): any {
|
|
71
|
+
incrementCounter(counterName, labels);
|
|
72
|
+
|
|
73
|
+
return originalMethod.apply(this, args);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return descriptor;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decorator for measuring gauge values
|
|
82
|
+
*/
|
|
83
|
+
export function MeasureGauge(
|
|
84
|
+
metricName: string,
|
|
85
|
+
getValue: () => number,
|
|
86
|
+
labels?: string[],
|
|
87
|
+
): MethodDecorator {
|
|
88
|
+
return (
|
|
89
|
+
target: any,
|
|
90
|
+
propertyKey: string | symbol,
|
|
91
|
+
descriptor: PropertyDescriptor,
|
|
92
|
+
): PropertyDescriptor => {
|
|
93
|
+
const originalMethod = descriptor.value;
|
|
94
|
+
|
|
95
|
+
descriptor.value = function (...args: any[]): any {
|
|
96
|
+
const result = originalMethod.apply(this, args);
|
|
97
|
+
|
|
98
|
+
// Update gauge after method execution
|
|
99
|
+
const updateGauge = (): void => {
|
|
100
|
+
try {
|
|
101
|
+
const value = getValue();
|
|
102
|
+
setGaugeValue(metricName, value, labels);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// eslint-disable-next-line no-console
|
|
105
|
+
console.warn(`Failed to update gauge ${metricName}:`, error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (result instanceof Promise) {
|
|
110
|
+
return result.then((res) => {
|
|
111
|
+
updateGauge();
|
|
112
|
+
|
|
113
|
+
return res;
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
updateGauge();
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return descriptor;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Decorator for automatic metric creation and injection
|
|
128
|
+
*/
|
|
129
|
+
export function InjectMetric(config: CustomMetricConfig): PropertyDecorator {
|
|
130
|
+
return (
|
|
131
|
+
target: any,
|
|
132
|
+
propertyKey: string | symbol,
|
|
133
|
+
): void => {
|
|
134
|
+
// For now, just log the configuration
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log(
|
|
137
|
+
`Metric ${config.name} will be injected into ${target.constructor.name}.${String(propertyKey)}`,
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Class decorator for automatic metric initialization
|
|
144
|
+
*/
|
|
145
|
+
export function WithMetrics(
|
|
146
|
+
options: { prefix?: string } = {},
|
|
147
|
+
): <T extends new (...args: any[]) => any>(constructor: T) => T {
|
|
148
|
+
return <T extends new (...args: any[]) => any>(constructor: T): T =>
|
|
149
|
+
class extends constructor {
|
|
150
|
+
constructor(...args: any[]) {
|
|
151
|
+
super(...args);
|
|
152
|
+
// For now, just log initialization
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.log(
|
|
155
|
+
`WithMetrics applied to ${constructor.name} with prefix: ${options.prefix || 'none'}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Helper functions for metric operations
|
|
163
|
+
*/
|
|
164
|
+
function recordDuration(metricName: string, startTime: number, labels?: string[]): void {
|
|
165
|
+
const metricsService = getMetricsService();
|
|
166
|
+
if (!metricsService) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
171
|
+
const histogram = metricsService.getMetric(metricName);
|
|
172
|
+
|
|
173
|
+
if (histogram && typeof histogram.observe === 'function') {
|
|
174
|
+
histogram.observe(labels ? { labels: labels.join(',') } : {}, duration);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function incrementCounter(metricName: string, labels?: string[]): void {
|
|
179
|
+
const metricsService = getMetricsService();
|
|
180
|
+
if (!metricsService) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const counter = metricsService.getMetric(metricName);
|
|
185
|
+
|
|
186
|
+
if (counter && typeof counter.inc === 'function') {
|
|
187
|
+
counter.inc(labels ? { labels: labels.join(',') } : {});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function setGaugeValue(metricName: string, value: number, labels?: string[]): void {
|
|
192
|
+
const metricsService = getMetricsService();
|
|
193
|
+
if (!metricsService) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const gauge = metricsService.getMetric(metricName);
|
|
198
|
+
|
|
199
|
+
if (gauge && typeof gauge.set === 'function') {
|
|
200
|
+
gauge.set(labels ? { labels: labels.join(',') } : {}, value);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get metrics service from global context
|
|
206
|
+
* This is a temporary solution until proper DI is implemented
|
|
207
|
+
*/
|
|
208
|
+
function getMetricsService(): any {
|
|
209
|
+
if (typeof globalThis !== 'undefined') {
|
|
210
|
+
return (globalThis as any).__onebunMetricsService;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Effect-based decorators
|
|
218
|
+
*/
|
|
219
|
+
export const measureExecutionTime = <A, E, R>(
|
|
220
|
+
metricName: string,
|
|
221
|
+
effect: Effect.Effect<A, E, R>,
|
|
222
|
+
): Effect.Effect<A, E, R | MetricsService> =>
|
|
223
|
+
Effect.gen(function* () {
|
|
224
|
+
const metricsService = yield* MetricsService;
|
|
225
|
+
const startTime = Date.now();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const result = yield* effect;
|
|
229
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
230
|
+
|
|
231
|
+
// Record to histogram if exists
|
|
232
|
+
const histogram = metricsService.getMetric(metricName);
|
|
233
|
+
if (histogram && typeof histogram.observe === 'function') {
|
|
234
|
+
histogram.observe({}, duration);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return result;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const duration = (Date.now() - startTime) / 1000;
|
|
240
|
+
|
|
241
|
+
// Still record the duration even on error
|
|
242
|
+
const histogram = metricsService.getMetric(metricName);
|
|
243
|
+
if (histogram && typeof histogram.observe === 'function') {
|
|
244
|
+
histogram.observe({ status: 'error' }, duration);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
test,
|
|
4
|
+
expect,
|
|
5
|
+
beforeEach,
|
|
6
|
+
afterEach,
|
|
7
|
+
} from 'bun:test';
|
|
8
|
+
import { Effect } from 'effect';
|
|
9
|
+
import { register } from 'prom-client';
|
|
10
|
+
|
|
11
|
+
import type { MetricsOptions } from './types';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_METRICS_OPTIONS,
|
|
15
|
+
createDefaultMetricsService,
|
|
16
|
+
makeDefaultMetricsService,
|
|
17
|
+
MetricsService,
|
|
18
|
+
MetricType,
|
|
19
|
+
} from './index';
|
|
20
|
+
|
|
21
|
+
describe('Metrics Index Module', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
register.clear();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
register.clear();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('DEFAULT_METRICS_OPTIONS', () => {
|
|
31
|
+
test('should export default metrics options with correct values', () => {
|
|
32
|
+
expect(DEFAULT_METRICS_OPTIONS).toBeDefined();
|
|
33
|
+
expect(DEFAULT_METRICS_OPTIONS.enabled).toBe(true);
|
|
34
|
+
expect(DEFAULT_METRICS_OPTIONS.path).toBe('/metrics');
|
|
35
|
+
expect(DEFAULT_METRICS_OPTIONS.collectHttpMetrics).toBe(true);
|
|
36
|
+
expect(DEFAULT_METRICS_OPTIONS.collectSystemMetrics).toBe(true);
|
|
37
|
+
expect(DEFAULT_METRICS_OPTIONS.collectGcMetrics).toBe(true);
|
|
38
|
+
expect(DEFAULT_METRICS_OPTIONS.prefix).toBe('onebun_');
|
|
39
|
+
expect(DEFAULT_METRICS_OPTIONS.systemMetricsInterval).toBe(5000);
|
|
40
|
+
expect(Array.isArray(DEFAULT_METRICS_OPTIONS.httpDurationBuckets)).toBe(true);
|
|
41
|
+
expect(DEFAULT_METRICS_OPTIONS.httpDurationBuckets.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should be a const object with readonly properties', () => {
|
|
45
|
+
// Test that the object properties are defined and accessible
|
|
46
|
+
expect(DEFAULT_METRICS_OPTIONS.enabled).toBeDefined();
|
|
47
|
+
expect(DEFAULT_METRICS_OPTIONS.path).toBeDefined();
|
|
48
|
+
expect(DEFAULT_METRICS_OPTIONS.prefix).toBeDefined();
|
|
49
|
+
|
|
50
|
+
// Test that it's exported as a const (structural check)
|
|
51
|
+
expect(typeof DEFAULT_METRICS_OPTIONS).toBe('object');
|
|
52
|
+
expect(DEFAULT_METRICS_OPTIONS).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('createDefaultMetricsService', () => {
|
|
57
|
+
test('should create metrics service with default options', async () => {
|
|
58
|
+
const serviceEffect = createDefaultMetricsService();
|
|
59
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
60
|
+
|
|
61
|
+
expect(service).toBeDefined();
|
|
62
|
+
expect(typeof service.getMetrics).toBe('function');
|
|
63
|
+
expect(typeof service.getContentType).toBe('function');
|
|
64
|
+
expect(typeof service.recordHttpRequest).toBe('function');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should create metrics service with override options', async () => {
|
|
68
|
+
const overrides: Partial<MetricsOptions> = {
|
|
69
|
+
enabled: false,
|
|
70
|
+
prefix: 'custom_',
|
|
71
|
+
collectHttpMetrics: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const serviceEffect = createDefaultMetricsService(overrides);
|
|
75
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
76
|
+
|
|
77
|
+
expect(service).toBeDefined();
|
|
78
|
+
|
|
79
|
+
// Test that disabled service returns empty metrics
|
|
80
|
+
const metrics = await service.getMetrics();
|
|
81
|
+
expect(metrics).toBe('');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should merge override options with defaults', async () => {
|
|
85
|
+
const overrides: Partial<MetricsOptions> = {
|
|
86
|
+
prefix: 'test_',
|
|
87
|
+
systemMetricsInterval: 10000,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const serviceEffect = createDefaultMetricsService(overrides);
|
|
91
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
92
|
+
|
|
93
|
+
expect(service).toBeDefined();
|
|
94
|
+
// The service should still be enabled (from defaults) but with custom prefix
|
|
95
|
+
const metrics = await service.getMetrics();
|
|
96
|
+
expect(typeof metrics).toBe('string');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should handle empty override options', async () => {
|
|
100
|
+
const serviceEffect = createDefaultMetricsService({});
|
|
101
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
102
|
+
|
|
103
|
+
expect(service).toBeDefined();
|
|
104
|
+
expect(typeof service.getMetrics).toBe('function');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should handle undefined override options', async () => {
|
|
108
|
+
const serviceEffect = createDefaultMetricsService(undefined);
|
|
109
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
110
|
+
|
|
111
|
+
expect(service).toBeDefined();
|
|
112
|
+
expect(typeof service.getMetrics).toBe('function');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should handle complex override options', async () => {
|
|
116
|
+
const overrides: Partial<MetricsOptions> = {
|
|
117
|
+
enabled: true,
|
|
118
|
+
path: '/custom-metrics',
|
|
119
|
+
defaultLabels: { service: 'test', version: '1.0' },
|
|
120
|
+
collectHttpMetrics: true,
|
|
121
|
+
collectSystemMetrics: false,
|
|
122
|
+
collectGcMetrics: false,
|
|
123
|
+
systemMetricsInterval: 30000,
|
|
124
|
+
prefix: 'myapp_',
|
|
125
|
+
httpDurationBuckets: [0.1, 0.5, 1.0],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const serviceEffect = createDefaultMetricsService(overrides);
|
|
129
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
130
|
+
|
|
131
|
+
expect(service).toBeDefined();
|
|
132
|
+
|
|
133
|
+
// Test that the service works with custom options
|
|
134
|
+
const counter = service.createCounter({
|
|
135
|
+
name: 'test_counter',
|
|
136
|
+
help: 'Test counter',
|
|
137
|
+
});
|
|
138
|
+
expect(counter).toBeDefined();
|
|
139
|
+
expect(typeof counter.inc).toBe('function');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('makeDefaultMetricsService', () => {
|
|
144
|
+
test('should create metrics service layer with default options', async () => {
|
|
145
|
+
const layer = makeDefaultMetricsService();
|
|
146
|
+
expect(layer).toBeDefined();
|
|
147
|
+
|
|
148
|
+
const program = Effect.gen(function* () {
|
|
149
|
+
const service = yield* MetricsService;
|
|
150
|
+
|
|
151
|
+
return service;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const service = await Effect.runPromise(
|
|
155
|
+
Effect.provide(program, layer),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(service).toBeDefined();
|
|
159
|
+
expect(typeof service.getMetrics).toBe('function');
|
|
160
|
+
expect(typeof service.getContentType).toBe('function');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should create metrics service layer with override options', async () => {
|
|
164
|
+
const overrides: Partial<MetricsOptions> = {
|
|
165
|
+
enabled: true,
|
|
166
|
+
prefix: 'layer_test_',
|
|
167
|
+
collectSystemMetrics: false,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const layer = makeDefaultMetricsService(overrides);
|
|
171
|
+
expect(layer).toBeDefined();
|
|
172
|
+
|
|
173
|
+
const program = Effect.gen(function* () {
|
|
174
|
+
const service = yield* MetricsService;
|
|
175
|
+
|
|
176
|
+
return service;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const service = await Effect.runPromise(
|
|
180
|
+
Effect.provide(program, layer),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(service).toBeDefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('should handle empty override options in layer', async () => {
|
|
187
|
+
const layer = makeDefaultMetricsService({});
|
|
188
|
+
expect(layer).toBeDefined();
|
|
189
|
+
|
|
190
|
+
const program = Effect.gen(function* () {
|
|
191
|
+
const service = yield* MetricsService;
|
|
192
|
+
|
|
193
|
+
return service;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const service = await Effect.runPromise(
|
|
197
|
+
Effect.provide(program, layer),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(service).toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should handle undefined override options in layer', async () => {
|
|
204
|
+
const layer = makeDefaultMetricsService(undefined);
|
|
205
|
+
expect(layer).toBeDefined();
|
|
206
|
+
|
|
207
|
+
const program = Effect.gen(function* () {
|
|
208
|
+
const service = yield* MetricsService;
|
|
209
|
+
|
|
210
|
+
return service;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const service = await Effect.runPromise(
|
|
214
|
+
Effect.provide(program, layer),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(service).toBeDefined();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('should create working layer with metrics functionality', async () => {
|
|
221
|
+
const overrides: Partial<MetricsOptions> = {
|
|
222
|
+
prefix: 'layer_',
|
|
223
|
+
enabled: true,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const layer = makeDefaultMetricsService(overrides);
|
|
227
|
+
|
|
228
|
+
const program = Effect.gen(function* () {
|
|
229
|
+
const service = yield* MetricsService;
|
|
230
|
+
|
|
231
|
+
// Test creating a metric
|
|
232
|
+
const counter = service.createCounter({
|
|
233
|
+
name: 'layer_test_counter',
|
|
234
|
+
help: 'Test counter for layer',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return { service, counter };
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const result = await Effect.runPromise(
|
|
241
|
+
Effect.provide(program, layer),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(result.service).toBeDefined();
|
|
245
|
+
expect(result.counter).toBeDefined();
|
|
246
|
+
expect(typeof result.counter.inc).toBe('function');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('exported types and functions', () => {
|
|
251
|
+
test('should export MetricType enum', () => {
|
|
252
|
+
expect(MetricType).toBeDefined();
|
|
253
|
+
expect(MetricType.COUNTER).toBe(MetricType.COUNTER);
|
|
254
|
+
expect(MetricType.GAUGE).toBe(MetricType.GAUGE);
|
|
255
|
+
expect(MetricType.HISTOGRAM).toBe(MetricType.HISTOGRAM);
|
|
256
|
+
expect(MetricType.SUMMARY).toBe(MetricType.SUMMARY);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('should export MetricsService', () => {
|
|
260
|
+
expect(MetricsService).toBeDefined();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('should export all expected functions and types', () => {
|
|
264
|
+
// Test that all main exports are available
|
|
265
|
+
expect(typeof createDefaultMetricsService).toBe('function');
|
|
266
|
+
expect(typeof makeDefaultMetricsService).toBe('function');
|
|
267
|
+
expect(DEFAULT_METRICS_OPTIONS).toBeDefined();
|
|
268
|
+
expect(MetricType).toBeDefined();
|
|
269
|
+
expect(MetricsService).toBeDefined();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('require calls in convenience functions', () => {
|
|
274
|
+
test('createDefaultMetricsService should handle require properly', async () => {
|
|
275
|
+
// This tests that the require('./metrics.service') works correctly
|
|
276
|
+
const serviceEffect = createDefaultMetricsService({ enabled: true });
|
|
277
|
+
|
|
278
|
+
// Should not throw and should return a valid Effect
|
|
279
|
+
expect(serviceEffect).toBeDefined();
|
|
280
|
+
expect(typeof serviceEffect).toBe('object');
|
|
281
|
+
|
|
282
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
283
|
+
expect(service).toBeDefined();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('makeDefaultMetricsService should handle require properly', async () => {
|
|
287
|
+
// This tests that the require('./metrics.service') works correctly
|
|
288
|
+
const layer = makeDefaultMetricsService({ enabled: true });
|
|
289
|
+
|
|
290
|
+
// Should not throw and should return a valid Layer
|
|
291
|
+
expect(layer).toBeDefined();
|
|
292
|
+
expect(typeof layer).toBe('object');
|
|
293
|
+
|
|
294
|
+
const program = Effect.gen(function* () {
|
|
295
|
+
return yield* MetricsService;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const service = await Effect.runPromise(
|
|
299
|
+
Effect.provide(program, layer),
|
|
300
|
+
);
|
|
301
|
+
expect(service).toBeDefined();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('integration with other metrics components', () => {
|
|
306
|
+
test('should work with decorators', async () => {
|
|
307
|
+
const serviceEffect = createDefaultMetricsService({
|
|
308
|
+
enabled: true,
|
|
309
|
+
prefix: 'integration_',
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
313
|
+
|
|
314
|
+
// Test creating various metric types
|
|
315
|
+
const counter = service.createCounter({
|
|
316
|
+
name: 'decorator_counter',
|
|
317
|
+
help: 'Counter for decorator integration test',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const gauge = service.createGauge({
|
|
321
|
+
name: 'decorator_gauge',
|
|
322
|
+
help: 'Gauge for decorator integration test',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const histogram = service.createHistogram({
|
|
326
|
+
name: 'decorator_histogram',
|
|
327
|
+
help: 'Histogram for decorator integration test',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
expect(counter).toBeDefined();
|
|
331
|
+
expect(gauge).toBeDefined();
|
|
332
|
+
expect(histogram).toBeDefined();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('should work with HTTP metrics', async () => {
|
|
336
|
+
const serviceEffect = createDefaultMetricsService({
|
|
337
|
+
enabled: true,
|
|
338
|
+
collectHttpMetrics: true,
|
|
339
|
+
prefix: 'http_',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const service = await Effect.runPromise(serviceEffect);
|
|
343
|
+
|
|
344
|
+
// Test recording HTTP metrics
|
|
345
|
+
expect(() => {
|
|
346
|
+
service.recordHttpRequest({
|
|
347
|
+
method: 'GET',
|
|
348
|
+
route: '/test',
|
|
349
|
+
statusCode: 200,
|
|
350
|
+
duration: 0.1,
|
|
351
|
+
controller: 'TestController',
|
|
352
|
+
action: 'test',
|
|
353
|
+
});
|
|
354
|
+
}).not.toThrow();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* \@onebun/metrics
|
|
3
|
+
*
|
|
4
|
+
* Prometheus-compatible metrics module for OneBun framework
|
|
5
|
+
* Provides automatic HTTP request metrics, system metrics, and custom metrics API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_HTTP_DURATION_BUCKETS, DEFAULT_SYSTEM_METRICS_INTERVAL } from './types';
|
|
9
|
+
|
|
10
|
+
// Re-export commonly used prom-client types
|
|
11
|
+
export type {
|
|
12
|
+
Counter,
|
|
13
|
+
Gauge,
|
|
14
|
+
Histogram,
|
|
15
|
+
Summary,
|
|
16
|
+
} from 'prom-client';
|
|
17
|
+
// Decorators
|
|
18
|
+
export {
|
|
19
|
+
CountCalls,
|
|
20
|
+
InjectMetric,
|
|
21
|
+
MeasureGauge,
|
|
22
|
+
MeasureTime,
|
|
23
|
+
measureExecutionTime,
|
|
24
|
+
WithMetrics,
|
|
25
|
+
} from './decorators';
|
|
26
|
+
// Core service
|
|
27
|
+
export {
|
|
28
|
+
createMetricsService,
|
|
29
|
+
MetricsService,
|
|
30
|
+
makeMetricsService,
|
|
31
|
+
} from './metrics.service';
|
|
32
|
+
|
|
33
|
+
// Middleware
|
|
34
|
+
export {
|
|
35
|
+
MetricsMiddleware,
|
|
36
|
+
recordHttpMetrics,
|
|
37
|
+
WithMetrics as WithMetricsMiddleware,
|
|
38
|
+
} from './middleware';
|
|
39
|
+
// Types
|
|
40
|
+
export type {
|
|
41
|
+
CustomMetricConfig,
|
|
42
|
+
HttpMetricsData,
|
|
43
|
+
MetricsOptions,
|
|
44
|
+
MetricsRegistry,
|
|
45
|
+
SystemMetricsData,
|
|
46
|
+
} from './types';
|
|
47
|
+
export { MetricType } from './types';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default metrics configuration
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_METRICS_OPTIONS = {
|
|
53
|
+
enabled: true,
|
|
54
|
+
path: '/metrics',
|
|
55
|
+
defaultLabels: {},
|
|
56
|
+
collectHttpMetrics: true,
|
|
57
|
+
collectSystemMetrics: true,
|
|
58
|
+
collectGcMetrics: true,
|
|
59
|
+
systemMetricsInterval: DEFAULT_SYSTEM_METRICS_INTERVAL,
|
|
60
|
+
prefix: 'onebun_',
|
|
61
|
+
httpDurationBuckets: DEFAULT_HTTP_DURATION_BUCKETS,
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convenience function to create metrics service with default options
|
|
66
|
+
*/
|
|
67
|
+
export const createDefaultMetricsService = (
|
|
68
|
+
overrides: Partial<import('./types').MetricsOptions> = {},
|
|
69
|
+
): ReturnType<typeof import('./metrics.service').createMetricsService> => {
|
|
70
|
+
const { createMetricsService } = require('./metrics.service');
|
|
71
|
+
|
|
72
|
+
return createMetricsService({
|
|
73
|
+
...DEFAULT_METRICS_OPTIONS,
|
|
74
|
+
...overrides,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convenience function to create metrics layer with default options
|
|
80
|
+
*/
|
|
81
|
+
export const makeDefaultMetricsService = (
|
|
82
|
+
overrides: Partial<import('./types').MetricsOptions> = {},
|
|
83
|
+
): ReturnType<typeof import('./metrics.service').makeMetricsService> => {
|
|
84
|
+
const { makeMetricsService } = require('./metrics.service');
|
|
85
|
+
|
|
86
|
+
return makeMetricsService({
|
|
87
|
+
...DEFAULT_METRICS_OPTIONS,
|
|
88
|
+
...overrides,
|
|
89
|
+
});
|
|
90
|
+
};
|