- 容器运行时
- 云原生
【免费下载链接】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:
本文以 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 章节,使用流程是:
- 在 Logentries 控制台添加一个manual TCP token log(手动配置、基于 Token 的 TCP 连接);
- 复制该日志源的token(形如
XXXX-XXXX-XXXX-XXXX); - 用 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关键规则有三条:
- token 与 prefix 内联在每条报文中——Logentries 按行首 token 识别日志来源,所以每个完整日志行都自带认证信息,这也是"纯 TCP、无握手"协议能工作的原因;
- 换行转换:payload 中的换行符
\n只替换前count-1个为 Unicode 的\u2028(Line Separator),保留最后一个换行作为日志行边界——多行日志(如 stack trace)因此被压成平台上的"一条日志"; - 行尾补换行:若最终 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-regex | Docker 通用日志选项,用于从环境变量/标签提取附加字段 |
tag | 通用日志选项,用于给日志附加标签模板 |
任何不在白名单内的选项都会返回unknown log opt '%s' for logentries log driver错误,保证配置错误在容器创建阶段即被拦截。
5.3 创建驱动实例:New
New(logentries.go#L42-L64)做两件事:
- 用
info.Config["logentries-token"]调用le_go.Connect(token)建立到 Logentries 的 TLS 长连接,失败则包装为error connecting to logentries返回; - 解析
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:
相关推荐
Docker CE 内置 Google Cloud Logging Go 客户端:客户端构建、异步缓冲写入与 gcplogs 日志驱动实战
Docker CE 内置 Google Cloud Logging Go 客户端:客户端构建、异步缓冲写入与 gcplogs 日志驱动实战 本文以 docker
容器运行时云原生Grafana Loki Docker 日志驱动配置详解
Grafana Loki Docker 日志驱动配置详解 概述 Grafana Loki 是一个高效的日志聚合系统,而 Docker 日志驱动则是将容器日志直接
可观测性日志分析后端微服务对象存储云原生深入理解 Android Browser Helper:Custom Tabs 与 TWA 的终极指南
深入理解 Android Browser Helper:Custom Tabs 与 TWA 的终极指南 Android Browser Helper 是 Goo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考