Kitex日志封装

2023-07-02T14:21:02+08:00 | 6分钟阅读 | 更新于 2023-07-02T14:22:02+08:00

@

这里是Kitex日志klog的处理


一、klog

file, err2 := os.OpenFile("log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err2 != nil {
    klog.Fatal(err2)
}
defer file.Close()
klog.SetLevel(klog.LevelInfo)
klog.SetOutput(file)

klog只是封装了日志标准,并没有实现线上日志功能,需要zap


二、Kitex封装zap

1、核心以来

# 核心日志库
go get go.uber.org/zap

# 日志轮转按大小切割稳定通用
go get github.com/natefinch/lumberjack

# 可选按时间+大小双维度切割
go get github.com/lestrrat-go/file-rotatelogs

# Kitex 日志接口
go get github.com/cloudwego/kitex/pkg/klog

2、步骤 1:定义统一配置结构体

对齐大厂运维规范,收敛所有日志配置项,支持环境差异化配置:

type LogConfig struct {
    // 基础配置
    Level      string // 日志级别: trace/debug/info/notice/warn/error/fatal
    Service    string // 服务名,自动注入每条日志
    Env        string // 环境: dev/test/prod
    Console    bool   // 是否同时输出控制台
    CallerSkip int    // 调用栈深度,封装层修正行号

    // 本地切片存储配置
    FilePath    string // 日志文件路径,如 /var/log/kitex/{service}.log
    MaxSize     int    // 单文件最大大小,单位MB
    MaxAge      int    // 日志保留天数
    MaxBackups  int    // 最大保留文件数
    Compress    bool   // 归档文件是否压缩
    RotateByDay bool   // 是否按自然天切割
    BufferSize  int    // 写入缓冲区大小,单位KB,0表示同步写入
}

3、步骤 2:实现本地切片 WriteSyncer

Zap 本身不提供文件轮转能力,需通过自定义 zapcore.WriteSyncer 实现本地切片存储,生产环境推荐大小 + 时间双触发切割

方案 A:基于 lumberjack(按大小切割,稳定极简)

适合对时间切割无强要求、仅需控制单文件大小的场景,也是 Zap 官方推荐方案:

func buildFileSyncer(cfg *LogConfig) zapcore.WriteSyncer {
    lumberJackLogger := &lumberjack.Logger{
        Filename:   cfg.FilePath,
        MaxSize:    cfg.MaxSize,    // 单文件最大MB
        MaxAge:     cfg.MaxAge,     // 保留天数
        MaxBackups: cfg.MaxBackups, // 最大备份数
        Compress:   cfg.Compress,   // gzip压缩归档
        LocalTime:  true,           // 使用本地时间命名
    }

    // 开启缓冲写入,降低磁盘IO频次
    if cfg.BufferSize > 0 {
        return &zapcore.BufferedWriteSyncer{
            WS:            zapcore.AddSync(lumberJackLogger),
            Size:          cfg.BufferSize * 1024,
            FlushInterval: time.Second * 5,
        }
    }
    return zapcore.AddSync(lumberJackLogger)
}

方案 B:按自然天 + 大小双维度切割(大厂生产标准)

支持按天自动切分,同时限制单文件大小,符合运维日志采集与排查习惯,基于 file-rotatelogs 实现:

func buildRotateSyncer(cfg *LogConfig) zapcore.WriteSyncer {
    // 日志路径模板,自动按日期生成后缀,如 app.log -> app.log.20260613
    logPath := fmt.Sprintf("%s.%s", cfg.FilePath, "%Y%m%d")

    rotator, _ := rotatelogs.New(
        logPath,
        rotatelogs.WithLinkName(cfg.FilePath),       // 保留软链接指向最新文件
        rotatelogs.WithMaxAge(time.Duration(cfg.MaxAge)*24*time.Hour),
        rotatelogs.WithRotationCount(uint(cfg.MaxBackups)),
        rotatelogs.WithRotationSize(int64(cfg.MaxSize)*1024*1024), // 单文件大小上限
        rotatelogs.WithClock(rotatelogs.Local),
    )

    if cfg.BufferSize > 0 {
        return &zapcore.BufferedWriteSyncer{
            WS:            zapcore.AddSync(rotator),
            Size:          cfg.BufferSize * 1024,
            FlushInterval: time.Second * 5,
        }
    }
    return zapcore.AddSync(rotator)
}

