✅ 你的日志需求拆解(我逐条对)
你要的是:
-
✅ 记录 用户操作
- 哪个接口
- 哪些参数
- 用户信息(可选)
-
✅ 区分日志级别
- info(操作日志)
- warn(警告)
- error(异常、报错)
-
✅ 按日期生成文件
-
✅ 文件名带:年月日 + 时分秒
-
✅ 单个文件不能太大,自动切分
-
✅ 日志保留 N 天,自动删除
-
✅ NestJS 可全局使用
👉 这套需求,winston + winston-daily-rotate-file 正好 100% 覆盖
✅ 技术选型(结论)
| 能力 | 方案 |
|---|---|
| 日志框架 | winston |
| 文件切割 | winston-daily-rotate-file |
| Nest 集成 | 自定义 Logger + Interceptor + ExceptionFilter |
| 日志格式 | JSON(方便后期 ELK / Loki) |
① 安装依赖
npm i winston winston-daily-rotate-file
② 日志目录结构(推荐)
logs/
├── info/
├── warn/
├── error/
③ 创建 Winston 日志实例(核心)
src/logger/winston.logger.ts
import * as winston from 'winston';
import 'winston-daily-rotate-file';
const baseFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.json(),
);
function createRotateTransport(level: string) {
return new winston.transports.DailyRotateFile({
level,
dirname: `logs/${level}`,
filename: `%DATE%-${level}.log`,
datePattern: 'YYYY-MM-DD-HH-mm-ss',
zippedArchive: true,
maxSize: '20m', // 单文件最大 20MB
maxFiles: '14d', // 保留 14 天
});
}
export const logger = winston.createLogger({
format: baseFormat,
transports: [
createRotateTransport('info'),
createRotateTransport('warn'),
createRotateTransport('error'),
],
});
📌 重点你关心的点:
| 需求 | 对应配置 |
|---|---|
| 文件名时间 | datePattern |
| 文件大小切割 | maxSize |
| 自动删除 | maxFiles: '14d' |
| 分级目录 | dirname |
④ NestJS 全局 Logger 封装
src/logger/app.logger.ts
import { LoggerService } from '@nestjs/common';
import { logger } from './winston.logger';
export class AppLogger implements LoggerService {
log(message: any, context?: string) {
logger.info({ message, context });
}
warn(message: any, context?: string) {
logger.warn({ message, context });
}
error(message: any, trace?: string, context?: string) {
logger.error({ message, trace, context });
}
}
⑤ 记录【用户操作日志】——Interceptor(核心)
src/logger/operation.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { logger } from './winston.logger';
@Injectable()
export class OperationLogInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const { method, url, body, query, params } = req;
const user = req.user || null;
const start = Date.now();
return next.handle().pipe(
tap(() => {
logger.info({
type: 'operation',
method,
url,
params,
query,
body,
user,
duration: Date.now() - start,
});
}),
);
}
}
📌 这一步 直接满足你:
- 哪个接口
- 哪些参数
- 用户信息
- 耗时
⑥ 全局异常日志(error)
src/logger/http-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';
import { logger } from './winston.logger';
@Catch()
export class AllExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const req = ctx.getRequest();
logger.error({
type: 'exception',
method: req.method,
url: req.url,
body: req.body,
error: exception.message,
stack: exception.stack,
});
throw exception;
}
}
⑦ main.ts 全局启用
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(new AppLogger());
app.useGlobalInterceptors(new OperationLogInterceptor());
app.useGlobalFilters(new AllExceptionFilter());
await app.listen(3000);
}
📂 最终日志示例
logs/info/2026-01-05-18-20-01-info.log
{
"timestamp": "2026-01-05 18:20:01",
"type": "operation",
"method": "POST",
"url": "/user/login",
"body": { "username": "test" },
"duration": 32
}
logs/error/2026-01-05-18-22-11-error.log
{
"timestamp": "2026-01-05 18:22:11",
"type": "exception",
"url": "/user/login",
"error": "password error"
}