日志与序列化
本教程共 47 篇 · 第 34 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:掌握 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 提供这几个级别:log、error、warn、debug、verbose、fatal。
控制日志级别
开发的时候你想看所有日志,生产环境只想看 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 流程。
思路是这样的:
- 把 Logger 注册为一个 provider
- 用
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
@SerializeOptions的type参数会让拦截器先把普通对象转成指定类的实例,再执行序列化。这样你就不用到处手动new了。
小结
日志部分:
- NestJS 内置 Logger 够用就用,不够用就继承
ConsoleLogger扩展 - 生产环境建议 JSON 格式输出,方便日志采集
- 用
bufferLogs: true保证启动阶段日志不丢失 - 第三方日志库(Pino、Winston)通过实现
LoggerService接口接入
序列化部分:
ClassSerializerInterceptor+class-transformer装饰器实现自动序列化@Exclude()排除敏感字段,@Expose()暴露计算属性,@Transform()做数据转换- 必须返回类实例,或者用
@SerializeOptions({ type })指定类型