Assist_Design/apps/bff/src/common/logging/logging.config.ts
T. Narantuya 111bbc8c91 Add email functionality and update environment configurations
- Introduced email configuration for both development and production environments in `.env.dev.example` and `.env.production.example`.
- Added SendGrid API key and email settings to support password reset and welcome emails.
- Implemented password reset and request password reset endpoints in the AuthController.
- Enhanced signup form to include additional fields such as Customer Number, address, nationality, date of birth, and gender.
- Updated various services and controllers to integrate email functionality and handle new user data.
- Refactored logging and error handling for improved clarity and maintainability.
- Adjusted Docker configuration for production deployment.
2025-08-23 17:24:37 +09:00

226 lines
6.3 KiB
TypeScript

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";
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 {
// Directory might already exist
}
}
// Base Pino configuration
const pinoConfig: PinoHttpOptions = {
level: logLevel,
name: appName,
base: {
service: appName,
environment: nodeEnv,
pid: process.pid,
},
timestamp: true,
// 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,
},
formatters: {
level: (label: string) => ({ level: label }),
bindings: () => ({}), // Remove default hostname/pid from every log
},
serializers: {
// Keep logs concise: omit headers by default
req: (req: {
method?: string;
url?: string;
remoteAddress?: string;
remotePort?: number;
}) => ({
method: req.method,
url: req.url,
remoteAddress: req.remoteAddress,
remotePort: req.remotePort,
}),
res: (res: { statusCode: number }) => ({
statusCode: res.statusCode,
}),
err: (err: {
constructor: { name: string };
message: string;
stack?: string;
code?: string;
status?: number;
}) => ({
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: IncomingMessage, res: ServerResponse) => {
const existingIdHeader = req.headers["x-correlation-id"];
const existingId = Array.isArray(existingIdHeader)
? existingIdHeader[0]
: existingIdHeader;
if (existingId) return existingId;
const correlationId = LoggingConfig.generateCorrelationId();
res.setHeader("x-correlation-id", correlationId);
return correlationId;
},
// 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;
},
// 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"}`;
},
},
};
}
/**
* 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;
}
}