日志打印是一个服务系统的必备功能,在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框架重构整个工程。分层的概念非常重要,只有合理的分层,你的代码才能各司其职,方便维护。整个过程下来,希望你能理解每一步的意义所在。
下阶段,我们将在现在的基础上开发一个简单的博客系统,你可以先思考下如何设计接口。