我有分寸
实现一个自己的 Docker 日志驱动
这本来是一个内部文档,不过似乎发布出来也不错,HyperContainer 支持原生的 Docker Logger,所以这篇日志讲得既是 Docker 的 Logger,也是 Hyper 的 Logger。
按,本文的参考代码主要位于
"github.com/docker/docker/daemon/logger",用于参考的 json-file logger 的代码位于"github.com/docker/docker/daemon/logger/jsonfilelog"。
关于 Docker Logger
Docker 的日志设计实际很简单,container 进程的标准输出和标准错误输出,就直接作为 container 的正常日志和错误日志。日志系统实际相当于两条管道(对于使用 tty 的 container,是一条),一端作为 stdout 和 stderr , attach 到 container 上,接收 container 输出的信息,另一端写入日志系统。
这个日志系统本身是一个具有可扩展性的框架,不仅内建支持了一些日志输出方式,比如 json-file, syslog,也很容易扩展自己的日志系统,只需要实现几个接口即可。
日志消息
日志消息是日志中信息的基本单位,不论是记日志,还是读日志,基本单位都是 Message ,形状是这样的
// Message is datastructure that represents record from some container.
type Message struct {
ContainerID string
Line []byte
Source string
Timestamp time.Time
}
字段的名字都很有描述性,基本不需要介绍, Source 字段是指 stdout 或 stderr,而消息的内容 (Line) 是不包含结尾换行的日志记录,如果写文件的话,需要日志驱动加上换行。
日志的记录
记录日志的东西就是 Logger ,它只需要实现这个接口:
type Logger interface {
Log(*Message) error
Name() string
Close() error
}
从简单的说起, Name() 就是返回日志驱动的名字,比如 json-file 日志驱动就会返回 "json-file" ,除此之外毫无信息量。而 Close() 用来停止记录日志,在 Container 结束的时候调用来结束写入。
记录日志的主要函数是 Log(*Message) error, 当然这里也没有什么特别的,还是非常直观易懂,唯一需要注意的是,如果写入有并发的话,需要使用 chan 或加锁同步一下,避免写出奇怪的东西来。
日志的读取
对于 Docker Logger 来说,同一个 Logger 不紧要支持日志的写入,还要支持日志的读取,读取者要实现 LogReader 接口
type LogReader interface {
// Read logs from underlying logging backend
ReadLogs(ReadConfig) *LogWatcher
}
这个接口只有一个方法,但实际上要比写入复杂,ReadLogs() 函数是有一个 ReadConfig 类型的参数的:
type ReadConfig struct {
Since time.Time
Tail int
Follow bool
}
这些是用户在读日志的时候可以指定的参数:日志开始时间 (Since),读取日志的最后几行的行数 (Tail,为 0 则是输出所有日志),以及是否 Follow,和 tail 命令一样,如果 Follow 为 true,那么,就需要一直等着程序的进一步输出,而不是显示完所有日志就完了。
ReadLogs() 并不直接返回日志内容,而是立刻返回一个 LogWatcher
type LogWatcher struct {
// For sending log messages to a reader.
Msg chan *Message
// For sending error messages that occur while while reading logs.
Err chan error
closeNotifier chan struct{}
}
通过这几个 chan 来给读者(一般是 logs API 的后端)异步返回日志信息、错误消息以及关闭提醒。这个 LogWatcher 有两个方法:
Close()关闭这个LogWatcherWatchClose(),返回一个<-chan,用于监听LogWatcher事件,用法大致如下
select {
case <-watcher.WatchClose():
//closed...
}
日志是允许多个 reader 同时读的,并且,当有 Follow 标记的时候,
日志工厂 (Creator)
Docker (或者说 hyperd) 会为每个 container 创建一个 Logger, 所以,日志驱动实际是通过一个 Creator 来创建日志的,每个日志驱动都要提供一个 Creator 函数
// Creator builds a logging driver instance with given context.
type Creator func(Context) (Logger, error)
这其中的 Context 是关于 Container 和 logger 配置的详细信息:
type Context struct {
Config map[string]string
ContainerID string
ContainerName string
ContainerEntrypoint string
ContainerArgs []string
ContainerImageID string
ContainerImageName string
ContainerCreated time.Time
ContainerEnv []string
ContainerLabels map[string]string
LogPath string
}
由日志用户在创建 Logger 的时候填入,其中 Config 字段是日志专用的日志配置信息,LogPath 是日志存储路径,但并非所有的日志系统都需要一个路径,其他字段都是 Container 的信息,以备日志驱动可能会用到。
实现日志驱动的时候需要注意的一点是,日志 Creator 的返回类型是 Logger 接口,而实际上,返回的这个 Logger 也要实现 LogReader,在使用中,会把 Logger cast 成 *logger.LogReader。
此外,日志驱动还可以提供一个方法来验证配置参数
type LogOptValidator func(cfg map[string]string) error
日志驱动可以用这个方法来检验用户给的日志配置的合法性,如果有错误,返回一个 error 就使得 Container 无法使用这个日志驱动。
注册日志驱动
日志驱动实现了上述的类型和工厂方法后,要注册自己来成为系统中可用的日志驱动
func RegisterLogDriver(name string, c Creator) error
func RegisterLogOptValidator(name string, l LogOptValidator) error
比如,json-file 就在自己的 init() 里注册了自己的 Creator 和 Validator:
func init() {
if err := logger.RegisterLogDriver(Name, New); err != nil {
logrus.Fatal(err)
}
if err := logger.RegisterLogOptValidator(Name, ValidateLogOpt); err != nil {
logrus.Fatal(err)
}
}
小结
没啥可小结的了,就这些。