在微服务与容器化部署普及之后,容器日志分散在各自的标准输出与挂载目录中,给排查问题带来不便。使用Golang编写一个轻量的日志收集程序,可以低成本地把各类容器日志汇聚到统一存储。

一、容器日志的来源与收集方式
容器日志通常分为两类:一类是容器运行时重定向到标准输出与标准错误的日志,Docker默认会写入json-file;另一类是容器内业务直接写到文件、并通过volume挂载出来的日志。Golang可以通过Docker SDK订阅前者的流,也可以通过遍历目录读取后者。
1. 使用Docker SDK读取标准输出
Docker提供了Logs接口,Golang客户端可传入容器ID并获取日志流。下面的示例展示如何连接本地Docker守护进程并持续读取某个容器的日志:
package main
import (
"context"
"fmt"
"io"
"os"
"github.com/docker/docker/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stdcopy"
)
func main() {
// 创建Docker客户端,默认读取环境变量DOCKER_HOST
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
fmt.Println("连接Docker失败:", err)
os.Exit(1)
}
cli.NegotiateAPIVersion(context.Background())
// 容器ID可从命令行参数或配置读取
containerID := "abc123def456"
// 设置日志选项:跟随输出、显示标准输出与错误
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Tail: "1",
}
reader, err := cli.ContainerLogs(context.Background(), containerID, options)
if err != nil {
fmt.Println("获取日志流失败:", err)
os.Exit(1)
}
defer reader.Close()
// Docker日志流带有8字节头,使用stdcopy分离标准输出与错误
_, err = stdcopy.StdCopy(os.Stdout, os.Stderr, reader)
if err != nil && err != io.EOF {
fmt.Println("读取日志异常:", err)
}
}
2. 采集挂载出来的日志文件
对于写入文件的容器日志,可以在宿主机上把volume目录映射给收集程序,用Golang的fsnotify监听文件变化并读取增量内容。核心思路是用fsnotify.Watcher监听创建与写入事件,再用bufio按行读取。
package main
import (
"bufio"
"fmt"
"os"
"github.com/fsnotify/fsnotify"
)
func watchLogDir(path string) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer watcher.Close()
err = watcher.Add(path)
if err != nil {
return err
}
for {
select {
case event := <-watcher.Events:
// 文件写入或创建时读取新内容
if event.Op&fsnotify.Write == fsnotify.Write {
readNewLines(event.Name)
}
case err := <-watcher.Errors:
fmt.Println("监听错误:", err)
}
}
}
func readNewLines(file string) {
f, err := os.Open(file)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println("收集到日志:", scanner.Text())
}
}
二、用协程与channel做并发消费
当同时收集几十个容器时,单协程读取会互相阻塞。可以用一个goroutine负责读流,把每行日志发送到channel,再由多个worker协程统一发送到后端,比如Kafka或HTTP接口。
package main
import "fmt"
func main() {
logChan := make(chan string, 100)
// 模拟一个容器日志读取协程
go func() {
for i := 0; i < 5; i++ {
logChan <- fmt.Sprintf("container-log-%d", i)
}
close(logChan)
}()
// 启动两个worker消费
done := make(chan bool)
for w := 0; w < 2; w++ {
go func(id int) {
for line := range logChan {
fmt.Printf("worker-%d 发送: %sn", id, line)
}
done <- true
}(w)
}
for w := 0; w < 2; w++ {
<-done
}
}
三、统一落盘与后端投递
收集到的日志建议先本地缓冲,再批量投递。可用Golang的time.Ticker每3秒将缓冲切片发送到Elasticsearch或Kafka,避免高频请求。若后端不可用,程序应把缓冲写回本地文件防止丢失。
| 后端类型 | 适用场景 | Go客户端 |
|---|---|---|
| Elasticsearch | 需要全文检索与看板 | olivere/elastic |
| Kafka | 高吞吐解耦 | Shopify/sarama |
| 本地文件 | 容灾缓冲 | os/os.WriteFile |
四、小结
用Golang实现容器日志统一收集并不复杂:通过Docker SDK拿标准输出,用fsnotify补文件日志,配合channel做并发,最后批量投递后端。这种方案依赖少、易于嵌入现有运维体系,适合中小团队快速落地。