NestJS 7.x 折腾记: (3) 采用nestjs-pino作为Nest logger

2022-03-08 14:55:40 浏览数 (1)

前言

内置的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已经完毕, 包括一些日志格式的展示! 有不对之处请留言,会及时修正! 谢谢阅读!!

0 人点赞