Skip to content

Commit 90cff0f

Browse files
authored
Merge pull request #365 from pohly/ktesting-timestamps
ktesting improvements
2 parents af72dbd + 1b27ee8 commit 90cff0f

File tree

7 files changed

+204
-48
lines changed

7 files changed

+204
-48
lines changed

internal/buffer/buffer.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ func (buf *Buffer) someDigits(i, d int) int {
110110
return copy(buf.Tmp[i:], buf.Tmp[j:])
111111
}
112112

113-
// FormatHeader formats a log header using the provided file name and line number.
113+
// FormatHeader formats a log header using the provided file name and line number
114+
// and writes it into the buffer.
114115
func (buf *Buffer) FormatHeader(s severity.Severity, file string, line int, now time.Time) {
115116
if line < 0 {
116117
line = 0 // not a real line number, but acceptable to someDigits
@@ -146,3 +147,30 @@ func (buf *Buffer) FormatHeader(s severity.Severity, file string, line int, now
146147
buf.Tmp[n+2] = ' '
147148
buf.Write(buf.Tmp[:n+3])
148149
}
150+
151+
// SprintHeader formats a log header and returns a string. This is a simpler
152+
// version of FormatHeader for use in ktesting.
153+
func (buf *Buffer) SprintHeader(s severity.Severity, now time.Time) string {
154+
if s > severity.FatalLog {
155+
s = severity.InfoLog // for safety.
156+
}
157+
158+
// Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand.
159+
// It's worth about 3X. Fprintf is hard.
160+
_, month, day := now.Date()
161+
hour, minute, second := now.Clock()
162+
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]
163+
buf.Tmp[0] = severity.Char[s]
164+
buf.twoDigits(1, int(month))
165+
buf.twoDigits(3, day)
166+
buf.Tmp[5] = ' '
167+
buf.twoDigits(6, hour)
168+
buf.Tmp[8] = ':'
169+
buf.twoDigits(9, minute)
170+
buf.Tmp[11] = ':'
171+
buf.twoDigits(12, second)
172+
buf.Tmp[14] = '.'
173+
buf.nDigits(6, 15, now.Nanosecond()/1000, '0')
174+
buf.Tmp[21] = ']'
175+
return string(buf.Tmp[:22])
176+
}

internal/serialize/keyvalues.go

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,15 @@ func MergeKVs(first, second []interface{}) []interface{} {
9595
return merged
9696
}
9797

98+
type Formatter struct {
99+
AnyToStringHook AnyToStringFunc
100+
}
101+
102+
type AnyToStringFunc func(v interface{}) string
103+
98104
// MergeKVsInto is a variant of MergeKVs which directly formats the key/value
99105
// pairs into a buffer.
100-
func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) {
106+
func (f Formatter) MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) {
101107
if len(first) == 0 && len(second) == 0 {
102108
// Nothing to do at all.
103109
return
@@ -107,7 +113,7 @@ func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) {
107113
// Nothing to be overridden, second slice is well-formed
108114
// and can be used directly.
109115
for i := 0; i < len(second); i += 2 {
110-
KVFormat(b, second[i], second[i+1])
116+
f.KVFormat(b, second[i], second[i+1])
111117
}
112118
return
113119
}
@@ -127,24 +133,28 @@ func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) {
127133
if overrides[key] {
128134
continue
129135
}
130-
KVFormat(b, key, first[i+1])
136+
f.KVFormat(b, key, first[i+1])
131137
}
132138
// Round down.
133139
l := len(second)
134140
l = l / 2 * 2
135141
for i := 1; i < l; i += 2 {
136-
KVFormat(b, second[i-1], second[i])
142+
f.KVFormat(b, second[i-1], second[i])
137143
}
138144
if len(second)%2 == 1 {
139-
KVFormat(b, second[len(second)-1], missingValue)
145+
f.KVFormat(b, second[len(second)-1], missingValue)
140146
}
141147
}
142148

149+
func MergeAndFormatKVs(b *bytes.Buffer, first, second []interface{}) {
150+
Formatter{}.MergeAndFormatKVs(b, first, second)
151+
}
152+
143153
const missingValue = "(MISSING)"
144154

