Skip to content

NestJS Pino 日志集成完整指南

NestJS 项目中集成 Pino 日志系统的最佳实践指南,包含从基础到企业级的完整配置方案。

核心知识点

1️⃣ 为什么选择 Pino?

  • 性能卓越: Pino 是 Node.js 中最快的日志库之一,基于异步写入,几乎不阻塞主线程
  • 结构化日志: 输出 JSON 格式,便于日志收集和分析(如 ELK、Loki 等)
  • 生态丰富: 支持多种传输方式(文件、控制台、远程服务等)
  • 低开销: 即使在高并发场景下也能保持稳定的性能表现

2️⃣ nestjs-pino 的作用

nestjs-pino 是 NestJS 生态中成熟的日志集成方案,提供了:

  • 自动请求日志记录(包含请求 ID、响应时间等)
  • 与 NestJS 依赖注入系统的无缝集成
  • 模块级别的日志配置灵活性

3️⃣ 核心配置概念

Pino Http 配置结构

typescript
{
  level: 'info',           // 日志级别
  transport: {            // 日志传输方式
    targets: []           // 支持多目标输出
  },
  serializers: {},        // 自定义序列化器
  base: {},              // 全局基础字段
  autoLogging: {}        // 自动日志配置
}

日志级别优先级

trace < debug < info < warn < error < fatal

设置某个级别后,只会记录该级别及以上的日志。


三种配置方案

方案一:基础配置

适用场景: 小型项目、快速原型开发

特点:

  • 配置简单直接
  • 开发/生产环境分离
  • 基础的日志滚动功能

核心代码:

typescript
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
import { join } from 'path';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        transport: {
          targets: [
            process.env.NODE_ENV === 'development'
              ? {
                  level: 'info',
                  target: 'pino-pretty',  // 开发环境美化输出
                  options: {
                    colorize: true,
                    translateTime: 'SYS:standard',
                    ignore: 'pid,hostname',
                    singleLine: true,
                  }
                }
              : {
                  level: 'info',
                  target: 'pino-roll',    // 生产环境文件滚动
                  options: {
                    file: join('logs', 'app.log'),
                    frequency: 'daily',
                    size: '10m',
                    mkdir: true,
                  }
                }
          ]
        }
      }
    })
  ],
})
export class AppModule {}

方案二:配置抽离

适用场景: 中型项目、需要维护多个环境配置

特点:

  • 配置文件独立,便于维护
  • 支持更复杂的环境判断逻辑
  • 代码结构更清晰

logger.config.ts:

typescript
import { join } from 'path';
import pino from 'pino';
import type { LoggerOptions } from 'pino';

interface CustomLoggerConfig extends LoggerOptions {
  level: string;
  development: {
    transport: {
      targets: Array<{
        level: string;
        target: string;
        options?: Record<string, unknown>;
      }>;
    };
  };
  production: {
    transport: {
      targets: Array<{
        level: string;
        target: string;
        options?: Record<string, unknown>;
      }>;
    };
  };
}

export const loggerConfig: CustomLoggerConfig = {
  level: process.env.LOG_LEVEL || 'info',

  development: {
    transport: {
      targets: [
        {
          level: 'debug',
          target: 'pino-pretty',
          options: {
            colorize: true,
            translateTime: 'SYS:standard',
            ignore: 'pid,hostname',
            singleLine: true,
          },
        },
      ],
    },
  },

  production: {
    transport: {
      targets: [
        {
          level: 'info',
          target: 'pino-roll',
          options: {
            file: join('logs', 'app-logs', 'app.log'),
            frequency: 'daily',
            size: '20m',
            mkdir: true,
            maxSize: '100m',
            maxFiles: 30,
            compression: 'gzip',
          },
        },
      ],
    },
  },

  serializers: {
    req: (req) => ({
      method: req.method,
      url: req.url,
      requestId: req.id || req.headers?.['x-request-id'],
    }),
    res: (res) => ({
      statusCode: res.statusCode,
    }),
  },

  base: {
    environment: process.env.NODE_ENV || 'development',
    app: 'nestjs-app',
  },

  timestamp: pino.stdTimeFunctions.isoTime as unknown as () => string,

  autoLogging: {
    ignore: (req) => {
      const ignoredPaths = ['/health', '/ping'];
      return ignoredPaths.some((path) => req.url?.startsWith(path));
    },
  },
};

