NestJS 入门(10):日志——为什么常用 Winston?

上一篇: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"
}

采集端按 levelmessagetimestamp 建索引即可。

本地开发若想看彩色文本,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,
});

字段尽量固定:requestIdmethodpathstatusCodedurationMserror

后面搜「这次请求慢在哪」才搜得到。


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=...

要点:

  1. 进来时生成 requestId(或读网关的 x-request-id
  2. 写进 req,响应头带上 X-Request-Id,方便前端/网关对照
  3. 业务日志都带上同一个 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 永远不要进日志

对照本系列:

  1. 分层、注入、模块、连库清楚了吗?
  2. 统一信封和异常 Filter 能对上日志里的 status 吗?
  3. 这条日志有级别吗?有 requestId 吗?会不会把密钥打出去?

系列导航

相关推荐
何事误红尘1 小时前
ZYNQ学习ARM裸机笔记():VITIS
学习
步行cgn2 小时前
MyBatis <where> 标签详解:让你的动态 SQL 更优雅、更安全
后端
RainCity2 小时前
Java Swing 自定义组件库分享(十六)
java·笔记·后端
沙盘客2 小时前
AFSIM 14篇 C++ 插件开发:扩展你的仿真能力
c++·后端
桦说编程2 小时前
如何对待中断异常:一个被吞掉的 InterruptedException 引发的思考
后端
不懂的浪漫2 小时前
吴恩达《AI Engineering Skills Map》译读:四项核心能力与持续学习底座
人工智能·学习
蒸蒸yyyyzwd3 小时前
cpp选手秋招准备 学习笔记day7 webserver
笔记·学习
lifallen3 小时前
DeepSeek Harness:把 Agent 做成可替换的运行时插件树
人工智能·学习·ai·开源软件·ai编程
自学机械人的小白ing4 小时前
深度学习系统学习
人工智能·深度学习·学习