Kitex日志

2024-01-17T14:21:02+08:00 | 6分钟阅读 | 更新于 2024-01-17T14:21:02+08:00

@

学习目标

学完本节,你应该能够:

  1. 说清"三件套"的分工:讲清楚为什么 Kitex 的生产日志需要"lumberjack 切割 + Zap 异步缓冲 + klog 适配"组合,而不是简单 log.Println
  2. 讲清级别映射:解释 klog 的 DebugLevel/InfoLevel/... 与 Zap 的 zapcore.Level 之间如何一一对应,以及"级别不同步"会带来什么问题。
  3. 独立搭建一套落地日志:照着代码把这套带按大小切割、异步批量写的 Kitex 日志接到自己的服务里。
  4. 理解关闭钩子里的 Sync:说清为什么 RegisterShutdownHook 里必须 asyncWriter.Sync(),否则可能丢日志。
  5. 面试讲取舍:被问到"异步缓冲 vs 同步写"“日志切割策略"时,能用工程视角讲清代价与适用场景。

前置知识:Go 基础(接口、结构体)、Kitex 基本用法、go.uber.org/zap 的基本概念(Encoder / Core / WriteSyncer)。

本章你会动手做的事

  • 把下面这套日志初始化代码接进一个 Kitex Server,跑起来看日志文件生成。
  • MaxSize 调得很小(如 1MB),连续写日志,观察 lumberjack 是否自动切出新文件。
  • 故意把 FlushInterval 调大(如 10 分钟),在写入后立刻 kill -9 进程,验证"未刷盘日志会丢失”。

基础日志

核心思路

  1. 日志切割:保留 lumberjack.Logger 实现日志文件自动切割(按大小、备份数、保留天数)。
  2. 异步缓冲:保留 zapcore.BufferedWriteSyncer 实现异步批量写日志,减少磁盘 IO。
  3. Zap适配:通过 klog.NewZapLogger 将 Zap Logger 适配为 klog.Logger 接口,替换原 Logrus 适配器。
  4. 级别同步:确保 klog 日志级别与 Zap 日志级别一致。

类比:这套日志链路就像一条"快递流水线"。业务代码(你)把日志"包裹"交给 klog(前台),klog 转手给 Zap Core(分拣中心),Zap 先塞进 BufferedWriteSyncer(暂存笼车,攒一批再发),最后由 lumberjack(仓库管理员)按体积归档到不同文件。每一步只干一件事,合起来才既快又不丢件。

下面这张图把"写一条日志"从头到尾走了一遍,建立整体直觉:

flowchart LR
    A[业务代码调用 klog.Info] --> B[klog.Logger 接口]
    B --> C[klog.NewZapLogger 适配]
    C --> D[Zap Core
编码为 JSON + 级别过滤] D --> E[BufferedWriteSyncer
异步缓冲 攒批] E --> F[lumberjack.Logger
按大小切割] F --> G[(磁盘日志文件)] H[服务关闭钩子] -->|Sync| E

代码实现

package main

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
    "github.com/kitex-contrib/obs-opentelemetry/logging/klog"
    "your_project/conf" // 替换为实际配置包路径(如内部conf模块)
    "time"
)

