• 时序数据库
  • 后端
  • 数据存储
  • 可观测性
  • 云原生

【免费下载链接】mimir

Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.

项目地址: https://gitcode.com/gh_mirrors/mimir/mimir
点击查看 免费下载

klog 是 Kubernetes 生态中 Go 语言最常用的分级日志库,也是 Grafana Mimir(Prometheus 水平可扩展、高可用、多租户长期存储系统)通过间接依赖引入的底层日志组件。本文以仓库内置的 klog v2.140.0 README 为骨架,结合 klog.go、contextual.go、klog_file.go 等源码,系统讲解 klog 的诞生背景、版本发布策略、从 glog 迁移的方法、命令行标志语义、V 分级日志、结构化日志(InfoS/ErrorS)、上下文日志与 logr 对接,以及如何与 glog、klog v1 共存,帮助读者在实际 Go 服务中完整掌握这套日志体系。

为什么会有 klog:glog 停摆后的必然分叉

klog 是 golang/glog 的一个永久性分叉(permanent fork),仓库内 README 开篇即给出这一判断。之所以必须分叉,是因为原版 glog 已不再处于活跃开发状态,其 README 明确写道:"The code in this repo [...] is not itself under development"(本仓库的代码本身不再开发)。这意味着原版 glog 无法解决 Kubernetes 生态持续演化带来的三类痛点:

  1. 容器化环境下的"坑"多且文档缺失:glog 在容器环境(如进程 PID、挂载日志目录)中暴露出大量 undocumented 的 gotcha;
  2. 缺少测试日志的便利手段:glog 不提供简单方式验证日志行为,削弱了依赖它的软件的稳定性;
  3. 长期目标需要可扩展的日志接口:Kubernetes 期望实现一个允许添加上下文(context)、更换输出格式(output format)的日志接口。

上述历史背景在 Kubernetes 仓库的 issue #61006、#70264 以及 sig-architecture / kubernetes-dev 邮件列表中均有记录。需要特别说明的是,Mimir 仓库中的 klog 是间接依赖:在 go.mod 中它以 k8s.io/klog/v2 v2.140.0 // indirect 的形式出现,经由 k8s.io/client-go、k8s.io/apimachinery 等 Kubernetes 客户端库引入(见 modules.txt)。Mimir 自身的业务日志基于 go-kit/log(如 pkg/ruler/notifier.go 中的 github.com/go-kit/log),而 klog 承担的是 Kubernetes 客户端内部日志职责。

版本发布策略:稳定 API 与 EXPERIMENTAL 豁免

klog 仓库采用语义化版本(Semantic Versioning),包含多个稳定性级别不同的 Go module:

  • k8s.io/klog/v2:稳定 API,使用 vX.Y.Z 标签发布(本仓库锁定 v2.140.0);
  • examples:无稳定 API、无标签,官方明确"永不打算稳定化"。

对于 API 稳定性承诺,存在一项例外:凡是在文档注释中被明确标记为 EXPERIMENTAL 的包、函数等,不在稳定性保证范围内,它们可能以不兼容方式变更甚至被整体移除。这一豁免机制的设计初衷是:仅用于测试代码,从而避免"两个不同 Kubernetes 依赖因为某个实验性 API 被改动而依赖互不兼容的 klog 版本"这一场景——即非测试代码不会因为实验 API 的变更而陷入版本冲突死锁。

从 glog 迁移到 klog/v2:四个关键步骤

README 给出了从 github.com/golang/glog 迁移到 k8s.io/klog/v2 的官方路线,结合源码可归纳为如下四步:

1. 替换 import 路径

将代码中所有 "github.com/golang/glog" 的导入路径替换为 "k8s.io/klog/v2"。由于 klog 保留了 glog 的函数签名风格(Info、Infof、Warning、Error、Fatal 等,见 klog.go),绝大多数调用点可以机械替换。

2. 显式调用 klog.InitFlags(nil)

这是与 glog 最显著的行为差异:klog 不再使用 init() 方法注册命令行标志,而是要求使用者在 flag.Parse() 之前显式调用 klog.InitFlags(nil) 完成标志初始化。其源码实现位于 klog.go:

// InitFlags is for explicitly initializing the flags.
func InitFlags(flagset *flag.FlagSet) {
	if flagset == nil {
		flagset = flag.CommandLine
	}
	commandLine.VisitAll(func(f *flag.Flag) {
		flagset.Var(f.Value, f.Name, f.Usage)
	})
}

klog 内部维护一个独立的 commandLine flag.FlagSet(默认值在包级 init() 中通过 commandLine.StringVar/BoolVar/Var 注册,见 klog.go),InitFlags 将该 FlagSet 中的所有标志复制到应用传入的 FlagSet(nil 时即全局 flag.CommandLine)。典型的初始化顺序:

