import { HttpException, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { hostname } from 'node:os'; import { Repository } from 'typeorm'; import { ApplicationErrorLog } from './application-error-log.entity'; import { ApplicationErrorLogInput } from './application-error-log.types'; import { sanitizeContext, sanitizeString, sanitizeValue } from './application-error-sanitizer'; import { markErrorAsLogged, wasErrorLogged } from './logged-error-marker'; interface ExtractedError { message: string; stackTrace?: string; errorType?: string; httpStatusCode?: number; responseCode?: string; } @Injectable() export class ApplicationErrorLoggerService { private readonly fallbackLogger = new Logger(ApplicationErrorLoggerService.name); constructor( @InjectRepository(ApplicationErrorLog) private readonly logs: Repository, private readonly config: ConfigService, ) {} async log(input: ApplicationErrorLogInput): Promise { if (wasErrorLogged(input.error)) { return; } const extracted = this.extractError(input.error); try { await this.logs.save( this.logs.create({ level: input.level ?? this.levelForStatus(input.requestContext?.statusCode ?? extracted.httpStatusCode), category: input.category, code: input.code ?? extracted.responseCode, message: extracted.message, stackTrace: extracted.stackTrace, errorType: extracted.errorType, backendModule: input.module, service: input.service, operation: input.operation, httpMethod: input.requestContext?.method, apiPath: input.requestContext?.path, httpStatusCode: input.requestContext?.statusCode ?? extracted.httpStatusCode, correlationId: input.requestContext?.correlationId, userId: input.requestContext?.userId, tenantId: input.requestContext?.tenantId, environment: this.config.get('NODE_ENV') ?? 'development', host: hostname(), context: sanitizeContext(input.context), handled: input.handled ?? false, }), ); markErrorAsLogged(input.error); } catch (logError) { this.fallbackLogger.error( `Application error log write failed: ${this.extractError(logError).message}; original: ${extracted.message}`, this.extractError(logError).stackTrace, ); } } private extractError(error: unknown): ExtractedError { if (error instanceof HttpException) { const response = error.getResponse(); const responseObject = typeof response === 'object' && response !== null ? response : undefined; const responseCode = responseObject && 'code' in responseObject && typeof responseObject.code === 'string' ? responseObject.code : undefined; return { message: sanitizeString(error.message || this.messageFromResponse(response)), stackTrace: error.stack ? sanitizeString(error.stack) : undefined, errorType: error.constructor.name, httpStatusCode: error.getStatus(), responseCode, }; } if (error instanceof Error) { return { message: sanitizeString(error.message || error.name), stackTrace: error.stack ? sanitizeString(error.stack) : undefined, errorType: error.constructor.name, }; } if (typeof error === 'string') { return { message: sanitizeString(error), errorType: 'String' }; } return { message: sanitizeString(JSON.stringify(sanitizeValue(error)) ?? 'Unknown non-error value'), errorType: error === null ? 'Null' : typeof error, }; } private messageFromResponse(response: string | object): string { if (typeof response === 'string') { return response; } if ('message' in response) { const message = response.message; return Array.isArray(message) ? message.join('; ') : String(message); } return 'HTTP exception'; } private levelForStatus(statusCode?: number): string { return !statusCode || statusCode >= 500 ? 'error' : 'warning'; } }