使用Golang统一Docker容器日志为指定JSON格式的方案咨询
统一Docker容器日志格式的Golang实现方案
问题根源
你遇到的格式差异来自三个核心原因:
- Docker容器日志驱动配置:若容器使用
json-file驱动,日志文件本身是结构化JSON,但直接读取API原始流会丢失元数据; - 容器应用输出格式:部分应用输出纯文本或自定义键值对格式,而非Docker标准JSON结构;
- 当前代码未处理Docker API日志流的头部元数据:每个日志条目前有8字节头部(4字节内容长度+4字节流标识),导致无法区分stdout/stderr,也无法正确解析结构化内容。
实现步骤
要统一成目标JSON格式,需完成以下操作:
- 解析Docker API返回的日志流头部,区分stdout/stderr并提取原始日志内容;
- 针对不同日志格式,统一解析为包含
log、stream、time的结构化条目; - 将所有条目封装到指定的输出结构中,序列化为标准JSON。
完整代码示例
package main import ( "bufio" "context" "encoding/json" "fmt" "io" "os" "strings" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/client" ) // 统一日志条目结构 type LogEntry struct { Log string `json:"log"` Stream string `json:"stream"` Time string `json:"time"` } // 最终输出结构 type ContainerLogResult struct { ContainerID string `json:"containerID"` Logs []LogEntry `json:"logs"` } // 解析Docker日志流头部,返回流类型和内容长度 func parseLogHeader(reader *bufio.Reader) (stream string, contentLength int, err error) { header := make([]byte, 8) _, err = io.ReadFull(reader, header) if err != nil { return "", 0, err } // 第5字节标识流类型:1=stdout,2=stderr switch header[4] { case 1: stream = "stdout" case 2: stream = "stderr" default: stream = "unknown" } // 前4字节为内容长度(大端序) contentLength = int(header[0])<<24 | int(header[1])<<16 | int(header[2])<<8 | int(header[3]) return stream, contentLength, nil } // 解析单条日志内容,生成统一格式的LogEntry func parseLogContent(content string, stream string) LogEntry { // 优先尝试解析为Docker标准JSON日志 var dockerStdLog struct { Log string `json:"log"` Stream string `json:"stream"` Time string `json:"time"` } if err := json.Unmarshal([]byte(content), &dockerStdLog); err == nil && dockerStdLog.Log != "" { return LogEntry{ Log: dockerStdLog.Log, Stream: dockerStdLog.Stream, Time: dockerStdLog.Time, } } // 处理自定义键值对格式日志(如time="..." msg="...") logEntry := LogEntry{ Log: content, Stream: stream, Time: time.Now().Format(time.RFC3339Nano), } // 提取日志中的时间字段 if timeIdx := strings.Index(content, `time="`); timeIdx != -1 { if endTimeIdx := strings.Index(content[timeIdx+6:], `"`); endTimeIdx != -1 { logEntry.Time = content[timeIdx+6 : timeIdx+6+endTimeIdx] } } // 提取日志中的msg字段作为主要内容 if msgIdx := strings.Index(content, `msg="`); msgIdx != -1 { if endMsgIdx := strings.Index(content[msgIdx+5:], `"`); endMsgIdx != -1 { logEntry.Log = content[msgIdx+5 : msgIdx+5+endMsgIdx] } } return logEntry } func main() { ctx := context.Background() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } defer cli.Close() containerID := "bcd693465a62" options := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Since: "", Until: "", Timestamps: false, // 关闭API自带时间戳,自行解析或生成 Follow: true, Tail: "", Details: false, // 关闭额外细节,降低解析复杂度 } out, err := cli.ContainerLogs(ctx, containerID, options) if err != nil { panic(err) } defer out.Close() reader := bufio.NewReader(out) result := ContainerLogResult{ ContainerID: containerID, Logs: []LogEntry{}, } for { stream, contentLen, err := parseLogHeader(reader) if err != nil { if err == io.EOF { break } fmt.Fprintf(os.Stderr, "解析日志头部失败: %v\n", err) continue } content := make([]byte, contentLen) _, err = io.ReadFull(reader, content) if err != nil { fmt.Fprintf(os.Stderr, "读取日志内容失败: %v\n", err) continue } logEntry := parseLogContent(string(content), stream) result.Logs = append(result.Logs, logEntry) // 实时输出统一格式的JSON(可替换为写入文件/数据库逻辑) jsonResult, _ := json.MarshalIndent(result, "", " ") fmt.Println(string(jsonResult)) } }
关键逻辑说明
- 日志头部解析:通过
parseLogHeader函数读取Docker API返回的8字节头部,区分日志来自stdout还是stderr,并获取日志内容的实际长度; - 多格式兼容:
- 优先解析Docker标准JSON日志,直接复用其
log、stream、time字段; - 对键值对格式日志,提取
time和msg字段填充到统一结构; - 纯文本日志直接使用原始内容作为
log,流类型来自头部解析结果,时间默认使用当前时间;
- 优先解析Docker标准JSON日志,直接复用其
- 实时处理:代码在每次解析完日志条目后,会序列化并输出完整结果,可根据需求修改为写入文件或存储到数据库。
内容的提问来源于stack exchange,提问作者Shri
相关产品推荐
相关产品推荐