export default loggerConfig;

app.module.ts:

typescript
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
import loggerConfig from './config/logger.config';

const pinoHttp = {
  ...loggerConfig,
  transport:
    process.env.NODE_ENV === 'production'
      ? loggerConfig.production.transport
      : loggerConfig.development.transport,
};

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp,
    }),
  ],
})
export class AppModule {}

方案三:企业级配置

适用场景: 生产环境、大型项目、微服务架构

特点:

  • 完整的序列化器配置
  • 多目标日志输出(普通日志 + 错误日志分离)
  • 请求上下文自动记录
  • 敏感信息过滤
  • 健康检查路径忽略

核心配置:

typescript
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
import { join } from 'path';
import pino from 'pino';

@Module({
  imports: [
    LoggerModule.forRoot({
      pinoHttp: {
        level: process.env.LOG_LEVEL || 'info',

        // 多目标输出
        transport:
          process.env.NODE_ENV === 'development'
            ? {
                targets: [
                  {
                    level: 'info',
                    target: 'pino-pretty',
                    options: {
                      colorize: true,
                      translateTime: 'SYS:standard',
                      ignore: 'pid,hostname',
                      singleLine: true,
                    },
                  },
                ],
              }
            : {
                targets: [
                  // 普通日志
                  {
                    level: 'info',
                    target: 'pino-roll',
                    options: {
                      file: join('logs', 'app-logs', 'app.log'),
                      frequency: 'daily',
                      size: '20m',
                      mkdir: true,
                      maxSize: '100m',
                      maxFiles: 30,
                      compression: 'gzip',
                    },
                  },
                  // 错误日志单独存储
                  {
                    level: 'error',
                    target: 'pino/file',
                    options: {
                      destination: join('logs', 'app-logs', 'error.log'),
                      mkdir: true,
                    },
                  },
                ],
              },

        // 请求序列化
        serializers: {
          req: (req) => ({
            method: req.method,
            url: req.url,
            requestId: req.id || req.headers?.['x-request-id'],
            userId: req.user?.id,
            ip: req.ip || req.headers?.['x-forwarded-for'],
            userAgent: req.headers?.['user-agent'],
            contentType: req.headers?.['content-type'],
          }),
          res: (res) => ({
            statusCode: res.statusCode,
          }),
          err: pino.stdSerializers.err as unknown as (err: unknown) => unknown,
        },

        // 自定义全局字段
        base: {
          environment: process.env.NODE_ENV,
          app: 'nestjs-app',
        },

        // 自动日志过滤
        autoLogging: {
          ignore: (req) => {
            const ignoredPaths = ['/health', '/ping', '/metrics'];
            return ignoredPaths.some((path) => req.url?.startsWith(path));
          },
        },

        // 时间格式
        timestamp: pino.stdTimeFunctions.isoTime as unknown as () => string,
      },
    }),
  ],
})
export class AppModule {}

关键依赖说明

1. nestjs-pino

作用: NestJS 与 Pino 的桥接模块

关键功能:

  • 提供 Logger 服务,可通过依赖注入使用
  • 自动记录 HTTP 请求/响应日志
  • 支持请求 ID 追踪

使用示例:

typescript
import { Controller, Get } from '@nestjs/common';
import { Logger } from 'nestjs-pino';

@Controller('users')
export class UsersController {
  constructor(private readonly logger: Logger) {}

  @Get()
  findAll() {
    this.logger.info('获取用户列表', { userId: 123 });
    this.logger.error('操作失败', { error: '详细错误信息' });
    return [];
  }
}

2. pino

作用: 核心日志库

重要类型:

  • LoggerOptions: 定义完整的配置类型
  • stdSerializers: 标准序列化器(如 err 用于错误对象)

3. pino-pretty

作用: 开发环境日志美化

