☰
Docker CE 日志转发到 Logentries:le_go 客户端库与 logentries 日志驱动实现详解
2026/10/13 6:11:51 网站建设 项目流程
  • 容器运行时
  • 云原生

【免费下载链接】docker-ce

:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:

项目地址:https://gitcode.com/gh_mirrors/do/docker-ce
点击查看免费下载

本文以 Docker CE 引擎 vendor 目录中内置的le_go库(README)为主体,完整讲解这个面向 Logentries 的 Go 日志客户端:从 Token 获取、go get安装、最小可运行示例,到Logger的连接管理、线程安全写入与报文格式等源码级细节,并延伸到 Docker CE 中基于它实现的logentries日志驱动,帮助你在生产环境中把容器日志通过 TCP 长连接稳定转发到 Logentries 平台。

1. le_go 是什么

le_go是一个 Golang 客户端库,用于向 Logentries(logentries.com)通过 TCP 连接发送日志。它在设计上做了两件事来保证易用性:

  • 与标准库log.Logger的接口保持兼容,即具备Print/Printf/Println、Fatal*/Panic*、Flags/SetFlags、Prefix/SetPrefix、Output等标准日志器方法集;
  • 实现了io.Writer接口(Write方法),因此可以直接作为任何需要io.Writer的组件的输出目标。

库的包注释在 le.go 开头给出了官方定位:"Package le_go provides a Golang client library for logging to logentries.com over a TCP connection",并说明它使用访问 Token 来发送日志事件。安装方式为:

go get github.com/bsphere/le_go

在 Docker CE 仓库中,该库被 vendor 在components/engine/vendor/github.com/bsphere/le_go/下,由引擎侧的日志驱动直接使用(见第 5 节)。

2. 快速上手:获取 Token 并发送第一条日志

按 README 的 Usage 章节,使用流程是:

  1. 在 Logentries 控制台添加一个manual TCP token log(手动配置、基于 Token 的 TCP 连接);
  2. 复制该日志源的token(形如XXXX-XXXX-XXXX-XXXX);
  3. 用 token 调用le_go.Connect建立连接,之后即可调用标准日志方法写入。

最小可运行示例(与 README 中给出的示例一致):

