构建企业级Docker日志驱动:将容器日志无缝发送到腾讯云CLS

源码地址:github.com/k8scat/dock...

在现代云原生架构中,容器化应用已经成为主流部署方式。随着容器数量的快速增长,如何高效地收集、存储和分析容器日志成为了一个关键挑战。传统的日志收集方式往往存在以下问题:

  • 日志分散在各个容器中,难以统一管理
  • 缺乏结构化的日志格式,不利于后续分析
  • 日志存储成本高,且难以进行实时查询
  • 缺乏统一的日志检索和监控机制

为了解决这些问题,我们开发了一个专门的 Docker 日志驱动,将容器日志直接发送到腾讯云的 CLS(Cloud Log Service)日志服务。这个驱动实现了与 Docker 日志系统的深度集成,提供了高性能、可靠的日志传输能力。

技术架构设计

整体架构

该日志驱动采用了模块化的设计架构,主要包含以下几个核心组件:

  1. Driver 模块:负责管理日志流和容器生命周期
  2. Logger 模块:处理日志格式化和发送逻辑
  3. Client 模块:封装腾讯云 CLS SDK 的调用
  4. Server 模块:提供 Docker 插件接口服务
  5. 配置管理模块:处理各种配置参数的解析和验证

这种分层架构确保了代码的可维护性和可扩展性,每个模块都有明确的职责边界。

核心数据结构

项目定义了多个关键的数据结构来支持日志驱动的功能:

go 复制代码
type Driver struct {
    streams          map[string]*logStream
    containerStreams map[string]*logStream
    mu               sync.RWMutex
    fs               fileSystem
    newTencentCLSLogger newTencentCLSLoggerFunc
    processLogs      func(stream *logStream)
    logger           *zap.Logger
}

type TencentCLSLogger struct {
    client            client
    formatter         *messageFormatter
    cfg               *loggerConfig
    buffer            chan string
    mu                sync.Mutex
    partialLogsBuffer *partialLogBuffer
    wg                sync.WaitGroup
    closed            chan struct{}
    logger            *zap.Logger
}

这些数据结构的设计充分考虑了并发安全性和资源管理,确保了在高并发场景下的稳定运行。

核心功能实现

日志流管理

日志驱动的核心功能是管理容器的日志流。每个容器启动时,驱动会创建一个独立的日志流来处理该容器的所有日志输出:

go 复制代码
func (d *Driver) StartLogging(streamPath string, containerDetails *ContainerDetails) (stream *logStream, err error) {
    d.logger.Info("starting logging", zap.String("stream_path", streamPath), zap.Any("container_details", containerDetails))
    
    d.mu.RLock()
    if _, ok := d.streams[streamPath]; ok {
        d.mu.RUnlock()
        return nil, errors.New("already logging")
    }
    d.mu.RUnlock()
    
    name := "container:" + containerDetails.ContainerName
    stream = &logStream{
        streamPath:       streamPath,
        containerDetails: containerDetails,
        logger:           d.logger.Named(name),
        fs:               d.fs,
        stop:             make(chan struct{}),
    }
    
    // 初始化日志流
    if err := d.initializeStream(stream); err != nil {
        return nil, err
    }
    
    // 启动日志处理协程
    go d.processLogs(stream)
    
    return stream, nil
}

这种设计确保了每个容器的日志都能被独立处理,避免了不同容器之间的日志混淆。

日志处理流程

日志处理采用了异步非阻塞的设计模式,确保不会影响容器的正常运行:

go 复制代码
func (d *Driver) defaultProcessLogs(stream *logStream, processedNotifier chan<- struct{}) {
    defer func() {
        if err := stream.Close(); err != nil {
            d.logger.Error("failed to close stream", zap.Error(err))
        }
    }()
    
    logs := NewLogs(stream)
    for logs.Next() {
        select {
        case <-stream.stop:
            return
        default:
        }
        
        entry := logs.Log()
        log := &logger.Message{
            Line:         entry.GetLine(),
            Source:       entry.GetSource(),
            Timestamp:    time.Unix(0, entry.GetTimeNano()),
            PLogMetaData: partialLog,
        }
        
        // 发送到腾讯云 CLS
        if err := stream.tencentCLSLogger.Log(log); err != nil {
            stream.logger.Error("failed to log to tencent cls logger", zap.Error(err))
        }
        
        // 可选:保存到本地文件
        if stream.jsonLogger != nil {
            if err := stream.jsonLogger.Log(log); err != nil {
                stream.logger.Error("failed to log to json logger", zap.Error(err))
            }
        }
    }
}

这种设计确保了日志处理的可靠性和性能,即使在网络不稳定的情况下也能保证日志的完整性。

日志格式化与模板

驱动支持灵活的日志格式配置,用户可以通过模板来自定义日志的输出格式:

go 复制代码
type messageFormatter struct {
    template         *fasttemplate.Template
    containerDetails *ContainerDetails
    attrs            map[string]string
}

func (f *messageFormatter) tagFunc(msg *logger.Message) fasttemplate.TagFunc {
    return func(w io.Writer, tag string) (int, error) {
        switch tag {
        case "log":
            return w.Write(msg.Line)
        case "timestamp":
            return w.Write([]byte(msg.Timestamp.UTC().Format(time.RFC3339)))
        case "container_id":
            return w.Write([]byte(f.containerDetails.ID()))
        case "container_name":
            return w.Write([]byte(f.containerDetails.Name()))
        case "image_name":
            return w.Write([]byte(f.containerDetails.ImageName()))
        case "daemon_name":
            return w.Write([]byte(f.containerDetails.DaemonName))
        }
        
        if value, ok := f.attrs[tag]; ok {
            return w.Write([]byte(value))
        }
        
        return 0, fmt.Errorf("%w: %s", errUnknownTag, tag)
    }
}

支持的模板标签包括:

  • {log}:原始日志内容
  • {timestamp}:日志时间戳
  • {container_id}:容器ID
  • {container_name}:容器名称
  • {image_name}:镜像名称
  • {daemon_name}:Docker 守护进程名称

腾讯云 CLS 集成

驱动使用腾讯云官方提供的 SDK 来实现与 CLS 服务的集成:

go 复制代码
type Client struct {
    logger   *zap.Logger
    cfg      ClientConfig
    producer *tencentcloud_cls_sdk_go.AsyncProducerClient
    callback *clsCallback
}

func (c *Client) SendMessage(text string) error {
    addLogMap := map[string]string{}
    if err := json.Unmarshal([]byte(text), &addLogMap); err != nil {
        c.logger.Debug("failed to unmarshal log", zap.String("log", text), zap.Error(err))
        addLogMap["content"] = text
    }
    
    // 添加实例信息
    if c.cfg.InstanceInfo != "" {
        instanceInfo := map[string]string{}
        if err := json.Unmarshal([]byte(c.cfg.InstanceInfo), &instanceInfo); err != nil {
            addLogMap["instance"] = c.cfg.InstanceInfo
        } else {
            for k, v := range instanceInfo {
                addLogMap["__instance__."+k] = v
            }
        }
    }
    
    // 添加容器详情
    if len(c.cfg.AppendContainerDetailsKeys) > 0 {
        for _, k := range c.cfg.AppendContainerDetailsKeys {
            switch k {
            case "container_id":
                addLogMap["__container_details__.container_id"] = c.cfg.ContainerDetails.ContainerID
            case "container_name":
                addLogMap["__container_details__.container_name"] = c.cfg.ContainerDetails.ContainerName
            // ... 其他字段
            }
        }
    }
    
    log := tencentcloud_cls_sdk_go.NewCLSLog(time.Now().Unix(), addLogMap)
    err := c.producer.SendLog(c.cfg.TopicID, log, c.callback)
    if err != nil {
        return fmt.Errorf("failed to send message: %w", err)
    }
    
    return nil
}

