如何在禁用日志语句中以非常低的成本在Go中进行跟踪日志记录

时间:2015-04-29 14:28:12

标签: logging go

将低级调试/跟踪日志记录语句保留在关键路径中非常有用,以便可以通过运行时配置启用它们。这个想法是你永远不会在生产中打开这样的登录(它会削弱性能),但你可以在生产环境中打开它(例如生产系统离线进行调试或一个与生产系统完全相同的测试系统。)

这种类型的日志记录有一个特殊要求:在关键路径上命中禁用日志语句的成本必须非常低:理想情况下是一个布尔测试。< / p>

在C / C ++中,我会使用一个LOG宏来执行此操作,该宏在检查标志之前不会评估任何参数。只有在启用时我们才会调用一些辅助函数来格式化&amp;发送日志消息。

那么如何在Go中做到这一点?

将io.Discard与log.Logger一起使用是一个非首发:它会在每次禁用时将其丢弃之前完全格式化日志消息。

我的第一个想法是

type EnabledLogger struct { Enabled bool; delegate *log.Logger;... }
// Implement the log.Logger interface methods as:
func (e EnabledLogger) Print(...) { if e.Enabled { e.delegate.Output(...) } }

这很接近。如果我说:

myEnabledLogger.Printf("foo %v: %v", x, y)

如果禁用,它不会格式化或记录任何内容,但评估参数x和y。对于基本类型或指针来说没问题,对于任意函数调用都不行 - 例如字符串化没有String()方法的值。

我认为有两种解决方法:

用于推迟呼叫的包装类型:

type Stringify { x *Thing }
func (s Stringify) String() { return someStringFn(s.x) }
enabledLogger.Printf("foo %v", Stringify{&aThing})

将所有内容包装在手动启用的检查中:

if enabledLog.Enabled {
     enabledLog.Printf("foo %v", someStringFn(x))
}

这两者都很冗长且容易出错,因此很容易让人忘记一步并悄悄引入令人讨厌的性能回归。

我开始喜欢Go了。请告诉我它可以解决这个问题:)

1 个答案:

答案 0 :(得分:2)

Go中的所有参数都保证被评估,并且语言中没有已定义的预处理器宏,因此您只能执行一些操作。

为了避免在日志记录参数中进行昂贵的函数调用,请使用fmt.Stringerfmt.GoStringer接口来延迟格式化,直到执行该函数。这样您仍然可以将普通类型传递给Printf函数。您可以使用自定义记录器自行扩展此模式,该记录器也会检查各种接口。这就是您在Stringify示例中使用的内容,您只能通过代码审查和单元测试来实施它。

type LogFormatter interface {
    LogFormat() string
}

// inside the logger
if i, ok := i.(LogFormatter); ok {
    fmt.Println(i.LogFormat())
}

您还可以通过记录器界面在运行时将整个记录器换出,或者在构建时使用build constraints完全替换它,但仍需要确保不会在记录参数中插入昂贵的调用。

像glog这样的一些软件包使用的另一种模式是使Logger本身成为一个bool。这并不能完全消除冗长,但它会让它更简洁。

type Log bool
func (l Log) Println(args ...interface{}) {
    fmt.Println(args...)
}

var debug Log = false

if debug {
    debug.Println("DEBUGGING")
}

你可以在go中进行宏预处理的最接近的是使用代码生成。这不适用于运行时可配置的跟踪,但至少可以提供一个单独的调试版本,可以在需要时将其放置到位。它可以像gofmt -r一样简单,使用text/template构建文件,或者通过解析代码和构建AST来完整生成。