import (
	"flag"

	"k8s.io/klog/v2"
)

func main() {
	klog.InitFlags(nil) // 必须在 flag.Parse() 之前
	flag.Parse()
	defer klog.Flush() // 程序退出前冲刷日志缓冲
	// ... 业务逻辑
}

3. 用 log_file 取代 log_dir 实现单文件日志

glog 时代的 log_dir 会把不同严重级别写到 program.host.user.log.SEVERITY.时间戳.PID 形式的多组文件;klog 新增 log_file 标志,可把所有日志写入单一文件。源码层面,settings 结构体中 logDir 与 logFile 是互斥的两个字段(klog.go),而 create() 函数在 klog_file.go 中优先处理 logFile:

func create(tag string, t time.Time, startup bool) (f *os.File, filename string, err error) {
	if logging.logFile != "" {
		f, err := openOrCreate(logging.logFile, startup)
		if err == nil {
			return f, logging.logFile, nil
		}
		return nil, "", fmt.Errorf("log: unable to create log: %v", err)
	}
	onceLogDirs.Do(createLogDirs)
	// ... 按 logDirs 候选目录逐个尝试
}

4. 用 klog.SetOutput() 重定向全部输出

如果想把 klog 产生的所有日志重定向到其他目标(例如 syslog),可以使用 klog.SetOutput(w io.Writer)。其实现会为从 FATAL 到 INFO 的所有严重级别统一设置输出 writer(klog.go):

// SetOutput sets the output destination for all severities
func SetOutput(w io.Writer) {
	logging.mu.Lock()
	defer logging.mu.Unlock()
	for s := severity.FatalLog; s >= severity.InfoLog; s-- {
		logging.file[s] = w
	}
}

需要逐级别定制时还可使用 klog.SetOutputBySeverity(name string, w io.Writer)(klog.go),name 必须是 INFO / WARNING / ERROR / FATAL 之一,否则会直接 panic。

核心命令行标志全解:输出路由与调试工具

klog 的全部运行时行为由一组标志驱动。以下基于 klog.go 的注册代码与包级文档(klog.go)整理:

标志默认值语义与关键说明
-logtostderrtrue日志写到标准错误而非文件。为 true 时 alsologtostderr、alsologtostderrthreshold、log_dir 及运行时 SetOutput 均无效果
-alsologtostderrfalse日志同时写到 stderr 与文件(logtostderr=true 时无效果)
-alsologtostderrthresholdINFOalsologtostderr=true 时,达到该严重级别的日志额外输出到 stderr(logtostderr=true 时无效果);默认 INFO 保持向后兼容
-stderrthresholdERROR达到该级别的日志在写文件的同时也写 stderr。logtostderr=true 时仅在 -legacy_stderr_threshold_behavior=false 下生效
-legacy_stderr_threshold_behaviortruetrue 时 logtostderr=true 下忽略 stderrthreshold(旧行为,默认直接全量写 stderr);false 时即使 logtostderr=true 也按 stderrthreshold 做级别过滤
-log_dir""日志文件写入该目录而非默认临时目录(logtostderr=true 时无效果)
-log_file""指定单一日志文件路径(logtostderr=true 时无效果),与 log_dir 互斥
-log_file_max_size1800(MB)单一日志文件的最大体积,0 表示不限制(logtostderr=true 时无效果)。计算逻辑见 klog.go
-log_backtrace_at""格式为 file.go:line,如 -log_backtrace_at=gopherflakes.go:234;命中该调用点时在 Info 日志中打印堆栈。注意与 vmodule 不同,此处必须带 .go 后缀(klog.go)
-v0全局 V 级别日志阈值,klog.V(n) 在 n <= v 时输出
-vmodule""文件级 V 级别控制,语法为 pattern=N 的逗号分隔列表;pattern 是去掉 .go 后缀的字面文件名或 glob,如 -vmodule=gopher*=3 让所有 gopher 开头的 Go 文件以 V=3 输出
-add_dir_headerfalse为日志头添加文件目录信息(klog.go)
-skip_headersfalse日志消息不添加前缀头部(常用于配合 SetOutput)
-one_outputfalse只写入原生严重级别对应的文件,不向更低级别传播(logtostderr=true 时无效果)
-skip_log_headersfalse打开日志文件时不写文件头(logtostderr=true 时无效果)

其中 vmodule 的解析在源码中值得细看:parseModuleSpec(klog.go)按逗号切分每个 pattern=N,N=0 的条目会被直接忽略(保留它会带来不必要的开销),负值则报错;pattern 不含 \*?[] 元字符时走字面量字符串比较(isLiteral),否则用 filepath.Match 做 glob 匹配(klog.go)。V 级别的检查快路径只做两次原子读比较,命中 vmodule 时才加锁并借助 vmap map[uintptr]Level 按调用点 PC 缓存级别,避免每次求值(klog.go)。