4、步骤 3:构建 Zap 核心实例

func NewZapLogger(cfg *LogConfig) (*zap.Logger, error) {
    // 1. 级别映射:Kitex 7级日志 -> Zap 级别
    level := mapKlogLevelToZap(cfg.Level)

    // 2. 编码器配置:生产环境JSON结构化,开发环境Console带颜色
    var encoder zapcore.Encoder
    encoderConfig := zap.NewProductionEncoderConfig()
    encoderConfig.TimeKey = "time"
    encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000")
    encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder

    if cfg.Env == "dev" {
        encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
        encoder = zapcore.NewConsoleEncoder(encoderConfig)
    } else {
        encoder = zapcore.NewJSONEncoder(encoderConfig)
    }

    // 3. 多输出源组合:本地文件 + 控制台(开发环境)
    var cores []zapcore.Core
    fileSyncer := buildRotateSyncer(cfg) // 或 buildFileSyncer
    cores = append(cores, zapcore.NewCore(encoder, fileSyncer, level))

    if cfg.Console {
        consoleSyncer := zapcore.AddSync(os.Stdout)
        cores = append(cores, zapcore.NewCore(encoder, consoleSyncer, level))
    }

    // 4. 可选:高流量场景开启日志采样,防止打爆磁盘
    // 每秒前100条全量输出,之后每100条输出1条
    teeCore := zapcore.NewTee(cores...)
    samplingCore := zapcore.NewSamplerWithOptions(
        teeCore, time.Second, 100, 100,
    )

    // 5. 通用选项:调用栈修正、基础字段注入
    opts := []zap.Option{
        zap.AddCaller(),
        zap.AddCallerSkip(cfg.CallerSkip), // 修正封装层导致的行号偏移
        zap.Fields(
            zap.String("service", cfg.Service),
            zap.String("env", cfg.Env),
        ),
    }

    return zap.New(samplingCore, opts...), nil
}

// Kitex日志级别 -> Zap级别映射
func mapKlogLevelToZap(level string) zapcore.LevelEnabler {
    switch strings.ToLower(level) {
    case "trace", "debug":
        return zap.DebugLevel
    case "info", "notice":
        return zap.InfoLevel
    case "warn":
        return zap.WarnLevel
    case "error":
        return zap.ErrorLevel
    case "fatal":
        return zap.FatalLevel
    default:
        return zap.InfoLevel
    }
}

5、步骤 4:适配 Kitex klog.FullLogger 接口

Kitex 的日志体系定义了 4 个子接口,必须全部实现才能通过 klog.SetLogger 注入:

  • Logger:基础分级日志(Info、Error 等)
  • FormatLogger:格式化日志(Infof、Errorf 等)
  • CtxLogger:带上下文的日志(CtxInfof 等,用于链路透传)
  • Control:运行时控制(SetLevel、SetOutput)
type ZapKitexLogger struct {
    logger *zap.Logger
    sugar  *zap.SugaredLogger
    level  klog.Level
}

// 构造适配实例,CallerSkip 设为2:适配层+封装层
func NewZapKitexLogger(zapLogger *zap.Logger) *ZapKitexLogger {
    return &ZapKitexLogger{
        logger: zapLogger,
        sugar:  zapLogger.Sugar(),
        level:  klog.LevelInfo,
    }
}

