- 新增高级日志配置工厂类,支持环境差异化配置 - 新增日志管理服务,提供定时清理和健康监控 - 支持生产环境多文件分类输出(app.log、error.log、access.log) - 支持开发环境美化输出和文件备份 - 添加自动日志清理和统计功能
278 lines
7.6 KiB
TypeScript
278 lines
7.6 KiB
TypeScript
/**
|
||
* 日志配置模块
|
||
*
|
||
* 功能描述:
|
||
* - 提供详细的日志配置选项
|
||
* - 支持日志文件轮转和管理
|
||
* - 根据环境自动调整日志策略
|
||
* - 提供日志文件清理和归档功能
|
||
*
|
||
* @author 开发团队
|
||
* @version 1.0.0
|
||
* @since 2024-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: {
|
||
time: (timestamp: any) => `🕐 ${timestamp}`,
|
||
level: (logLevel: any) => {
|
||
const levelEmojis: Record<number, string> = {
|
||
10: '🔍', // trace
|
||
20: '🐛', // debug
|
||
30: '📝', // info
|
||
40: '⚠️', // warn
|
||
50: '❌', // error
|
||
60: '💀', // fatal
|
||
};
|
||
return `${levelEmojis[logLevel] || '📝'} ${logLevel}`;
|
||
},
|
||
},
|
||
},
|
||
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).substr(2, 9)}`;
|
||
}
|
||
|
||
/**
|
||
* 确保日志目录存在
|
||
*
|
||
* @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;
|
||
}
|
||
}
|
||
} |