V 分级日志:零开销求值与两种写法

klog 继承并优化了 glog 的 V-style 日志模型:通过把日志调用绑定到布尔返回值,可以在日志被关闭时避免求值参数("By binding methods to booleans it is possible to use the log package without paying the expense of evaluating the arguments")。README 中给出了两种推荐写法:

// 写法一:显式判断,日志关闭时连参数表达式都不执行,最省
if klog.V(2) {
	klog.Info("Starting transaction...")
}

// 写法二:链式调用,更短
klog.V(2).Infoln("Processed", nItems, "elements")

V(level Level) 返回 Verbose 类型(klog.go),其是否输出取决于两个条件:调用级别 level 小于等于 -v 标志值,或者小于等于 -vmodule 中匹配到该源文件的 pattern 所设级别。Verbose 还提供 Enabled()、Info、Infof、Infoln、InfoS、ErrorS 等全套受保护方法(klog.go),每个方法内部先检查 v.enabled 再进入日志管线。VDepth(depth, level) 则用于在多层封装下正确回溯调用帧以匹配 vmodule。

结构化日志:InfoS / ErrorS 与 key=value 输出

klog/v2 的另一大演进是原生支持结构化日志(structured logging)。README 未展开,但 klog.go 的源码注释给出了完整示例与输出格式:

klog.InfoS("Pod status updated", "pod", "kubedns", "status", "ready")
// 输出:
// I1025 00:15:15.525108       1 controller_utils.go:116] "Pod status updated" pod="kubedns" status="ready"

错误场景则用 ErrorS(err, msg, kv...),err 会自动序列化为 err="..." 字段:

klog.ErrorS(err, "Failed to update pod status")
// 输出:
// E1025 00:15:15.525108       1 controller_utils.go:114] "Failed to update pod status" err="timeout"

实现上,printS(klog.go)会先用 strconv.AppendQuote 对消息整体加引号(即使含换行也只出现在 value 中),再通过 serialize.FormatKVs 把 key/value 对序列化。与之配套的辅助工具包括:

  • klog.Format(obj)(format.go):包装任意类型,返回漂亮的缩进 JSON 字符串,同时实现 logr.Marshaler 防止 logr 后端再次调用其 String/MarshalJSON;
  • klog.ObjectRef{Name, Namespace}(k8s_references.go):以 namespace/name 形式打印 Kubernetes 对象引用,MarshalLog 通过自定义类型剥离 String 方法以保证结构化后端按 struct 输出。

日志行头格式沿用 C++ glog 定义(klog.go):

Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...

其中 L 为单字符级别(I=INFO、W=WARNING、E=ERROR、F=FATAL),时间戳含微秒,随后是线程 ID、文件名与行号。

严重级别与缓冲冲刷机制

klog 提供 INFO / WARNING / ERROR / FATAL 四级输出,并遵循 glog 的"向低级别传播"传统:Error 同时写入 ERROR、WARNING、INFO 日志,Fatal 则写满全部四级后再退出。这一行为由 output() 中的 switch s + fallthrough 链实现(klog.go),-one_output=true 可关闭传播。

日志输出采用缓冲 + 定时冲刷模型:

  • 每个日志文件由 syncBuffer(bufio.Writer + os.File)包裹,缓冲区大小为 256KB(klog.go);
  • 全局默认冲刷间隔为 5 秒(flushInterval = 5 * time.Second,klog.go),由 flushDaemon 后台 goroutine 周期调用 lockAndFlushAll;
  • 单文件模式下达到 -log_file_max_size(或 log_dir 模式下的内置 MaxSize = 1800MB,见 klog_file.go)时自动轮转(rotateFile,klog.go);
  • 程序退出前应调用 klog.Flush(),否则缓冲中的日志可能丢失;StopFlushDaemon() 可在优雅关停时停止后台冲刷并做一次 flush(klog.go)。

FATAL 语义方面:klog.Fatal* 会打印全 goroutine 堆栈后以 OsExit(255) 退出;klog.Exit* 则不带堆栈、以 OsExit(1) 退出(见 klog.go 与 exit.go 中可覆写的 OsExit、ExitFlushTimeout = 10s、FlushAndExit(flushTimeout, exitCode))。

上下文日志与 logr 对接:SetLogger / Background / FromContext

