From 8509d6ab6f4ba193fedc5f6b5681a96bb68c9ffc Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 30 Jan 2026 17:24:16 +0100 Subject: [PATCH] ktesting: support multi-line result from AnyToStringHook When the hook returns a multi-line string, for example a YAML dump, then continuing directly after the end of that string with the next key/value pair was a bit hard to read: dra_manager.go:288: I0129 17:41:55.722711] scheduler: Finished GatherAllocatedState allocatedDevices=: ... - testdra-all-usesallresources-qzvpg.driver/worker-7/worker-7-device-005 err=: nil In this example, allocatedDevices was passed by the hook to gomega.Format which added the type description and then produced YAML. With this change, such output becomes: dra_manager.go:288: I0129 17:41:55.722711] scheduler: Finished GatherAllocatedState allocatedDevices=< : ... - testdra-all-usesallresources-qzvpg.driver/worker-7/worker-7-device-005 > err=: nil --- internal/serialize/keyvalues.go | 15 +++++++++++++-- ktesting/example_test.go | 22 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/internal/serialize/keyvalues.go b/internal/serialize/keyvalues.go index 9c3858ec..73f91ea5 100644 --- a/internal/serialize/keyvalues.go +++ b/internal/serialize/keyvalues.go @@ -22,6 +22,7 @@ import ( "fmt" "slices" "strconv" + "strings" "github.com/go-logr/logr" ) @@ -188,11 +189,21 @@ func findObsoleteEntry(entries []obsoleteKV, key string) int { // formatAny is the fallback formatter for a value. It supports a hook (for // example, for YAML encoding) and itself uses JSON encoding. func (f Formatter) formatAny(b *bytes.Buffer, v interface{}) { - b.WriteRune('=') if f.AnyToStringHook != nil { - b.WriteString(f.AnyToStringHook(v)) + str := f.AnyToStringHook(v) + if strings.Contains(str, "\n") { + // If it's multi-line, then pass it through writeStringValue to get start/end delimiters, + // which separates it better from any following key/value pair. + writeStringValue(b, str) + return + } + // Otherwise put it directly after the separator, on the same lime, + // The assumption is that the hook returns something where start/end are obvious. + b.WriteRune('=') + b.WriteString(str) return } + b.WriteRune('=') formatAsJSON(b, v) } diff --git a/ktesting/example_test.go b/ktesting/example_test.go index 34844ef6..e099d10c 100644 --- a/ktesting/example_test.go +++ b/ktesting/example_test.go @@ -19,6 +19,7 @@ package ktesting_test import ( "errors" "fmt" + "strings" "time" "k8s.io/klog/v2/ktesting" @@ -30,12 +31,22 @@ func ExampleUnderlier() { ktesting.Verbosity(4), ktesting.BufferLogs(true), ktesting.AnyToString(func(value interface{}) string { - return fmt.Sprintf("### %+v ###", value) + // This simple demo formats with %+v, + // splits into multiple lines at spaces (much too simplistic for unknown data, but works here), + // then adds ### as delimiters. + str := fmt.Sprintf("%+v", value) + str = strings.ReplaceAll(str, " ", "\n") + return fmt.Sprintf("### %s ###", str) }), ), ) - logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ Field int }{Field: 1}) + logger.Error(errors.New("failure"), "I failed", "what", "something", + // The AnyToString callback logs this with multiple lines. + "data", struct{ X, Y int }{10, 20}, + // This with a single line. + "moreData", struct{ Field int }{Field: 1}, + ) logger.WithValues("request", 42).WithValues("anotherValue", "fish").Info("hello world") logger.WithValues("request", 42, "anotherValue", "fish").Info("hello world 2", "yetAnotherValue", "thanks") logger.WithName("example").Info("with name") @@ -65,13 +76,16 @@ func ExampleUnderlier() { } // Output: - // ERROR I failed err="failure" what="something" data=### {Field:1} ### + // ERROR I failed err="failure" what="something" data=< + // ### {X:10 + // Y:20} ### + // > moreData=### {Field:1} ### // INFO hello world request=### 42 ### anotherValue="fish" // INFO hello world 2 request=### 42 ### anotherValue="fish" yetAnotherValue="thanks" // INFO example: with name // INFO higher verbosity // - // log entry #0: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:ERROR Prefix: Message:I failed Verbosity:0 Err:failure WithKVList:[] ParameterKVList:[what something data {Field:1}]} + // log entry #0: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:ERROR Prefix: Message:I failed Verbosity:0 Err:failure WithKVList:[] ParameterKVList:[what something data {X:10 Y:20} moreData {Field:1}]} // log entry #1: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[]} // log entry #2: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world 2 Verbosity:0 Err: WithKVList:[request 42 anotherValue fish] ParameterKVList:[yetAnotherValue thanks]} // log entry #3: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix:example Message:with name Verbosity:0 Err: WithKVList:[] ParameterKVList:[]}