这里是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的实例,缺任何一个方法都会导致编译失败或运行时异常;尤其不要遗漏Notice和Ctx系列方法。 - SetLogger 时机:该方法非并发安全,必须在服务初始化阶段(main 最开头或 init)调用,运行中禁止动态替换日志实例。
- 框架日志与业务日志分离:Kitex 框架自身日志(连接池、超时、重试)和业务日志可分开配置,比如框架日志只打 Warn 以上级别,避免框架日志淹没业务日志。
- 不要重复封装:业务层直接使用
klog.CtxInfof等标准方法即可,无需再封装一层业务日志包,避免调用栈深度叠加失控。
4. 运维与可观测性规范
- 字段命名统一:结构化日志的 key 必须全公司统一(如
trace_id、service、error、cost),否则日志平台无法建立统一索引和告警规则。 - 敏感信息脱敏:通过 Zap Hook 机制对手机号、身份证、密钥、请求参数中的敏感字段自动脱敏,避免合规风险。
- 级别动态可调:结合配置中心(如 Apollo、Nacos)实现日志级别热更新,排查问题时临时开启 Debug 级别,无需重启服务。
- 错误分级告警:在 Zap Hook 中对接告警系统,Fatal 级别立即触发电话告警,Error 级别聚合后告警,避免无效告警。