1. 使用 Winston 替换 NestJS 默认日志系统
步骤:
-
安装依赖:
pnpm install winston @types/winston -
创建自定义 Logger 类(
src/logger/winston.logger.ts):import { LoggerService, Injectable } from '@nestjs/common'; import { createLogger, format, transports, Logger } from 'winston'; @Injectable() export class WinstonLogger implements LoggerService { private readonly logger: Logger; constructor() { this.logger = createLogger({ level: 'info', format: format.combine( format.timestamp(), format.json(), ), transports: [new transports.Console()], }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, { trace }); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } } -
全局启用 Logger(
main.ts) 目的是替换掉框架默认的日志系统,统一使用winstin日志import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { WinstonLogger } from './logger/winston.logger'; async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: new WinstonLogger(), // 覆盖默认 Logger }); await app.listen(3000); } bootstrap();
4.**创建全局 Logger 模块(logger.module.ts):
全局模块是方便在各个模块中直接使用日志,不用再在每个模块中导入日志模块
import { Global, Module } from '@nestjs/common';
import { WinstonLogger } from './winston.logger';
@Global() // 标记为全局模块
@Module({
providers: [WinstonLogger],
exports: [WinstonLogger], // 导出供其他模块使用
})
export class LoggerModule {}
-
在根模块中导入(
app.module.ts):import { LoggerModule } from './logger/logger.module'; @Module({ imports: [LoggerModule], // 导入全局模块 // ...其他配置 }) export class AppModule {} -
Service 中直接使用(无需修改
app.module.ts的providers):@Injectable() export class UserService { constructor(private readonly logger: WinstonLogger) {} // 自动注入 }
2. 配置 Winston 使用文件滚动日志
步骤:
-
安装依赖:
pnpm install winston-daily-rotate-file -
修改 Winston 配置(
winston.logger.ts):import * as DailyRotateFile from 'winston-daily-rotate-file'; // 在构造函数中添加文件滚动配置 constructor() { this.logger = createLogger({ // ... 其他配置 transports: [ new transports.Console(), new DailyRotateFile({ dirname: 'logs', filename: 'application-%DATE%.log', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d', }), ], }); }
3. 数据库日志(以 TypeORM 为例)
步骤:
-
创建 TypeORM 自定义 Logger(
src/logger/typeorm.logger.ts):import { Logger } from 'typeorm'; import { WinstonLogger } from './winston.logger'; export class TypeOrmCustomLogger implements Logger { constructor(private readonly winstonLogger: WinstonLogger) {} logQuery(query: string, parameters?: any[]) { this.winstonLogger.verbose(`Query: ${query} ${parameters ? JSON.stringify(parameters) : ''}`); } logError(error: string, query: string, parameters?: any[]) { this.winstonLogger.error(`Error: ${error}`, { query, parameters }); } // 其他方法(logSchema, logMigration, logQuerySlow)类似 } -
在 TypeORM 配置中使用自定义 Logger(
app.module.ts):import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmCustomLogger } from './logger/typeorm.logger'; import { WinstonLogger } from './logger/winston.logger'; @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', // ... 其他配置 logger: new TypeOrmCustomLogger(new WinstonLogger()), logging: true, // 开启所有类型日志 }), ], }) export class AppModule {}
4. Redis 日志(以 ioredis 为例)
步骤:
- 在 Redis 客户端配置中绑定事件(
redis.provider.ts):import { Logger } from '@nestjs/common'; import Redis from 'ioredis'; import { WinstonLogger } from './winston.logger'; export const redisProvider = { provide: 'REDIS_CLIENT', useFactory: (logger: WinstonLogger) => { const client = new Redis({ host: 'localhost', port: 6379, }); client.on('connect', () => logger.log('Redis connected')); client.on('error', (err) => logger.error('Redis error', err)); return client; }, inject: [WinstonLogger], };
5. 日志记录到 MySQL 数据库
步骤:
-
安装依赖:
npm install winston-mysql -
创建 MySQL Transport 配置(修改
winston.logger.ts):import * as MySQLTransport from 'winston-mysql'; // 在构造函数中添加 MySQL Transport constructor() { this.logger = createLogger({ // ... 其他配置 transports: [ // ... 其他 transports new MySQLTransport({ host: 'localhost', user: 'root', password: 'password', database: 'logs_db', table: 'system_logs', fields: { level: 'level', message: 'message', timestamp: 'timestamp', meta: 'meta', }, }), ], }); } -
创建 MySQL 表:
CREATE TABLE system_logs ( id INT AUTO_INCREMENT PRIMARY KEY, level VARCHAR(20), message TEXT, timestamp DATETIME, meta JSON );
5.HTTP 拦截器日志
1. 创建 HTTP 日志拦截器
新建文件 src/interceptors/http-logging.interceptor.ts,实现请求/响应日志记录:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { WinstonLogger } from '../logger/winston.logger';
@Injectable()
export class HttpLoggingInterceptor implements NestInterceptor {
constructor(private readonly logger: WinstonLogger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const httpContext = context.switchToHttp();
const request = httpContext.getRequest();
const response = httpContext.getResponse();
const { method, originalUrl, body, headers } = request;
const userAgent = headers['user-agent'] || '';
const ip = request.ip || headers['x-forwarded-for'] || 'unknown';
// 记录请求开始
this.logger.log({
message: 'HTTP Request Start',
type: 'HTTP_REQUEST',
method,
url: originalUrl,
userAgent,
ip,
// 可选:根据需求决定是否记录请求体(敏感信息需过滤)
// body: this.sanitizeBody(body),
});
const startTime = Date.now();
return next.handle().pipe(
tap({
next: (data) => {
// 记录成功响应
const duration = Date.now() - startTime;
this.logger.log({
message: 'HTTP Response Success',
type: 'HTTP_RESPONSE',
method,
url: originalUrl,
statusCode: response.statusCode,
duration: `${duration}ms`,
// 可选:记录响应体(数据量大时可关闭)
// responseBody: data,
});
},
error: (err) => {
// 记录错误响应
const duration = Date.now() - startTime;
this.logger.error({
message: 'HTTP Response Error',
type: 'HTTP_ERROR',
method,
url: originalUrl,
statusCode: err.getStatus?.() || 500,
duration: `${duration}ms`,
error: err.message,
stack: err.stack, // 仅在开发环境记录堆栈
});
},
}),
);
}
// 可选:敏感信息脱敏
private sanitizeBody(body: Record<string, any>): Record<string, any> {
const sanitized = { ...body };
if (sanitized.password) sanitized.password = '******';
if (sanitized.token) sanitized.token = '******';
return sanitized;
}
}
2. 注册全局拦截器
在 app.module.ts 中全局启用拦截器:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { WinstonLogger } from './logger/winston.logger';
import { HttpLoggingInterceptor } from './interceptors/http-logging.interceptor';
@Module({
providers: [
// 其他 providers...
{
provide: APP_INTERCEPTOR,
useClass: HttpLoggingInterceptor,
},
WinstonLogger, // 确保 Logger 可注入
],
})
export class AppModule {}
3. 日志输出示例
在 winston.logger.ts 的日志格式中,format.json() 会将日志转为 JSON 结构。发送一个 GET /api/users 请求后,日志文件会输出:
请求日志:
{
"level": "info",
"message": "HTTP Request Start",
"type": "HTTP_REQUEST",
"method": "GET",
"url": "/api/users",
"userAgent": "PostmanRuntime/7.32.3",
"ip": "127.0.0.1",
"timestamp": "2023-10-05T08:20:00.000Z"
}
响应成功日志:
{
"level": "info",
"message": "HTTP Response Success",
"type": "HTTP_RESPONSE",
"method": "GET",
"url": "/api/users",
"statusCode": 200,
"duration": "45ms",
"timestamp": "2023-10-05T08:20:00.045Z"
}
响应错误日志:
{
"level": "error",
"message": "HTTP Response Error",
"type": "HTTP_ERROR",
"method": "POST",
"url": "/api/login",
"statusCode": 401,
"duration": "12ms",
"error": "Invalid credentials",
"stack": "Error: Invalid credentials\n at AuthService.login (...)",
"timestamp": "2023-10-05T08:21:30.000Z"
}