Nest.js 脚手架改造(四)日志

填坑 算是比较重要的日志模块

我这里把日志文件分成两种,一种是request,记录请求的日志,无论是正常数据返回还是业务异常范围(比如参数、验签错误),其实也就是HTTP Code:200的日志。

另一种是应用层面异常的日志。

日志输出的库选择了log4js

log4js configure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 日志输出格式
const layout = {
type: 'pattern',
pattern: '[%d{yyyy-MM-dd hh:mm:ss}] [%p] %h %z %m'
}

configure({
appenders: {
console: {
type: 'console'
},
// 请求日志
stdout: {
type: 'dateFile',
filename: `${loggerDir}request.log`, // 日志文件名
// 文件名是否带上pattern dateFile默认pattern是yyyy-MM-dd
alwaysIncludePattern: true,
// 保留文件后缀名 false情况下会变成 request.log.2019-11-21
keepFileExt: true,
layout
},
// 应用异常日志
errorout: {
type: 'dateFile',
filename: `${loggerDir}error.log`,
alwaysIncludePattern: true,
keepFileExt: true,
layout
}
},
categories: {
default: { appenders: ['console'], level: 'all' },
request: { appenders: ['stdout'], level: 'info' },
error: { appenders: ['errorout'], level: 'error' }
}
})

LoggerModule

这里选择实现LoggerSerivce接口(其实最后才发现不实现也能用…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// logger.service.ts
import { LoggerService, Injectable } from '@nestjs/common'
import { configure, getLogger, Logger } from 'log4js'
import { ConfigService } from '../config/config.service'


@Injectable()
export class CustomLoggerService implements LoggerService {
private readonly requestLogger: Logger
private readonly errorLogger: Logger

constructor(private readonly configService: ConfigService) {
// 创建logger 参数指的是categories
this.requestLogger = getLogger('request')
this.errorLogger = getLogger('error')

const loggerDir = this.configService.get('LOGS_DIR')

// configure({}) 略 看上一part
}

log(message: string): void {
this.info(message)
}

info(message: string): void {
this.requestLogger.info(message)
}

warn(message: string): void {
this.errorLogger.warn(message)
}

error(message: string): void {
this.errorLogger.error(message)
}

requestError(message: string): void {
this.requestLogger.error(message)
}
}
1
2
3
4
5
6
7
8
9
// logger.module.ts
import { Module } from '@nestjs/common'
import { CustomLoggerService } from './logger.service'

@Module({
providers: [CustomLoggerService],
exports: [CustomLoggerService]
})
export class LoggerModule {}

最后记得在app.module.ts中import进来

使用

请求日志

请求的出口有两个,一个是包装正常数据的api.interceptor.ts,另一个是捕获业务异常的http-exception.filter.ts

构造函数依赖注入LoggerService

1
constructor(private readonly logger: CustomLoggerService) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// api.interceptor.ts
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
return next.handle().pipe(
map(data => {
const res = {
code: ApiCode.SUCCESS,
msg: 'success',
data
}

this.doLog(context, res)

return res
})
)
}

doLog(context: ExecutionContext, res: Response<T>): void {
const ctx = context.switchToHttp()
const request: Request = ctx.getRequest()
const { url, headers, method, body } = request
const ua = headers['user-agent']

this.logger.info(
`${method} ${url} ${ua} ${JSON.stringify(body)} ${JSON.stringify(res)}`
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// http-exception.filter.ts
catch(exception, host: ArgumentsHost): void {
const ctx = host.switchToHttp()
const response: Response = ctx.getResponse()
const request: Request = ctx.getRequest()
const status = exception.getStatus()

const data = {} as ResponseData

data.msg = (exception as ApiException).getErrorMessage()
data.code = (exception as ApiException).getErrorCode()

this.doLog(request, data)

response.status(status).json(data)
}

doLog(request: Request, data): void {
const { url, headers, method, body } = request
const ua = headers['user-agent']

this.logger.requestError(
`${method} ${url} ${ua} ${JSON.stringify(body)} ${JSON.stringify(data)}`
)
}

应用异常日志

应用异常的日志默认走的是Nest内置的日志模块(因为没有捕获),也就是控制台里输出的那些,因此还需要一个捕获其他异常的filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// global-exception.filter.ts
import { Catch, ArgumentsHost, Injectable } from '@nestjs/common'
import { BaseExceptionFilter } from '@nestjs/core'
import { CustomLoggerService } from '../../extends/logger/logger.service'
import { Request } from 'express'

@Injectable()
@Catch()
export class GlobalExceptionsFilter extends BaseExceptionFilter {
constructor(private readonly logger: CustomLoggerService) {
super()
}

catch(exception: Error, host: ArgumentsHost): void {
super.catch(exception, host)

const ctx = host.switchToHttp()
const request: Request = ctx.getRequest()

this.doLog(request, exception)
}

doLog(request: Request, exception: Error): void {
const { url, headers, method, body } = request
const ua = headers['user-agent']

this.logger.error(
`${method} ${url} ${ua} ${JSON.stringify(body)} ${exception.stack}`
)
}
}

注册的时候需要注意先后顺序问题

1
2
3
4
5
// app.module.ts
providers: [
{ provide: APP_FILTER, useClass: GlobalExceptionsFilter },
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
]

异常走的filter自下到上的,假如反过来,业务异常会先走到GlobalExceptionsFilter里,HttpExceptionFilter是捕获不到的。