首页 / NestJS 入门教程 / 日志与序列化

NestJS 入门教程

日志与序列化

本教程共 47 篇 · 第 34 篇 · 更新于 2026-08-09 · 约 12 分钟阅读

NestJSLogger日志序列化ClassSerializerInterceptorclass-transformer

本节目标:掌握 NestJS 日志系统的配置与自定义,学会用序列化拦截器自动过滤和转换响应数据。

日志:你的排查问题的第一道防线

开发的时候你可能觉得 console.log 够用了。等项目上了生产,出了问题要翻日志排查,你就会发现 console.log 根本扛不住——没有时间戳、没有日志级别、没有上下文信息,一堆白底黑字混在一起,根本分不清谁是谁。

NestJS 自带了一套日志系统,开箱即用,也支持你换成第三方的日志库。

内置 Logger 基础用法

NestJS 默认的日志输出长这样:

[Nest] 19096   - 12/08/2024, 7:12:59 AM   [NestFactory] Starting Nest application...

方括号里是上下文名,后面是日志内容。这个格式已经比 console.log 强了不少。

在你的服务里,实例化一个 Logger 就能用:

import { Logger, Injectable } from '@nestjs/common';

@Injectable()
class CatsService {
  private readonly logger = new Logger(CatsService.name);

  findAll() {
    this.logger.log('查询所有猫咪数据');
    // ...
  }
}

Logger 提供这几个级别:logerrorwarndebugverbosefatal

控制日志级别

开发的时候你想看所有日志,生产环境只想看 error 和 warn。这很简单:

// main.ts
const app = await NestFactory.create(AppModule, {
  logger: ['error', 'warn'],
});

传一个数组,指定要显示的级别。

Note

日志级别是级联的。比如你设了 'log',比它严重的 'warn''error''fatal' 也会自动显示。不需要每个都写。

想彻底关掉日志:

const app = await NestFactory.create(AppModule, {
  logger: false,
});

自定义日志格式

内置的 ConsoleLogger 支持一些基础定制:

import { ConsoleLogger } from '@nestjs/common';

const app = await NestFactory.create(AppModule, {
  logger: new ConsoleLogger({
    prefix: 'MyApp',     // 日志前缀,默认是 "Nest"
    timestamp: true,     // 显示时间差
    colors: false,       // 关闭颜色输出
    json: true,          // 输出 JSON 格式
  }),
});

开启 JSON 格式后,日志会变成结构化输出:

{
  "level": "log",
  "pid": 19096,
  "timestamp": 1607370779834,
  "message": "Starting Nest application...",
  "context": "NestFactory"
}
Tip

生产环境强烈建议开启 JSON 格式。日志采集工具(ELK、Loki 等)解析 JSON 非常方便,纯文本格式反而难处理。

自定义 Logger

内置的 Logger 够用就行,不够用就自己写一个。实现 LoggerService 接口即可:

import { LoggerService, Injectable } from '@nestjs/common';

@Injectable()
export class MyLogger implements LoggerService {
  log(message: any, ...optionalParams: any[]) {
    // 你的逻辑:写文件、发远程、格式化...
    console.log(`[LOG] ${message}`);
  }

  error(message: any, ...optionalParams: any[]) {
    console.error(`[ERROR] ${message}`);
  }

  warn(message: any, ...optionalParams: any[]) {
    console.warn(`[WARN] ${message}`);
  }

  debug(message: any, ...optionalParams: any[]) {
    console.debug(`[DEBUG] ${message}`);
  }

  verbose(message: any, ...optionalParams: any[]) {
    console.log(`[VERBOSE] ${message}`);
  }

  fatal(message: any, ...optionalParams: any[]) {
    console.error(`[FATAL] ${message}`);
  }
}

然后在启动时用你的 Logger 替换默认的:

const app = await NestFactory.create(AppModule, {
  logger: new MyLogger(),
});

继承内置 Logger

从头写太累?可以继承 ConsoleLogger,只改你需要改的部分:

import { ConsoleLogger } from '@nestjs/common';

export class MyLogger extends ConsoleLogger {
  error(message: any, stack?: string, context?: string) {
    // 在原有逻辑基础上加你的定制
    // 比如:发告警通知
    this.sendAlert(message);
    super.error(message, stack, context);
  }

  private sendAlert(message: string) {
    // 发钉钉、发邮件...
  }
}
Tip

继承时记得调 super 方法,不然内置的格式化逻辑就丢了。

通过依赖注入使用 Logger

前面直接 new MyLogger() 的方式没有走依赖注入。如果你的 Logger 需要注入 ConfigService 之类的依赖,就得走 DI 流程。

思路是这样的:

  1. 把 Logger 注册为一个 provider
  2. app.useLogger() 让 NestJS 系统日志也用这个实例
// logger.module.ts
import { Module } from '@nestjs/common';
import { MyLogger } from './my-logger.service';

@Module({
  providers: [MyLogger],
  exports: [MyLogger],
})
export class LoggerModule {}
// main.ts
const app = await NestFactory.create(AppModule, {
  bufferLogs: true, // 缓冲日志,等自定义 Logger 就绪后再输出
});
app.useLogger(app.get(MyLogger));

bufferLogs: true 很关键——它保证在自定义 Logger 加载之前,日志不会丢失。如果启动过程中出错,NestJS 会回退到默认的 ConsoleLogger 来打印错误。