145155
// KVListFormat serializes all key/value pairs into the provided buffer.
146156
// A space gets inserted before the first pair and between each pair.
147-
func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
157+
func (f Formatter) KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
148158
for i := 0; i < len(keysAndValues); i += 2 {
149159
var v interface{}
150160
k := keysAndValues[i]
@@ -153,13 +163,17 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
153163
} else {
154164
v = missingValue
155165
}
156-
KVFormat(b, k, v)
166+
f.KVFormat(b, k, v)
157167
}
158168
}
159169

170+
func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
171+
Formatter{}.KVListFormat(b, keysAndValues...)
172+
}
173+
160174
// KVFormat serializes one key/value pair into the provided buffer.
161175
// A space gets inserted before the pair.
162-
func KVFormat(b *bytes.Buffer, k, v interface{}) {
176+
func (f Formatter) KVFormat(b *bytes.Buffer, k, v interface{}) {
163177
b.WriteByte(' ')
164178
// Keys are assumed to be well-formed according to
165179
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments
@@ -203,7 +217,7 @@ func KVFormat(b *bytes.Buffer, k, v interface{}) {
203217
case string:
204218
writeStringValue(b, true, value)
205219
default:
206-
writeStringValue(b, false, fmt.Sprintf("%+v", value))
220+
writeStringValue(b, false, f.AnyToString(value))
207221
}
208222
case []byte:
209223
// In https://github.com/kubernetes/klog/pull/237 it was decided
@@ -220,8 +234,20 @@ func KVFormat(b *bytes.Buffer, k, v interface{}) {
220234
b.WriteByte('=')
221235
b.WriteString(fmt.Sprintf("%+q", v))
222236
default:
223-
writeStringValue(b, false, fmt.Sprintf("%+v", v))
237+
writeStringValue(b, false, f.AnyToString(v))
238+
}
239+
}
240+
241+
func KVFormat(b *bytes.Buffer, k, v interface{}) {
242+
Formatter{}.KVFormat(b, k, v)
243+
}
244+
245+
// AnyToString is the historic fallback formatter.
246+
func (f Formatter) AnyToString(v interface{}) string {
247+
if f.AnyToStringHook != nil {
248+
return f.AnyToStringHook(v)
224249
}
250+
return fmt.Sprintf("%+v", v)
225251
}
226252

227253
// StringerToString converts a Stringer to a string,

ktesting/contextual_test.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import (
1616
)
1717

1818
func TestContextual(t *testing.T) {
19-
logger, ctx := ktesting.NewTestContext(t)
19+
var buffer ktesting.BufferTL
20+
logger, ctx := ktesting.NewTestContext(&buffer)
2021

2122
doSomething(ctx)
2223

@@ -33,8 +34,14 @@ func TestContextual(t *testing.T) {
3334
}
3435

3536
actual := testingLogger.GetBuffer().String()
36-
expected := `INFO hello world
37-
INFO foo: hello also from me
37+
if actual != "" {
38+
t.Errorf("testinglogger should not have buffered, got:\n%s", actual)
39+
}
40+
41+
actual = buffer.String()
42+
actual = headerRe.ReplaceAllString(actual, "${1}xxx] ")
43+
expected := `Ixxx] hello world
44+
Ixxx] foo: hello also from me
3845
`
3946
if actual != expected {
4047
t.Errorf("mismatch in captured output, expected:\n%s\ngot:\n%s\n", expected, actual)

ktesting/example_test.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,17 @@ import (
2525
)
2626

2727
func ExampleUnderlier() {
28-
logger := ktesting.NewLogger(ktesting.NopTL{}, ktesting.NewConfig(ktesting.Verbosity(4)))
28+
logger := ktesting.NewLogger(ktesting.NopTL{},
29+
ktesting.NewConfig(
30+
ktesting.Verbosity(4),
31+
ktesting.BufferLogs(true),
32+
ktesting.AnyToString(func(value interface{}) string {
33+
return fmt.Sprintf("### %+v ###", value)
34+
}),
35+
),
36+
)
2937

30-
logger.Error(errors.New("failure"), "I failed", "what", "something")
38+
logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ field int }{field: 1})
3139
logger.WithValues("request", 42).WithValues("anotherValue", "fish").Info("hello world")
3240
logger.WithValues("request", 42, "anotherValue", "fish").Info("hello world 2", "yetAnotherValue", "thanks")
3341
logger.WithName("example").Info("with name")
@@ -57,15 +65,36 @@ func ExampleUnderlier() {
5765
}
5866

5967
// Output:
60-
// ERROR I failed err="failure" what="something"
61-
// INFO hello world request=42 anotherValue="fish"
62-
// INFO hello world 2 request=42 anotherValue="fish" yetAnotherValue="thanks"
68+
// ERROR I failed err="failure" what="something" data=### {field:1} ###
69+
// INFO hello world request=### 42 ### anotherValue="fish"
70+
// INFO hello world 2 request=### 42 ### anotherValue="fish" yetAnotherValue="thanks"
6371
// INFO example: with name
6472
// INFO higher verbosity
6573
//
66-
// 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]}
74+
// 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}]}
6775
// log entry #1: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world Verbosity:0 Err:<nil> WithKVList:[request 42 anotherValue fish] ParameterKVList:[]}
6876
// log entry #2: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:hello world 2 Verbosity:0 Err:<nil> WithKVList:[request 42 anotherValue fish] ParameterKVList:[yetAnotherValue thanks]}
6977
// log entry #3: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix:example Message:with name Verbosity:0 Err:<nil> WithKVList:[] ParameterKVList:[]}
7078
// log entry #4: {Timestamp:0001-01-01 00:00:00 +0000 UTC Type:INFO Prefix: Message:higher verbosity Verbosity:4 Err:<nil> WithKVList:[] ParameterKVList:[]}
7179
}
80+
81+
func ExampleNewLogger() {
82+
var buffer ktesting.BufferTL
83+
logger := ktesting.NewLogger(&buffer, ktesting.NewConfig())
84+
85+
logger.Error(errors.New("failure"), "I failed", "what", "something", "data", struct{ field int }{field: 1})
86+
logger.V(5).Info("Logged at level 5.")
87+
logger.V(6).Info("Not logged at level 6.")
88+
89+
testingLogger, ok := logger.GetSink().(ktesting.Underlier)
90+
if !ok {
91+
panic("Should have had a ktesting LogSink!?")
92+
}
93+
fmt.Printf(">> %s <<\n", testingLogger.GetBuffer().String()) // Should be empty.
94+
fmt.Print(headerRe.ReplaceAllString(buffer.String(), "${1}...] ")) // Should not be empty.
95+
96+
// Output:
97+
// >> <<
98+
// E...] I failed err="failure" what="something" data={field:1}
99+
// I...] Logged at level 5.
100+
}

ktesting/options.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"flag"
2121
"strconv"
2222

23+
"k8s.io/klog/v2/internal/serialize"
2324
"k8s.io/klog/v2/internal/verbosity"
2425
)
2526

@@ -47,9 +48,25 @@ type Config struct {
4748
type ConfigOption func(co *configOptions)
4849

4950
type configOptions struct {
51+
anyToString serialize.AnyToStringFunc
5052
verbosityFlagName string
5153
vmoduleFlagName string
5254
verbosityDefault int
55+
bufferLogs bool
56+
}
57+
58+
// AnyToString overrides the default formatter for values that are not
59+
// supported directly by klog. The default is `fmt.Sprintf("%+v")`.
60+
// The formatter must not panic.
61+
//
62+
// # Experimental
63+
//
64+
// Notice: This function is EXPERIMENTAL and may be changed or removed in a
65+
// later release.
66+
func AnyToString(anyToString func(value interface{}) string) ConfigOption {
67+
return func(co *configOptions) {
68+
co.anyToString = anyToString
69+
}
5370
}
5471

5572
// VerbosityFlagName overrides the default -testing.v for the verbosity level.
@@ -94,6 +111,21 @@ func Verbosity(level int) ConfigOption {
94111
}
95112
}
96113

114+
// BufferLogs controls whether log entries are captured in memory in addition
115+
// to being printed. Off by default. Unit tests that want to verify that
116+
// log entries are emitted as expected can turn this on and then retrieve
117+
// the captured log through the Underlier LogSink interface.
118+
//
119+
// # Experimental
120+
//
121+
// Notice: This function is EXPERIMENTAL and may be changed or removed in a
122+
// later release.
123+
func BufferLogs(enabled bool) ConfigOption {
124+
return func(co *configOptions) {
125+
co.bufferLogs = enabled
126+
}
127+
}
128+
97129
// NewConfig returns a configuration with recommended defaults and optional
98130
// modifications. Command line flags are not bound to any FlagSet yet.
99131
//

0 commit comments

Comments
 (0)