klog/v2 的重要设计目标是成为可扩展的日志接口。它基于 go-logr 抽象实现"上下文日志"(contextual logging),源码位于 contextual.go,相关能力包括:

  • klog.SetLogger(logr.Logger)(及 SetLoggerWithOptions):把某个 logr 实现(如 zapr、zerologr)注册为 klog 的后端。注册后,传统 klog.Info* 等调用产生的日志行会被重定向到该实现,不再走 klog 自身输出;ClearLogger() 可移除(contextual.go);
  • klog.Background():获取程序的全局 fallback logger;klog.TODO() 是临时替代(代码暂时无法从调用方拿到 logger 时的兜底);klog.FromContext(ctx) 从 context 中取出调用方注入的 logger,取不到则回退到 Background()(contextual.go);
  • klog.NewContext(ctx, logger) / klog.LoggerWithValues / klog.LoggerWithName:在 context 中携带 logger 或为 logger 附加 kv/名称;
  • klog.EnableContextualLogging(false):可整体关闭上下文日志,使 FromContext 直接返回全局 logger、NewContext 等变成原样透传,以省去查找开销(须在初始化阶段、启动 goroutine 之前调用);
  • 选项型 API:ContextualLogger(true) 允许该 logger 被应用直接调用;WriteKlogBuffer(fn) 让非结构化调用(如 Infof)产出的带头部缓冲区原样交给后端;FlushLogger(fn) 注册冲刷回调(contextual.go)。

此外 klog.LogFilter(klog.go)提供全局日志过滤接口,可对 Filter/FilterF/FilterS 做脱敏处理,防止日志意外泄露密钥,适合安全敏感场景。

共存方案:klog v1 与 klog v2、klog 与 glog

README 专门给出了两个共存场景的官方示例:

与 klog/v1 共存:仓库内 examples/coexist_klog_v1_and_v2/ 展示了同时使用 k8s.io/klog(v1)与 k8s.io/klog/v2 的代码组织方式。注意:vendor/k8s.io/klog/v2 目录下并没有捆绑 v1 模块,实际项目中需要单独引入 v1 依赖。

与 glog 共存:examples/coexist_glog/coexist_glog.go 展示了如何初始化并同步两个库在全局 flag.CommandLine FlagSet 上的标志,并将 stderr 设为合并输出(把 alsologtostderr 或 logtostderr 设为 true)。源码上,klog 的这一能力由 InitFlags(flagset) 的"从内部 commandLine 复制到目标 FlagSet"机制保证——同一套标志可以同时注入应用自己的 FlagSet,从而与 glog 的标志互不冲突。

在本仓库中的存在形态:依赖链路与定位

在 Grafana Mimir 仓库中,klog 以 vendor 目录形式固化,定位如下:

  • 依赖声明:go.mod 中 k8s.io/klog/v2 v2.140.0 // indirect,由 k8s.io/client-go 等 Kubernetes 客户端库传递引入;
  • vendor 清单:vendor/modules.txt 列出了实际打包的子包:internal/buffer、internal/clock、internal/dbg、internal/serialize、internal/severity、internal/sloghandler、internal/verbosity、textlogger;
  • 使用方式:Mimir 自身的告警、指标等业务日志基于 go-kit/log(例如 pkg/ruler/notifier.go 的 gklog "github.com/go-kit/log"),klog 则随 Kubernetes 依赖服务化运行。若你在 Mimir 相关的 Go 工程中开发自定义组件并希望获得 Kubernetes 风格的日志输出,可直接按上文迁移步骤接入 k8s.io/klog/v2。

小结:从 glog 到 klog/v2 的迁移决策清单

  1. 替换 import 为 k8s.io/klog/v2,函数签名基本不变;
  2. 在 flag.Parse() 前显式调用 klog.InitFlags(nil)(klog 不再用 init() 注册标志);
  3. 容器化部署优先使用 -log_file 单文件输出,配合 -log_file_max_size 控制轮转;
  4. 需要接入 syslog 等外部通道时使用 klog.SetOutput(io.Writer);
  5. 排查复杂问题时组合 -v、-vmodule 与 -log_backtrace_at 精确定位调用点;
  6. 新代码优先使用 InfoS/ErrorS 结构化日志与 klog.Format 输出可读的复杂对象;
  7. 需要替换日志后端时通过 SetLogger 接入任意 logr 实现,并利用 FromContext/Background 实现上下文日志;
  8. 涉及 v1/v2 或 glog 并存时,参考 examples/coexist_* 的初始化与标志同步模式。
  • 时序数据库
  • 后端
  • 数据存储
  • 可观测性
  • 云原生

【免费下载链接】mimir

Grafana Mimir provides horizontally scalable, highly available, multi-tenant, long-term storage for Prometheus.

项目地址: https://gitcode.com/gh_mirrors/mimir/mimir
点击查看 免费下载
Logo

邀请您加入社区

更多推荐