Skip to content

Commit f101ff6

Browse files
committed
implemented the hooks
1 parent 8d86d52 commit f101ff6

6 files changed

Lines changed: 287 additions & 14 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { MonitoringHooksService } from '../services/monitoring-hooks.service';
2+
3+
describe('MonitoringHooksService', () => {
4+
let service: MonitoringHooksService;
5+
6+
beforeEach(() => {
7+
service = new MonitoringHooksService();
8+
});
9+
10+
it('increments counters using label-insensitive ordering', () => {
11+
service.incrementCounter('http_requests_total', 1, {
12+
statusCode: 200,
13+
method: 'GET',
14+
});
15+
service.incrementCounter('http_requests_total', 2, {
16+
method: 'GET',
17+
statusCode: 200,
18+
});
19+
20+
const snapshot = service.getSnapshot();
21+
22+
expect(snapshot.counters).toEqual([
23+
{
24+
name: 'http_requests_total',
25+
labels: { method: 'GET', statusCode: '200' },
26+
value: 3,
27+
},
28+
]);
29+
});
30+
31+
it('tracks gauges and histogram summaries', () => {
32+
service.setGauge('http_requests_in_flight', 2, { method: 'POST' });
33+
service.observeHistogram('http_request_duration_ms', 100, {
34+
endpoint: '/api/scanner',
35+
});
36+
service.observeHistogram('http_request_duration_ms', 300, {
37+
endpoint: '/api/scanner',
38+
});
39+
40+
const snapshot = service.getSnapshot();
41+
42+
expect(snapshot.gauges).toEqual([
43+
{
44+
name: 'http_requests_in_flight',
45+
labels: { method: 'POST' },
46+
value: 2,
47+
},
48+
]);
49+
expect(snapshot.histograms).toEqual([
50+
{
51+
name: 'http_request_duration_ms',
52+
labels: { endpoint: '/api/scanner' },
53+
count: 2,
54+
sum: 400,
55+
min: 100,
56+
max: 300,
57+
average: 200,
58+
},
59+
]);
60+
});
61+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Controller, Get } from '@nestjs/common';
2+
import { ApiOperation, ApiTags } from '@nestjs/swagger';
3+
import { MonitoringHooksService } from '../services/monitoring-hooks.service';
4+
5+
@ApiTags('Monitoring')
6+
@Controller('metrics')
7+
export class MetricsController {
8+
constructor(private readonly monitoringHooksService: MonitoringHooksService) {}
9+
10+
@Get()
11+
@ApiOperation({ summary: 'Expose in-memory monitoring metrics snapshot' })
12+
getMetrics() {
13+
return this.monitoringHooksService.getSnapshot();
14+
}
15+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './performance-monitoring.module';
2+
export * from './services/performance-metric.service';
3+
export * from './services/monitoring-hooks.service';
4+
export * from './middleware/performance-logging.middleware';
5+
export * from './entities/api-performance-metric.entity';

apps/api-service/src/performance-monitoring/middleware/performance-logging.middleware.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
22
import { Request, Response, NextFunction } from 'express';
3+
import { MonitoringHooksService } from '../services/monitoring-hooks.service';
34
import { PerformanceMetricService } from '../services/performance-metric.service';
45

56
export interface ApiRequest extends Request {
@@ -11,7 +12,10 @@ export interface ApiRequest extends Request {
1112
export class PerformanceLoggingMiddleware implements NestMiddleware {
1213
private readonly logger = new Logger(PerformanceLoggingMiddleware.name);
1314

14-
constructor(private readonly performanceMetricService: PerformanceMetricService) {}
15+
constructor(
16+
private readonly performanceMetricService: PerformanceMetricService,
17+
private readonly monitoringHooksService: MonitoringHooksService,
18+
) {}
1519

1620
async use(req: ApiRequest, res: Response, next: NextFunction) {
1721
const requestId = req.headers['x-request-id'] as string || this.generateRequestId();
@@ -20,13 +24,28 @@ export class PerformanceLoggingMiddleware implements NestMiddleware {
2024
req.requestId = requestId;
2125
req.startTime = startTime;
2226

23-
// Capture response finish event
24-
const originalSend = res.send;
25-
res.send = (data: any) => {
27+
this.monitoringHooksService.adjustGauge('http_requests_in_flight', 1, {
28+
method: req.method,
29+
});
30+
31+
res.on('finish', () => {
2632
const duration = Date.now() - startTime;
2733
const statusCode = res.statusCode;
28-
29-
// Log performance metric asynchronously
34+
35+
this.monitoringHooksService.incrementCounter('http_requests_total', 1, {
36+
method: req.method,
37+
endpoint: this.categorizeEndpoint(req.path),
38+
statusCode,
39+
});
40+
this.monitoringHooksService.adjustGauge('http_requests_in_flight', -1, {
41+
method: req.method,
42+
});
43+
this.monitoringHooksService.observeHistogram('http_request_duration_ms', duration, {
44+
method: req.method,
45+
endpoint: this.categorizeEndpoint(req.path),
46+
statusCode,
47+
});
48+
3049
this.logPerformance({
3150
method: req.method,
3251
path: req.path,
@@ -38,9 +57,7 @@ export class PerformanceLoggingMiddleware implements NestMiddleware {
3857
}).catch(err => {
3958
this.logger.error(`Failed to log performance metric: ${err.message}`);
4059
});
41-
42-
return originalSend.call(res, data);
43-
};
60+
});
4461

4562
next();
4663
}
Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
1-
import { Module } from '@nestjs/common';
1+
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
22
import { TypeOrmModule } from '@nestjs/typeorm';
33
import { ApiPerformanceMetric, ApiPerformanceAggregate } from './entities/api-performance-metric.entity';
4+
import { MetricsController } from './controllers/metrics.controller';
5+
import { PerformanceLoggingMiddleware } from './middleware/performance-logging.middleware';
6+
import { MonitoringHooksService } from './services/monitoring-hooks.service';
47
import { PerformanceMetricService } from './services/performance-metric.service';
58
import { PerformanceController } from './controllers/performance.controller';
69

710
@Module({
811
imports: [
912
TypeOrmModule.forFeature([ApiPerformanceMetric, ApiPerformanceAggregate]),
1013
],
11-
controllers: [PerformanceController],
12-
providers: [PerformanceMetricService],
13-
exports: [PerformanceMetricService],
14+
controllers: [PerformanceController, MetricsController],
15+
providers: [
16+
PerformanceMetricService,
17+
MonitoringHooksService,
18+
PerformanceLoggingMiddleware,
19+
],
20+
exports: [PerformanceMetricService, MonitoringHooksService],
1421
})
15-
export class PerformanceMonitoringModule {}
22+
export class PerformanceMonitoringModule implements NestModule {
23+
configure(consumer: MiddlewareConsumer) {
24+
consumer
25+
.apply(PerformanceLoggingMiddleware)
26+
.exclude(
27+
{ path: 'health', method: RequestMethod.ALL },
28+
{ path: 'metrics', method: RequestMethod.ALL },
29+
)
30+
.forRoutes({ path: '*', method: RequestMethod.ALL });
31+
}
32+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { Injectable } from '@nestjs/common';
2+
3+
export interface MetricLabels {
4+
[key: string]: string | number | boolean | undefined;
5+
}
6+
7+
export interface CounterMetricSnapshot {
8+
name: string;
9+
labels: Record<string, string>;
10+
value: number;
11+
}
12+
13+
export interface GaugeMetricSnapshot {
14+
name: string;
15+
labels: Record<string, string>;
16+
value: number;
17+
}
18+
19+
export interface HistogramMetricSnapshot {
20+
name: string;
21+
labels: Record<string, string>;
22+
count: number;
23+
sum: number;
24+
min: number;
25+
max: number;
26+
average: number;
27+
}
28+
29+
export interface MonitoringSnapshot {
30+
counters: CounterMetricSnapshot[];
31+
gauges: GaugeMetricSnapshot[];
32+
histograms: HistogramMetricSnapshot[];
33+
}
34+
35+
interface HistogramMetricState {
36+
count: number;
37+
sum: number;
38+
min: number;
39+
max: number;
40+
}
41+
42+
@Injectable()
43+
export class MonitoringHooksService {
44+
private readonly counters = new Map<string, number>();
45+
private readonly gauges = new Map<string, number>();
46+
private readonly histograms = new Map<string, HistogramMetricState>();
47+
48+
incrementCounter(name: string, value = 1, labels?: MetricLabels): number {
49+
const key = this.buildMetricKey(name, labels);
50+
const next = (this.counters.get(key) || 0) + value;
51+
this.counters.set(key, next);
52+
return next;
53+
}
54+
55+
setGauge(name: string, value: number, labels?: MetricLabels): number {
56+
const key = this.buildMetricKey(name, labels);
57+
this.gauges.set(key, value);
58+
return value;
59+
}
60+
61+
adjustGauge(name: string, delta: number, labels?: MetricLabels): number {
62+
const key = this.buildMetricKey(name, labels);
63+
const next = (this.gauges.get(key) || 0) + delta;
64+
this.gauges.set(key, next);
65+
return next;
66+
}
67+
68+
observeHistogram(name: string, value: number, labels?: MetricLabels): HistogramMetricSnapshot {
69+
const key = this.buildMetricKey(name, labels);
70+
const current = this.histograms.get(key) || {
71+
count: 0,
72+
sum: 0,
73+
min: value,
74+
max: value,
75+
};
76+
77+
const next: HistogramMetricState = {
78+
count: current.count + 1,
79+
sum: current.sum + value,
80+
min: Math.min(current.min, value),
81+
max: Math.max(current.max, value),
82+
};
83+
84+
this.histograms.set(key, next);
85+
return this.toHistogramSnapshot(key, next);
86+
}
87+
88+
getSnapshot(): MonitoringSnapshot {
89+
return {
90+
counters: Array.from(this.counters.entries()).map(([key, value]) =>
91+
this.toNumberSnapshot(key, value),
92+
),
93+
gauges: Array.from(this.gauges.entries()).map(([key, value]) =>
94+
this.toNumberSnapshot(key, value),
95+
),
96+
histograms: Array.from(this.histograms.entries()).map(([key, value]) =>
97+
this.toHistogramSnapshot(key, value),
98+
),
99+
};
100+
}
101+
102+
reset(): void {
103+
this.counters.clear();
104+
this.gauges.clear();
105+
this.histograms.clear();
106+
}
107+
108+
private buildMetricKey(name: string, labels?: MetricLabels): string {
109+
return JSON.stringify({
110+
name,
111+
labels: this.normalizeLabels(labels),
112+
});
113+
}
114+
115+
private normalizeLabels(labels?: MetricLabels): Record<string, string> {
116+
if (!labels) {
117+
return {};
118+
}
119+
120+
return Object.entries(labels)
121+
.filter(([, value]) => value !== undefined)
122+
.sort(([left], [right]) => left.localeCompare(right))
123+
.reduce<Record<string, string>>((acc, [key, value]) => {
124+
acc[key] = String(value);
125+
return acc;
126+
}, {});
127+
}
128+
129+
private parseMetricKey(key: string): { name: string; labels: Record<string, string> } {
130+
return JSON.parse(key) as { name: string; labels: Record<string, string> };
131+
}
132+
133+
private toNumberSnapshot(
134+
key: string,
135+
value: number,
136+
): CounterMetricSnapshot | GaugeMetricSnapshot {
137+
const parsed = this.parseMetricKey(key);
138+
return {
139+
...parsed,
140+
value,
141+
};
142+
}
143+
144+
private toHistogramSnapshot(
145+
key: string,
146+
histogram: HistogramMetricState,
147+
): HistogramMetricSnapshot {
148+
const parsed = this.parseMetricKey(key);
149+
return {
150+
...parsed,
151+
count: histogram.count,
152+
sum: histogram.sum,
153+
min: histogram.min,
154+
max: histogram.max,
155+
average: histogram.count === 0 ? 0 : histogram.sum / histogram.count,
156+
};
157+
}
158+
}

0 commit comments

Comments
 (0)