WEBKT

NestJS 项目中 Winston 日志配置全攻略:开发、测试与生产环境的最佳实践

59 0 0 0

为什么 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);
}
}

在这个例子中,我们:

  1. 导入必要的模块createLoggerformattransportsDailyRotateFile
  2. 定义日志格式:使用 printf 格式化日志输出,包括时间戳、日志级别和消息内容。
  3. 创建 DailyRotateFile 传输器:用于将日志写入文件,并进行日志轮转(按日期、大小等)。
  4. 创建 createLogger 实例:配置日志级别、格式和传输器。这里我们使用了控制台和文件两种传输器。
  5. 实现 LoggerService 接口:定义了 logerrorwarndebugverbose 方法,用于输出不同级别的日志。

在 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();
}
}

在这个例子中,我们:

  1. AppModule 中将 WinstonLoggerService 注册为 provider,这样它就可以被注入到其他组件中。
  2. 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 传输器,将日志输出到文件。
  • 日志文件配置:设置 filenamedirnamedatePatternzippedArchivemaxSizemaxFiles 等参数,实现日志轮转。

环境配置的实现方式

那么,如何根据不同的环境来加载不同的配置呢?这里介绍几种常用的方法:

1. 环境变量

使用环境变量是最常见也是最灵活的方法。我们可以在不同的环境中设置不同的环境变量,然后在代码中读取这些变量,从而加载不同的配置。

  1. 设置环境变量

    .env 文件中设置环境变量(例如,NODE_ENV=developmentNODE_ENV=production 等)。

    # .env.development
    NODE_ENV=development
    # .env.production
    NODE_ENV=production

    注意: 实际项目中 .env 文件通常会根据不同的环境分开,比如 .env.development.env.production 等。

  2. 读取环境变量

    在 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 日志的配置和最佳实践。希望这些内容能帮助你在开发、测试和生产环境中,更好地配置和管理日志,提高项目的稳定性和可维护性。
记住,日志是程序开发中不可或缺的一部分。合理地配置和使用日志,可以帮助我们快速定位问题、分析性能、进行安全审计,最终提升整个项目的质量。
如果你还有其他问题或想法,欢迎随时和我交流!我们下次再见!
老码农 NestJSWinston日志配置

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7876