上一篇:NestJS 入门(9):连上数据库,SQL 写在哪? 讲了数据和表结构。
服务跑起来之后,排障靠的是日志:哪次请求 401 了、登录失败是哪个 email、某次生成耗了多久。
Nest 自带 Logger,本地够用。生产里 Node 社区最常见的是 Winston:分级、JSON、写文件/控制台、按天切割,一套就能接上。
1. 为什么不要满地 console.log
typescript
console.log('login ok', user); // 可能把 password 哈希打出来
console.log('error', e); // 没有级别,不好过滤
console.log(new Date(), req.url); // 格式每次不一样,机器搜不动
问题很具体:
| 需求 | console.log |
正经日志库 |
|---|---|---|
| 只要 error | 全混在 stdout | level: 'error' |
| 按请求串起来 | 没有统一字段 | 带 requestId |
| 给 ELK / Loki 采集 | 自由文本难解析 | 一行一条 JSON |
| 本地彩色、生产 JSON | 自己 if | transport 分开配 |
所以目标不是「能打印」,而是:有级别、有结构、能按一次请求把前后日志对上。
2. Nest 自带 Logger:入门先会这个
typescript
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class AuthService {
private readonly logger = new Logger(AuthService.name);
async login(email: string, password: string) {
this.logger.log(`login attempt: ${email}`);
this.logger.warn('invalid credentials');
this.logger.error('token sign failed', err?.stack);
}
}
- 第二个参数(类名)会出现在日志里,方便看是哪个 Service 打的
- 级别:
log/error/warn/debug/verbose
够写业务、够看启动信息。不够的地方是:默认偏给人看的文本,切文件、JSON、按天轮转要自己拼。这时上 Winston。
3. Winston 是干什么的?
Winston 本身和 Nest 无关,是 Node 日志库。核心就三块:
text
logger.info('hello', { userId: '1' })
→ format(时间、JSON、颜色)
→ transports(打到哪:Console / File / 远程)
Nest 里通常用 nest-winston,把 Winston 接到 Nest 的 LoggerService 接口上,这样:
new Logger(Xxx.name)仍能用app.useLogger(...)后,框架自己的启动日志也走 Winston
bash
npm i winston nest-winston
4. 接到 Nest:最小可跑
main.ts:
typescript
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
],
}),
});
await app.listen(3000);
}
生产一行会接近:
json
{
"level": "info",
"message": "API service running on http://localhost:3000",
"timestamp": "2026-08-15T10:00:00.000Z",
"context": "Bootstrap"
}
采集端按 level、message、timestamp 建索引即可。
本地开发若想看彩色文本,Console 可以换成:
typescript
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
),
})
本地给人看,生产给机器看 :两个 transport 或按 NODE_ENV 分支即可。
5. 业务里怎么打
注入 Nest 的 logger(nest-winston 提供后,仍用官方 Logger 最省事):
typescript
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async findByEmail(email: string) {
this.logger.debug(`findByEmail ${email}`);
const user = await this.users.findOne({ where: { email } });
if (!user) {
this.logger.warn(`user not found: ${email}`);
}
return user;
}
}
也可以做成全局模块,注入自己的封装(和第六篇 @Global() 同一套路),统一带上 service: 'api':
typescript
this.logger.info('POST /api/auth/login 200', {
requestId,
method: 'POST',
path: '/api/auth/login',
statusCode: 200,
durationMs: 12,
});
字段尽量固定:requestId、method、path、statusCode、durationMs、error。
后面搜「这次请求慢在哪」才搜得到。
6. 请求级日志:Interceptor 打出入
第四篇的拦截器管信封;日志也可以挂一个全局 Interceptor,在请求结束时打一行:
text
INFO GET /api/projects 200 durationMs=35 requestId=1723-ab12
ERROR POST /api/auth/login 500 durationMs=8 requestId=1723-cd99 error=...
要点:
- 进来时生成
requestId(或读网关的x-request-id) - 写进
req,响应头带上X-Request-Id,方便前端/网关对照 - 业务日志都带上同一个
requestId
没有 requestId 时,十条 user not found 对不上是哪次点击。
7. 级别怎么用
| 级别 | 什么时候 |
|---|---|
error |
失败了,需要人看:连库失败、未捕获异常 |
warn |
不正常但还能继续:校验失败、降级、token 过期 |
info |
正常关键路径:启动成功、请求进出(可抽样) |
debug |
开发细节:SQL、中间变量;生产默认关掉 |
生产 LOG_LEVEL=info 即可。
debug 全开会把磁盘和费用打爆,也更容易把敏感信息带出去。
8. 写文件、按天切(知道即可)
typescript
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
Docker / K8s 里更常见的是 只打 stdout JSON ,由平台采集,应用自己不落盘。
自己管文件时再加 winston-daily-rotate-file 按天切割、限制保留天数。
入门先 Console JSON;等真正部署再决定文件还是 stdout。
9. 不要记进日志的东西
- 密码、token、cookie、API Key
- 身份证号、完整银行卡
- 超大 body / embedding 向量
登录失败打 email 可以;打 password 不行。
Authorization: Bearer ... 不要整段进日志。
10. 和「自己包一层 console」的关系
有的项目不引入 Winston,而是:
typescript
console.log(JSON.stringify({
level: 'info',
message,
service: 'api',
timestamp: new Date().toISOString(),
requestId,
}));
结构也对,采集也能吃。缺的是:transport、按级别分文件、和 Nest 内置 Logger 打通、生态里现成的 rotate。
小项目可以自制 JSON console;要分级、切文件、统一框架日志,再上 Winston。
换库不换习惯:级别 + 结构化字段 + requestId。
11. 小结
- 日志要分级、要结构,不要散落
console.log - Nest
Logger够入门;生产常用 Winston + nest-winston - 一行 JSON:
level/message/timestamp/requestId - 请求进出适合全局 Interceptor;业务细节打在 Service
- 生产默认
info,密码和 token 永远不要进日志
对照本系列:
- 分层、注入、模块、连库清楚了吗?
- 统一信封和异常 Filter 能对上日志里的 status 吗?
- 这条日志有级别吗?有 requestId 吗?会不会把密钥打出去?