72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
import type { Request, Response } from 'express';
|
|
import type { Options } from 'pino-http';
|
|
|
|
import { Global, Module } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { LoggerModule } from 'nestjs-pino';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { constants as httpStatus } from 'node:http2';
|
|
|
|
import { Environment } from '../../config/env.validation';
|
|
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;
|
|
|
|
function completion(request: Request, response: Response, value: { responseTime: number }) {
|
|
const route: unknown = request.route;
|
|
const path = typeof route === 'object' && route !== null && 'path' in route &&
|
|
typeof route.path === 'string'
|
|
? `${request.baseUrl}${route.path}`
|
|
: '<unmatched>';
|
|
return {
|
|
method: request.method,
|
|
route: path,
|
|
status: response.statusCode,
|
|
durationMs: Math.round(value.responseTime),
|
|
...(typeof response.locals.publicErrorCode === 'string' ? { code: response.locals.publicErrorCode } : {}),
|
|
...(typeof response.locals.invalidFields === 'string' ? { invalidFields: response.locals.invalidFields } : {}),
|
|
};
|
|
}
|
|
|
|
export function loggingOptions(config: ConfigService<Environment, true>): Options<Request, Response> {
|
|
return {
|
|
level: config.getOrThrow('LOG_LEVEL'),
|
|
messageKey: 'message',
|
|
// ponytail: development-only pretty output; production never loads pino-pretty.
|
|
...(process.env['NODE_ENV'] === 'development'
|
|
? { transport: { target: 'pino-pretty', options: { colorize: true, singleLine: true, messageKey: 'message' } } }
|
|
: {}),
|
|
genReqId: (_request, response) => {
|
|
const id = randomUUID();
|
|
response.setHeader('X-Request-ID', id);
|
|
return id;
|
|
},
|
|
customProps: (request) => {
|
|
const incoming = request.header('x-request-id');
|
|
return { requestId: request.id, ...(incoming && uuid.test(incoming) ? { clientRequestId: incoming } : {}) };
|
|
},
|
|
customLogLevel: (_request, response) => {
|
|
if (response.statusCode >= httpStatus.HTTP_STATUS_INTERNAL_SERVER_ERROR) return 'error';
|
|
if ([httpStatus.HTTP_STATUS_FORBIDDEN, httpStatus.HTTP_STATUS_TOO_MANY_REQUESTS, httpStatus.HTTP_STATUS_UNAUTHORIZED].includes(response.statusCode)) return 'warn';
|
|
return 'info';
|
|
},
|
|
customSuccessMessage: () => 'http.request.completed',
|
|
customErrorMessage: () => 'http.request.completed',
|
|
customSuccessObject: completion,
|
|
customErrorObject: (request, response, _error, value: { responseTime: number }) => completion(request, response, value),
|
|
redact: ['req.headers.authorization', 'req.headers.cookie', 'req.body'],
|
|
serializers: { req: () => undefined },
|
|
};
|
|
}
|
|
|
|
@Global()
|
|
@Module({
|
|
imports: [LoggerModule.forRootAsync<Request, Response>({
|
|
inject: [ConfigService],
|
|
useFactory: (config: ConfigService<Environment, true>) => ({ pinoHttp: loggingOptions(config), assignResponse: true }),
|
|
})],
|
|
providers: [StructuredLogger],
|
|
exports: [StructuredLogger],
|
|
})
|
|
export class LoggingModule {}
|