在上一篇文章中我实现了一个支持Debug、Info、Error等多个级别的日志库,并将日志写到了磁盘文件中,代码比较简单,适合练手。有兴趣的可以通过这个链接前往:github.com/bosima/ylog…
工程实践中,我们往往还需要对日志进行采集,将日志归集到一起,然后用于各种处理分析,比如生产环境上的错误分析、异常告警等等。在日志消息系统领域,Kafka久负盛名,这篇文章就以将日志发送到Kafka来实现日志的采集;同时考虑到日志分析时对结构化数据的需求,这篇文章还会提供一种输出Json格式日志的方法。
这个升级版的日志库还要保持向前兼容,即还能够使用普通文本格式,以及写日志到磁盘文件,这两个特性和要新增的两个功能分别属于同类处理,因此我这里对它们进行抽象,形成两个接口:格式化接口、写日志接口。
格式化接口
所谓格式化,就是日志的格式处理。这个日志库目前要支持两种格式:普通文本和Json。
为了在不同格式之上提供一个统一的抽象,ylog中定义 logEntry 来代表一条日志:
type logEntry struct { Ts time.Time `json:"ts"` File string `json:"file"` Line int `json:"line"` Level LogLevel `json:"level"` Msg string `json:"msg"` }
格式化接口的能力就是将日志从logEntry格式转化为其它某种数据格式。ylog中对它的定义是:
type LoggerFormatter interface { Format(*logEntry, *[]byte) error }
第1个参数是一个logEntry实例,也就是要被格式化的日志,第2个参数是日志格式化之后要写入的容器。
普通文本格式化器
其实现是这样的:
type textFormatter struct { } func NewTextFormatter() *textFormatter { return &textFormatter{} } func (f *textFormatter) Format(entry *logEntry, buf *[]byte) error { formatTime(buf, entry.Ts) *buf = append(*buf, ' ') file := toShort(entry.File) *buf = append(*buf, file...) *buf = append(*buf, ':') itoa(buf, entry.Line, -1) *buf = append(*buf, ' ') *buf = append(*buf, levelNames[entry.Level]...) *buf = append(*buf, ' ') *buf = append(*buf, entry.Msg...) return nil }
可以看到它的主要功能就是将logEntry中的各个字段按照某种顺序平铺开来,中间用空格分隔。
其中的很多数据处理方法参考了Golang标准日志库中的数据格式化处理代码,有兴趣的可以去Github中详细查看。
这里对日期时间格式化为字符串做了特别的优化,在标准日志库中为了将年、月、日、时、分、秒、毫秒、微秒等格式化指定长度的字符串,使用了一个函数:
func itoa(buf *[]byte, i int, wid int) { // Assemble decimal in reverse order. var b [20]byte bp := len(b) - 1 for i >= 10 || wid > 1 { wid-- q := i / 10 b[bp] = byte('0' + i - q*10) bp-- i = q } // i < 10 b[bp] = byte('0' + i) *buf = append(*buf, b[bp:]...) }
其逻辑大概就是将数字中的每一位转换为字符并存入byte中,注意这里初始化byte数组的时候是20位,这是int64最大的数字位数。
其实时间字符串中的每个部分位数都是固定的,比如年是4位、月日时分秒都是2位,根本不需要20位,所以这个空间可以节省;还有这里用了循环,这对于CPU的分支预测可能有那么点影响,所以我这里分别对不同位数写了专门的格式化方法,以2位数为例:
func itoa2(buf *[]byte, i int) { q := i / 10 s := byte('0' + i - q*10) f := byte('0' + q) *buf = append(*buf, f, s) }
Json文本格式化器
其实现是这样的:
type jsonFormatter struct { } func NewJsonFormatter() *jsonFormatter { return &jsonFormatter{} } func (f *jsonFormatter) Format(entry *logEntry, buf *[]byte) (err error) { entry.File = toShortFile(entry.File) jsonBuf, err := json.Marshal(entry) *buf = append(*buf, jsonBuf...) return }
代码也很简单,使用标准库的json序列化方法将logEntry实例转化为Json格式的数据。
对于Json格式,后续考虑支持用户自定义Json字段,这里暂时先简单处理。
写日志接口
写日志就是将日志输出到别的目标,比如ylog要支持的输出到磁盘文件、输出到Kafka等。
前边格式化接口将格式化后的数据封装到了 []byte 中,写日志接口就是将格式化处理的输出 []byte 写到某种输出目标中。参考Golang中各种Writer的定义,ylog中对它的定义是:
type LoggerWriter interface { Ensure(*logEntry) error Write([]byte) error Sync() error Close() error }
这里有4个方法:
- Ensure 确保输出目标已经准备好接收数据,比如打开要写入的文件、创建Kafka连接等等。
- Write 向输出目标写数据。
- Sync 要求输出目标将缓存持久化,比如写数据到磁盘时,操作系统会有缓存,通过这个方法要求缓存数据写入磁盘。
- Close 写日志结束,关闭输出目标。