这种设计确保了日志能够以结构化的形式发送到 CLS,便于后续的查询和分析。

配置管理与灵活性

配置参数设计

驱动支持丰富的配置参数,满足不同场景的需求:

  • 必需参数

    • endpoint:腾讯云 CLS 服务端点
    • secret_id:腾讯云 API 密钥 ID
    • secret_key:腾讯云 API 密钥
    • topic_id:CLS 主题 ID
  • 可选参数

    • template:日志格式模板
    • filter-regex:日志过滤正则表达式
    • retries:重试次数
    • timeout:请求超时时间
    • no-file:是否禁用本地文件存储
    • keep-file:容器停止后是否保留日志文件

配置解析与验证

驱动实现了完善的配置解析和验证机制:

go 复制代码
func parseLoggerConfig(containerDetails *ContainerDetails) (*loggerConfig, error) {
    clientConfig, err := parseClientConfig(containerDetails)
    if err != nil {
        return nil, fmt.Errorf("failed to parse client config: %w", err)
    }
    
    attrs, err := containerDetails.ExtraAttributes(nil)
    if err != nil {
        return nil, fmt.Errorf("failed to parse extra attributes: %w", err)
    }
    
    cfg := defaultLoggerConfig
    cfg.ClientConfig = clientConfig
    cfg.Attrs = attrs
    
    // 解析模板配置
    if template, ok := containerDetails.Config[cfgTemplateKey]; ok {
        cfg.Template = template
    }
    
    // 解析过滤正则表达式
    if filterRegex, ok := containerDetails.Config[cfgFilterRegexKey]; ok {
        cfg.FilterRegex, err = regexp.Compile(filterRegex)
        if err != nil {
            return nil, fmt.Errorf("failed to parse %q option: %w", cfgFilterRegexKey, err)
        }
    }
    
    if err := cfg.Validate(containerDetails.Config); err != nil {
        return nil, err
    }
    
    return &cfg, nil
}

这种设计确保了配置的正确性和一致性,避免了因配置错误导致的问题。

错误处理与可靠性

重试机制

驱动实现了完善的重试机制,确保在网络不稳定的情况下能够可靠地发送日志:

go 复制代码
type ClientConfig struct {
    Endpoint     string
    SecretID     string
    SecretKey    string
    TopicID      string
    InstanceInfo string
    
    AppendContainerDetailsKeys []string
    ContainerDetails           *ContainerDetails
    
    // 重试配置
    Retries int
    
    // 超时配置
    Timeout time.Duration
}

部分日志处理

对于跨多个日志条目的长日志,驱动实现了部分日志的缓冲和组装机制:

go 复制代码
type partialLogBuffer struct {
    logs map[string]*logger.Message
    mu   sync.Mutex
}

func (b *partialLogBuffer) Append(log *logger.Message) (*logger.Message, bool) {
    if log.PLogMetaData == nil {
        panic("log must be partial")
    }
    
    b.mu.Lock()
    defer b.mu.Unlock()
    
    plog, exists := b.logs[log.PLogMetaData.ID]
    if !exists {
        plog = new(logger.Message)
        *plog = *log
        b.logs[plog.PLogMetaData.ID] = plog
        plog.Line = make([]byte, 0, 16*1024)
        plog.PLogMetaData = nil
    }
    
    plog.Line = append(plog.Line, log.Line...)
    
    if log.PLogMetaData.Last {
        delete(b.logs, log.PLogMetaData.ID)
        return plog, true
    }
    
    return nil, false
}

这种设计确保了长日志的完整性,避免了日志被截断的问题。

性能优化与监控

异步处理

驱动采用了异步处理模式,确保日志发送不会阻塞容器的正常运行:

lua 复制代码
func (l *TencentCLSLogger) Log(log *logger.Message) error {
    if l.isClosed() {
        return errLoggerClosed
    }
    
    if log.PLogMetaData != nil {
        assembledLog, last := l.partialLogsBuffer.Append(log)
        if !last {
            return nil
        }
        *log = *assembledLog
    }
    
    if l.cfg.FilterRegex != nil && !l.cfg.FilterRegex.Match(log.Line) {
        l.logger.Debug("message is filtered out by regex", zap.String("regex", l.cfg.FilterRegex.String()))
        return nil
    }
    
    text := l.formatter.Format(log)
    l.send(text)
    return nil
}

日志级别控制

驱动支持灵活的日志级别控制,便于调试和监控:

go 复制代码
func newLogger(env string, logLevel string) (*zap.Logger, error) {
    var cfg zap.Config
    if env == "production" {
        cfg = zap.NewProductionConfig()
    } else {
        cfg = zap.NewDevelopmentConfig()
    }
    
    var err error
    cfg.Level, err = zap.ParseAtomicLevel(logLevel)
    if err != nil {
        return nil, fmt.Errorf("failed to parse log level: %w", err)
    }
    
    cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    
    return cfg.Build()
}

部署与使用

插件安装

驱动以 Docker 插件的形式提供,安装过程简单:

bash 复制代码
# 安装插件
docker plugin install k8scat/docker-log-driver-tencent-cls:latest \
  --alias tencent-cls \
  --grant-all-permissions

容器级别使用

在运行容器时指定日志驱动:

ini 复制代码
docker run --log-driver=tencent-cls \
    --log-opt endpoint="<endpoint>" \
    --log-opt secret_id="<secret_id>" \
    --log-opt secret_key="<secret_key>" \
    --log-opt topic_id="<topic_id>" \
    --log-opt template="{container_name}: {log}" \
    your_image

全局配置

在 Docker 守护进程级别配置默认日志驱动:

json 复制代码
{
  "log-driver": "tencent-cls",
  "log-opts": {
    "endpoint": "<endpoint>",
    "secret_id": "<secret_id>",
    "secret_key": "<secret_key>",
    "topic_id": "<topic_id>"
  }
}

总结

这个 Docker 日志驱动项目展示了如何构建一个企业级的日志收集解决方案。通过深度集成 Docker 的日志系统,我们实现了高性能、可靠的日志传输能力。项目的主要特点包括:

  • 高性能:采用异步处理模式,确保不影响容器性能
  • 高可靠:实现了完善的重试机制和错误处理
  • 高灵活:支持丰富的配置选项和自定义模板
  • 易部署:以 Docker 插件形式提供,安装使用简单
  • 易维护:模块化设计,代码结构清晰

这个项目为容器化应用的日志管理提供了一个完整的解决方案,能够满足企业级应用的各种需求。未来可以考虑添加更多功能,如日志压缩、加密传输、多租户支持等,进一步提升产品的竞争力。

相关推荐
中东大鹅1 分钟前
SpringBoot创建项目的方式
java·spring boot·github
艺杯羹2 小时前
MyBatis 之缓存机制核心解析
java·后端·spring·mybatis
Brookty2 小时前
【Java学习】匿名内部类的向外访问机制
java·开发语言·后端·学习
程序员爱钓鱼2 小时前
Go语言实战案例-快速排序实现
后端·google·go
程序员爱钓鱼2 小时前
Go语言实战案例-链表的实现与遍历
后端·google·go
超浪的晨10 小时前
Java 实现 B/S 架构详解:从基础到实战,彻底掌握浏览器/服务器编程
java·开发语言·后端·学习·个人开发
追逐时光者11 小时前
一款超级经典复古的 Windows 9x 主题风格 Avalonia UI 控件库,满满的回忆杀!
后端·.net
Python涛哥11 小时前
go语言基础教程:【1】基础语法:变量
开发语言·后端·golang
我命由我1234512 小时前
PostgreSQL 保留关键字冲突问题:语法错误 在 “user“ 或附近的 LINE 1: CREATE TABLE user
数据库·后端·sql·mysql·postgresql·问题·数据库系统
LUCIAZZZ12 小时前
final修饰符不可变的底层
java·开发语言·spring boot·后端·spring·操作系统