常用配置:

typescript
{
  colorize: true,              // 彩色输出
  translateTime: 'SYS:standard', // 人类可读时间格式
  singleLine: true,            // 单行显示
  ignore: 'pid,hostname',      // 忽略的字段
  messageFormat: '[{level}] {msg}' // 自定义消息格式
}

4. pino-roll

作用: 生产环境日志文件滚动

关键参数:

typescript
{
  file: 'logs/app.log',        // 日志文件路径
  frequency: 'daily',          // 滚动频率: daily | hourly
  size: '20m',                 // 单文件大小限制
  maxSize: '100m',             // 总大小限制
  maxFiles: 30,                // 保留文件数量
  compression: 'gzip',         // 压缩算法
  mkdir: true                  // 自动创建目录
}

最佳实践总结

✅ 推荐做法

1. 环境分离配置

  • 开发环境: 使用 pino-pretty 提升可读性
  • 生产环境: 使用 pino-roll 实现文件滚动和压缩

2. 日志级别设置

  • 开发环境: debug 级别,便于调试
  • 生产环境: info 级别,平衡详细度和性能

3. 敏感信息保护

通过 serializers 只记录必要字段:

typescript
serializers: {
  req: (req) => ({
    method: req.method,
    url: req.url,
    // ❌ 不要记录: req.headers.authorization
    // ❌ 不要记录: req.headers.cookie
    // ✅ 可以记录: req.headers['user-agent']
  })
}

4. 请求追踪

typescript
serializers: {
  req: (req) => ({
    requestId: req.id || req.headers?.['x-request-id'],
    userId: req.user?.id,
    ip: req.ip || req.headers?.['x-forwarded-for'],
  })
}

5. 日志分类存储

  • 普通日志: 所有级别,用于日常分析
  • 错误日志: 仅 error 级别,用于快速排查问题

6. 健康检查忽略

typescript
autoLogging: {
  ignore: (req) => {
    return ['/health', '/ping', '/metrics'].some(path =>
      req.url?.startsWith(path)
    );
  }
}

❌ 避免做法

1. 不要在生产环境使用 pino-pretty

  • 美化输出会降低性能
  • JSON 格式更利于日志收集系统处理

2. 不要记录完整的请求头

可能包含敏感信息:

  • authorization / cookie / set-cookie
  • x-api-key / x-auth-token
  • 其他自定义认证头

3. 不要设置过低的日志级别

  • trace 级别会产生大量日志
  • 影响性能和存储成本

4. 不要忽略错误堆栈

typescript
// ✅ 正确
err: pino.stdSerializers.err

// ❌ 错误
err: (err) => ({ message: err.message })

环境变量配置

.env 文件配置

bash
# 应用环境
NODE_ENV=production

# 日志级别: trace | debug | info | warn | error | fatal
LOG_LEVEL=info

# 应用版本(用于日志追踪)
APP_VERSION=1.0.0

# 日志存储路径
LOG_DIR=./logs

# 是否启用请求日志
ENABLE_REQUEST_LOGGING=true

# 是否启用错误堆栈跟踪
ENABLE_STACK_TRACE=true

package.json 脚本

json
{
  "scripts": {
    "start": "cross-env NODE_ENV=production nest start",
    "start:dev": "cross-env NODE_ENV=development nest start --watch",
    "start:prod": "cross-env NODE_ENV=production node dist/main"
  }
}

说明:

  • cross-env 确保跨平台环境变量设置兼容性
  • 不同环境通过 NODE_ENV 自动切换配置

常见问题排查

Q1: 日志文件没有生成?

检查清单:

  1. ✅ 确认 NODE_ENV 环境变量正确设置
  2. ✅ 检查日志目录是否有写入权限
  3. ✅ 生产环境才会写入文件,开发环境输出到控制台
  4. ✅ 确认 pino-roll 已正确安装

Q2: 日志格式不符合预期?

解决方案:

  • 开发环境应该看到美化输出,如果看到 JSON 说明配置错误
  • 检查 pino-pretty 是否正确安装
  • 确认 transport 配置的 target 路径正确

