Skip to content

Commit 44eadc3

Browse files
committed
add slog support
When compiled with Go >= 1.21, special support for log/slog gets enabled automatically: - The formatting of key/value pairs supports slog.Value and slog.LogValuer, regardless of where those come from. - A slog.GroupValue is formatted as a JSON object. - The logger returned by klog.Background() and the textlogger support usage as a backend for the slog API, via slogr.NewSlogHandler, with builtin support for handling a slog.Record. However, -vmodule does not work when called like that because stack unwinding during the Enabled check finds the wrong source code. - KObj (= ObjectRef) and the type behind KObjSlice support the slog.LogValuer interface and thus get formatted properly by slog backends. The klogr package doesn't get updated. It shouldn't be used anymore. To make that clearer, it gets marked as "deprecated". Examples demonstrate the resulting output. That output has to be exactly the same when used as test, so pid and time get set to fixed values.
1 parent cc856bb commit 44eadc3

17 files changed

+915
-79
lines changed

contextual_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ limitations under the License.
1717
package klog_test
1818

1919
import (
20+
"context"
2021
"fmt"
22+
"runtime"
23+
"testing"
2124

2225
"github.com/go-logr/logr"
2326
"k8s.io/klog/v2"
@@ -56,3 +59,43 @@ func ExampleFlushLogger() {
5659
// Output:
5760
// flushing...
5861
}
62+
63+
func BenchmarkPassingLogger(b *testing.B) {
64+
b.Run("with context", func(b *testing.B) {
65+
ctx := klog.NewContext(context.Background(), klog.Background())
66+
var finalCtx context.Context
67+
for n := b.N; n > 0; n-- {
68+
finalCtx = passCtx(ctx)
69+
}
70+
runtime.KeepAlive(finalCtx)
71+
})
72+
73+
b.Run("without context", func(b *testing.B) {
74+
logger := klog.Background()
75+
var finalLogger klog.Logger
76+
for n := b.N; n > 0; n-- {
77+
finalLogger = passLogger(logger)
78+
}
79+
runtime.KeepAlive(finalLogger)
80+
})
81+
}
82+
83+
func BenchmarkExtractLogger(b *testing.B) {
84+
b.Run("from context", func(b *testing.B) {
85+
ctx := klog.NewContext(context.Background(), klog.Background())
86+
var finalLogger klog.Logger
87+
for n := b.N; n > 0; n-- {
88+
finalLogger = extractCtx(ctx)
89+
}
90+
runtime.KeepAlive(finalLogger)
91+
})
92+
}
93+
94+
//go:noinline
95+
func passCtx(ctx context.Context) context.Context { return ctx }
96+
97+
//go:noinline
98+
func extractCtx(ctx context.Context) klog.Logger { return klog.FromContext(ctx) }
99+
100+
//go:noinline
101+
func passLogger(logger klog.Logger) klog.Logger { return logger }

internal/buffer/buffer.go

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import (
3030
var (
3131
// Pid is inserted into log headers. Can be overridden for tests.
3232
Pid = os.Getpid()
33+
34+
// Time, if set, will be used instead of the actual current time.
35+
Time *time.Time
3336
)
3437

3538
// Buffer holds a single byte.Buffer for reuse. The zero value is ready for
@@ -121,6 +124,9 @@ func (buf *Buffer) FormatHeader(s severity.Severity, file string, line int, now
121124

122125
// Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand.
123126
// It's worth about 3X. Fprintf is hard.
127+
if Time != nil {
128+
now = *Time
129+
}
124130
_, month, day := now.Date()
125131
hour, minute, second := now.Clock()
126132
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]
@@ -156,6 +162,9 @@ func (buf *Buffer) SprintHeader(s severity.Severity, now time.Time) string {
156162

157163
// Avoid Fprintf, for speed. The format is so simple that we can do it quickly by hand.
158164
// It's worth about 3X. Fprintf is hard.
165+
if Time != nil {
166+
now = *Time
167+
}
159168
_, month, day := now.Date()
160169
hour, minute, second := now.Clock()
161170
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]

internal/serialize/keyvalues.go

+4-67
Original file line numberDiff line numberDiff line change
@@ -172,73 +172,6 @@ func KVListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
172172
Formatter{}.KVListFormat(b, keysAndValues...)
173173
}
174174

175-
// KVFormat serializes one key/value pair into the provided buffer.
176-
// A space gets inserted before the pair.
177-
func (f Formatter) KVFormat(b *bytes.Buffer, k, v interface{}) {
178-
b.WriteByte(' ')
179-
// Keys are assumed to be well-formed according to
180-
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments
181-
// for the sake of performance. Keys with spaces,
182-
// special characters, etc. will break parsing.
183-
if sK, ok := k.(string); ok {
184-
// Avoid one allocation when the key is a string, which
185-
// normally it should be.
186-
b.WriteString(sK)
187-
} else {
188-
b.WriteString(fmt.Sprintf("%s", k))
189-
}
190-
191-
// The type checks are sorted so that more frequently used ones
192-
// come first because that is then faster in the common
193-
// cases. In Kubernetes, ObjectRef (a Stringer) is more common
194-
// than plain strings
195-
// (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
196-
switch v := v.(type) {
197-
case textWriter:
198-
writeTextWriterValue(b, v)
199-
case fmt.Stringer:
200-
writeStringValue(b, StringerToString(v))
201-
case string:
202-
writeStringValue(b, v)
203-
case error:
204-
writeStringValue(b, ErrorToString(v))
205-
case logr.Marshaler:
206-
value := MarshalerToValue(v)
207-
// A marshaler that returns a string is useful for
208-
// delayed formatting of complex values. We treat this
209-
// case like a normal string. This is useful for
210-
// multi-line support.
211-
//
212-
// We could do this by recursively formatting a value,
213-
// but that comes with the risk of infinite recursion
214-
// if a marshaler returns itself. Instead we call it
215-
// only once and rely on it returning the intended
216-
// value directly.
217-
switch value := value.(type) {
218-
case string:
219-
writeStringValue(b, value)
220-
default:
221-
f.formatAny(b, value)
222-
}
223-
case []byte:
224-
// In https://github.com/kubernetes/klog/pull/237 it was decided
225-
// to format byte slices with "%+q". The advantages of that are:
226-
// - readable output if the bytes happen to be printable
227-
// - non-printable bytes get represented as unicode escape
228-
// sequences (\uxxxx)
229-
//
230-
// The downsides are that we cannot use the faster
231-
// strconv.Quote here and that multi-line output is not
232-
// supported. If developers know that a byte array is
233-
// printable and they want multi-line output, they can
234-
// convert the value to string before logging it.
235-
b.WriteByte('=')
236-
b.WriteString(fmt.Sprintf("%+q", v))
237-
default:
238-
f.formatAny(b, v)
239-
}
240-
}
241-
242175
func KVFormat(b *bytes.Buffer, k, v interface{}) {
243176
Formatter{}.KVFormat(b, k, v)
244177
}
@@ -251,6 +184,10 @@ func (f Formatter) formatAny(b *bytes.Buffer, v interface{}) {
251184
b.WriteString(f.AnyToStringHook(v))
252185
return
253186
}
187+
formatAsJSON(b, v)
188+
}
189+
190+
func formatAsJSON(b *bytes.Buffer, v interface{}) {
254191
encoder := json.NewEncoder(b)
255192
l := b.Len()
256193
if err := encoder.Encode(v); err != nil {
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//go:build !go1.21
2+
// +build !go1.21
3+
4+
/*
5+
Copyright 2023 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package serialize
21+
22+
import (
23+
"bytes"
24+
"fmt"
25+
26+
"github.com/go-logr/logr"
27+
)
28+
29+
// KVFormat serializes one key/value pair into the provided buffer.
30+
// A space gets inserted before the pair.
31+
func (f Formatter) KVFormat(b *bytes.Buffer, k, v interface{}) {
32+
// This is the version without slog support. Must be kept in sync with
33+
// the version in keyvalues_slog.go.
34+
35+
b.WriteByte(' ')
36+
// Keys are assumed to be well-formed according to
37+
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md#name-arguments
38+
// for the sake of performance. Keys with spaces,
39+
// special characters, etc. will break parsing.
40+
if sK, ok := k.(string); ok {
41+
// Avoid one allocation when the key is a string, which
42+
// normally it should be.
43+
b.WriteString(sK)
44+
} else {
45+
b.WriteString(fmt.Sprintf("%s", k))
46+
}
47+
48+
// The type checks are sorted so that more frequently used ones
49+
// come first because that is then faster in the common
50+
// cases. In Kubernetes, ObjectRef (a Stringer) is more common
51+
// than plain strings
52+
// (https://github.com/kubernetes/kubernetes/pull/106594#issuecomment-975526235).
53+
switch v := v.(type) {
54+
case textWriter:
55+
writeTextWriterValue(b, v)
56+
case fmt.Stringer:
57+
writeStringValue(b, StringerToString(v))
58+
case string:
59+
writeStringValue(b, v)
60+
case error:
61+
writeStringValue(b, ErrorToString(v))
62+
case logr.Marshaler:
63+
value := MarshalerToValue(v)
64+
// A marshaler that returns a string is useful for
65+
// delayed formatting of complex values. We treat this
66+
// case like a normal string. This is useful for
67+
// multi-line support.
68+
//
69+
// We could do this by recursively formatting a value,
70+
// but that comes with the risk of infinite recursion
71+
// if a marshaler returns itself. Instead we call it
72+
// only once and rely on it returning the intended
73+
// value directly.
74+
switch value := value.(type) {
75+
case string:
76+
writeStringValue(b, value)
77+
default:
78+
f.formatAny(b, value)
79+
}
80+
case []byte:
81+
// In https://github.com/kubernetes/klog/pull/237 it was decided
82+
// to format byte slices with "%+q". The advantages of that are:
83+
// - readable output if the bytes happen to be printable
84+
// - non-printable bytes get represented as unicode escape
85+
// sequences (\uxxxx)
86+
//
87+
// The downsides are that we cannot use the faster
88+
// strconv.Quote here and that multi-line output is not
89+
// supported. If developers know that a byte array is
90+
// printable and they want multi-line output, they can
91+
// convert the value to string before logging it.
92+
b.WriteByte('=')
93+
b.WriteString(fmt.Sprintf("%+q", v))
94+
default:
95+
f.formatAny(b, v)
96+
}
97+
}

0 commit comments

Comments
 (0)