Assist_Design/apps/bff/src/common/logging/logging.config.ts

181 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-08-21 15:24:40 +09:00
import { Params } from 'nestjs-pino';
import { ConfigService } from '@nestjs/config';
import { join } from 'path';
import { mkdir } from 'fs/promises';
export class LoggingConfig {
static async createPinoConfig(configService: ConfigService): Promise<Params> {
const nodeEnv = configService.get<string>('NODE_ENV', 'development');
const logLevel = configService.get<string>('LOG_LEVEL', 'info');
const appName = configService.get<string>('APP_NAME', 'customer-portal-bff');
// Ensure logs directory exists for production
if (nodeEnv === 'production') {
try {
await mkdir('logs', { recursive: true });
} catch (error) {
// Directory might already exist
}
}
// Base Pino configuration
const pinoConfig: any = {
level: logLevel,
name: appName,
base: {
service: appName,
environment: nodeEnv,
pid: process.pid,
},
timestamp: true,
formatters: {
level: (label: string) => ({ level: label }),
bindings: () => ({}), // Remove default hostname/pid from every log
},
serializers: {
// Custom serializers for sensitive data
req: (req: any) => ({
method: req.method,
url: req.url,
headers: LoggingConfig.sanitizeHeaders(req.headers),
remoteAddress: req.remoteAddress,
remotePort: req.remotePort,
}),
res: (res: any) => ({
statusCode: res.statusCode,
headers: LoggingConfig.sanitizeHeaders(res.getHeaders?.() || {}),
}),
err: (err: any) => ({
type: err.constructor.name,
message: err.message,
stack: err.stack,
...(err.code && { code: err.code }),
...(err.status && { status: err.status }),
}),
},
};
// Development: Pretty printing
if (nodeEnv === 'development') {
pinoConfig.transport = {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'yyyy-mm-dd HH:MM:ss',
ignore: 'pid,hostname',
singleLine: false,
hideObject: false,
},
};
}
// Production: File logging with rotation
if (nodeEnv === 'production') {
pinoConfig.transport = {
targets: [
// Console output for container logs
{
target: 'pino/file',
level: logLevel,
options: { destination: 1 }, // stdout
},
// Combined log file
{
target: 'pino/file',
level: 'info',
options: {
destination: join('logs', `${appName}-combined.log`),
mkdir: true,
},
},
// Error log file
{
target: 'pino/file',
level: 'error',
options: {
destination: join('logs', `${appName}-error.log`),
mkdir: true,
},
},
],
};
}
return {
pinoHttp: {
...pinoConfig,
// Auto-generate correlation IDs
genReqId: (req: any, res: any) => {
const existingId = req.headers['x-correlation-id'];
if (existingId) return existingId;
const correlationId = LoggingConfig.generateCorrelationId();
res.setHeader('x-correlation-id', correlationId);
return correlationId;
},
// Custom log messages
customLogLevel: (req: any, res: any, err: any) => {
if (res.statusCode >= 400 && res.statusCode < 500) return 'warn';
if (res.statusCode >= 500 || err) return 'error';
if (res.statusCode >= 300 && res.statusCode < 400) return 'debug';
return 'info';
},
customSuccessMessage: (req: any, res: any) => {
return `${req.method} ${req.url} ${res.statusCode}`;
},
customErrorMessage: (req: any, res: any, err: any) => {
return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
},
},
};
}
/**
* Sanitize headers to remove sensitive information
*/
private static sanitizeHeaders(headers: any): any {
if (!headers || typeof headers !== 'object') {
return headers;
}
const sensitiveKeys = [
'authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token',
'password', 'secret', 'token', 'jwt', 'bearer'
];
const sanitized = { ...headers };
for (const key in sanitized) {
if (sensitiveKeys.some(sensitive =>
key.toLowerCase().includes(sensitive.toLowerCase())
)) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
/**
* Generate correlation ID
*/
private static generateCorrelationId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Get log levels for different environments
*/
static getLogLevels(level: string): string[] {
const logLevels: Record<string, string[]> = {
error: ['error'],
warn: ['error', 'warn'],
info: ['error', 'warn', 'info'],
debug: ['error', 'warn', 'info', 'debug'],
verbose: ['error', 'warn', 'info', 'debug', 'verbose'],
};
return logLevels[level] || logLevels.info;
}
}