121 lines
4.2 KiB
TypeScript
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';
|
|
}
|
|
}
|