NestJS 项目中 Winston 日志配置全攻略:开发、测试与生产环境的最佳实践
为什么 Winston?
准备工作:安装 Winston 和 @nestjs/platform-express
基础配置:创建 Winston 日志服务
在 NestJS 模块中使用 Winston 日志服务
不同环境的配置策略
开发环境 (Development)
测试环境 (Testing)
生产环境 (Production)
环境配置的实现方式
1. 环境变量
你好,老伙计!我是你的老朋友,一个热衷于技术分享的“老码农”。
今天,我们来聊聊 NestJS 项目中至关重要的话题——日志配置。尤其是在不同的环境(开发、测试、生产)下,如何灵活、安全地配置 Winston 日志,并遵循最佳实践。别担心,我会用最通俗易懂的方式,结合实际案例,让你轻松掌握!
为什么 Winston?
首先,为什么要选择 Winston?
Winston 是一个功能强大、灵活的日志记录库,它提供了多种日志级别、输出格式和存储方式,能满足各种复杂的日志需求。与其他日志库相比,Winston 的优势在于:
- 灵活性高:支持自定义日志级别、格式化器、传输器(transports),可以轻松地将日志输出到控制台、文件、数据库、云服务等。
- 易于扩展:提供了丰富的插件和中间件,可以方便地扩展 Winston 的功能,如日志压缩、日志审计等。
- 社区活跃:拥有庞大的用户群体和活跃的社区,可以方便地找到解决方案和技术支持。
准备工作:安装 Winston 和 @nestjs/platform-express
在开始配置之前,我们需要先安装 Winston 和 NestJS 的 Express 平台。
npm install winston @nestjs/platform-express --save
安装完成后,我们就可以开始配置 Winston 了。
基础配置:创建 Winston 日志服务
首先,我们创建一个 Winston 日志服务,用于集中管理日志配置和输出。
// src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; const { combine, timestamp, printf, colorize, errors } = format; @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor() { // 自定义日志格式 const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; return `${formattedTimestamp} [${level.toUpperCase()}] ${formattedMessage}`; }); // 创建 DailyRotateFile 传输器 const dailyRotateFileTransport = new DailyRotateFile({ filename: 'application-%DATE%.log', // 日志文件名格式 dirname: 'logs', // 日志文件存放目录 datePattern: 'YYYY-MM-DD', // 日期格式 zippedArchive: true, // 是否压缩旧的日志文件 maxSize: '20m', // 单个日志文件的最大大小 maxFiles: '14d', // 保留的日志文件数量 format: combine( timestamp(), errors({ stack: true }), logFormat, ), }); this.logger = createLogger({ level: 'info', // 默认日志级别 format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), // 为控制台输出添加颜色 logFormat, ), transports: [ new transports.Console(), // 输出到控制台 dailyRotateFileTransport, // 输出到文件 ], }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, trace); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } }
在这个例子中,我们:
- 导入必要的模块:
createLogger
、format
、transports
和DailyRotateFile
。 - 定义日志格式:使用
printf
格式化日志输出,包括时间戳、日志级别和消息内容。 - 创建
DailyRotateFile
传输器:用于将日志写入文件,并进行日志轮转(按日期、大小等)。 - 创建
createLogger
实例:配置日志级别、格式和传输器。这里我们使用了控制台和文件两种传输器。 - 实现
LoggerService
接口:定义了log
、error
、warn
、debug
和verbose
方法,用于输出不同级别的日志。
在 NestJS 模块中使用 Winston 日志服务
接下来,我们需要在 NestJS 模块中使用我们创建的 Winston 日志服务。
// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { WinstonLoggerService } from './logger/winston.logger'; @Module({ imports: [], controllers: [AppController], providers: [AppService, WinstonLoggerService], }) export class AppModule {}
// src/app.controller.ts import { Controller, Get, Inject, LoggerService } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor( private readonly appService: AppService, @Inject(WinstonLoggerService) private readonly logger: LoggerService, ) {} @Get() getHello(): string { this.logger.log('Hello World! This is a log message.'); this.logger.error('This is an error message.', 'Something went wrong.'); this.logger.warn('This is a warning message.'); this.logger.debug('This is a debug message.'); this.logger.verbose('This is a verbose message.'); return this.appService.getHello(); } }
在这个例子中,我们:
- 在
AppModule
中将WinstonLoggerService
注册为 provider,这样它就可以被注入到其他组件中。 - 在
AppController
中注入WinstonLoggerService
,并通过它来记录日志。
不同环境的配置策略
现在,我们来讨论在不同的环境(开发、测试、生产)下,如何配置 Winston 日志。
开发环境 (Development)
开发环境的重点在于快速迭代和调试。因此,我们需要:
- 详细的日志信息:输出所有级别的日志,包括调试信息(debug)和详细信息(verbose)。
- 控制台输出:方便在开发过程中查看日志。
- 错误堆栈信息:便于快速定位问题。
- 禁用日志轮转:避免频繁的日志文件切换,影响开发效率。
// src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; const { combine, timestamp, printf, colorize, errors } = format; @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor() { const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; return `${formattedTimestamp} [${level.toUpperCase()}] ${formattedMessage}`; }); // 开发环境配置:禁用文件日志,只输出到控制台 const transportsDev = [ new transports.Console({ level: 'debug', format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), }), ]; this.logger = createLogger({ level: 'debug', // 设置为 debug 级别,输出所有日志 format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), transports: transportsDev, }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, trace); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } }
关键修改:
level: 'debug'
:将日志级别设置为debug
,确保输出所有级别的日志。transports
:只配置Console
传输器,不输出到文件。
测试环境 (Testing)
测试环境的重点在于自动化测试和问题排查。我们需要:
- 控制台输出:方便查看测试结果和日志。
- 简洁的日志信息:避免输出过多的调试信息,影响测试结果的可读性。
- 可选的文件输出:根据需要,可以将日志输出到文件,方便分析测试失败原因。
// src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; const { combine, timestamp, printf, colorize, errors } = format; @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor() { const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; return `${formattedTimestamp} [${level.toUpperCase()}] ${formattedMessage}`; }); // 测试环境配置:根据需要选择输出到控制台或文件 const transportsTest = [ new transports.Console({ level: 'info', format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), }), // 可以选择开启文件输出,方便分析测试结果 // new DailyRotateFile({ // filename: 'test-application-%DATE%.log', // dirname: 'logs', // datePattern: 'YYYY-MM-DD', // zippedArchive: true, // maxSize: '20m', // maxFiles: '14d', // format: combine( // timestamp(), // errors({ stack: true }), // logFormat, // ), // }), ]; this.logger = createLogger({ level: 'info', // 设置为 info 级别,输出关键信息 format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), transports: transportsTest, }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, trace); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } }
关键修改:
level: 'info'
:将日志级别设置为info
,输出关键信息。transports
:默认只配置Console
传输器,可以根据需要开启文件输出。
生产环境 (Production)
生产环境的重点在于稳定性和安全性。我们需要:
- 关键日志信息:只输出错误(error)和警告(warn)级别的日志,减少磁盘 I/O 压力。
- 文件输出:将日志输出到文件,方便审计和问题排查。
- 日志轮转:避免日志文件过大,占用磁盘空间。
- 安全配置:保护敏感信息,如密码、API 密钥等。
// src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; const { combine, timestamp, printf, errors } = format; @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor() { const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; return `${formattedTimestamp} [${level.toUpperCase()}] ${formattedMessage}`; }); // 生产环境配置:只输出 error 和 warn 级别的日志到文件 const transportsProd = [ new DailyRotateFile({ filename: 'application-%DATE%.log', dirname: 'logs', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d', format: combine( timestamp(), errors({ stack: true }), logFormat, ), level: 'warn', // 生产环境只记录 warn 和 error 级别的日志 }), ]; this.logger = createLogger({ level: 'warn', // 设置为 warn 级别,只记录 warn 和 error 级别的日志 format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), logFormat, ), transports: transportsProd, }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, trace); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } }
关键修改:
level: 'warn'
:将日志级别设置为warn
,只输出错误和警告级别的日志。transports
:只配置DailyRotateFile
传输器,将日志输出到文件。- 日志文件配置:设置
filename
、dirname
、datePattern
、zippedArchive
、maxSize
和maxFiles
等参数,实现日志轮转。
环境配置的实现方式
那么,如何根据不同的环境来加载不同的配置呢?这里介绍几种常用的方法:
1. 环境变量
使用环境变量是最常见也是最灵活的方法。我们可以在不同的环境中设置不同的环境变量,然后在代码中读取这些变量,从而加载不同的配置。
设置环境变量:
在
.env
文件中设置环境变量(例如,NODE_ENV=development
、NODE_ENV=production
等)。# .env.development NODE_ENV=development # .env.production NODE_ENV=production 注意: 实际项目中
.env
文件通常会根据不同的环境分开,比如.env.development
、.env.production
等。读取环境变量:
在 NestJS 项目中,可以使用
@nestjs/config
模块来读取环境变量。
npm install @nestjs/config --save
```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { WinstonLoggerService } from './logger/winston.logger'; @Module({ imports: [ConfigModule.forRoot()], // 导入 ConfigModule controllers: [AppController], providers: [AppService, WinstonLoggerService, ConfigService], }) export class AppModule {} ``` 然后,在 `WinstonLoggerService` 中,我们可以通过 `ConfigService` 来读取环境变量: ```typescript // src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; import { ConfigService } from '@nestjs/config'; const { combine, timestamp, printf, colorize, errors } = format; @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor(private readonly configService: ConfigService) { const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; return `${formattedTimestamp} [${level.toUpperCase()}] ${formattedMessage}`; }); const env = this.configService.get('NODE_ENV') || 'development'; // 根据环境变量加载不同的配置 let transports; if (env === 'production') { transports = [ new DailyRotateFile({ filename: 'application-%DATE%.log', dirname: 'logs', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d', format: combine( timestamp(), errors({ stack: true }), logFormat, ), level: 'warn', }), ]; } else { transports = [ new transports.Console({ level: 'debug', format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), }), ]; } this.logger = createLogger({ level: env === 'production' ? 'warn' : 'debug', format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), transports, }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, trace); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } } ``` 3. **运行项目**: 在不同的环境下运行项目,例如: ```bash # 开发环境 npm run start:dev # 生产环境 NODE_ENV=production npm run start:prod ``` ### 2. 配置文件 除了环境变量,我们还可以使用配置文件(如 `config.ts` 或 `config.json`)来存储不同环境的配置。这种方式更易于维护和管理,尤其是在配置项较多的时候。 1. **创建配置文件**: 创建一个 `config` 目录,并在其中创建不同环境的配置文件,例如: ``` config/ ├── development.ts ├── production.ts └── index.ts ``` ```typescript // config/development.ts export default { logLevel: 'debug', transports: ['console'], }; ``` ```typescript // config/production.ts export default { logLevel: 'warn', transports: ['file'], }; ``` ```typescript // config/index.ts import developmentConfig from './development'; import productionConfig from './production'; const env = process.env.NODE_ENV || 'development'; const config = { development: developmentConfig, production: productionConfig, }[env]; export default config; ``` 2. **读取配置文件**: 在 `WinstonLoggerService` 中,我们可以读取配置文件: ```typescript // src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; import config from '../../config'; // 导入配置文件 const { combine, timestamp, printf, colorize, errors } = format; @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor() { const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; return `${formattedTimestamp} [${level.toUpperCase()}] ${formattedMessage}`; }); // 根据配置文件加载不同的配置 let transportsConfig; if (config.transports.includes('console')) { transportsConfig = new transports.Console({ level: config.logLevel, format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), }); } const dailyRotateFileTransport = new DailyRotateFile({ filename: 'application-%DATE%.log', dirname: 'logs', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d', format: combine( timestamp(), errors({ stack: true }), logFormat, ), level: config.logLevel, }); const transports = config.transports.includes('file') ? [transportsConfig, dailyRotateFileTransport] : [transportsConfig]; this.logger = createLogger({ level: config.logLevel, format: combine( timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), errors({ stack: true }), colorize(), logFormat, ), transports, }); } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(message, trace); } warn(message: string) { this.logger.warn(message); } debug(message: string) { this.logger.debug(message); } verbose(message: string) { this.logger.verbose(message); } } ``` ### 3. 使用不同的日志服务实例 对于一些复杂的项目,你还可以为不同的环境创建不同的日志服务实例。这种方式可以更灵活地控制日志的输出,但是代码量会增加。 ## 保护敏感信息 在日志记录中,保护敏感信息至关重要。我们需要避免将密码、API 密钥、数据库连接字符串等敏感信息记录到日志中。 1. **过滤敏感信息**: 在记录日志之前,对要记录的信息进行过滤,去除或替换敏感信息。例如,可以使用正则表达式或自定义函数来过滤敏感信息。 ```typescript // src/logger/winston.logger.ts import { Injectable, LoggerService } from '@nestjs/common'; import { createLogger, format, transports } from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; const { combine, timestamp, printf, colorize, errors } = format; // 过滤敏感信息 function filterSensitiveInfo(message: string): string { // 替换密码 message = message.replace(/password: \w+/g, 'password: ***'); // 替换 API 密钥 message = message.replace(/apiKey: \w+/g, 'apiKey: ***'); return message; } @Injectable() export class WinstonLoggerService implements LoggerService { private readonly logger; constructor() { const logFormat = printf(({ level, message, timestamp, stack }) => { const formattedTimestamp = timestamp; const formattedMessage = stack ? `${message} - ${stack}` : message; // 在输出前过滤敏感信息 const filteredMessage = filterSensitiveInfo(formattedMessage); return `${formattedTimestamp} [${level.toUpperCase()}] ${filteredMessage}`; }); // ... (其他配置) } log(message: string) { this.logger.info(message); } error(message: string, trace: string) { this.logger.error(filterSensitiveInfo(message), trace); } warn(message: string) { this.logger.warn(filterSensitiveInfo(message)); } debug(message: string) { this.logger.debug(filterSensitiveInfo(message)); } verbose(message: string) { this.logger.verbose(filterSensitiveInfo(message)); } } ``` 2. **使用占位符**: 在记录日志时,可以使用占位符来代替敏感信息。例如,可以使用 `{{password}}` 来代替密码,然后在记录日志时,将占位符替换为实际值。 ```typescript this.logger.error('数据库连接失败,密码为 {{password }}', { password: '***' }); ``` 3. **加密敏感信息**: 如果需要在日志中记录敏感信息,可以对这些信息进行加密,并在需要时进行解密。但这种方法会增加代码复杂度,需要谨慎使用。 ## 最佳实践 除了上述配置和实现方法,还有一些最佳实践可以帮助你更好地使用 Winston 日志: * **统一日志格式**:使用统一的日志格式,包括时间戳、日志级别、消息内容等,方便日志的检索和分析。 * **明确日志级别**:根据不同的情况,选择合适的日志级别。例如,使用 `debug` 级别记录详细的调试信息,使用 `error` 级别记录错误信息。 * **日志轮转**:在生产环境中,使用日志轮转功能,避免日志文件过大,占用磁盘空间。 * **日志审计**:定期审计日志,发现潜在的安全问题和性能问题。 * **集中式日志管理**:将日志输出到集中式日志管理系统,如 ELK Stack (Elasticsearch, Logstash, Kibana)、Splunk 等,方便日志的集中存储、检索和分析。 * **异步日志**:对于高并发的场景,可以使用异步日志,避免日志输出阻塞应用程序的执行。 * **异常处理**:对于未捕获的异常,使用全局异常过滤器来记录日志,确保不会丢失任何错误信息。 ## 总结 好了,老伙计!今天我们一起探讨了 NestJS 项目中 Winston 日志的配置和最佳实践。希望这些内容能帮助你在开发、测试和生产环境中,更好地配置和管理日志,提高项目的稳定性和可维护性。 记住,日志是程序开发中不可或缺的一部分。合理地配置和使用日志,可以帮助我们快速定位问题、分析性能、进行安全审计,最终提升整个项目的质量。 如果你还有其他问题或想法,欢迎随时和我交流!我们下次再见!