Skip to content
第 184 / 250 章Node⏱ 12 分钟阅读

第 184 章:日志系统(Logger)

学习目标

  • 掌握内置 Logger
  • 学会自定义 Logger
  • 接入第三方日志库(Pino/Winston)
  • 实现日志分级与归档

一、内置 Logger

1.1 基本使用

typescript
import { Logger } from '@nestjs/common';

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

  create(dto: CreateUserDto) {
    this.logger.log('Creating user');          // 普通
    this.logger.error('Something failed', 'stack trace');
    this.logger.warn('Warning message');
    this.logger.debug('Debug info');
    this.logger.verbose('Verbose info');
  }
}

输出:

[Nest] 12345  - 12/15/2024, 10:30:00 AM     [UserService] Creating user
[Nest] 12345  - 12/15/2024, 10:30:00 AM     [UserService] Warning message

1.2 日志级别

级别方法优先级
loglog()默认
errorerror()最高
warnwarn()
debugdebug()
verboseverbose()最低

二、全局 Logger 配置

2.1 启动时配置

typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log'],    // 启用的级别
    // logger: false,                   // 关闭所有
  });
  await app.listen(3000);
}

2.2 进程级日志

typescript
const app = await NestFactory.create(AppModule);

app.useLogger(app.get(Logger));

// 之后所有框架日志走自定义 Logger

三、自定义 Logger

3.1 实现 LoggerService

typescript
import { LoggerService } from '@nestjs/common';

export class MyLogger implements LoggerService {
  log(message: any, ...optionalParams: any[]) {
    console.log(`[LOG] ${this.format(message)}`, ...optionalParams);
  }

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

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

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

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

  private format(message: any): string {
    const ts = new Date().toISOString();
    return `${ts} ${typeof message === 'object' ? JSON.stringify(message) : message}`;
  }
}

3.2 替换全局 Logger

typescript
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useLogger(new MyLogger());
  await app.listen(3000);
}

四、Pino(高性能日志库)

4.1 安装

bash
pnpm add nestjs-pino pino-http

4.2 注册

typescript
import { LoggerModule } from 'nestjs-pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
        transport: {
          target: 'pino-pretty',    // 开发用 pretty
          options: { singleLine: true },
        },
        // production JSON
        // transport: undefined,
      },
    }),
  ],
})
export class AppModule {}

4.3 注入使用

typescript
import { PinoLogger } from 'nestjs-pino';

@Injectable()
export class UserService {
  constructor(private logger: PinoLogger) {
    this.logger.setContext(UserService.name);
  }

  create(dto: CreateUserDto) {
    this.logger.info('Creating user');
    this.logger.warn({ dto }, 'Email already exists');
    this.logger.error({ userId: 1 }, 'Create failed');
  }
}

4.4 请求日志

Pino 自动记录所有 HTTP 请求,无需手写:

json
{
  "level": 30,
  "time": 1700000000000,
  "req": {
    "method": "POST",
    "url": "/api/users",
    "remoteAddress": "::1",
    "headers": {}
  },
  "res": {
    "statusCode": 201
  },
  "responseTime": 12
}

五、Winston(功能丰富)

5.1 安装

bash
pnpm add nest-winston winston

5.2 配置

typescript
import { WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import 'winston-daily-rotate-file';   // 按天归档

@Module({
  imports: [
    WinstonModule.forRoot({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json(),
      ),
      transports: [
        new winston.transports.Console({
          format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple(),
          ),
        }),
        new winston.transports.DailyRotateFile({
          filename: 'logs/app-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          zippedArchive: true,
          maxSize: '20m',
          maxFiles: '14d',
        }),
        new winston.transports.DailyRotateFile({
          filename: 'logs/error-%DATE%.log',
          datePattern: 'YYYY-MM-DD',
          level: 'error',
        }),
      ],
    }),
  ],
})
export class AppModule {}

5.3 使用

typescript
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';

@Injectable()
export class UserService {
  constructor(
    @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: Logger,
  ) {}

  create(dto: CreateUserDto) {
    this.logger.log('Creating user');
    this.logger.error('Failed', { trace: '...' });
    this.logger.warn('warning', { context: 'UserService' });
  }
}

六、日志中间件

typescript
// logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger('HTTP');

  intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    const { method, url, body, query, params, headers } = req;
    const userAgent = headers['user-agent'] || '';
    const ip = req.ip;

    const start = Date.now();

    return next.handle().pipe(
      tap(() => {
        const res = context.switchToHttp().getResponse();
        this.logger.log(
          `${method} ${url} ${res.statusCode} ${Date.now() - start}ms ${ip} ${userAgent}`,
        );
      }),
    );
  }
}

七、日志规范

7.1 结构化日志

typescript
// ❌ 不推荐
this.logger.log('User with id 1 not found');

// ✅ 推荐:结构化
this.logger.warn({
  message: 'User not found',
  userId: 1,
  action: 'findOne',
  duration: 12,
});

7.2 包含上下文

typescript
// ❌ 不推荐
this.logger.error('failed');

// ✅ 推荐:带 context
this.logger.error('failed', 'Stack trace here', 'UserService');

7.3 区分环境

typescript
const isDev = process.env.NODE_ENV === 'development';

this.logger.debug('Debug info', isDev ? JSON.stringify(data) : '');
// 生产不打 debug

八、日志归档与清理

8.1 按天归档

typescript
new winston.transports.DailyRotateFile({
  filename: 'logs/app-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,      // 自动压缩
  maxSize: '20m',           // 单文件 20MB
  maxFiles: '30d',          // 保留 30 天
})

8.2 集中式日志(ELK)

typescript
// pino + elasticsearch
pinoHttp: {
  level: 'info',
  transport: {
    target: 'pino-elasticsearch',
    options: {
      node: process.env.ES_NODE,
      index: 'myapp-logs',
    },
  },
},

九、本章小结

特点
内置 Logger简单够用
Pino高性能,JSON 友好
Winston功能丰富,支持归档
Bunyan老牌日志库
级别用途
error错误
warn警告
log一般
debug调试
verbose详细

动手练习

  1. 用内置 Logger 记录 Service 操作
  2. 集成 nestjs-pino,替换默认日志
  3. 配置 winston 按天归档,保留 14 天
  4. 写一个 LoggingInterceptor 记录所有 HTTP 请求

推荐阅读


下一章:第 185 章:数据验证与 DTO

本站基于 VitePress 构建 · 由 Codebook 团队维护