func setupKlog() {
    // ========== 1. 配置日志切割(lumberjack) ==========
    lumberJackLogger := &lumberjack.Logger{
        Filename:   conf.GetConf().Kitex.LogFileName,   // 日志文件路径
        MaxSize:    conf.GetConf().Kitex.LogMaxSize,    // 单文件最大大小(MB)
        MaxBackups: conf.GetConf().Kitex.LogMaxBackups, // 最多保留旧日志数
        MaxAge:     conf.GetConf().Kitex.LogMaxAge,     // 日志保留天数
    }

    // ========== 2. 配置异步缓冲写(zapcore.BufferedWriteSyncer) ==========
    asyncWriter := &zapcore.BufferedWriteSyncer{
        WS: zapcore.AddSync(lumberJackLogger), // 绑定lumberjack到zap的写同步器
        FlushInterval: time.Minute,           // 缓冲区刷新间隔(1分钟)
    }

    // ========== 3. 设置klog日志级别(同步到Zap) ==========
    logLevel := conf.GetConf().Kitex.LogLevel() // 从配置获取日志级别(如"info"、"debug")
    klog.SetLevel(logLevel)                     // 设置klog的日志级别

    // 转换klog级别 → Zap级别
    var zapLevel zap.Level
    switch klog.Level() { // klog.Level()返回当前设置的级别(如klog.InfoLevel)
    case klog.DebugLevel:
        zapLevel = zap.DebugLevel
    case klog.InfoLevel:
        zapLevel = zap.InfoLevel
    case klog.WarnLevel:
        zapLevel = zap.WarnLevel
    case klog.ErrorLevel:
        zapLevel = zap.ErrorLevel
    case klog.FatalLevel:
        zapLevel = zap.FatalLevel
    default:
        zapLevel = zap.InfoLevel
    }

    // ========== 4. 配置Zap Encoder(日志格式) ==========
    encoderConfig := zap.NewProductionEncoderConfig()
    encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 时间格式(ISO8601)
    encoder := zapcore.NewJSONEncoder(encoderConfig)      // JSON格式编码(也可使用ConsoleEncoder)

    // ========== 5. 创建Zap Core(日志核心:编码器+写同步器+级别) ==========
    core := zapcore.NewCore(
        encoder,          // 日志编码器(JSON格式)
        asyncWriter,      // 异步缓冲写同步器(含日志切割)
        zap.NewAtomicLevelAt(zapLevel), // 日志级别(动态控制输出)
    )

    // ========== 6. 创建Zap Logger并适配为klog.Logger ==========
    zapLogger := zap.New(core, zap.AddCaller()) // 可选:添加调用者信息(显示代码行号)
    klogLogger := klog.NewZapLogger(zapLogger)  // 适配为klog的Logger接口

    // ========== 7. 绑定klog的Logger和输出 ==========
    klog.SetLogger(klogLogger) // 替换原Logrus适配的Logger

    // ========== 8. 注册服务关闭钩子(保证日志落盘) ==========
    server.RegisterShutdownHook(func() {
        asyncWriter.Sync() // 服务关闭时,强制刷写缓冲区的日志
    })
}

// 示例:在main函数中调用setupKlog
func main() {
    // 初始化配置、服务实例等...
    setupKlog()

    // 启动服务(如Kitex Server)
    // server.Run(...)
}

⚠️ 新手必踩的坑:异步缓冲会丢日志。上面 FlushInterval: time.Minute 意味着日志在内存里最多攒 1 分钟才落盘。如果进程被 kill -9 或 panic 没走关闭钩子,BufferedWriteSyncer 里没刷的日志就丢了。生产环境一定要注册 RegisterShutdownHookSync();对"绝不能丢"的审计日志,考虑不用异步缓冲,或把间隔调小。

⚠️ 新手必踩的坑:klog 与 Zap 级别不一致。如果只设了 klog.SetLevel 却没把级别同步给 Zap Core,会出现"klog 说是 debug,Zap 却按 info 过滤"的情况,导致你以为开了 debug 实际看不到。务必走示例里那段 switch klog.Level() 的映射。

自测题与动手练习

自测题(合上书能答出来,才算懂)

  1. lumberjack 的 MaxSizeMaxBackupsMaxAge 三个参数分别控制什么?哪个是按"大小"触发切割、哪个是按"天数"清理旧文件?
  2. zapcore.BufferedWriteSyncer 的作用是什么?它和直接 zapcore.AddSync(file) 写文件相比,牺牲了什么、换来了什么?
  3. 为什么要把 Zap Logger 通过 klog.NewZapLogger 适配成 klog.Logger?Kitex 内部是用哪个日志接口打日志的?
  4. 服务正常 Shutdown 时,为什么必须调 asyncWriter.Sync()?如果忘了,什么场景下会丢日志?
  5. 如果线上想临时把日志级别从 info 调到 debug 排查问题,zap.NewAtomicLevelAt 相比 zapcore.NewCore 里写死的级别好在哪儿?

动手练习(建议真做一遍)

  1. 把这套 setupKlog 接进一个最小 Kitex Server,跑起来后 ls 日志目录,确认文件按 LogFileName 生成、格式是 JSON。
  2. MaxSize 改成 1(MB),写个循环疯狂打日志,观察 lumberjack 是否切出 xxx-1.logxxx-2.log 等备份文件。
  3. 故意把 FlushInterval 改成 10 * time.Minute,写入几条日志后立刻 kill -9 <pid>,再用 tail 看文件——验证未落盘的日志确实丢了,从而理解 Sync 的必要性。

本章小结

  • Kitex 生产日志的标准做法是 lumberjack(切割)+ Zap BufferedWriteSyncer(异步缓冲)+ klog 适配(接口对齐) 三件套,各管一摊:归档、攒批、接口。
  • 级别必须双向同步klog.SetLevel 之后要显式映射成 Zap 的 zapcore.Level,否则级别过滤会错乱。
  • 异步有代价BufferedWriteSyncer 提升磁盘 IO 效率,但进程异常退出可能丢缓冲中的日志,务必用关闭钩子 Sync() 兜底。
  • 下一篇可以接着看 Kitex 的链路追踪(OpenTelemetry)如何与这套日志通过 traceId 串起来,做统一可观测性。
About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!