@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,374 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import {
3
+ describe,
4
+ test,
5
+ expect,
6
+ beforeEach,
7
+ afterEach,
8
+ } from 'bun:test';
9
+ import { Effect } from 'effect';
10
+ import { register } from 'prom-client';
11
+
12
+ import type { MetricsOptions, HttpMetricsData } from './types';
13
+
14
+ import {
15
+ MetricsService,
16
+ makeMetricsService,
17
+ createMetricsService,
18
+ } from './metrics.service';
19
+
20
+ describe('MetricsService', () => {
21
+ beforeEach(async () => {
22
+ // Clear the registry before each test
23
+ register.clear();
24
+ });
25
+
26
+ afterEach(async () => {
27
+ register.clear();
28
+ });
29
+
30
+ describe('MetricsService tag', () => {
31
+ test('should be defined as Context tag', () => {
32
+ expect(MetricsService).toBeDefined();
33
+ });
34
+ });
35
+
36
+ describe('makeMetricsService', () => {
37
+ test('should create a layer with default options', async () => {
38
+ const layer = makeMetricsService();
39
+ expect(layer).toBeDefined();
40
+
41
+ const program = Effect.gen(function* () {
42
+ const metricsService = yield* MetricsService;
43
+
44
+ return metricsService;
45
+ });
46
+
47
+ const result = await Effect.runPromise(
48
+ Effect.provide(program, layer),
49
+ );
50
+
51
+ expect(result).toBeDefined();
52
+ expect(typeof result.getMetrics).toBe('function');
53
+ expect(typeof result.getContentType).toBe('function');
54
+ expect(typeof result.recordHttpRequest).toBe('function');
55
+ expect(typeof result.createCounter).toBe('function');
56
+ expect(typeof result.createGauge).toBe('function');
57
+ expect(typeof result.createHistogram).toBe('function');
58
+ expect(typeof result.createSummary).toBe('function');
59
+ expect(typeof result.getMetric).toBe('function');
60
+ expect(typeof result.clear).toBe('function');
61
+ expect(typeof result.getRegistry).toBe('function');
62
+ expect(typeof result.startSystemMetricsCollection).toBe('function');
63
+ expect(typeof result.stopSystemMetricsCollection).toBe('function');
64
+ });
65
+
66
+ test('should create a layer with custom options', async () => {
67
+ const options: MetricsOptions = {
68
+ enabled: false,
69
+ prefix: 'custom_',
70
+ path: '/custom-metrics',
71
+ };
72
+
73
+ const layer = makeMetricsService(options);
74
+ expect(layer).toBeDefined();
75
+
76
+ const program = Effect.gen(function* () {
77
+ const metricsService = yield* MetricsService;
78
+
79
+ return metricsService;
80
+ });
81
+
82
+ const result = await Effect.runPromise(
83
+ Effect.provide(program, layer),
84
+ );
85
+
86
+ expect(result).toBeDefined();
87
+ });
88
+
89
+ test('should create layer with all configuration options', async () => {
90
+ const options: MetricsOptions = {
91
+ enabled: true,
92
+ path: '/custom-metrics',
93
+ defaultLabels: { service: 'test', version: '1.0' },
94
+ collectHttpMetrics: false,
95
+ collectSystemMetrics: false,
96
+ collectGcMetrics: false,
97
+ systemMetricsInterval: 10000,
98
+ prefix: 'myapp_',
99
+ httpDurationBuckets: [0.1, 0.5, 1.0],
100
+ };
101
+
102
+ const layer = makeMetricsService(options);
103
+ const program = Effect.gen(function* () {
104
+ const metricsService = yield* MetricsService;
105
+
106
+ return metricsService;
107
+ });
108
+
109
+ const result = await Effect.runPromise(
110
+ Effect.provide(program, layer),
111
+ );
112
+
113
+ expect(result).toBeDefined();
114
+ });
115
+ });
116
+
117
+ describe('createMetricsService', () => {
118
+ test('should create service with default options', async () => {
119
+ const serviceEffect = createMetricsService();
120
+ const service = await Effect.runPromise(serviceEffect);
121
+
122
+ expect(service).toBeDefined();
123
+ expect(typeof service.getMetrics).toBe('function');
124
+ expect(typeof service.getContentType).toBe('function');
125
+ });
126
+
127
+ test('should create service with custom options', async () => {
128
+ const options: MetricsOptions = {
129
+ enabled: true,
130
+ prefix: 'test_',
131
+ collectHttpMetrics: false,
132
+ };
133
+
134
+ const serviceEffect = createMetricsService(options);
135
+ const service = await Effect.runPromise(serviceEffect);
136
+
137
+ expect(service).toBeDefined();
138
+ });
139
+
140
+ test('should create service with disabled metrics', async () => {
141
+ const serviceEffect = createMetricsService({ enabled: false });
142
+ const service = await Effect.runPromise(serviceEffect);
143
+
144
+ const metrics = await service.getMetrics();
145
+ expect(metrics).toBe('');
146
+ });
147
+ });
148
+
149
+ describe('MetricsService functionality', () => {
150
+ let service: any;
151
+
152
+ beforeEach(async () => {
153
+ const serviceEffect = createMetricsService({
154
+ enabled: true,
155
+ prefix: 'test_',
156
+ collectHttpMetrics: true,
157
+ collectSystemMetrics: true,
158
+ });
159
+ service = await Effect.runPromise(serviceEffect);
160
+ });
161
+
162
+ test('should return content type', () => {
163
+ const contentType = service.getContentType();
164
+ expect(typeof contentType).toBe('string');
165
+ expect(contentType).toContain('text/plain');
166
+ });
167
+
168
+ test('should get metrics in string format', async () => {
169
+ const metrics = await service.getMetrics();
170
+ expect(typeof metrics).toBe('string');
171
+ });
172
+
173
+ test('should record HTTP request metrics', () => {
174
+ const httpData: HttpMetricsData = {
175
+ method: 'GET',
176
+ route: '/test',
177
+ statusCode: 200,
178
+ duration: 0.1,
179
+ controller: 'TestController',
180
+ action: 'test',
181
+ };
182
+
183
+ expect(() => service.recordHttpRequest(httpData)).not.toThrow();
184
+ });
185
+
186
+ test('should record HTTP metrics with missing optional fields', () => {
187
+ const httpData: HttpMetricsData = {
188
+ method: 'POST',
189
+ route: '/api/test',
190
+ statusCode: 201,
191
+ duration: 0.05,
192
+ };
193
+
194
+ expect(() => service.recordHttpRequest(httpData)).not.toThrow();
195
+ });
196
+
197
+ test('should handle lowercase HTTP method', () => {
198
+ const httpData: HttpMetricsData = {
199
+ method: 'post',
200
+ route: '/test',
201
+ statusCode: 200,
202
+ duration: 0.1,
203
+ };
204
+
205
+ expect(() => service.recordHttpRequest(httpData)).not.toThrow();
206
+ });
207
+
208
+ test('should create custom counter', () => {
209
+ const counter = service.createCounter({
210
+ name: 'test_counter',
211
+ help: 'Test counter',
212
+ });
213
+
214
+ expect(counter).toBeDefined();
215
+ expect(typeof counter.inc).toBe('function');
216
+ });
217
+
218
+ test('should create custom counter with label names', () => {
219
+ const counter = service.createCounter({
220
+ name: 'labeled_counter',
221
+ help: 'Counter with labels',
222
+ labelNames: ['method', 'status'],
223
+ });
224
+
225
+ expect(counter).toBeDefined();
226
+ expect(typeof counter.inc).toBe('function');
227
+ });
228
+
229
+ test('should create custom gauge', () => {
230
+ const gauge = service.createGauge({
231
+ name: 'test_gauge',
232
+ help: 'Test gauge',
233
+ });
234
+
235
+ expect(gauge).toBeDefined();
236
+ expect(typeof gauge.set).toBe('function');
237
+ });
238
+
239
+ test('should create custom histogram', () => {
240
+ const histogram = service.createHistogram({
241
+ name: 'test_histogram',
242
+ help: 'Test histogram',
243
+ buckets: [0.1, 0.5, 1, 5],
244
+ });
245
+
246
+ expect(histogram).toBeDefined();
247
+ expect(typeof histogram.observe).toBe('function');
248
+ });
249
+
250
+ test('should create custom histogram with default buckets', () => {
251
+ const histogram = service.createHistogram({
252
+ name: 'test_histogram_default',
253
+ help: 'Test histogram with default buckets',
254
+ });
255
+
256
+ expect(histogram).toBeDefined();
257
+ });
258
+
259
+ test('should create custom summary', () => {
260
+ const summary = service.createSummary({
261
+ name: 'test_summary',
262
+ help: 'Test summary',
263
+ percentiles: [0.5, 0.9, 0.99],
264
+ });
265
+
266
+ expect(summary).toBeDefined();
267
+ expect(typeof summary.observe).toBe('function');
268
+ });
269
+
270
+ test('should create custom summary with default percentiles', () => {
271
+ const summary = service.createSummary({
272
+ name: 'test_summary_default',
273
+ help: 'Test summary with defaults',
274
+ });
275
+
276
+ expect(summary).toBeDefined();
277
+ });
278
+
279
+ test('should get metric by name', () => {
280
+ // Create a metric first
281
+ service.createCounter({
282
+ name: 'findable_counter',
283
+ help: 'A counter that can be found',
284
+ });
285
+
286
+ // Try to find it
287
+ expect(() => service.getMetric('findable_counter')).not.toThrow();
288
+ expect(() => service.getMetric('test_findable_counter')).not.toThrow();
289
+ });
290
+
291
+ test('should clear all metrics', () => {
292
+ // Create some metrics
293
+ service.createCounter({ name: 'counter_to_clear', help: 'Test' });
294
+ service.createGauge({ name: 'gauge_to_clear', help: 'Test' });
295
+
296
+ expect(() => service.clear()).not.toThrow();
297
+ });
298
+
299
+ test('should get registry', () => {
300
+ const registry = service.getRegistry();
301
+
302
+ expect(registry).toBeDefined();
303
+ expect(typeof registry.getMetrics).toBe('function');
304
+ expect(typeof registry.getContentType).toBe('function');
305
+ expect(typeof registry.clear).toBe('function');
306
+ expect(registry.register).toBeDefined();
307
+ });
308
+
309
+ test('should start and stop system metrics collection', () => {
310
+ expect(() => service.startSystemMetricsCollection()).not.toThrow();
311
+ expect(() => service.stopSystemMetricsCollection()).not.toThrow();
312
+ });
313
+
314
+ test('should handle multiple start/stop calls', () => {
315
+ service.startSystemMetricsCollection();
316
+ expect(() => service.startSystemMetricsCollection()).not.toThrow();
317
+
318
+ service.stopSystemMetricsCollection();
319
+ expect(() => service.stopSystemMetricsCollection()).not.toThrow();
320
+ });
321
+ });
322
+
323
+ describe('disabled metrics service', () => {
324
+ test('should not record HTTP metrics when disabled', async () => {
325
+ const serviceEffect = createMetricsService({
326
+ enabled: false,
327
+ collectHttpMetrics: false,
328
+ });
329
+ const service = await Effect.runPromise(serviceEffect);
330
+
331
+ const httpData: HttpMetricsData = {
332
+ method: 'GET',
333
+ route: '/test',
334
+ statusCode: 200,
335
+ duration: 0.1,
336
+ };
337
+
338
+ expect(() => service.recordHttpRequest(httpData)).not.toThrow();
339
+ });
340
+
341
+ test('should not start system metrics when disabled', async () => {
342
+ const serviceEffect = createMetricsService({
343
+ enabled: false,
344
+ collectSystemMetrics: false,
345
+ });
346
+ const service = await Effect.runPromise(serviceEffect);
347
+
348
+ expect(() => service.startSystemMetricsCollection()).not.toThrow();
349
+ });
350
+ });
351
+
352
+ describe('configuration handling', () => {
353
+ test('should handle empty options object', async () => {
354
+ const serviceEffect = createMetricsService({});
355
+ const service = await Effect.runPromise(serviceEffect);
356
+ expect(service).toBeDefined();
357
+ });
358
+
359
+ test('should handle undefined options', async () => {
360
+ const serviceEffect = createMetricsService(undefined);
361
+ const service = await Effect.runPromise(serviceEffect);
362
+ expect(service).toBeDefined();
363
+ });
364
+
365
+ test('should handle partial options', async () => {
366
+ const serviceEffect = createMetricsService({
367
+ prefix: 'partial_',
368
+ enabled: true,
369
+ });
370
+ const service = await Effect.runPromise(serviceEffect);
371
+ expect(service).toBeDefined();
372
+ });
373
+ });
374
+ });