如果需要winston作为日志工具可以参看下面仓库
nest.js的服务端的仓库地址: https://github.com/yufengctbu/nest-service.git (目前还在整理中)
1、安装
yarn add log4js -S
2、配置
a、创建日志模块
// 创建日志模块
nest g module /lib/log4js
// 在log4js文件夹下创建constants.ts存放常量
// constants.ts内容如下
export const LOG4JS_PROVIDER = 'LOG4JS_PROVIDER';
export const LOG4JS_CONFIG = 'LOG4JS_CONFIG';
export const LoggerScope = 'Log4jsModule';
b、创建log4js的配置文件接口
export interface Log4jsConfig {
/**
* 日志存放路径
* @default "logs/http"
* */
filename?: string;
/**
* 文件名规则,默认 'yyyy-MM-dd.log'
* The following strings are recognized in the pattern:
* - yyyy : the full year, use yy for just the last two digits
* - MM : the month
* - dd : the day of the month
* - hh : the hour of the day (24-hour clock)
* - mm : the minute of the hour
* - ss : seconds
* - SSS : milliseconds (although I'm not sure you'd want to roll your logs every millisecond)
* - O : timezone (capital letter o)
*/
fileNameSuffixPattern?: string;
/**
*
* The number of old files that matches the pattern to keep (excluding the hot file).
* @default 90
*/
numBackups?: number;
}
注意:以上只是随手写了些配置,如果需要其他配置的,可以添加
c、创建log4js的服务
nest g service /lib/log4js
import { ConfigService } from '@nestjs/config';
import { NextFunction, Request, Response } from 'express';
import { Injectable, Logger } from '@nestjs/common';
import * as log4js from 'log4js';
import { Log4jsConfig } from './log4js.interface';
import { LOG4JS_DEFAULT_HTTP_FILENAME } from './log4js.contants';
@Injectable()
export class Log4jsService extends Logger {
public constructor(private readonly log4jsConfig: Log4jsConfig, private readonly configService: ConfigService) {
super();
this.initialize();
}
private initialize(): void {
// 如果全局配置是否需要在控制台中输出错误日志console
const errorConfig = this.configService.get('log.errorConsole') ? ['ruleFile', 'ruleConsole'] : ['ruleFile'];
log4js.configure({
appenders: {
ruleFile: {
type: 'dateFile',
// 模式是否包含在当前日志文件名中,默认为false
alwaysIncludePattern: true,
// 文件写入路径
filename: `logs/${this.log4jsConfig.filename || LOG4JS_DEFAULT_HTTP_FILENAME}.log`,
// 文件名日期格式,例如:'http.2023-04-23.log'
pattern: 'yyyy-MM-dd',
// 保留多少天以前的日志文件
numBackups: this.log4jsConfig.numBackups || 30,
flags: 'a', // 打开文件时使用的标志。默认值是'a'
encoding: 'utf-8', // 日志文件编码格式,默认是utf-8
// 是否对超过保留期限的日志文件进行压缩(gzip)
compress: true,
keepFileExt: true, // 是否保留文件扩展名,设置为true有助于区分不同的日志文件
},
ruleConsole: { type: 'stdout' }, // 在控制台输出
// 如果后续需要日志分文件,如错误日志,info日志,可以再定义类型
},
categories: {
default: { appenders: ['ruleFile'], level: 'info' },
console: { appenders: ['ruleConsole'], level: 'info' },
warn: { appenders: ['ruleConsole'], level: 'warn' },
error: { appenders: errorConfig, level: 'error' },
},
});
}
// 格式化log并且写入文件
public useLogger(req: Request, res: Response, next: NextFunction) {
log4js.connectLogger(log4js.getLogger('default'), {
level: 'info',
format: (request: Request, response: Response, format: (str: string) => string) => {
/**
* :method: HTTP 请求方法(例如 GET、POST 等)
* :url: 请求的 URL
* :status: 响应状态码(例如 200、404、500 等)
* :response-time: 响应时间(以毫秒为单位)
* :referrer: 请求来源网址
* :http-version: HTTP 版本
* :remote-addr: 发送请求的客户端 IP 地址
* :user-agent: 发送请求的客户端 User-Agent
* :content-length: 响应内容长度
*/
const requestId = request.requestId;
return format(`【requestId: ${requestId}】\n Ip: :remote-addr \n Method-Code: :method :status \n Url: ${decodeURI(
request.url,
)} \n Param: ${JSON.stringify(req.params)} \n Query: ${JSON.stringify(req.query)} \n Body: ${JSON.stringify(
req.body,
)} \n Headers: ${JSON.stringify(req.headers)} \n Client: :user-agent \n ${'='.repeat(20)}
`);
},
})(req, res, next);
}
// 常规的log只输出到控制台
public log(message: any, context?: string): void {
const logger = log4js.getLogger(context || 'console');
logger.info(message);
}
// 常规的warn, 输出到控制台
public warn(message: any, context?: string): void {
const logger = log4js.getLogger(context || 'warn');
logger.warn(message);
}
// 进行错误日志打印, 输出控制台, 并且写入文件
public error(message: any, trace?: string, context?: string): void {
const logger = log4js.getLogger(context || 'error');
logger.error(message, trace);
}
}
d、创建日志的provider
import { Log4jsService } from './log4js.service';
import { Log4jsConfig } from './interfaces';
import { Provider } from '@nestjs/common';
import { LOG4JS_PROVIDER, LOG4JS_CONFIG } from './log4js.constants';
export const log4jsProvider: Provider = {
provide: LOG4JS_PROVIDER,
useFactory: (config: Log4jsConfig): Log4jsService => {
return new Log4jsService(config);
},
inject: [{ token: LOG4JS_CONFIG, optional: true }], // 这里引入 的是key为LOG4JS_CONFIG的服务
};
e、配置log4js.modules.ts如下
import { DynamicModule, Global, Module } from '@nestjs/common'; import { log4jsProvider } from './log4js.providers'; import { Log4jsConfig } from './interfaces'; import { LOG4JS_CONFIG } from './log4js.constants'; /** * 目前仅支持将用户请求写入日志,基于 log4js */ @Global() @Module({ providers: [log4jsProvider], exports: [log4jsProvider], }) export class Log4jsModule { public static withConfig(config: Log4jsConfig): DynamicModule { const providers = [{ provide: LOG4JS_CONFIG, useValue: config }]; // 这是用LOG4JS_CONFIG常量做为传入的config的key,方便调用, 与provider中的一一对应 return { module: Log4jsModule, providers: providers, exports: providers, }; } }
f、在log4js中创建中间件
nest g middleware /lib/log4js
import { Inject, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { LOG4JS_PROVIDER } from './log4js.constants';
import { Log4jsService } from './log4js.service';
@Injectable()
export abstract class Log4jsMiddleware implements NestMiddleware {
constructor(@Inject(LOG4JS_PROVIDER) private readonly log4js: Log4jsService) {} //注入log4的provider,方便调用
use(req: Request, res: Response, next: NextFunction) {
this.log4js.useLogger(req, res, next);
}
}
通过中间件的配置,这里就可以收集到所有请求的参数
在app.use中进行全局配置,或者在app.module.ts中进行配置使用
g、在拦截器中配置log4js,这样就可以收集到所有的成功请求的响应
import { Log4jsService } from '@app/libs/log4js/log4js.service';
import { formatResponseData } from '@app/utils/response.helper';
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
public constructor(private readonly log4js: Log4jsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const caller = next.handle();
// 暂时不需要提供成功请求的log,如果有需求,可以添加
return caller.pipe(
map((data: any) => {
const requestId = context.switchToHttp().getRequest().requestId;
return formatResponseData(data, requestId);
}),
);
}
}
这个按需求配置, 注意这里的constructor中的参数,需要传入,所以在main.ts中需要进行传入
// 配置全局的拦截器
app.useGlobalInterceptors(new TransformInterceptor(app.get(LOG4JS_PROVIDER)));
这里的获取方法用的是app.get()方法,只要有在全局注册,那么就可以进行获取
h、在ExceptionFilter中进行配置,这样就可以获取所有的错误日志
import { Log4jsService } from '@app/libs/log4js/log4js.service';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
import { CustomException } from './fail-operation.exception';
@Catch()
export class FailExceptionFilter implements ExceptionFilter {
public constructor(private readonly log4js: Log4jsService) {}
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
//@ts-ignore
const requestId = request.requestId;
this.log4js.logError(`【requestId: ${requestId}】\n message: ${
exception.message
} \n stack: ${exception.stack} \n ${'='.repeat(20)}
`);
const res = exception.getResponse
? exception.getResponse()
: {
statusCode: status,
message:
exception instanceof CustomException
? exception.message
: 'Internal server error',
};
Object.assign(res, { requestId });
response.status(status).json(res);
}
}
注意该方法也需要在main.ts中进行参数传入
app.useGlobalFilters(new FailExceptionFilter(app.get(LOG4JS_PROVIDER)));
这个时候当有请求产生,就会在指定的日志文件夹下生成一个日志记录, 如果有需求还可以制作些日志展示下载页面,根据requestId查询日志的页面