日志系统
一、日志级别与使用场景
error — 需要立即处理的错误(未捕获异常、数据库连接失败)
warn — 潜在问题(重试成功、配置缺失但有默认值)
log — 业务关键事件(用户注册、订单创建)
debug — 调试信息(SQL 查询、函数参数)— 生产环境关闭
verbose — 详细的跟踪信息 — 仅开发时使用二、NestJS 内置 Logger
typescript
// 在 Service 中使用
@Injectable()
export class UsersService {
// 用类名作为 context,方便定位日志来源
private readonly logger = new Logger(UsersService.name);
async create(dto: CreateUserDto) {
this.logger.log(`创建用户: email=${dto.email}`);
try {
const user = await this.userRepo.save(dto);
this.logger.log(`用户创建成功: id=${user.id}`);
return user;
} catch (err) {
// error 的第二参数是堆栈信息
this.logger.error(`用户创建失败: ${err.message}`, err.stack);
throw err;
}
}
}
// 在 main.ts 中控制日志级别
const app = await NestFactory.create(AppModule, {
logger: process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log'] // 生产:只记录重要日志
: ['error', 'warn', 'log', 'debug', 'verbose'], // 开发:全量
});三、Winston 集成(生产级结构化日志)
bash
npm install nest-winston winston winston-daily-rotate-filetypescript
// logger/logger.module.ts
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import 'winston-daily-rotate-file';
const { combine, timestamp, json, colorize, printf, errors } = winston.format;
// 开发环境:彩色可读格式
const devFormat = combine(
colorize(),
timestamp({ format: 'HH:mm:ss' }),
errors({ stack: true }), // 包含堆栈信息
printf(({ level, message, timestamp, context, traceId, ...meta }) => {
const ctx = context ? `[${context}]` : '';
const tid = traceId ? `[${traceId}]` : '';
const extra = Object.keys(meta).length ? JSON.stringify(meta) : '';
return `${timestamp} ${level} ${ctx}${tid} ${message} ${extra}`;
}),
);
// 生产环境:JSON 格式(便于 ELK/Datadog 解析)
const prodFormat = combine(
timestamp(),
errors({ stack: true }),
json(),
);
export const winstonConfig = {
transports: [
// 控制台输出
new winston.transports.Console({
format: process.env.NODE_ENV === 'production' ? prodFormat : devFormat,
}),
// 错误日志文件(日志滚动,自动删除 30 天前的文件)
new winston.transports.DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
maxFiles: '30d',
format: prodFormat,
}),
// 全量日志文件
new winston.transports.DailyRotateFile({
filename: 'logs/combined-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxFiles: '14d',
format: prodFormat,
}),
],
};typescript
// app.module.ts 中集成
@Module({
imports: [
WinstonModule.forRoot(winstonConfig),
],
})
export class AppModule {}
// main.ts 替换 NestJS 默认 Logger
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
const app = await NestFactory.create(AppModule);
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));四、请求日志拦截器
每个请求自动记录方法、路径、状态码、耗时、用户:
typescript
// common/interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url, ip, body } = req;
const userId = req.user?.id ?? 'anonymous';
const traceId = req.traceId ?? '-';
const startTime = Date.now();
return next.handle().pipe(
tap((data) => {
const res = context.switchToHttp().getResponse();
const duration = Date.now() - startTime;
this.logger.log(
`${method} ${url} ${res.statusCode} ${duration}ms`,
{ userId, traceId, ip, duration },
);
}),
catchError((err) => {
const duration = Date.now() - startTime;
this.logger.error(
`${method} ${url} ERROR ${duration}ms: ${err.message}`,
{ userId, traceId, stack: err.stack },
);
throw err; // 重新抛出,让异常过滤器处理
}),
);
}
}typescript
// 全局注册
app.useGlobalInterceptors(new LoggingInterceptor());五、TraceId 追踪链路
为每个请求生成唯一 ID,方便在日志中追踪完整链路:
typescript
// common/middleware/trace.middleware.ts
import { randomUUID } from 'crypto';
@Injectable()
export class TraceMiddleware implements NestMiddleware {
use(req: Request & { traceId?: string }, res: Response, next: () => void) {
// 支持从上游(nginx、CDN)传递的 traceId
req.traceId = (req.headers['x-trace-id'] as string) ?? randomUUID();
// 将 traceId 透传到响应头,方便前端报错时定位
(res as any).setHeader('X-Trace-Id', req.traceId);
next();
}
}
// app.module.ts
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TraceMiddleware).forRoutes('*');
}
}typescript
// 在 Service 中使用 AsyncLocalStorage 传递 traceId(不需要每个函数传参)
import { AsyncLocalStorage } from 'async_hooks';
export const requestContext = new AsyncLocalStorage<{ traceId: string }>();
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
create(dto: CreateUserDto) {
const { traceId } = requestContext.getStore() ?? {};
this.logger.log('创建用户', { traceId, email: dto.email });
}
}六、结构化日志最佳实践
typescript
// ✅ 结构化日志:把变量作为独立字段,而不是字符串插值
this.logger.log('用户登录', {
userId: user.id,
email: user.email,
ip: clientIp,
userAgent: req.headers['user-agent'],
});
// ❌ 字符串插值:难以在日志平台过滤和统计
this.logger.log(`用户 ${user.id}(${user.email})从 ${clientIp} 登录`);
// ✅ 错误日志包含足够上下文
this.logger.error('支付失败', {
orderId: order.id,
amount: order.amount,
userId: order.userId,
errorCode: paymentError.code,
stack: paymentError.stack,
});
// ✅ 敏感信息脱敏
this.logger.log('用户注册', {
email: email.replace(/(.{3}).*(@.*)/, '$1***$2'), // a***@example.com
// 不要记录密码、Token、银行卡号等
});七、生产日志配置建议
| 环境 | 控制台 | 文件 | 级别 | 格式 |
|---|---|---|---|---|
| 开发 | 彩色人类可读 | 无 | debug+ | printf |
| 测试 | 简洁(不干扰测试输出) | 无 | error | 简单 |
| 生产 | JSON | 滚动文件 | warn+ | JSON |
typescript
// 测试环境关闭日志(避免污染测试输出)
const app = await NestFactory.create(AppModule, {
logger: process.env.NODE_ENV === 'test' ? false : undefined,
});可运行 Demo:
practice/07-extensions— NestJS Logger 在 Gateway/Scheduler/Queue 中的用法
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| 日志中没有 traceId | 未使用 AsyncLocalStorage 传播 context | 在中间件中 store.run({ traceId }, next);Logger 中读取 store.getStore() |
| 生产日志文件过大 | 未配置日志轮转 | DailyRotateFile 设置 maxSize: '50m',maxFiles: '30d' |
| 结构化日志被当字符串打印 | Logger.log('xxx', { key: val }) | Winston 用 logger.info({ message: 'xxx', key: val }) 输出 JSON |
| 测试中充满日志噪音 | 测试环境未关闭日志 | NestFactory.create(AppModule, { logger: false }) 在测试中静默 |