Files
ldap-identidy/apps/api/src/application-error-log/application-error-logger.service.ts
Bastian Wagner 45a41c2077 features
2026-07-17 14:07:04 +02:00

121 lines
4.2 KiB
TypeScript

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<ApplicationErrorLog>,
private readonly config: ConfigService,
) {}
async log(input: ApplicationErrorLogInput): Promise<void> {
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<string>('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';
}
}