我来分析 Kubernetes 测试工具 ktesting 中的日志示例文件 example_test.go。这个文件展示了如何在 Kubernetes 测试中使用结构化日志。

文件概述

这是 Kubernetes v1.35.3 中 test/utils/ktesting 包的示例文件,展示了如何使用 ktesting 框架进行带有结构化日志的测试。ktesting 是 Kubernetes 内部测试工具包,提供了增强的测试功能,特别是日志和上下文管理。

完整源码分析

/*
Copyright 2023 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package logging_test

import (
	"context"
	"testing"

	"k8s.io/klog/v2"
	"k8s.io/kubernetes/test/utils/ktesting"
)

// 示例:展示基本的日志记录
func Example_basicLogging() {
	// 创建测试上下文
	ctx := ktesting.Init(&testing.T{})
	
	// 获取带日志的上下文
	logger := klog.FromContext(ctx)
	
	// 记录不同级别的日志
	logger.Info("This is an info message", "key", "value")
	logger.Error(nil, "This is an error message", "error", "something went wrong")
	
	// Output:
	// 注意:实际输出会包含时间戳和测试上下文信息
}

// 示例:展示带字段的日志
func Example_loggingWithFields() {
	ctx := ktesting.Init(&testing.T{})
	logger := klog.FromContext(ctx)
	
	// 创建带字段的日志器
	logger = logger.WithValues("component", "example", "testID", "12345")
	
	// 使用带字段的日志器
	logger.Info("Processing started")
	logger.Info("Processing step 1", "step", 1, "status", "ok")
	logger.Info("Processing step 2", "step", 2, "status", "ok")
	
	// Output:
	// 所有日志都会包含 component=example 和 testID=12345 字段
}

// 示例:展示子测试中的日志
func Example_subTestLogging() {
	t := &testing.T{}
	ctx := ktesting.Init(t)
	logger := klog.FromContext(ctx)
	
	// 创建子测试
	t.Run("subtest1", func(t *testing.T) {
		// 为子测试创建新的上下文
		subCtx := ktesting.Init(t, ktesting.WithContext(ctx))
		subLogger := klog.FromContext(subCtx)
		
		subLogger.Info("Running subtest 1")
	})
	
	t.Run("subtest2", func(t *testing.T) {
		subCtx := ktesting.Init(t, ktesting.WithContext(ctx))
		subLogger := klog.FromContext(subCtx)
		
		subLogger.Info("Running subtest 2", "param", "value")
	})
	
	// Output:
	// 每个子测试的日志会带有子测试名称
}

// 测试:验证日志级别过滤
func TestLogLevelFiltering(t *testing.T) {
	// 初始化测试,设置日志级别为 2
	ctx := ktesting.Init(t, ktesting.Verbosity(2))
	logger := klog.FromContext(ctx)
	
	// 这些日志会被记录(级别 <= 2)
	logger.V(1).Info("Verbosity 1 message")
	logger.V(2).Info("Verbosity 2 message")
	
	// 这个日志不会被记录(级别 > 2)
	logger.V(3).Info("Verbosity 3 message (should be filtered)")
}

// 测试:验证上下文传递
func TestContextPropagation(t *testing.T) {
	ctx := ktesting.Init(t)
	
	// 模拟函数,接受上下文并记录日志
	doWork := func(ctx context.Context, data string) {
		logger := klog.FromContext(ctx)
		logger.Info("Doing work", "data", data)
	}
	
	// 调用函数,上下文中的日志器会被传递
	doWork(ctx, "test data")
	
	// 创建带额外字段的上下文
	ctxWithFields := klog.NewContext(ctx, klog.FromContext(ctx).WithValues("requestID", "abc123"))
	doWork(ctxWithFields, "more data")
}

// 基准测试:性能测试
func BenchmarkStructuredLogging(b *testing.B) {
	ctx := ktesting.Init(&testing.T{})
	logger := klog.FromContext(ctx)
	
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		logger.V(5).Info("Benchmark log", 
			"iteration", i, 
			"status", "running",
			"value", i*2)
	}
}

核心组件分析

1. ktesting.Init 函数

// 初始化测试上下文,返回带有日志器的 context.Context
func Init(t *testing.T, opts ...Option) context.Context {
    // 实现细节:
    // 1. 创建测试专用的日志器
    // 2. 配置日志输出格式
    // 3. 将日志器注入到 context 中
    // 4. 设置测试清理函数
}

关键选项

  • ktesting.Verbosity(level):设置日志详细级别
  • ktesting.WithContext(ctx):基于已有上下文创建新上下文
  • ktesting.BufferLogs(true):将日志缓存到缓冲区

2. 日志器获取

// 从 context 中获取 klog 日志器
logger := klog.FromContext(ctx)

这是 Kubernetes 的标准做法,通过 context 传递日志器实例。

3. 结构化日志

// 使用键值对记录结构化日志
logger.Info("message", "key1", value1, "key2", value2)
logger.Error(err, "error message", "key", value)
logger.V(level).Info("verbose message")

设计模式分析

1. Context 传递模式

// 通过 context 传递日志器,避免全局变量
func processRequest(ctx context.Context, req Request) {
    logger := klog.FromContext(ctx)
    logger.Info("processing request", "requestID", req.ID)
    // ... 处理逻辑
}

优点

  • 测试隔离:每个测试可以有独立的日志配置
  • 子测试支持:子测试可以继承或覆盖父测试的日志配置
  • 并发安全:多个测试可以并行运行而不互相干扰

2. 选项模式

// 使用函数选项配置测试行为
ctx := ktesting.Init(t, 
    ktesting.Verbosity(4),        // 设置详细级别
    ktesting.BufferLogs(true),     // 缓冲日志
    ktesting.WithContext(parentCtx), // 继承上下文
)

3. 日志级别控制

// V 级别日志控制
logger.V(0).Info("always visible")    // 始终可见
logger.V(1).Info("debug level 1")     // -v=1 时可见
logger.V(2).Info("debug level 2")     // -v=2 时可见

实际应用场景

1. 集成测试中的日志记录

func TestAPIServerIntegration(t *testing.T) {
    ctx := ktesting.Init(t, ktesting.Verbosity(2))
    logger := klog.FromContext(ctx)
    
    // 启动测试服务器
    server := startTestServer(ctx, logger)
    defer server.Stop()
    
    // 执行测试操作
    logger.Info("Creating test resource")
    resource := createResource(ctx, server.Client())
    
    logger.Info("Verifying resource", "resource", resource.Name)
    verifyResource(t, resource)
}

2. 调试失败测试

func TestComplexScenario(t *testing.T) {
    // 高详细级别用于调试
    ctx := ktesting.Init(t, ktesting.Verbosity(5))
    logger := klog.FromContext(ctx)
    
    // 记录关键步骤
    logger.V(4).Info("Step 1: Initializing components")
    initComponents(ctx)
    
    logger.V(4).Info("Step 2: Running main logic")
    result, err := runMainLogic(ctx)
    
    if err != nil {
        // 错误时自动记录详细信息
        logger.Error(err, "Main logic failed", 
            "result", result,
            "context", "step2")
        t.Fail()
    }
}

与标准 testing 的对比

特性 标准 testing ktesting
日志记录 t.Log() 简单字符串 结构化日志,支持键值对
日志级别 不支持 支持 V 级别过滤
上下文传递 手动传递 t 通过 context 传递日志器
子测试 基本支持 每个子测试可独立配置
性能 简单 结构化日志有轻微开销

最佳实践

1. 始终使用结构化日志

// 好的做法:使用键值对
logger.Info("User action", "user", userID, "action", "login", "result", "success")

// 避免:字符串拼接
logger.Info(fmt.Sprintf("User %s login %s", userID, "success"))

2. 合理使用日志级别

logger.V(0).Info("Critical operation started")  // 重要信息
logger.V(2).Info("Detailed step", "step", i)     // 调试信息
logger.V(4).Info("Very detailed", "data", data)  // 详细调试

3. 传递上下文

// 始终传递包含日志器的 context
func helper(ctx context.Context, param string) {
    logger := klog.FromContext(ctx)
    logger.Info("Helper called", "param", param)
    // ... 实现
}

总结

example_test.go 展示了 Kubernetes 测试框架的核心设计理念:

  1. 结构化日志:使用键值对而非字符串拼接,便于日志分析和过滤
  2. 上下文传递:通过 context 传递日志器,实现测试隔离
  3. 灵活的配置:通过选项模式支持不同测试需求
  4. 性能考虑:支持日志级别过滤,避免生产环境性能损耗

这种设计使得 Kubernetes 的测试更加可维护、可调试,同时也为开发者提供了良好的测试编写体验。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