本文基于docker-1.12.6。
在docker中使用logrus来打印到终端,类似于fmt.Print系列函数,我们来跟踪看下它的原理。
logrus在docker的vendor/src/github.com/Sirupsen/logrus目录以一个包的形式存在。它提供了不同的打印级别,如logrus.Debugf(), logrus.Infof(), logrus.Warnf() 等等,其中logrus.Infof()是默认的打印级别,下面我们就以logrus.Infof()入手,探讨一下它的原理。
logrus.Infof()函数定义在vendor/src/github.com/Sirupsen/logrus/exported.go文件中,如下:
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
std.Infof(format, args...)
}
可以看到logrus.Info()实际上调用的是std.Infof()。这个std又是什么呢?它是一个type Logger struct结构的引用:
var (
// std is the name of the standard logger in stdlib `log`
std = New() //std是type Logger struct结构的一个对象
)
std在所有程序运行之前就通过这个New()函数得以创建,New()定义在vendor/src/github.com/Sirupsen/logrus/logger.go文件:
func New() *Logger {
return &Logger{
Out: os.Stderr,
Formatter: new(TextFormatter),
Hooks: make(LevelHooks),
Level: InfoLevel,
}
}
上面的New()创建并初始化一个Logger对象:
Logger.Out初始化为os.Stderr,即系统标准出错; Logger.Formatter初始化为一个TextFormatter对象;Logger.Hooks初始化一个LevelHooks接口对象,用于容器的日志记录;Logger.Level 使用Infolevel级别来初始化,也即使默认打印级别。
同样在此文件中,又为 Logger结构实现了Infof()方法:func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infof(format, args...)
}
}
Logger.Infof()先判断日志的级别,如果级别大于InfoLevel就:1) 先通过NewEntry(logger)创建一个type Entry struct对象;2) 再调用Entry的方法Entry.Infof().
type Entry struct结构以及它的方法Entry.Infof()定义在vendor/src/github.com/Sirupsen/logrus/entry.go文件中:
func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.log(InfoLevel, fmt.Sprint(args...))
}
}
可以看到,调用链从Entry.Infof()-->走到-->Entry.Info(),最终走到Entry.log()。这个Entry.log()就是最终的logrus.Infof()实现打印和日志记录的函数。下面我们就来分析一下:
func (entry Entry) log(level Level, msg string) {
entry.Time = time.Now()
entry.Level = level
entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()
}
reader, err := entry.Reader()
if err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
entry.Logger.mu.Unlock()
}
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
_, err = io.Copy(entry.Logger.Out, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
panic(&entry)
}
}
这个log()函数就是容器中调试信息与日志记录的基本骨架,具体做了下面一些事情:
1 首先调用entry.Logger.Hooks.Fire()来完成 已注册到entry.Logger.Hooks[]中的 所有日志driver的日志记录工作;
// Fire all the hooks for the passed level. Used by `entry.log` to fire
// appropriate hooks for a log entry.
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
for _, hook := range hooks[level] {
if err := hook.Fire(entry); err != nil {
return err
}
}
return nil
}
这个函数在vendor/src/github.com/Sirupsen/logrus/hooks.go文件中,具体的日志记录实现将在以后再去分析。
2 调用reader, err := entry.Reader() 来格式化将要打印的信息:
vendor/src/github.com/Sirupsen/logrus/entry.go
// Returns a reader for the entry, which is a proxy to the formatter.
func (entry *Entry) Reader() (*bytes.Buffer, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
return bytes.NewBuffer(serialized), err
}
其中entry.Logger.Fromatter在前面初始化为TextFormatter对象,这里就调用TextFormatter.Fromat()来进行格式化,其实现在vendor/src/github.com/Sirupsen/logrus/text_formatter.go文件中。
3 将格式化后的信息写到etnry.Logger.Out对应的文件。
具体来讲是通过_, err = io.Copy(entry.Logger.Out, reader)这个函数将reader中的信息拷贝到entry.Logger.Out,前面已经将entry.Logger.Out初始化为os.Stderr;而reader则是第2步创建的butes.Buffer结构。