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}` : ''; 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): Options { 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({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ pinoHttp: loggingOptions(config), assignResponse: true }), })], providers: [StructuredLogger], exports: [StructuredLogger], }) export class LoggingModule {}