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

209 lines
6.1 KiB
TypeScript
Raw Normal View History

2025-08-22 17:02:49 +09:00
import type { Params } from "nestjs-pino";
import type { Options as PinoHttpOptions } from "pino-http";
import type { IncomingMessage, ServerResponse } from "http";
import type { ConfigService } from "@nestjs/config";
import { join } from "path";
import { mkdir } from "fs/promises";
2025-08-21 15:24:40 +09:00
export class LoggingConfig {
static async createPinoConfig(configService: ConfigService): Promise<Params> {
2025-08-22 17:02:49 +09:00
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");
2025-08-21 15:24:40 +09:00
// Ensure logs directory exists for production
2025-08-22 17:02:49 +09:00
if (nodeEnv === "production") {
2025-08-21 15:24:40 +09:00
try {
2025-08-22 17:02:49 +09:00
await mkdir("logs", { recursive: true });
} catch {
2025-08-21 15:24:40 +09:00
// Directory might already exist
}
}
// Base Pino configuration
2025-08-22 17:02:49 +09:00
const pinoConfig: PinoHttpOptions = {
2025-08-21 15:24:40 +09:00
level: logLevel,
name: appName,
base: {
service: appName,
environment: nodeEnv,
pid: process.pid,
},
timestamp: true,
2025-08-22 17:02:49 +09:00
// Ensure sensitive fields are redacted across all logs
redact: {
paths: [
// Common headers
"req.headers.authorization",
"req.headers.cookie",
// Auth
"password",
"password2",
"token",
"secret",
"jwt",
"apiKey",
// Custom params that may carry secrets
"params.password",
"params.password2",
"params.secret",
"params.token",
],
remove: true,
},
2025-08-21 15:24:40 +09:00
formatters: {
level: (label: string) => ({ level: label }),
bindings: () => ({}), // Remove default hostname/pid from every log
},
serializers: {
2025-08-22 17:02:49 +09:00
// Keep logs concise: omit headers by default
req: (req: { method?: string; url?: string; remoteAddress?: string; remotePort?: number }) => ({
2025-08-21 15:24:40 +09:00
method: req.method,
url: req.url,
remoteAddress: req.remoteAddress,
remotePort: req.remotePort,
}),
2025-08-22 17:02:49 +09:00
res: (res: { statusCode: number }) => ({
2025-08-21 15:24:40 +09:00
statusCode: res.statusCode,
}),
2025-08-22 17:02:49 +09:00
err: (err: { constructor: { name: string }; message: string; stack?: string; code?: string; status?: number }) => ({
2025-08-21 15:24:40 +09:00
type: err.constructor.name,
message: err.message,
stack: err.stack,
...(err.code && { code: err.code }),
...(err.status && { status: err.status }),
}),
},
};
// Development: Pretty printing
2025-08-22 17:02:49 +09:00
if (nodeEnv === "development") {
2025-08-21 15:24:40 +09:00
pinoConfig.transport = {
2025-08-22 17:02:49 +09:00
target: "pino-pretty",
2025-08-21 15:24:40 +09:00
options: {
colorize: true,
2025-08-22 17:02:49 +09:00
translateTime: "yyyy-mm-dd HH:MM:ss",
ignore: "pid,hostname",
2025-08-21 15:24:40 +09:00
singleLine: false,
hideObject: false,
},
};
}
// Production: File logging with rotation
2025-08-22 17:02:49 +09:00
if (nodeEnv === "production") {
2025-08-21 15:24:40 +09:00
pinoConfig.transport = {
targets: [
// Console output for container logs
{
2025-08-22 17:02:49 +09:00
target: "pino/file",
2025-08-21 15:24:40 +09:00
level: logLevel,
options: { destination: 1 }, // stdout
},
// Combined log file
{
2025-08-22 17:02:49 +09:00
target: "pino/file",
level: "info",
2025-08-21 15:24:40 +09:00
options: {
2025-08-22 17:02:49 +09:00
destination: join("logs", `${appName}-combined.log`),
2025-08-21 15:24:40 +09:00
mkdir: true,
},
},
// Error log file
{
2025-08-22 17:02:49 +09:00
target: "pino/file",
level: "error",
2025-08-21 15:24:40 +09:00
options: {
2025-08-22 17:02:49 +09:00
destination: join("logs", `${appName}-error.log`),
2025-08-21 15:24:40 +09:00
mkdir: true,
},
},
],
};
}
return {
pinoHttp: {
...pinoConfig,
// Auto-generate correlation IDs
2025-08-22 17:02:49 +09:00
genReqId: (req: IncomingMessage, res: ServerResponse) => {
const existingIdHeader = req.headers["x-correlation-id"];
const existingId = Array.isArray(existingIdHeader) ? existingIdHeader[0] : existingIdHeader;
2025-08-21 15:24:40 +09:00
if (existingId) return existingId;
2025-08-22 17:02:49 +09:00
2025-08-21 15:24:40 +09:00
const correlationId = LoggingConfig.generateCorrelationId();
2025-08-22 17:02:49 +09:00
res.setHeader("x-correlation-id", correlationId);
2025-08-21 15:24:40 +09:00
return correlationId;
},
2025-08-22 17:02:49 +09:00
// Custom log levels: only warn on 4xx and error on 5xx
customLogLevel: (_req: IncomingMessage, res: ServerResponse, err?: unknown) => {
if (res.statusCode >= 400 && res.statusCode < 500) return "warn";
if (res.statusCode >= 500 || err) return "error";
return "silent" as any;
2025-08-21 15:24:40 +09:00
},
2025-08-22 17:02:49 +09:00
// Suppress success messages entirely
customSuccessMessage: () => "",
customErrorMessage: (req: IncomingMessage, res: ServerResponse, err: { message?: string }) => {
const method = req.method ?? "";
const url = req.url ?? "";
return `${method} ${url} ${res.statusCode} - ${err.message ?? "error"}`;
2025-08-21 15:24:40 +09:00
},
},
};
}
/**
* Sanitize headers to remove sensitive information
*/
private static sanitizeHeaders(headers: any): any {
2025-08-22 17:02:49 +09:00
if (!headers || typeof headers !== "object") {
2025-08-21 15:24:40 +09:00
return headers;
}
const sensitiveKeys = [
2025-08-22 17:02:49 +09:00
"authorization",
"cookie",
"set-cookie",
"x-api-key",
"x-auth-token",
"password",
"secret",
"token",
"jwt",
"bearer",
2025-08-21 15:24:40 +09:00
];
const sanitized = { ...headers };
for (const key in sanitized) {
2025-08-22 17:02:49 +09:00
if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive.toLowerCase()))) {
sanitized[key] = "[REDACTED]";
2025-08-21 15:24:40 +09:00
}
}
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[]> = {
2025-08-22 17:02:49 +09:00
error: ["error"],
warn: ["error", "warn"],
info: ["error", "warn", "info"],
debug: ["error", "warn", "info", "debug"],
verbose: ["error", "warn", "info", "debug", "verbose"],
2025-08-21 15:24:40 +09:00
};
2025-08-22 17:02:49 +09:00
2025-08-21 15:24:40 +09:00
return logLevels[level] || logLevels.info;
}
}