Nest.js 日志系统 与 如何集成 winston
Nest
附带一个默认的内部日志记录器实现,它在实例化过程中以及在一些不同的情况下使用,比如发生异常等等(例如系统记录)。这由 @nestjs/common 包中的 Logger 类实现。你可以全面控制如下的日志系统的行为:
- 完全禁用日志
- 指定日志系统详细水平(例如,展示错误,警告,调试信息等)
- 覆盖默认日志记录器的时间戳(例如使用 ISO8601 标准作为日期格式)
- 完全覆盖默认日志记录器
- 通过扩展自定义默认日志记录器
- 使用依赖注入来简化编写和测试你的应用
- 你也可以使用内置日志记录器,或者创建你自己的应用来记录你自己应用水平的事件和消息。
更多高级的日志功能,可以使用任何 Node.js 日志包,比如 Winston,来生成一个完全自定义的生产环境水平的日志系统。
基础配置
Nest
的日志是可以设置关闭的,在应用构建中(NestFactory.create()
),只需要传递第二个参数(可选的,Object
)设置logger
为false
即可。(默认为true
)
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
declare const module: any;
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: false // 关闭所有日志
});
app.setGlobalPrefix('api');
await app.listen(3001);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
declare const module: any;
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: false // 关闭所有日志
});
app.setGlobalPrefix('api');
await app.listen(3001);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
它同时还支持设置显示等级,只设置一个字符串形式的 logger 属性数组以确定要显示的日志水平:
// main.ts
//... 省略
const app = await NestFactory.create(AppModule, {
logger: ["error", "warn"], // log等级的日志将不会显示出来
});
//... 省略
// main.ts
//... 省略
const app = await NestFactory.create(AppModule, {
logger: ["error", "warn"], // log等级的日志将不会显示出来
});
//... 省略
等级优先级为 error
> warn
> log
, 即如果只设置了logger: ['warn']
的情况下,error
依旧会打印出来。
在 Controller (控制器)中使用
使用的方法也极其简单,只需要new logger
后,即可使用,同时在new
的过程中,还可以传递一个 name 来标识是在哪个controller
下输出的日志,它是全局的注入,所以这里并不需要在**.model.ts
中去完成注入。
export class UserController {
private logger = new Looger(UserController.name) // 标识
// 用法
this.logger.warn()
this.logger.log()
this.logger.error()
}
export class UserController {
private logger = new Looger(UserController.name) // 标识
// 用法
this.logger.warn()
this.logger.log()
this.logger.error()
}
集成 winston
一般官方内置的日志系统,多用于在开发中进行调试。 如果存在把日志写入到文件一类的操作,那么内置的会存在一定的局限性。 为此采用winston
库就可以大大改善对日志的记录方式(有其他的库解决方案),它是一款强大的全面而且高度集成的日志库。
先进行一个安装,这里安装winston
本体,以及与Nest
的集成方案nest-winston
;
pnpm i nest-winston winston
pnpm i nest-winston winston
如果要集成winston
简单的说,就是去替换当前内置的日志系统(自定义日志),首先,先在main.ts
处进行初始化日志的输出模式与替换;
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as winston from 'winston';
import { utilities, WinstonModule } from 'nest-winston';
declare const module: any;
async function bootstrap() {
//配置winston
const instance = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike(),
),
}),
],
});
const app = await NestFactory.create(AppModule, {
// 替换内置logger
logger: WinstonModule.createLogger({
instance,
}),
});
app.setGlobalPrefix('api');
await app.listen(3001);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as winston from 'winston';
import { utilities, WinstonModule } from 'nest-winston';
declare const module: any;
async function bootstrap() {
//配置winston
const instance = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike(),
),
}),
],
});
const app = await NestFactory.create(AppModule, {
// 替换内置logger
logger: WinstonModule.createLogger({
instance,
}),
});
app.setGlobalPrefix('api');
await app.listen(3001);
if (module.hot) {
module.hot.accept();
module.hot.dispose(() => app.close());
}
}
bootstrap();
然后就可以通过依赖注入的形式传递到每个模块中,需要注意的是,这里因为属于跨模块使用,所以需要在app.modules.ts
注入的时候,同时也要把exports
出来,并且还需要把app.modules.ts
当成一个全局模块,不然Nest.js
会不认识这个模块。
// app.module.ts
@Global()
@Module({
imports: [
//省略
],
providers: [Logger],
exports: [Logger],
})
// app.module.ts
@Global()
@Module({
imports: [
//省略
],
providers: [Logger],
exports: [Logger],
})
使用的情况,这里拿user.controller.ts
举例;
// user.controller.ts
export class UserController {
constructor(
private userService: UserService,
@Inject(Logger) private readonly logger: Logger,
) { }
}
// user.controller.ts
export class UserController {
constructor(
private userService: UserService,
@Inject(Logger) private readonly logger: Logger,
) { }
}
生成本地日志文件
有了上述集成了winston
后,如果要生成当前日志到本地项目文件夹,还需要借助winston-daily-rotate-file
库;
pnpm i winston-daily-rotate-file
pnpm i winston-daily-rotate-file
然后回到配置winston
的main.ts
中,创建一个DailyRotateFile
配置;
// 省略
import "winston-daily-rotate-file";
async function bootstrap() {
//配置winston
const instance = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike()
),
}),
new winston.transports.DailyRotateFile({
dirname: process.cwd() + "/log", // 文件到哪个目录
filename: "alterEgo_log-%DATE%.log", // 输出日志文件名
datePattern: "YYYY-MM-DD",
zippedArchive: true, // 是否压缩
maxSize: "20m",
maxFiles: "7d",
level: "warn", // 不同 level 会划分到不同文件
}),
new winston.transports.DailyRotateFile({
dirname: process.cwd() + "/logs", // 文件到哪个目录
filename: "Err_alterEgo_log-%DATE%.log", // 输出日志文件名
datePattern: "YYYY-MM-DD",
zippedArchive: true, // 是否压缩
maxSize: "20m",
maxFiles: "7d",
level: "error",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple()
),
}),
],
});
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
instance,
}),
});
// 省略
}
// 省略
import "winston-daily-rotate-file";
async function bootstrap() {
//配置winston
const instance = winston.createLogger({
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike()
),
}),
new winston.transports.DailyRotateFile({
dirname: process.cwd() + "/log", // 文件到哪个目录
filename: "alterEgo_log-%DATE%.log", // 输出日志文件名
datePattern: "YYYY-MM-DD",
zippedArchive: true, // 是否压缩
maxSize: "20m",
maxFiles: "7d",
level: "warn", // 不同 level 会划分到不同文件
}),
new winston.transports.DailyRotateFile({
dirname: process.cwd() + "/logs", // 文件到哪个目录
filename: "Err_alterEgo_log-%DATE%.log", // 输出日志文件名
datePattern: "YYYY-MM-DD",
zippedArchive: true, // 是否压缩
maxSize: "20m",
maxFiles: "7d",
level: "error",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple()
),
}),
],
});
const app = await NestFactory.create(AppModule, {
logger: WinstonModule.createLogger({
instance,
}),
});
// 省略
}
其中,DailyRotateFile
支持的属性分别为:
属性名 | 作用 |
---|---|
dirname | 文件输出到哪个目录下的文件夹 |
filename | 日志文件名,其中 %DATE% 为时间 |
datePattern | 时间格式 |
zippedArchive | 是否需要压缩,默认为 false |
maxSize | 单个日志文件大小上限,超出则会换成另一个文件 |
maxFiles | 日志最大保留时间 |
level | 当前配置的日志写入等级(比如如果是 error,warn 与 info 将不会写入到文件中) |
format | 自定义输出模式 |
现在,只要使用到logger
的时候就会根据level
记录到本地项目中。
结合异常过滤器来记录日志
一般来说接口的报错往往都需要记录到本地的日志当中,但在各个接口活着服务中,写上try.. catch
的代码会让代码看起来非常的不美观,Nest
中生命周期的最后一环就是过滤器,其中内置的异常层负责处理整个应用程序中的所有抛出的异常。当捕获到未处理的异常时,最终用户将收到友好的响应,所以不必去写try.. catch
,当报错的时候交给异常过滤器捕获,然后在其写入日志即可。
先在 src 创建filter/http-exception.filter.ts
, 它存在则固定的写法,方法必须含有catch
方法,并且需要被@Catch
装饰器修饰;
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from "@nestjs/common";
@Catch()
export class HttpExceptionFiltern implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {}
}
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from "@nestjs/common";
@Catch()
export class HttpExceptionFiltern implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {}
}
其中, host
中有switchToHttp
方法,它返回当前报错的上下文信息,exception
是nest
的原生报错应该返回的信息,ok 有了这一点,就可以很简单设置返回内容,以及生存本地错误日志,稍微再改造一下这个异常过滤器,把logger
依赖注入过来。
// http-excetion-filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
LoggerService,
} from '@nestjs/common';
@Catch()
export class HttpExceptionFiltern implements ExceptionFilter {
constructor(private logger: LoggerService) { } // 注入looger
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
this.logger.error({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// http-excetion-filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
LoggerService,
} from '@nestjs/common';
@Catch()
export class HttpExceptionFiltern implements ExceptionFilter {
constructor(private logger: LoggerService) { } // 注入looger
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
this.logger.error({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
然后再main.ts
把logger
注入到异常过滤器
app.useGlobalFilters(new HttpExceptionFiltern(logger));
app.useGlobalFilters(new HttpExceptionFiltern(logger));
模块化日志系统
上述完成后,基本上就可以输出本地日志和使用 winston 做为日志的打印。但这多少会让代码显得杂乱,并且当前还少了环境变量的配置(比如 dev 开发环境不需要记录 error 的错误日志),Nest
提供了丰富的模块化系统,所以将就把日志重构改为模块化,同时接入环境变量来控制日志输出的配置;
快速通过nest-cli
创建一个日志模块;
nest g mo logs
nest g mo logs
去到logs.moduel.ts
中,使用异步注册的方式创建winston
日志(因为我们需要借助当前环境变量设置不同的日志模式),
// log.module.ts
import { Module } from '@nestjs/common';
import { LogsController } from './logs.controller';
import { LogsService } from './logs.service';
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston';
import { ConfigService } from '@nestjs/config';
import * as winston from 'winston';
import { Console } from 'winston/lib/winston/transports';
import { logEmum } from 'src/emum/config.emum';
import DailyRotateFile = require('winston-daily-rotate-file');
@Module({
imports: [
WinstonModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const consoleTransPorts = new Console({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike(),
),
});
const dailyailyRotateFileError = new DailyRotateFile({
dirname: process.cwd() + '/logs',
filename: 'Err_alterEgo_log-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '7d',
level: 'warn',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple(),
),
});
const dailyailyRotateFileInfo = new DailyRotateFile({
dirname: process.cwd() + '/logs', // 文件到哪个目录
filename: 'Err_alterEgo_log-%DATE%.log', // 输出日志文件名
datePattern: 'YYYY-MM-DD',
zippedArchive: true, // 是否压缩
maxSize: '20m',
maxFiles: '7d',
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple(),
),
});
return {
transports: [
consoleTransPorts,
...(configService.get(logEmum.NOT_LOG)
? []
: [
dailyailyRotateFileError,
dailyailyRotateFileInfo,
]),
],
} as WinstonModuleOptions;
},
}),
],
controllers: [LogsController],
providers: [LogsService],
})
export class LogsModule { }
// log.module.ts
import { Module } from '@nestjs/common';
import { LogsController } from './logs.controller';
import { LogsService } from './logs.service';
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston';
import { ConfigService } from '@nestjs/config';
import * as winston from 'winston';
import { Console } from 'winston/lib/winston/transports';
import { logEmum } from 'src/emum/config.emum';
import DailyRotateFile = require('winston-daily-rotate-file');
@Module({
imports: [
WinstonModule.forRootAsync({
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const consoleTransPorts = new Console({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
utilities.format.nestLike(),
),
});
const dailyailyRotateFileError = new DailyRotateFile({
dirname: process.cwd() + '/logs',
filename: 'Err_alterEgo_log-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '7d',
level: 'warn',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple(),
),
});
const dailyailyRotateFileInfo = new DailyRotateFile({
dirname: process.cwd() + '/logs', // 文件到哪个目录
filename: 'Err_alterEgo_log-%DATE%.log', // 输出日志文件名
datePattern: 'YYYY-MM-DD',
zippedArchive: true, // 是否压缩
maxSize: '20m',
maxFiles: '7d',
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple(),
),
});
return {
transports: [
consoleTransPorts,
...(configService.get(logEmum.NOT_LOG)
? []
: [
dailyailyRotateFileError,
dailyailyRotateFileInfo,
]),
],
} as WinstonModuleOptions;
},
}),
],
controllers: [LogsController],
providers: [LogsService],
})
export class LogsModule { }
另外的,这里额外在环境变量中新增了NOT_LOG
,依旧和之前采用TypeOrm
方式通过枚举在configServer
中获取,ok 删掉之前在main.ts
配置的东西,然后需要在app.module.ts
中引入当前模块,最后采用nest-winston
文档中替换当前项目logger
的命令,回到main.ts
;
// main.ts
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston";
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
// main.ts
import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston";
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
那么nest
是采取依赖注入的形式,如何使用这个被替换的logger
呢,既然是依赖注入,那么只需要在想使用的地方的构造器
中Inject
上即可,举个例子:
import {
Controller,
Delete,
Get,
Post,
Query,
Inject,
LoggerService,
} from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private readonly logger: LoggerService,
) {
this.logger.log('user model init end'); // 这样logger就会被打印出来,同时会触发记录
}
}
import {
Controller,
Delete,
Get,
Post,
Query,
Inject,
LoggerService,
} from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.entity';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
@Controller('user')
export class UserController {
constructor(
private userService: UserService,
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private readonly logger: LoggerService,
) {
this.logger.log('user model init end'); // 这样logger就会被打印出来,同时会触发记录
}
}
同理,上述搓的全局异常拦截器也如此:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
Inject,
LoggerService,
} from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
@Catch()
export class HttpExceptionFiltern implements ExceptionFilter {
constructor(
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private readonly logger: LoggerService,
) { }
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
const massage = exception.message;
this.logger.error("['error']", {
code: status,
timestamp: new Date().toISOString(),
path: request.url,
massage,
});
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
massage,
});
}
}
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
Inject,
LoggerService,
} from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
@Catch()
export class HttpExceptionFiltern implements ExceptionFilter {
constructor(
@Inject(WINSTON_MODULE_NEST_PROVIDER)
private readonly logger: LoggerService,
) { }
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
const massage = exception.message;
this.logger.error("['error']", {
code: status,
timestamp: new Date().toISOString(),
path: request.url,
massage,
});
response.status(status).json({
code: status,
timestamp: new Date().toISOString(),
path: request.url,
massage,
});
}
}