Q3: 如何实现日志远程收集?

推荐方案:

  1. 使用 pino-multistream:
typescript
import pino from 'pino';
import multistream from 'pino-multistream';

const streams = multistream([
  { level: 'info', stream: process.stdout },
  { level: 'error', stream: fs.createWriteStream('error.log') }
]);
  1. 或使用 Filebeat + Logstash + Elasticsearch 组合
  2. Pino 的 JSON 输出天然兼容主流日志系统

Q4: 如何在单元测试中验证日志?

方案:

typescript
import pino from 'pino';

const transport = pino.transport({
  target: 'pino/file',
  options: { destination: Buffer.toString() }
});

const logger = pino(transport);

// 测试中可以读取 buffer 内容验证

Q5: 生产环境日志量太大怎么办?

优化方案:

  1. 提高日志级别到 infowarn
  2. 增加健康检查路径的忽略规则
  3. 调整日志滚动配置,减少保留天数
  4. 考虑使用日志采样策略

使用示例

在 Controller 中使用

typescript
import { Controller, Get, Param } from '@nestjs/common';
import { Logger } from 'nestjs-pino';

@Controller('users')
export class UsersController {
  constructor(private readonly logger: Logger) {}

  @Get()
  findAll() {
    this.logger.info('获取所有用户');
    return [];
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    this.logger.info('获取用户详情', { userId: id });
    return {};
  }
}

在 Service 中使用

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

@Injectable()
export class UsersService {
  constructor(private readonly logger: Logger) {}

  async create(data: any) {
    this.logger.debug('创建用户', { userData: data });

    try {
      // 业务逻辑
      this.logger.info('用户创建成功', { userId: data.id });
    } catch (error) {
      this.logger.error('用户创建失败', {
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }
}

异步日志记录

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

@Injectable()
export class OrderService {
  constructor(private readonly logger: Logger) {}

  async processOrder(orderId: string) {
    const startTime = Date.now();

    try {
      // 处理订单逻辑

      const duration = Date.now() - startTime;
      this.logger.info('订单处理完成', {
        orderId,
        duration: `${duration}ms`,
      });
    } catch (error) {
      this.logger.error('订单处理失败', {
        orderId,
        error: error.message,
        duration: `${Date.now() - startTime}ms`,
      });
      throw error;
    }
  }
}

进阶技巧

1. 自定义日志格式

typescript
serializers: {
  req: (req) => ({
    method: req.method,
    url: req.url,
    // 添加自定义字段
    correlationId: req.headers?.['x-correlation-id'],
    sessionId: req.session?.id,
  })
}

2. 日志分级存储策略

typescript
targets: [
  // DEBUG 级别 - 仅开发
  {
    level: 'debug',
    target: 'pino/file',
    options: { destination: join('logs', 'debug.log') }
  },
  // INFO 级别 - 一般信息
  {
    level: 'info',
    target: 'pino-roll',
    options: { file: join('logs', 'app.log'), maxFiles: 7 }
  },
  // ERROR 级别 - 错误单独存储
  {
    level: 'error',
    target: 'pino-roll',
    options: {
      file: join('logs', 'error.log'),
      maxFiles: 90  // 保留 90 天
    }
  }
]

3. 性能监控日志

typescript
// 添加性能监控中间件
app.use((req, res, next) => {
  const startTime = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - startTime;
    if (duration > 1000) {
      req.log.warn('慢请求', {
        url: req.url,
        method: req.method,
        duration: `${duration}ms`,
      });
    }
  });

  next();
});

总结

NestJS 集成 Pino 的核心价值:

  1. 性能优先: 通过异步日志记录,最小化对业务逻辑的影响
  2. 结构化输出: JSON 格式便于日志分析和监控系统集成
  3. 环境适配: 开发环境美化,生产环境高效
  4. 生产就绪: 日志滚动、压缩、分类等企业级特性
  5. 可观测性: 请求追踪、错误完整记录、性能监控

核心价值: 通过结构化日志提升系统可观测性,为问题排查和性能优化提供强有力的数据支撑。

基于 VitePress 构建