69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { constants as httpStatus } from 'node:http2';
|
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
import type { NextFunction, Request, Response } from 'express';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { RequestContextService } from './request-context.service';
|
|
import { StructuredLogger } from './structured-logger.service';
|
|
|
|
const uuid =
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
@Injectable()
|
|
export class RequestLoggingMiddleware implements NestMiddleware {
|
|
constructor(
|
|
private readonly requestContext: RequestContextService,
|
|
private readonly logger: StructuredLogger,
|
|
) {}
|
|
|
|
use(request: Request, response: Response, next: NextFunction): void {
|
|
const incoming = request.header('x-request-id');
|
|
const clientRequestId =
|
|
incoming && uuid.test(incoming) ? incoming : undefined;
|
|
const existingRequestId = (request as Request & { id?: unknown }).id;
|
|
const requestId =
|
|
typeof existingRequestId === 'string' ? existingRequestId : randomUUID();
|
|
(request as unknown as { id: string }).id = requestId;
|
|
const startedAt = performance.now();
|
|
|
|
response.setHeader('X-Request-ID', requestId);
|
|
response.on('finish', () => {
|
|
const routeRequest: unknown = request;
|
|
const routePath = (routeRequest as { route?: { path?: unknown } }).route
|
|
?.path;
|
|
const route =
|
|
typeof routePath === 'string'
|
|
? `${request.baseUrl}${routePath}`
|
|
: '<unmatched>';
|
|
const context = {
|
|
method: request.method,
|
|
route,
|
|
status: response.statusCode,
|
|
durationMs: Math.round(performance.now() - startedAt),
|
|
...(response.locals.publicErrorCode
|
|
? { code: response.locals.publicErrorCode as string }
|
|
: {}),
|
|
...(response.locals.errorStack
|
|
? { stack: response.locals.errorStack as string }
|
|
: {}),
|
|
};
|
|
const level =
|
|
response.statusCode >= httpStatus.HTTP_STATUS_INTERNAL_SERVER_ERROR
|
|
? 'error'
|
|
: response.statusCode === httpStatus.HTTP_STATUS_TOO_MANY_REQUESTS ||
|
|
response.statusCode === httpStatus.HTTP_STATUS_UNAUTHORIZED ||
|
|
response.statusCode === httpStatus.HTTP_STATUS_FORBIDDEN
|
|
? 'warn'
|
|
: 'info';
|
|
this.logger.event(level, 'http.request.completed', context);
|
|
});
|
|
|
|
this.requestContext.run(
|
|
{
|
|
requestId,
|
|
...(clientRequestId ? { clientRequestId } : {}),
|
|
},
|
|
next,
|
|
);
|
|
}
|
|
}
|