背景
在实现日志收集系统之后,为了更好的定位处理流程,作了一个简单的封装: 给 golang日志添加后缀.
日志后缀和日志跟踪有什么关系?
如果我们有一批海量日志,还有一个日志收集系统,我们要面对的就是如何在这么多日志中过滤出自己想要的内容。
而使用关键字来定位内容是很自然的解决办法:
举个例子,假设我们要追踪一个http请求的处理过程,那么首先,对于每一次http请求的处理我们给它一个特殊的id,然后所有的日志都附带这个id,这样过滤这个id我们就能得到这次处理所有的日志了;更进一步,在这个内部还可能有各个不同的过程,也分别加以一个特殊的id,再在日志中附带上,然后我们就可以做进一步的过滤。
现有的分布式日志跟踪,基本思路都是这个样子,区别大都是这些”id”之间的关系以及其他一些细节实现。
为什么是后缀?
golang的标准日志库是支持前缀处理的,为什么我们要专门封装后缀操作呢?
我们可以回顾一下之前提到的日志跟踪流程:
如果我们在日志前缀里放这些”id”,那么首先面对的问题就是在最后的日志查看的时候我们不得不看到前面大段的”id”,而它们应该是一个协助过滤的作用,不是我们想要看到的日志重点,这是对日志查看体验的极大损害;
而且很多日志展示平台,为了避免过长日志问题,显示的时候只显示一定长度的日志,剩下的需要手动点开查询,这时在前缀放这些”id”就显得更加不智了;
同时,在每个处理阶段,我们可能附加的”id”的数目是不同的,越深层的日志,它的”id”数目越多;
所以如果想要通过日志重写来分割这些日志也是有困难的(当然不是不能做到),即使通过设定规则做到,我们也需要付出规则的维护成本和重写日志的成本
而如果我们放在后缀之中,在查看日志时我们只需要看前面有意义的日志部分就可以了
在这里,一般的经验是,标准的、固定长度、较短(一般)的标识(例如时间、日志级别、代码行数等)我们会放在前缀中,而不定长度、自定义规则、可能较长的标识还是放在后缀更体验友好一些
golang日志添加后缀 实现
需要的基础库
// Import library.
import (
"fmt"
"io"
"log"
"os"
)
简单的封装类
// Structure declaration.
type Logger struct {
*log.Logger
suffix string
}
结构体创建函数
// New creates a new Logger.
func New(out io.Writer, prefix string, suffix string, flag int) *Logger {
originLogger := log.New(out, prefix, flag)
l := &Logger{
Logger: originLogger,
suffix: suffix,
}
return l
}
- 实践中,我们可能根据需要,封装一个默认实现
// Default creates a new Logger with default parameters.
func Default() *Logger {
var std = log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds|log.Lshortfile)
return &Logger{
Logger: std,
suffix: "",
}
}
自定义方法
在实践中,我们至少需要一个Suffix()
方法和一个SetSuffix()
方法,前者返回现有的后缀,后者设置后缀.
这两个方法和标准日志库的Prefix()
以及SetPrefix()
是一致的
因为我们后缀的特殊性——我们可能根据处理阶段深度的不同,在继承上一层的后缀同时添加新的后缀,因此需要一个新的方法AddSuffix()
,用于添加后缀.
// Suffix returns the output suffix for the logger.
func (l *Logger) Suffix() string {
return l.suffix
}
// SetSuffix sets the output suffix for the logger.
func (l *Logger) SetSuffix(suffix string) {
l.suffix = suffix
}
// AddSuffix add new output suffix for the logger.
func (l *Logger) AddSuffix(suffix string) {
l.suffix = l.suffix + " " + suffix
}
重写标准日志库方法
因为新增了后缀,我们需要重写标准日志库的输出方法:Printf()
// Rewrite log.Logger.Printf().
func (l *Logger) Printf(format string, v ...interface{}) {
content := fmt.Sprintf(format, v...)
l.Logger.Output(2, fmt.Sprintf("%s %s", content, l.suffix))
}
在实践中一般
Printf
就可以满足需求,如果有需要,可以把其他一些输出方法按照相同方法重写这里没有重写和标准库类似的标准输出函数,是因为在实践中在类成员中添加Logger对象更不易出错,也易于管理;标准输出函数更适用于临时的、简单的日志输出,那种需求直接使用标准日志库就够了
到这里,我们的封装就完成啦~
应用示例
我们给出一个应用示例(假设我们的自定义日志库为logger
)
type LevelOne struct {
log *logger.Logger
}
type LevelTwo struct {
log *logger.Logger
}
type LevelThree struct {
log *logger.Logger
}
func NewLevelOne() *LevelOne {
return &LevelOne{
log: logger.Default(),
}
}
func NewLevelTwo(one *LevelOne) *LevelTwo {
var two = &LevelTwo{
log: logger.Default(),
}
two.log.SetSuffix(one.log.Suffix())
return two
}
func NewLevelThree(two *LevelTwo) *LevelThree {
var three = &LevelThree{
log: logger.Default(),
}
three.log.SetSuffix(two.log.Suffix())
return three
}
func main() {
one := NewLevelOne()
one.log.SetSuffix("此方,彼方-https://cibifang.com")
one.log.Printf("Test Level %d", 1)
two := NewLevelTwo(one)
two.log.AddSuffix("此方,彼方-分类目录:开发")
two.log.Printf("Test Level %d", 2)
three := NewLevelThree(two)
three.log.AddSuffix("此方,彼方-文章:https://cibifang.com/golang日志添加后缀/")
three.log.Printf("Test Level %d", 3)
}
- 输出如下
$ go run test.go
2018/07/28 13:48:12.511136 test.go:94: Test Level 1 此方,彼方-https://cibifang.com
2018/07/28 13:48:12.517017 test.go:98: Test Level 2 此方,彼方-https://cibifang.com 此方,彼方-分类目录:开发
2018/07/28 13:48:12.517732 test.go:102: Test Level 3 此方,彼方-https://cibifang.com 此方,彼方-分类目录:开发 此方,彼方-文章:https://cibifang.com/golang日志添加后缀/
总结
看完整个文章,我们发现整体的实现其实是很简单的
写这篇的主要原因是讲一下为什么我要做这样一个简单的封装、以及日志前缀/后缀在实践中的不同作用
其实对于go日志标准库为什么不提供后缀接口我是有些不解的,感觉这应该是一个普遍的需求…如果你知道原因,或者发现文章有什么问题,欢迎在评论中提出~
参考
- golang标准日志库文档
标准文档总是那么好用,如果你有对细节的一些疑惑,可以查看该文档,甚至可以直接点进去看实现,都是很简单的
发表评论