OverRainbow

how to build a level logger

☕️ 1 min read

你可以从本文了解到

背景

由于最近在熟悉一个新项目,其中自定义了一个level logger,但是其输出的行号却不是真实调用logger的地方,而是在封装的类内部。 那么,这一定是一个bug,我们应该如何去修复呢?

那么我们自己来实现一个level logger该怎么做呢? 首先,我们看正常情况如何去封装一个logger,然后如何给这个logger加上level管理的功能。

1、封装一个最简单的logger

我们可以看到这篇问答,提供了一个清晰的demo,其主要代码如下

var Debugger = function(gState, klass) {
    // 存储新log函数的局部变量
  this.debug = {}

  if (gState && klass.isDebug) {
    for (var m in console)
      // 遍历console的方法
      if (typeof console[m] == 'function')
        // bind了this之后,存进debug中
        this.debug[m] = console[m].bind(window.console, klass.toString()+": ")
  }else{
    for (var m in console)
      if (typeof console[m] == 'function')
        this.debug[m] = function(){}
  }
  return this.debug
}

isDebug = true //global debug state

debug = Debugger(isDebug, this)

debug.log('Hello log!')
debug.trace('Hello trace!')

“行号”实际上就是window.console.xxx方法被调用的所在位置。

所以这个Debugger在做的事情就是让你在调用它的方法的时候,实际上是间接在操作window.console.xxx

为了实现这样的效果,就可以通过bind方法,产生一个新的函数并且存下来。

这样在外部调用Debugger存下来的方法,就相当于在window.console上面直接调用方法。自然就可以打印正确的行号了。

2、增加level管理功能

关于level管理功能,我们先找一个比较通用的库loglevel,先来看一下它的API签名怎么设计的。

log.trace(msg)
log.debug(msg)
log.info(msg)
log.warn(msg)
log.error(msg)
log.setLevel(level, [persist])
log.setDefaultLevel(level)
log.resetLevel() 
log.enableAll()
log.disableAll()
log.getLevel()
log.getLogger(loggerName)
log.getLoggers()

其中最核心的方法就是setLevel,

我们在看代码之前可以想象大致有2种实现思路。

1、所有log都经由一个函数处理,这个函数内部判断当前level是否要打印。

2、根据不同的level,动态绑定函数(如果某个level不要打印,就绑定空函数)

方案分析:

方案1:这会存在一个问题,假如这个函数不是直接返回了bind后的函数,即使这个统一函数内部封装了window.console的调用,但由于真实调用位置始终在logger函数内,所以调用logger函数所在的行号是无法体现的的。(这个logger函数要能保留行号,也必须满足前面的bind原理,即内部不做封装,只做bind。但如果只做bind,是不能实现区分level而选择性调用的。也就是说这个方案实现起来存在难以跨域的障碍。)

方案2:这是loglevel实现的方案。每次setLevel的时候绑定一次。实现原理跟前面最简单logger是一样的。至于打印参数什么的,可以在bind的时候传入。

3、总结。回到bug本身

那本文定位到的我们的一个logger中行号始终不变,其根本原因是采用了level实现方案1。为了修这个bug,核心就是要实现“调用自定义logger的方法就相当于在调用window.console的方法“。只要遵循这条原则,一切都迎刃而解了。其他都是实现细节问题。