Transient 作用域

如果你希望每个服务拿到独立的 Logger 实例(各自有自己的 context),需要把 Logger 设为 TRANSIENT 作用域:

import { Injectable, Scope, ConsoleLogger } from '@nestjs/common';

@Injectable({ scope: Scope.TRANSIENT })
export class MyLogger extends ConsoleLogger {
  customLog() {
    this.log('这是一条自定义日志');
  }
}

然后在服务里注入并设置 context:

@Injectable()
export class CatsService {
  constructor(private myLogger: MyLogger) {
    this.myLogger.setContext('CatsService');
  }

  findAll() {
    this.myLogger.warn('准备返回猫咪数据');
    this.myLogger.customLog();
    // ...
  }
}
Note

为什么用 TRANSIENT?因为 setContext 会修改实例的状态。如果是单例,所有服务共享一个 Logger 实例,后设置的 context 会覆盖前面的。

接入第三方日志库

生产环境很多人用 Winston 或 Pino。接入方式就是实现 LoggerService 接口,把方法委托给第三方库。这里以 Pino 为例:

import { Injectable, LoggerService } from '@nestjs/common';
import pino from 'pino';

@Injectable()
export class PinoLogger implements LoggerService {
  private logger = pino({
    level: 'info',
    transport: {
      target: 'pino-pretty', // 开发环境美化输出
    },
  });

  log(message: any, ...args: any[]) {
    this.logger.info(message, ...args);
  }

  error(message: any, ...args: any[]) {
    this.logger.error(message, ...args);
  }

  warn(message: any, ...args: any[]) {
    this.logger.warn(message, ...args);
  }

  debug(message: any, ...args: any[]) {
    this.logger.debug(message, ...args);
  }

  verbose(message: any, ...args: any[]) {
    this.logger.trace(message, ...args);
  }

  fatal(message: any, ...args: any[]) {
    this.logger.fatal(message, ...args);
  }
}

序列化:控制返回给客户端的数据

接口返回的数据,不是数据库查出来啥就原封不动丢回去的。密码字段得去掉,某些字段要改名,关联对象可能只取其中一个属性。

这些操作叫序列化。手动处理容易遗漏,NestJS 提供了一个拦截器帮你自动完成。

ClassSerializerInterceptor

这个拦截器基于 class-transformer 包。先装依赖:

npm install class-transformer

然后在实体类上用装饰器声明规则:

import { Exclude } from 'class-transformer';

export class UserEntity {
  id: number;
  firstName: string;
  lastName: string;

  @Exclude()
  password: string;

  constructor(partial: Partial<UserEntity>) {
    Object.assign(this, partial);
  }
}

@Exclude() 标记的属性在序列化时会被自动去掉。

在控制器方法上用拦截器:

import { UseInterceptors } from '@nestjs/common';
import { ClassSerializerInterceptor } from '@nestjs/common';

@UseInterceptors(ClassSerializerInterceptor)
@Get()
findOne(): UserEntity {
  return new UserEntity({
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    password: 'secret123',
  });
}

客户端收到的响应里不会有 password 字段:

{
  "id": 1,
  "firstName": "John",
  "lastName": "Doe"
}
Tip

注意必须返回类的实例。如果你返回的是普通对象(plain object),序列化拦截器不会生效。这是很多人踩过的坑。

全局生效

不想每个方法都加 @UseInterceptors?可以全局注册:

// main.ts
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(HttpAdapterHost)));

或者在模块级别注册,这样更灵活。只要实体类上标了 @Exclude(),所有返回该类的接口都会自动过滤。

更多序列化技巧

暴露计算属性

@Expose() 可以让 getter 方法出现在序列化结果里:

import { Expose } from 'class-transformer';

export class UserEntity {
  firstName: string;
  lastName: string;

  @Expose()
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

序列化后响应里会多一个 fullName 字段。

转换嵌套对象

@Transform() 可以对嵌套属性做转换:

import { Transform } from 'class-transformer';

export class UserEntity {
  // 只返回角色名,不返回整个角色对象
  @Transform(({ value }) => value.name)
  role: RoleEntity;
}

序列化普通对象

如果你不想每次都 new UserEntity(),可以用 @SerializeOptions 指定类型:

@UseInterceptors(ClassSerializerInterceptor)
@SerializeOptions({ type: UserEntity })
@Get()
findOne(): UserEntity {
  // 返回普通对象也行,会被自动转成 UserEntity 实例再序列化
  return {
    id: 1,
    firstName: 'John',
    lastName: 'Doe',
    password: 'secret',
  };
}
Note

@SerializeOptionstype 参数会让拦截器先把普通对象转成指定类的实例,再执行序列化。这样你就不用到处手动 new 了。

小结

日志部分:

  • NestJS 内置 Logger 够用就用,不够用就继承 ConsoleLogger 扩展
  • 生产环境建议 JSON 格式输出,方便日志采集
  • bufferLogs: true 保证启动阶段日志不丢失
  • 第三方日志库(Pino、Winston)通过实现 LoggerService 接口接入

序列化部分:

  • ClassSerializerInterceptor + class-transformer 装饰器实现自动序列化
  • @Exclude() 排除敏感字段,@Expose() 暴露计算属性,@Transform() 做数据转换
  • 必须返回类实例,或者用 @SerializeOptions({ type }) 指定类型