前言
内置的logger不是很满足个人的需求, 所以找了下社区主流的日志实现, 从log4js,winston, 到选型pino . 是另外两个不好么,那倒不是. 萝卜青菜各有所爱吧, pino够轻量,自定义还算丰富,性能还很高!!
效果图
开发模式
代码语言:javascript复制INFO [2020-11-09 08:45:12.336 0000] (56588 on crper-MBP.local): AppController {/api/v1}:
context: "RoutesResolver"
INFO [2020-11-09 08:45:12.340 0000] (56588 on crper-MBP.local): Mapped {/api/v1, GET} route
context: "RouterExplorer"
INFO [2020-11-09 08:45:12.341 0000] (56588 on crper-MBP.local): Mapped {/api/v1/post, POST} route
context: "RouterExplorer"
INFO [2020-11-09 08:45:12.341 0000] (56588 on crper-MBP.local): Mapped {/api/v1/user, GET} route
context: "RouterExplorer"
INFO [2020-11-09 08:45:12.342 0000] (56588 on crper-MBP.local): Mapped {/api/v1/netease-news/:id, GET} route
context: "RouterExplorer"
INFO [2020-11-09 08:45:12.344 0000] (56588 on crper-MBP.local): Nest application successfully started
context: "NestApplication"
Swagger文档链接: http://localhost:3000/api-docs
Restful接口链接: http://localhost:3000/api/v1
AppController newDz Before...
AppController newDz After... 1761ms
INFO [2020-11-09 08:45:52.082 0000] (56588 on crper-MBP.local): request completed
响应信息: {
"statusCode": 200,
"headers": {
"content-security-policy": "default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
"x-dns-prefetch-control": "off",
"expect-ct": "max-age=0",
"x-frame-options": "SAMEORIGIN",
"strict-transport-security": "max-age=15552000; includeSubDomains",
"x-download-options": "noopen",
"x-content-type-options": "nosniff",
"x-permitted-cross-domain-policies": "none",
"referrer-policy": "no-referrer",
"x-xss-protection": "0",
"content-type": "application/json; charset=utf-8",
"content-length": "853",
"etag": "W/"355-KpR/5mF8Y34126QG9UV2LArJxBw"",
"vary": "Accept-Encoding"
}
}
响应时间(ms): 1772
请求信息: {
"id": 1,
"method": "GET",
"url": "/api/v1/netease-news/11",
"headers": {
"host": "localhost:3000",
"user-agent": "insomnia/2020.4.2",
"content-type": "application/json",
"accept": "*/*",
"content-length": "23"
},
"remoteAddress": "::ffff:127.0.0.1",
"remotePort": 61069,
"httpVersion": "1.1",
"params": {
"0": "/netease-news/11"
},
"query": {},
"body": {
"asdfs": "12321"
}
}
生产模式
代码语言:javascript复制2020-11-09 16:46 08:00: /Users/linqunhe/Code/aozhe/h3yun-frontend-bff/h3yun-bff-core/config/env/dev.local.env
2020-11-09 16:46 08:00: /Users/linqunhe/Code/aozhe/h3yun-frontend-bff/h3yun-bff-core/config/env/http.env
2020-11-09 16:46 08:00: /Users/linqunhe/Code/aozhe/h3yun-frontend-bff/h3yun-bff-core/config/env/report.env
2020-11-09 16:46 08:00: {"level":30,"time":1604911609493,"pid":57279,"hostname":"crper-MBP.local","context":"RoutesResolver","msg":"AppController {/api/v1}:"}
2020-11-09 16:46 08:00: {"level":30,"time":1604911609494,"pid":57279,"hostname":"crper-MBP.local","context":"RouterExplorer","msg":"Mapped {/api/v1, GET} route"}
2020-11-09 16:46 08:00: {"level":30,"time":1604911609495,"pid":57279,"hostname":"crper-MBP.local","context":"RouterExplorer","msg":"Mapped {/api/v1/post, POST} route"}
2020-11-09 16:46 08:00: {"level":30,"time":1604911609495,"pid":57279,"hostname":"crper-MBP.local","context":"RouterExplorer","msg":"Mapped {/api/v1/user, GET} route"}
2020-11-09 16:46 08:00: {"level":30,"time":1604911609495,"pid":57279,"hostname":"crper-MBP.local","context":"RouterExplorer","msg":"Mapped {/api/v1/netease-news/:id, GET} route"}
2020-11-09 16:46 08:00: {"level":30,"time":1604911609498,"pid":57279,"hostname":"crper-MBP.local","context":"NestApplication","msg":"Nest application successfully started"}
2020-11-09 16:46 08:00: Swagger文档链接: http://localhost:3000/api-docs
2020-11-09 16:46 08:00: Restful接口链接: http://localhost:3000/api/v1
2020-11-09 16:46 08:00: AppController newDz Before...
2020-11-09 16:46 08:00: AppController newDz After... 1552ms
2020-11-09 16:46 08:00: {"level":30,"time":1604911614547,"pid":57279,"hostname":"crper-MBP.local","请求信息":{"id":1,"method":"GET","url":"/api/v1/netease-news/11","headers":{"host":"localhost:3000","user-agent":"insomnia/2020.4.2","content-type":"application/json","accept":"*/*","content-length":"23"},"remoteAddress":"::ffff:127.0.0.1","remotePort":61103,"httpVersion":"1.1","params":{"0":"/netease-news/11"},"query":{},"body":{"asdfs":"12321"}},"响应信息":{"statusCode":200,"headers":{"content-security-policy":"default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests","x-dns-prefetch-control":"off","expect-ct":"max-age=0","x-frame-options":"SAMEORIGIN","strict-transport-security":"max-age=15552000; includeSubDomains","x-download-options":"noopen","x-content-type-options":"nosniff","x-permitted-cross-domain-policies":"none","referrer-policy":"no-referrer","x-xss-protection":"0","content-type":"application/json; charset=utf-8","content-length":"853","etag":"W/"355-KpR/5mF8Y34126QG9UV2LArJxBw"","vary":"Accept-Encoding"}},"响应时间(ms)":1561,"msg":"request completed"}
实战
安装
代码语言:javascript复制# nestjs-pino 是基于pino封装的nest模块,可以拿来即用!
# https://github.com/iamolegga/nestjs-pino
yarn add nestjs-pino
# pino 日志美化工具(用于开发模式美滋滋,看效果图的开发模式)
yarn add -D pino-pretty
配置
主入口(main.ts)
关闭内置的nest logger功能
代码语言:javascript复制import { AppModule } from './app.module';
import { NestFactory } from '@nestjs/core';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
cors: false, // 关闭cors
logger: false, // 关闭内置logger
});
await app.listen(configService.get('SERVE_LISTENER_PORT'));
}
bootstrap()
根模块配置(app.module)
正如我们上篇文章说的,都搞了配置中心了, 那肯定这边同步引用相关配置啊,不然意义何在! 还是同样的姿势,从typescript声明入手
代码语言:javascript复制import { DynamicModule } from "@nestjs/common";
import { LoggerModuleAsyncParams, Params } from "./params";
export declare class LoggerModule {
static forRoot(params?: Params | undefined): DynamicModule; // 同步注册的方式,看下面参数声明
static forRootAsync(params: LoggerModuleAsyncParams): DynamicModule; // 异步加载配置!我们用这个
}
export interface Params {
pinoHttp?: pinoHttp.Options | DestinationStream | [pinoHttp.Options, DestinationStream]; // pino-http的参数配置
exclude?: Parameters<MiddlewareConfigProxy["exclude"]>; // 就是可以设置排除不作用的路由区域,具体可以看下官方文档的中间件部分!
forRoutes?: Parameters<MiddlewareConfigProxy["forRoutes"]>; // 同理,上面,路由作用域
useExisting?: true; // 这个东西就是检测已存在的pino就不用这个了,比如用了Fastify作为底层,它内置logger就是走的pino!!!
renameContext?: string; // 重命名上下文,一般不碰它,默认context也很好理解
}
export interface LoggerModuleAsyncParams extends Pick<ModuleMetadata, "imports" | "providers"> {
useFactory: (...args: any[]) => Params | Promise<Params>; // 工厂函数,返回上面的params定义的规格就能识别
inject?: any[]; // 是否要注入一些provider提供的功能,我们会用到(配置中心),
// 用inject必然会依赖module,也就是import,定义里面也pick了ModuleMetadata的定义!
}
代码语言:javascript复制import * as Joi from '@hapi/joi';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HttpModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LoggerModule } from 'nestjs-pino';
import envReportConfig from './config/env/report.config';
import envSwaggerConfig from './config/env/swagger.config';
import { getDirAllFileNameArr } from './utils/get-dir-all-file-name-arr';
import { pinoHttpOption } from './config/module/pino-http-option.config';
@Module({
imports: [
LoggerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
return { pinoHttp: pinoHttpOption(configService.get('NODE_ENV')) };
},
}),
ConfigModule.forRoot({
encoding: 'utf-8',
envFilePath: [...getDirAllFileNameArr()],
expandVariables: true, // 开启嵌套变量
ignoreEnvVars: true,
load: [envReportConfig, envSwaggerConfig],
validationSchema: Joi.object({
H3_APM_SERVER_URL: Joi.string().default(''),
H3_LATEINOS_REPORT_URL: Joi.string().default(''),
SERVE_LISTENER_PORT: Joi.number().default(3000),
SWAGGER_SETUP_PATH: Joi.string().default('api-docs'),
SWAGGER_ENDPOINT_PREFIX: Joi.string().default('api/v1'),
SWAGGER_UI_TITLE: Joi.string().default('Swagger文档标题'),
SWAGGER_UI_TITLE_DESC: Joi.string().default('赶紧改相关配置啊~~'),
SWAGGER_API_VERSION: Joi.string().default('1.0'),
HTTP_TIMEOUT: Joi.number().default(5000),
HTTP_MAX_REDIRECTS: Joi.number().default(5),
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
}),
validationOptions: {
allowUnknown: false, // 控制是否允许环境变量中未知的键。默认为true。
abortEarly: true, // 如果为true,在遇到第一个错误时就停止验证;如果为false,返回所有错误。默认为false。
},
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
pinoHttpOption
我把pino的配置抽离出去了, 不然密密麻麻一坨,维护性很差!!
代码语言:javascript复制// pino-http 配置
// https://github.com/pinojs/pino-http
import PinoHttp from 'pino-http';
export function pinoHttpOption(envDevMode = 'development'): PinoHttp.Options {
return {
customAttributeKeys: {
req: '请求信息',
res: '响应信息',
err: '错误信息',
responseTime: '响应时间(ms)',
},
level: envDevMode !== 'production' ? 'debug' : 'info',
customLogLevel(res: { statusCode: number }, err: any) {
if (res.statusCode >= 400 && res.statusCode < 500) {
return 'warn';
} else if (res.statusCode >= 500 || err) {
return 'error';
}
return 'info';
},
serializers: {
req(req: {
httpVersion: any;
raw: { httpVersion: any; params: any; query: any; body: any };
params: any;
query: any;
body: any;
}) {
req.httpVersion = req.raw.httpVersion;
req.params = req.raw.params;
req.query = req.raw.query;
req.body = req.raw.body;
return req;
},
err(err: {
params: any;
raw: { params: any; query: any; body: any };
query: any;
body: any;
}) {
err.params = err.raw.params;
err.query = err.raw.query;
err.body = err.raw.body;
return err;
},
},
prettyPrint:
envDevMode === 'development'
? {
colorize: true,
levelFirst: true,
translateTime: 'yyyy-mm-dd HH:MM:ss.l o',
}
: false,
};
}
总结
至此,用pino替换内置logger已经完毕, 包括一些日志格式的展示! 有不对之处请留言,会及时修正! 谢谢阅读!!