// ---------- Logger 接口实现 ----------
func (l *ZapKitexLogger) Trace(v ...interface{})  { l.logger.Debug(fmt.Sprint(v...)) }
func (l *ZapKitexLogger) Debug(v ...interface{})  { l.logger.Debug(fmt.Sprint(v...)) }
func (l *ZapKitexLogger) Info(v ...interface{})   { l.logger.Info(fmt.Sprint(v...)) }
func (l *ZapKitexLogger) Notice(v ...interface{}) { l.logger.Info(fmt.Sprint(v...)) }
func (l *ZapKitexLogger) Warn(v ...interface{})   { l.logger.Warn(fmt.Sprint(v...)) }
func (l *ZapKitexLogger) Error(v ...interface{})  { l.logger.Error(fmt.Sprint(v...)) }
func (l *ZapKitexLogger) Fatal(v ...interface{})  { l.logger.Fatal(fmt.Sprint(v...)) }

// ---------- FormatLogger 接口实现 ----------
func (l *ZapKitexLogger) Tracef(format string, v ...interface{})  { l.logger.Debugf(format, v...) }
func (l *ZapKitexLogger) Debugf(format string, v ...interface{})  { l.logger.Debugf(format, v...) }
func (l *ZapKitexLogger) Infof(format string, v ...interface{})   { l.logger.Infof(format, v...) }
func (l *ZapKitexLogger) Noticef(format string, v ...interface{}) { l.logger.Infof(format, v...) }
func (l *ZapKitexLogger) Warnf(format string, v ...interface{})   { l.logger.Warnf(format, v...) }
func (l *ZapKitexLogger) Errorf(format string, v ...interface{})  { l.logger.Errorf(format, v...) }
func (l *ZapKitexLogger) Fatalf(format string, v ...interface{})  { l.logger.Fatalf(format, v...) }

// ---------- CtxLogger 接口实现(核心:链路ID透传) ----------
func (l *ZapKitexLogger) CtxTracef(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Debugf(format, v...)
}
func (l *ZapKitexLogger) CtxDebugf(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Debugf(format, v...)
}
func (l *ZapKitexLogger) CtxInfof(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Infof(format, v...)
}
func (l *ZapKitexLogger) CtxNoticef(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Infof(format, v...)
}
func (l *ZapKitexLogger) CtxWarnf(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Warnf(format, v...)
}
func (l *ZapKitexLogger) CtxErrorf(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Errorf(format, v...)
}
func (l *ZapKitexLogger) CtxFatalf(ctx context.Context, format string, v ...interface{}) {
    l.withCtx(ctx).Fatalf(format, v...)
}

// 从Context提取链路字段,自动注入日志
func (l *ZapKitexLogger) withCtx(ctx context.Context) *zap.SugaredLogger {
    if ctx == nil {
        return l.sugar
    }
    // 示例:从OpenTelemetry Context提取trace_id、span_id
    span := trace.SpanFromContext(ctx)
    if span.SpanContext().IsValid() {
        return l.sugar.With(
            zap.String("trace_id", span.SpanContext().TraceID().String()),
            zap.String("span_id", span.SpanContext().SpanID().String()),
        )
    }
    return l.sugar
}

// ---------- Control 接口实现 ----------
func (l *ZapKitexLogger) SetLevel(level klog.Level) {
    l.level = level
    // 动态修改Zap日志级别(需使用zap.AtomicLevel)
}
func (l *ZapKitexLogger) SetOutput(w io.Writer) {
    // 可选实现,一般不动态切换输出
}

6、步骤 5:注入 Kitex 服务

在服务启动最早期完成注入,确保框架启动日志也能被捕获:

func init() {
    // 1. 初始化Zap核心
    cfg := &LogConfig{
        Level:      "info",
        Service:    "user-service",
        Env:        "prod",
        FilePath:   "/var/log/kitex/user-service.log",
        MaxSize:    200,   // 单文件200MB
        MaxAge:     7,     // 保留7天
        MaxBackups: 14,    // 最多14个文件
        Compress:   true,
        BufferSize: 256,   // 256KB缓冲区
        CallerSkip: 2,
    }

    zapLogger, err := NewZapLogger(cfg)
    if err != nil {
        panic(err)
    }

    // 2. 适配Kitex接口并全局注入
    kitexLogger := NewZapKitexLogger(zapLogger)
    klog.SetLogger(kitexLogger)
    klog.SetLevel(klog.LevelInfo)
}

三、 [[动态日志级别热更新]]


大厂级注意事项

1. 本地切片存储工程规范

  • 路径与命名标准化:统一使用 /var/log/{业务域}/{服务名}.log 路径,归档文件带日期 + 序号后缀,保留 latest 软链接,适配日志采集 Agent 的固定采集规则。
  • 双维度轮转策略:必须同时设置文件大小上限保留天数 / 数量,避免单文件过大无法打开,也避免日志无限增长占满磁盘;生产环境推荐单文件 100-500MB,保留 7-14 天。
  • 磁盘降级容错:日志写入失败(磁盘满、权限错误)时,绝对不能阻塞业务线程或导致服务 panic;lumberjack 内部已做错误吞掉处理,自研实现需增加降级逻辑(如丢弃日志、降级到 stderr)。
  • 归档压缩:历史日志默认开启 gzip 压缩,可减少 80% 以上磁盘占用;排查问题时再按需解压。

2. 性能与稳定性保障

  • 缓冲写入必开:高并发服务必须使用 BufferedWriteSyncer,将频繁的小 IO 合并为批量写入,降低磁盘压力;设置 5s 以内的刷盘间隔,兼顾性能与日志实时性。
  • 日志采样机制:QPS 过千的服务必须开启 Zap 内置采样器,对高频重复日志(如参数校验失败、缓存未命中)进行采样,避免日志量暴涨打爆磁盘和采集链路。
  • 调用栈深度校准:每多一层封装就需要对应增加 CallerSkip,否则日志显示的文件名、行号会指向封装层而非业务代码,失去排查价值。
  • 优雅关闭:服务退出前必须调用 zapLogger.Sync() 强制刷盘,尤其是开启缓冲写入时,否则会丢失最后几秒的日志。
  • 优先强类型字段:性能敏感链路使用 zap.Logger 强类型 Field,少用 SugaredLogger 的格式化和 interface{} 参数,减少反射与内存分配。

3. Kitex 集成避坑点

  • 接口必须全实现klog.SetLogger 要求传入完整实现 FullLogger 的实例,缺任何一个方法都会导致编译失败或运行时异常;尤其不要遗漏 NoticeCtx 系列方法。
  • SetLogger 时机:该方法非并发安全,必须在服务初始化阶段(main 最开头或 init)调用,运行中禁止动态替换日志实例。
  • 框架日志与业务日志分离:Kitex 框架自身日志(连接池、超时、重试)和业务日志可分开配置,比如框架日志只打 Warn 以上级别,避免框架日志淹没业务日志。
  • 不要重复封装:业务层直接使用 klog.CtxInfof 等标准方法即可,无需再封装一层业务日志包,避免调用栈深度叠加失控。

4. 运维与可观测性规范

  • 字段命名统一:结构化日志的 key 必须全公司统一(如 trace_idserviceerrorcost),否则日志平台无法建立统一索引和告警规则。
  • 敏感信息脱敏:通过 Zap Hook 机制对手机号、身份证、密钥、请求参数中的敏感字段自动脱敏,避免合规风险。
  • 级别动态可调:结合配置中心(如 Apollo、Nacos)实现日志级别热更新,排查问题时临时开启 Debug 级别,无需重启服务。
  • 错误分级告警:在 Zap Hook 中对接告警系统,Fatal 级别立即触发电话告警,Error 级别聚合后告警,避免无效告警。
About Me

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

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

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

目标

学AI,加油!加油!