Skip to content

Commit 2dde8b3

Browse files
committed
feat(metrics): expose per-path per-status per-method express metrics
1 parent d542eff commit 2dde8b3

File tree

4 files changed

+38
-6
lines changed

4 files changed

+38
-6
lines changed

__tests__/fake-serv.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ describe('fake-serv', () => {
7878
expect(res.text).toMatch(/nodejs_version_info{version/);
7979
expect(res.text).toMatch(/# UNIT http_server_duration ms/);
8080
expect(res.text).toMatch(/world_requests_total{method="get"} 1/);
81+
expect(res.text).toContain(
82+
'http_request_duration_seconds_bucket{status_code="200",method="GET",path="/world"',
83+
);
8184
});
8285

8386
// Clean shutdown

src/express-app/app.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ export async function startApp<
118118
app.set('trust proxy', config.trustProxy);
119119
}
120120

121-
app.use(loggerMiddleware(app, logging?.logRequestBody, logging?.logResponseBody));
121+
const histogram = app.locals.meter.createHistogram('http_request_duration_seconds', {
122+
description: 'Duration of HTTP requests in seconds',
123+
});
124+
125+
app.use(loggerMiddleware(app, histogram, logging?.logRequestBody, logging?.logResponseBody));
122126

123127
// Allow the service to add locals, etc. We put this before the body parsers
124128
// so that the req can decide whether to save the raw request body or not.
@@ -236,7 +240,7 @@ export async function startApp<
236240
app.use(notFoundMiddleware());
237241
}
238242
if (errors?.enabled) {
239-
app.use(errorHandlerMiddleware(app, errors?.unnest, errors?.render));
243+
app.use(errorHandlerMiddleware(app, histogram, errors?.unnest, errors?.render));
240244
}
241245

242246
return app;

src/telemetry/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ export function startGlobalTelemetry(serviceName: string) {
5555
resourceDetectors: getResourceDetectors(),
5656
metricReader: prometheusExporter,
5757
instrumentations: [getAutoInstrumentations()],
58+
views: [
59+
new opentelemetry.metrics.View({
60+
instrumentName: 'http_request_duration_seconds',
61+
instrumentType: opentelemetry.metrics.InstrumentType.HISTOGRAM,
62+
aggregation: new opentelemetry.metrics.ExplicitBucketHistogramAggregation(
63+
[0.003, 0.03, 0.1, 0.3, 1.5, 10],
64+
true,
65+
),
66+
}),
67+
],
5868
});
5969
telemetrySdk.start();
6070
}

src/telemetry/requestLogger.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RequestHandler, Request, Response, ErrorRequestHandler } from 'express';
2+
import { Histogram } from '@opentelemetry/api';
23

34
import { ServiceError } from '../error';
45
import type { AnyServiceLocals, RequestWithApp, ServiceExpress, ServiceLocals } from '../types';
@@ -44,6 +45,7 @@ function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<Configuratio
4445
error: Error | undefined,
4546
req: Request,
4647
res: Response,
48+
histogram: Histogram,
4749
) {
4850
const prefs = (res.locals as WithLogPrefs)[LOG_PREFS];
4951
if (prefs.logged) {
@@ -61,6 +63,14 @@ function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<Configuratio
6163
dur,
6264
};
6365

66+
const path = req.route ? req.route.path : null;
67+
histogram.record(dur, {
68+
status_code: endLog.s,
69+
method: endLog.m,
70+
path,
71+
service: app.locals.name,
72+
});
73+
6474
if (res.locals.user?.id) {
6575
endLog.u = res.locals.user.id;
6676
}
@@ -95,7 +105,12 @@ function finishLog<SLocals extends AnyServiceLocals = ServiceLocals<Configuratio
95105

96106
export function loggerMiddleware<
97107
SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
98-
>(app: ServiceExpress<SLocals>, logRequests?: boolean, logResponses?: boolean): RequestHandler {
108+
>(
109+
app: ServiceExpress<SLocals>,
110+
histogram: Histogram,
111+
logRequests?: boolean,
112+
logResponses?: boolean,
113+
): RequestHandler {
99114
const { logger, service } = app.locals;
100115
return function gblogger(req, res, next) {
101116
const prefs: LogPrefs = {
@@ -135,15 +150,15 @@ export function loggerMiddleware<
135150
service.getLogFields?.(req as RequestWithApp<SLocals>, preLog);
136151
logger.info(preLog, 'pre');
137152

138-
const logWriter = () => finishLog(app, undefined, req, res);
153+
const logWriter = () => finishLog(app, undefined, req, res, histogram);
139154
res.on('finish', logWriter);
140155
next();
141156
};
142157
}
143158

144159
export function errorHandlerMiddleware<
145160
SLocals extends AnyServiceLocals = ServiceLocals<ConfigurationSchema>,
146-
>(app: ServiceExpress<SLocals>, unnest?: boolean, returnError?: boolean) {
161+
>(app: ServiceExpress<SLocals>, histogram: Histogram, unnest?: boolean, returnError?: boolean) {
147162
const gbErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
148163
let loggable: Partial<ServiceError> = error;
149164
const body = error.response?.body || error.body;
@@ -159,7 +174,7 @@ export function errorHandlerMiddleware<
159174
// Set the status to error, even if we aren't going to render the error.
160175
res.status(loggable.status || 500);
161176
if (returnError) {
162-
finishLog(app, error, req, res);
177+
finishLog(app, error, req, res, histogram);
163178
const prefs = (res.locals as WithLogPrefs)[LOG_PREFS];
164179
prefs.logged = true;
165180
res.json({

0 commit comments

Comments
 (0)