1.13 添加日志

104 阅读3分钟

日志打印是一个服务系统的必备功能,在Java里常用的有log4j库,Node.js也有类似的,而在Deno中可以使用date_file_log来打印日志,最重要是有时间、日志级别和对应的信息。

config.yaml

修改config.yaml文件

port: 8000
log:
  appenders:
    dateFile:
      filename: logs/deno
      daysToKeep: 10
      pattern: yyyy-MM-dd.log
  categories:
    default:
      appenders:
        - dateFile
        - console
      level: DEBUG

如果想要打印日志文件,appenders里就需要有dateFile,如果只需要控制台打印(比如使用容器化部署,通常有专门的日志平台),就把它注释掉。

level代表最低日志级别,由低到高为DEBUG、INFO、WARNING、ERROR、CRITICAL。

比如当前配置了INFO,那么使用logger.debug打印的信息就被过滤掉了,不会生产到控制台或日志文件中。

globals.ts

修改globals.ts,给Config加一个log属性。

import { DateFileLogConfig } from "date_file_log";

export interface Config {
  port: number;
  version: string;
  log: DateFileLogConfig;
}

import_map.json

修改import_map.json文件,新增一条:

"date_file_log": "https://deno.land/x/date_file_log@v0.2.6/mod.ts",

log.ts

新建src/tools/log.ts,暴露一个logger实例,和一个Logger类,旨在嵌入到其它类中使用。

// deno-lint-ignore-file no-explicit-any
import { getLogger, initLog } from "date_file_log";
import globals from "../globals.ts";
import { Injectable, Reflect } from "oak_nest";

await initLog(globals.log);

export const logger = getLogger();

@Injectable({
  singleton: false,
})
export class Logger {
  constructor() {
    this.debug = this.debug.bind(this);
    this.info = this.info.bind(this);
    this.warn = this.warn.bind(this);
    this.error = this.error.bind(this);
  }
  private get pre() {
    const parent = Reflect.getMetadata("meta:container", this);
    return parent?.name;
  }

  protected write(
    methodName: "warning" | "info" | "debug" | "error",
    ...messages: any[]
  ): void {
    if (this?.pre) {
      logger[methodName](this.pre, ...messages);
    } else {
      const [first, ...others] = messages;
      logger[methodName](first, ...others);
    }
  }

  debug(...messages: any[]) {
    this.write("debug", ...messages);
  }

  info(...messages: any[]) {
    this.write("info", ...messages);
  }

  warn(...messages: any[]) {
    this.write("warning", ...messages);
  }

  error(...messages: any[]) {
    this.write("error", ...messages);
  }
}

Logger类主要是用来给具体的Controller或者Service注入使用,功能其实只是打印信息时自动把当前父类的名称打印出来,方便排错定位,这要求每个Logger的实例都是不同的,所以Injectable要把singleton设置为false。

main.ts

修改src/main.ts,删除app.use()处理错误信息的一段,修改为:

import { anyExceptionFilter } from "oak_exception";
import { logger } from "./tools/log.ts";

const app = await NestFactory.create(AppModule);

app.use(anyExceptionFilter({
  logger,
  isHeaderResponseTime: true,
  isDisableFormat404: false,
  isLogCompleteError: true,
}));

anyExceptionFilter的源码很简单,就是对之前错误处理的封装,这里不再赘述。

在页面上访问http://localhost:8000,就能看到本地logs目录下有个日志文件比如deno.2022-06-13.log,内容如下:

2022-06-13 22:05:41 [DEBUG] - GET http://localhost:8000/ [200] - 4ms

Controller中使用

修改app.controller.ts:

import { Controller, Get } from "oak_nest";
import { Logger } from "./tools/log.ts";
import { readYaml } from "./tools/utils.ts";

@Controller("")
export class AppController {
  constructor(private readonly logger: Logger) {}

  @Get("/")
  async version() {
    const scriptsConfig = await readYaml<{ version: string }>("scripts.yml");
    this.logger.info(`version: ${scriptsConfig.version}`);
    return `<html><h2>${scriptsConfig.version}</h2></html>`;
  }
}

在构造函数中加上之前的Logger。

再次访问http://localhost:8000,看到新增一条日志:

2022-06-13 22:46:14 [INFO] - [AppController] version: 0.0.0

这里,把当前控制器的名称也打印出来了。

同理,在Service中也可以使用。

作业

思考下,我为什么要在Logger构造函数里重新绑定this,这样做有什么好处?不绑定可以吗?

第一阶段小结

我们第一阶段就到此结束了。

我们从使用oak框架的hello world开始,到使用路由开发RESTful风格的增删改查接口,一步步引入了Service、DAO层,做了业务层与数据处理层的解耦,最终使用oak_nest框架重构整个工程。分层的概念非常重要,只有合理的分层,你的代码才能各司其职,方便维护。整个过程下来,希望你能理解每一步的意义所在。

下阶段,我们将在现在的基础上开发一个简单的博客系统,你可以先思考下如何设计接口。