/** * 日志配置模块 * * 功能描述: * - 提供详细的日志配置选项 * - 支持日志文件轮转和管理 * - 根据环境自动调整日志策略 * - 提供日志文件清理和归档功能 * * @author moyin * @version 1.0.0 * @since 2025-12-13 */ import { ConfigService } from '@nestjs/config'; import * as path from 'path'; import * as fs from 'fs'; /** * 日志配置工厂类 * * 职责: * - 根据环境变量生成 Pino 日志配置 * - 管理日志文件的创建和轮转 * - 提供不同环境的日志策略 */ export class LoggerConfigFactory { /** * 创建 Pino 日志配置 * * 功能描述: * 根据环境变量和配置生成完整的 Pino 日志配置对象 * * 业务逻辑: * 1. 读取环境变量配置 * 2. 确保日志目录存在 * 3. 根据环境选择不同的输出策略 * 4. 配置日志轮转和清理策略 * * @param configService 配置服务实例 * @returns Pino 日志配置对象 */ static createLoggerConfig(configService: ConfigService) { const isProduction = configService.get('NODE_ENV') === 'production'; const logDir = configService.get('LOG_DIR', './logs'); const logLevel = configService.get('LOG_LEVEL', isProduction ? 'info' : 'debug'); const appName = configService.get('APP_NAME', 'pixel-game-server'); // 确保日志目录存在 this.ensureLogDirectory(logDir); return { pinoHttp: { level: logLevel, // 根据环境配置不同的输出策略 transport: this.createTransportConfig(isProduction, logDir, logLevel), // 自定义序列化器 serializers: this.createSerializers(), // 基础字段 base: { pid: process.pid, hostname: require('os').hostname(), app: appName, version: process.env.npm_package_version || '1.0.0', }, // HTTP 请求日志配置 autoLogging: true, // 自定义日志级别判断 customLogLevel: this.customLogLevel, // 自定义请求ID生成 genReqId: (req: any) => req.headers['x-request-id'] || this.generateRequestId(), // 自定义成功响应消息 customSuccessMessage: (req: any, res: any) => { return `${req.method} ${req.url} completed in ${res.responseTime}ms`; }, // 自定义错误响应消息 customErrorMessage: (req: any, _res: any, err: any) => { return `${req.method} ${req.url} failed: ${err.message}`; }, }, }; } /** * 创建传输配置 * * @param isProduction 是否为生产环境 * @param logDir 日志目录 * @param logLevel 日志级别 * @returns 传输配置对象 * @private */ private static createTransportConfig(isProduction: boolean, logDir: string, logLevel: string) { if (isProduction) { // 生产环境:多目标输出,包含日志轮转 return { targets: [ { // 应用日志(所有级别) target: 'pino/file', options: { destination: path.join(logDir, 'app.log'), mkdir: true, }, level: 'info', }, { // 错误日志(仅错误和致命错误) target: 'pino/file', options: { destination: path.join(logDir, 'error.log'), mkdir: true, }, level: 'error', }, { // 访问日志(HTTP 请求) target: 'pino/file', options: { destination: path.join(logDir, 'access.log'), mkdir: true, }, level: 'info', }, { // 控制台输出(用于容器日志收集) target: 'pino/file', options: { destination: 1, // stdout }, level: 'warn', }, ], }; } else { // 开发环境:美化输出 + 文件备份 return { targets: [ { // 控制台美化输出 target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:yyyy-mm-dd HH:MM:ss', ignore: 'pid,hostname', messageFormat: '{app} [{level}] {msg}', // 移除 customPrettifiers 以避免 Worker 线程序列化问题 }, level: logLevel, }, { // 开发环境文件输出 target: 'pino/file', options: { destination: path.join(logDir, 'dev.log'), mkdir: true, }, level: 'debug', }, ], }; } } /** * 创建序列化器配置 * * @returns 序列化器配置对象 * @private */ private static createSerializers() { return { req: (req: any) => ({ id: req.id, method: req.method, url: req.url, path: req.route?.path, parameters: req.params, query: req.query, headers: { host: req.headers.host, 'user-agent': req.headers['user-agent'], 'content-type': req.headers['content-type'], 'content-length': req.headers['content-length'], authorization: req.headers.authorization ? '[REDACTED]' : undefined, }, ip: req.ip, ips: req.ips, hostname: req.hostname, }), res: (res: any) => ({ statusCode: res.statusCode, statusMessage: res.statusMessage, headers: { 'content-type': res.getHeader('content-type'), 'content-length': res.getHeader('content-length'), }, responseTime: res.responseTime, }), err: (err: any) => ({ type: err.constructor.name, message: err.message, stack: err.stack, code: err.code, statusCode: err.statusCode, }), }; } /** * 自定义日志级别判断 * * @param _req HTTP 请求对象 * @param res HTTP 响应对象 * @param err 错误对象 * @returns 日志级别 * @private */ private static customLogLevel(_req: any, res: any, err: any) { if (res.statusCode >= 400 && res.statusCode < 500) { return 'warn'; } else if (res.statusCode >= 500 || err) { return 'error'; } else if (res.statusCode >= 300 && res.statusCode < 400) { return 'info'; } return 'info'; } /** * 生成请求ID * * @returns 唯一的请求ID * @private */ private static generateRequestId(): string { return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; } /** * 确保日志目录存在 * * @param logDir 日志目录路径 * @private */ private static ensureLogDirectory(logDir: string): void { try { if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir, { recursive: true }); console.log(`📁 Created log directory: ${logDir}`); } } catch (error) { console.error(`❌ Failed to create log directory: ${logDir}`, error); throw error; } } }