package main import "github.com/bsphere/le_go" func main() { le, err := le_go.Connect("XXXX-XXXX-XXXX-XXXX") // replace with token if err != nil { panic(err) } defer le.Close() le.Println("another test message") }

README 中有一条重要的使用提示:Logger 是阻塞的。由于每次写入都要等待 TCP 连接完成发送,在高频日志场景下可以用 goroutine 包一层,避免阻塞主流程:

go le.Println("some message")

3. Logger 结构与连接管理

3.1 Logger 的字段

从源码看,Logger持有一次 TCP 长连接的全部状态(le.go):

type Logger struct { conn net.Conn // 到 logentries.com 的 TCP(TLS) 连接 flag int // 日志 flag(与 log.Logger 语义对齐) mu sync.Mutex // 保护 Write 的互斥锁 prefix string // 日志前缀 token string // Logentries 访问 token buf []byte // 复用缓冲区 }

注释明确说明:所有Logger操作都是线程安全的且是阻塞的,若需要非阻塞可以放到 goroutine 中调用。

3.2 Connect 与 TLS 直连

Connect(token)(le.go#L34-L48)创建一个Logger并立即打开连接。真正的拨号逻辑在openConnection(le.go#L59-L67)中:

conn, err := tls.Dial("tcp", "data.logentries.com:443", &tls.Config{})

也就是说,日志数据面走的是TLS over TCP 直连data.logentries.com:443,不经过 HTTP API。Close()则负责关闭底层net.Conn,并做了空连接保护(le.go#L50-L57)。

3.3 连接活性检测与自动重连

le_go对长连接断线问题有一套轻量处理机制:

  • isOpenConnection()(le.go#L69-L91)通过"立刻超时的读取"来探测连接是否仍然活着:先把读截止时间设为当前时刻(SetReadDeadline(time.Now())),然后尝试读 1 个字节。如果返回的net.Error是超时,说明连接仍然可读(数据面对端不会主动推送数据,读必然超时),于是恢复读截止时间并判定为"连接打开";其他错误则判定连接已关闭。
  • ensureOpenConnection()(le.go#L93-L103)在每次写入前调用:若连接已断开则自动重新openConnection(),因此调用方不需要自己管理重连。

从源码结构看,该机制不维护心跳或断点续传,仅在写入路径上按需重拨,属于"写前检查、断了就重连"的简单策略。

4. 写入路径:报文格式与线程安全

4.1 Write 与 io.Writer

Write(p []byte)(le.go#L186-L200)是库的核心:

func (logger *Logger) Write(p []byte) (n int, err error) { if err := logger.ensureOpenConnection(); err != nil { return 0, err } logger.mu.Lock() defer logger.mu.Unlock() logger.makeBuf(p) return logger.conn.Write(logger.buf) }

流程是:先确保连接可用,再加互斥锁串行化并发写入,最后把组好的logger.buf一次性写到 TCP 连接上。这就是le_go实现io.Writer并保证线程安全的入口。

4.2 makeBuf:token、前缀与换行处理

报文如何拼装由makeBuf完成(le.go#L202-L216):

func (logger *Logger) makeBuf(p []byte) { count := strings.Count(string(p), lineSep) p = []byte(strings.Replace(string(p), lineSep, "\u2028", count-1)) logger.buf = logger.buf[:0] logger.buf = append(logger.buf, (logger.token + " ")...) logger.buf = append(logger.buf, (logger.prefix + " ")...) logger.buf = append(logger.buf, p...) if !strings.HasSuffix(string(logger.buf), lineSep) { logger.buf = append(logger.buf, (lineSep)...) } }

可以归纳出最终发送到 Logentries 的一行报文结构:

<token> <prefix> <payload>\n

关键规则有三条:

  1. token 与 prefix 内联在每条报文中——Logentries 按行首 token 识别日志来源,所以每个完整日志行都自带认证信息,这也是"纯 TCP、无握手"协议能工作的原因;
  2. 换行转换:payload 中的换行符\n只替换前count-1个为 Unicode 的\u2028(Line Separator),保留最后一个换行作为日志行边界——多行日志(如 stack trace)因此被压成平台上的"一条日志";
  3. 行尾补换行:若最终 buffer 不以\n结尾则自动追加,保证 Logentries 按行解析。

注意makeBuf复用logger.buf前缀(buf[:0]),源码注释也声明它本身不是并发安全的,必须由Write持锁调用。

4.3 与 log.Logger 兼容的方法集

除Write外,Logger提供了与log.Logger对齐的完整方法集(le.go#L105-L184):

方法行为
Print/Printf/Println常规写入,内部统一走Output→Write
Fatal/Fatalf/Fatalln先写日志,随后os.Exit(1)
Panic/Panicf/Panicln先写日志,随后panic格式化后的消息
Flags/SetFlags读取/设置日志 flag(存储于Logger.flag)
Prefix/SetPrefix读取/设置前缀,前缀会在makeBuf中拼入每条报文
Output(calldepth, s)标准log.Logger抽象方法,直接把s交给Write

其中Output实现非常直接(le.go#L128-L133):它不做二次格式化,收到的字符串原样进入Write管线。SetPrefix设置的值则是真正生效的——它会被拼在 token 之后;而SetFlags设置的 flag 仅被存储并可由Flags()读回,从源码结构看并不参与本库的格式化过程。

5. Docker CE 如何使用 le_go:logentries 日志驱动

在 Docker CE 引擎中,le_go的唯一消费方是logentries日志驱动(logentries.go),用于把容器日志转发到 Logentries 端点。

5.1 驱动注册与依赖引入

驱动通过init()完成自注册(logentries.go#L30-L37):

func init() { if err := logger.RegisterLogDriver(name, New); err != nil { logrus.Fatal(err) } if err := logger.RegisterLogOptValidator(name, ValidateLogOpt); err != nil { logrus.Fatal(err) } }

而驱动包本身由 dockerd 通过 blank import 引入,Linux 与 Windows 的装配点分别是 logdrivers_linux.go 和 logdrivers_windows.go 中的:

_ "github.com/docker/docker/daemon/logger/logentries"

5.2 支持哪些日志选项

ValidateLogOpt(logentries.go#L96-L116)定义了该驱动的选项白名单:

选项说明
logentries-token必填。Logentries 的访问 token,为空直接报Missing logentries token
line-only布尔值。为true时只转发日志行本身,不附加容器元数据
env/env-regex、labels/labels-regexDocker 通用日志选项,用于从环境变量/标签提取附加字段
tag通用日志选项,用于给日志附加标签模板

任何不在白名单内的选项都会返回unknown log opt '%s' for logentries log driver错误,保证配置错误在容器创建阶段即被拦截。

5.3 创建驱动实例:New

New(logentries.go#L42-L64)做两件事:

  1. 用info.Config["logentries-token"]调用le_go.Connect(token)建立到 Logentries 的 TLS 长连接,失败则包装为error connecting to logentries返回;
  2. 解析line-only选项(strconv.ParseBool),得到lineOnly布尔值。

构造出的logentries结构体保存了containerID、containerName、writer *le_go.Logger、lineOnly以及额外的extra字段集合。

5.4 日志转发:两种模式

Log(msg *logger.Message)(logentries.go#L66-L86)根据lineOnly走两条路径:

结构化模式(默认,line-only未开启)——组装一条包含容器元数据的记录,交给le_go的Println:

data := map[string]string{ "container_id": f.containerID, "container_name": f.containerName, "source": msg.Source, // stdout / stderr "log": string(msg.Line), } for k, v := range f.extra { data[k] = v } ts := msg.Timestamp logger.PutMessage(msg) f.writer.Println(f.tag, ts, data)

line-only 模式——只转发原始日志行,不带任何元数据:

line := string(msg.Line) logger.PutMessage(msg) f.writer.Println(line)

两条路径都先logger.PutMessage(msg)把消息对象归还到对象池再发送,避免高频日志下的内存分配压力。Close()则直接委托给le_go的Close关闭 TCP 连接(logentries.go#L88-L90)。

结合第 3、4 节的客户端实现可以推断其运行特性:每条容器日志行最终会变成token prefix 内容\n的一行 TLS 报文直发到data.logentries.com:443;驱动实例持有的连接在断线后由le_go在下一次写入时自动重拨;Docker 侧的日志拷贝由通用的 logger copier 机制驱动,驱动只需实现Log/Close/Name。

5.5 使用方式(适用前提)

在支持logentries驱动的 dockerd 上,典型用法是按上述选项指定 token,例如创建容器时:

docker run --log-driver=logentries \ --log-opt logentries-token=<your-token> \ --log-opt line-only=false \ <image>

需要注意的前提与限制:

  • token 必须来自 Logentries 控制台中manual TCP token log类型的日志源;
  • 数据面要求能出站到data.logentries.com:443(TLS),受容器网络与主机防火墙约束;
  • le_go的写入是阻塞的(见 README 的 Note),在 Docker 场景中该写入由 daemon 内部的日志拷贝协程承担,不会阻塞容器进程本身;
  • 本文所有结论均以当前仓库 vendor 版本components/engine/vendor/github.com/bsphere/le_go/le.go及components/engine/daemon/logger/logentries/logentries.go的源码为准。

6. 小结

le_go用一个极小的代码量解决了"把 Go 程序日志以标准log.Logger/io.Writer语义经 TCP 长连接推到 Logentries"的问题:token 内联报文、\u2028压缩多行日志、写前探测加锁写入、断线自动重连。Docker CE 引擎在其上封装出logentries日志驱动,通过logentries-token、line-only等选项把容器日志(stdout/stderr、容器 ID 与名称、来源流、时间戳以及 env/labels 附加字段)结构化地转发到 Logentries 平台。理解这一条从le_go客户端到驱动注册的完整链路,是排查 Docker 容器日志转发到 Logentries 时连接、格式与选项配置问题的基础。

  • 容器运行时
  • 云原生

【免费下载链接】docker-ce

:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:

项目地址:https://gitcode.com/gh_mirrors/do/docker-ce
点击查看免费下载
上一篇:Formily Antd TimePicker 组件全解析:JSON Schema 与 JSX 双模式时间选择实战指南
下一篇:构建实时地图服务:Japronto地理空间数据处理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询