Skip to content

Commit 0d288c8

Browse files
committed
v2: rework contextual attrs and levels
This change removes the WithAttrs and WithLevel functions and replaces them with being able to specify context keys on the handler. This means programs can be written to use well-known keys and avoid tightly coupling to this package. Signed-off-by: Hank Donnay <hdonnay@redhat.com>
1 parent 08a589d commit 0d288c8

File tree

4 files changed

+137
-137
lines changed

4 files changed

+137
-137
lines changed

v2/examples_test.go

Lines changed: 102 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,42 @@ import (
55
"log/slog"
66
"os"
77
"runtime/pprof"
8+
"slices"
89

910
"go.opentelemetry.io/otel/baggage"
1011
)
1112

12-
var ExampleOpts = Options{
13+
var ExampleOptions = Options{
14+
Level: LevelEverything,
1315
OmitTime: true,
1416
OmitSource: true,
17+
ContextKey: SetAttrs,
18+
LevelKey: SetLevel,
1519
}
1620

21+
type ctxkey int
22+
23+
// Context key variables for the examples.
24+
const (
25+
_ ctxkey = iota
26+
SetAttrs
27+
SetLevel
28+
)
29+
1730
func Example() {
18-
h := NewHandler(os.Stdout, &ExampleOpts)
31+
h := NewHandler(os.Stdout, &ExampleOptions)
1932
slog.New(h).With("a", "b").Info("test", "c", "d")
2033

2134
// Output:
2235
// {"level":"INFO","msg":"test","a":"b","c":"d"}
2336
}
2437

38+
// In this example, some values are extracted from the pprof labels and inserted
39+
// into the record.
2540
func Example_pprof() {
2641
ctx := pprof.WithLabels(context.Background(), pprof.Labels("test_kind", "example"))
2742
pprof.SetGoroutineLabels(ctx)
28-
h := NewHandler(os.Stdout, &ExampleOpts)
43+
h := NewHandler(os.Stdout, &ExampleOptions)
2944
slog.New(h).InfoContext(ctx, "test")
3045

3146
// Output:
@@ -39,72 +54,119 @@ func must[T any](t T, err error) T {
3954
return t
4055
}
4156

57+
// In this example, some values are extracted from the OpenTelemetry baggage and
58+
// inserted into the record.
4259
func Example_baggage() {
43-
BaggageOpts := Options{
44-
OmitTime: true,
45-
OmitSource: true,
46-
Baggage: func(_ string) bool { return true },
47-
}
60+
opts := ExampleOptions
61+
opts.Baggage = func(_ string) bool { return true }
4862
b := must(baggage.New(
4963
must(baggage.NewMember("test_kind", "example")),
5064
))
5165
ctx := baggage.ContextWithBaggage(context.Background(), b)
52-
h := NewHandler(os.Stdout, &BaggageOpts)
66+
h := NewHandler(os.Stdout, &opts)
5367
slog.New(h).InfoContext(ctx, "test")
5468

5569
// Output:
5670
// {"level":"INFO","msg":"test","baggage":{"test_kind":"example"}}
5771
}
5872

59-
func ExampleWithLevel() {
60-
opts := Options{
61-
OmitTime: true,
62-
OmitSource: true,
73+
// In this example, the handler is configured with a very high minimum level, so
74+
// without the per-record level filtering there would be no log messages.
75+
func Example_with_Level() {
76+
// Per-record filter levels.
77+
filters := []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError}
78+
// Levels of records to emit.
79+
levels := []slog.Level{slog.LevelDebug - 4, slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError}
80+
// With is a helper function to add the log level to the Context at the known key.
81+
//
82+
// Typically, a module would provide a helper to do this.
83+
with := func(ctx context.Context, l slog.Level) context.Context {
84+
return context.WithValue(ctx, SetLevel, l)
6385
}
64-
levels := []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError}
86+
87+
// Setup:
6588
ctx := context.Background()
89+
opts := ExampleOptions
90+
opts.Level = slog.Level(100)
6691
h := NewHandler(os.Stdout, &opts)
92+
log := slog.New(h)
93+
94+
// Usage:
95+
a := slog.String("filter", "NONE")
6796
for _, l := range levels {
68-
ctx := WithLevel(ctx, l)
69-
for i, l := range levels {
70-
slog.New(h).LogAttrs(ctx, l, "normal log message", slog.Int("i", i))
97+
log.LogAttrs(ctx, l, "", a)
98+
}
99+
for _, l := range filters {
100+
a = slog.String("filter", l.String())
101+
ctx := with(ctx, l)
102+
for _, l := range levels {
103+
log.LogAttrs(ctx, l, "", a)
71104
}
72105
}
73106

74107
// Output:
75-
// {"level":"DEBUG","msg":"normal log message","i":0}
76-
// {"level":"INFO","msg":"normal log message","i":1}
77-
// {"level":"WARN","msg":"normal log message","i":2}
78-
// {"level":"ERROR","msg":"normal log message","i":3}
79-
// {"level":"INFO","msg":"normal log message","i":1}
80-
// {"level":"WARN","msg":"normal log message","i":2}
81-
// {"level":"ERROR","msg":"normal log message","i":3}
82-
// {"level":"WARN","msg":"normal log message","i":2}
83-
// {"level":"ERROR","msg":"normal log message","i":3}
84-
// {"level":"ERROR","msg":"normal log message","i":3}
108+
// {"level":"DEBUG","msg":"","filter":"DEBUG"}
109+
// {"level":"INFO","msg":"","filter":"DEBUG"}
110+
// {"level":"WARN","msg":"","filter":"DEBUG"}
111+
// {"level":"ERROR","msg":"","filter":"DEBUG"}
112+
// {"level":"INFO","msg":"","filter":"INFO"}
113+
// {"level":"WARN","msg":"","filter":"INFO"}
114+
// {"level":"ERROR","msg":"","filter":"INFO"}
115+
// {"level":"WARN","msg":"","filter":"WARN"}
116+
// {"level":"ERROR","msg":"","filter":"WARN"}
117+
// {"level":"ERROR","msg":"","filter":"ERROR"}
85118
}
86119

87-
func ExampleWithAttrs() {
88-
opts := Options{
89-
OmitTime: true,
90-
OmitSource: true,
120+
// In this example, there are values stored in the Context at a known key and
121+
// then automatically retrieved and integrated into the record by the handler.
122+
func Example_with_Attrs() {
123+
// With is a helper function to add values to the Context at the known key.
124+
//
125+
// Typically, a module would provide a helper to do this, and do it with
126+
// less garbage. Any ordering or replacement semantics need to happen here;
127+
// this example does not implement being able to remove keys from the
128+
// Context.
129+
with := func(ctx context.Context, args ...any) context.Context {
130+
var s []slog.Attr
131+
if v, ok := ctx.Value(SetAttrs).(slog.Value); ok {
132+
s = v.Group()
133+
}
134+
s = append(s, slog.Group("", args...).Value.Group()...)
135+
seen := make(map[string]struct{}, len(s))
136+
del := func(a slog.Attr) bool {
137+
_, ok := seen[a.Key]
138+
seen[a.Key] = struct{}{}
139+
return ok
140+
}
141+
slices.Reverse(s)
142+
s = slices.DeleteFunc(s, del)
143+
slices.Reverse(s)
144+
return context.WithValue(ctx, SetAttrs, slog.GroupValue(s...))
91145
}
146+
// Setup:
92147
ctx := context.Background()
93-
h := NewHandler(os.Stdout, &opts)
148+
h := NewHandler(os.Stdout, &ExampleOptions)
94149
l := slog.New(h)
150+
151+
// Usage:
95152
l.InfoContext(ctx, "without ctx attrs", "a", "b")
96-
ctx = WithAttrs(ctx, "contextual", "value")
97-
l.InfoContext(ctx, "with ctx attrs", "a", "b")
98153
{
99-
ctx := WithLevel(ctx, slog.LevelDebug)
100-
l.DebugContext(ctx, "with ctx attrs", "a", "b")
154+
ctx := with(ctx, "contextual", "value")
155+
l.InfoContext(ctx, "with ctx attrs", "a", "b")
156+
{
157+
ctx := context.WithValue(ctx, SetLevel, slog.LevelDebug)
158+
ctx = with(ctx, "contextual", "level")
159+
l.DebugContext(ctx, "with ctx attrs", "a", "b")
160+
}
161+
ctx = with(ctx, "appended", "value")
162+
l.InfoContext(ctx, "with more ctx attrs")
101163
}
102-
ctx = WithAttrs(ctx, "contextual", slog.GroupValue())
103-
l.InfoContext(ctx, "removed ctx attrs", "a", "b")
164+
l.InfoContext(ctx, "without ctx attrs", "a", "b")
104165

105166
// Output:
106167
// {"level":"INFO","msg":"without ctx attrs","a":"b"}
107168
// {"level":"INFO","msg":"with ctx attrs","contextual":"value","a":"b"}
108-
// {"level":"DEBUG","msg":"with ctx attrs","contextual":"value","a":"b"}
109-
// {"level":"INFO","msg":"removed ctx attrs","a":"b"}
169+
// {"level":"DEBUG","msg":"with ctx attrs","contextual":"level","a":"b"}
170+
// {"level":"INFO","msg":"with more ctx attrs","contextual":"value","appended":"value"}
171+
// {"level":"INFO","msg":"without ctx attrs","a":"b"}
110172
}

v2/handler.go

Lines changed: 23 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -73,93 +73,11 @@ import (
7373
"log/slog"
7474
"runtime"
7575
"runtime/pprof"
76-
"slices"
77-
"strings"
7876

7977
"go.opentelemetry.io/otel/baggage"
8078
"go.opentelemetry.io/otel/trace"
8179
)
8280

83-
type (
84-
ctxLevelKey struct{}
85-
ctxAttrKey struct{}
86-
)
87-
88-
var (
89-
// CtxLevel is for per-Context log levels.
90-
ctxLevel ctxLevelKey
91-
// CtxAttr is for per-Context slog.Attr elements.
92-
ctxAttr ctxAttrKey
93-
)
94-
95-
// WithLevel overrides the minimum log level for all records created with the
96-
// returned context.
97-
func WithLevel(ctx context.Context, l slog.Level) context.Context {
98-
return context.WithValue(ctx, &ctxLevel, l)
99-
}
100-
101-
// WithAttrs records the provided arguments to be added as additional
102-
// [slog.Attr] elements to any records created with the returned context.
103-
//
104-
// Adding Attrs for a previously added Key replaces that Attr in the returned
105-
// Context. To remove an Attr, use an empty [slog.Group] as the value.
106-
//
107-
// This is more expensive than adding Attrs via [slog.Logger.With], which should
108-
// be preferred when function signatures allow.
109-
func WithAttrs(ctx context.Context, args ...any) context.Context {
110-
// This is based roughly on how the [slog.Record.Add] method is implemented.
111-
if len(args) == 0 {
112-
return ctx
113-
}
114-
115-
var cur []slog.Attr
116-
if prev, ok := ctx.Value(&ctxAttr).(*[]slog.Attr); ok {
117-
cur = make([]slog.Attr, len(*prev), len(*prev)+(len(args)/2))
118-
copy(cur, *prev)
119-
} else {
120-
cur = make([]slog.Attr, 0, len(args)/2)
121-
}
122-
123-
var a slog.Attr
124-
for len(args) > 0 {
125-
a, args = argsToAttr(args)
126-
cur = append(cur, a)
127-
}
128-
slices.SortStableFunc(cur, func(a, b slog.Attr) int {
129-
return strings.Compare(a.Key, b.Key)
130-
})
131-
// We want to keep only the last instance of a key, so this needs two
132-
// [slices.Reverse] calls.
133-
slices.Reverse(cur)
134-
cur = slices.CompactFunc(cur, func(a, b slog.Attr) bool {
135-
return a.Key == b.Key
136-
})
137-
cur = slices.DeleteFunc(cur, func(a slog.Attr) bool {
138-
v := a.Value
139-
return v.Kind() == slog.KindGroup && len(v.Group()) == 0
140-
})
141-
cur = slices.Clip(cur)
142-
slices.Reverse(cur)
143-
144-
return context.WithValue(ctx, &ctxAttr, &cur)
145-
}
146-
147-
// ArgsToAttr slices off up to two elements to construct a [slog.Attr] and
148-
// returns it along with a slice of the remaining elements.
149-
func argsToAttr(args []any) (slog.Attr, []any) {
150-
switch x := args[0].(type) {
151-
case string:
152-
if len(args) == 1 {
153-
return slog.Group(x), nil
154-
}
155-
return slog.Any(x, args[1]), args[2:]
156-
case slog.Attr:
157-
return x, args[1:]
158-
default:
159-
return slog.Any(`!BADKEY`, x), args[1:]
160-
}
161-
}
162-
16381
// Some extra [slog.Level] aliases and syslog(3) compatible levels (as
16482
// implemented in this package).
16583
//
@@ -235,16 +153,14 @@ type Options struct {
235153
// Level is the minimum level that a log message must have to be processed
236154
// by the Handler.
237155
//
238-
// This can be overridden on a per-message basis by [WithLevel].
156+
// This can be overridden on a per-message basis by storing a [slog.Level]
157+
// at [LevelKey].
239158
Level slog.Leveler
240159
// Baggage is a selection function for keys in the OpenTelemetry Baggage
241160
// contained in the [context.Context] used with a log message.
242161
Baggage func(key string) bool
243162
// WriteError is a hook for receiving errors that occurred while attempting
244163
// to write the log message.
245-
//
246-
// The [slog] logging methods current do not have any means of reporting the
247-
// errors that Handler implementations return.
248164
WriteError func(context.Context, error)
249165
// OmitSource controls whether source position information should be
250166
// emitted.
@@ -256,6 +172,18 @@ type Options struct {
256172
//
257173
// When connected to the Journal, this setting has no effect.
258174
ProseFormat bool
175+
// ContextKey is a value to be used with [context.Context.Value] to retrieve a
176+
// [slog.Value] Group.
177+
//
178+
// Setting this to a value that results in retrieving any other type will
179+
// panic the program.
180+
ContextKey any
181+
// LevelKey is a value to be used with [context.Context.Value] to retrieve a
182+
// [slog.Leveler] to use on a per-record basis.
183+
//
184+
// Setting this to a value that results in retrieving any other type will
185+
// panic the program.
186+
LevelKey any
259187

260188
// ForceANSI is a hook for testing to force ANSI color output.
261189
forceANSI bool
@@ -267,8 +195,10 @@ func (h *handler[S]) Enabled(ctx context.Context, l slog.Level) bool {
267195
if h.opts.Level != nil {
268196
min = h.opts.Level.Level()
269197
}
270-
if cl, ok := ctx.Value(&ctxLevel).(slog.Level); ok {
271-
min = cl
198+
if h.opts.LevelKey != nil {
199+
if cl, ok := ctx.Value(h.opts.LevelKey).(slog.Leveler); ok {
200+
min = cl.Level()
201+
}
272202
}
273203
return l >= min
274204
}
@@ -351,9 +281,11 @@ func (h *handler[S]) Handle(ctx context.Context, r slog.Record) (err error) {
351281
if h.prefmt != nil {
352282
b.Write(*h.prefmt)
353283
}
354-
if p, ok := ctx.Value(&ctxAttr).(*[]slog.Attr); ok {
355-
for _, a := range *p {
356-
h.appendAttr(b, s, a)
284+
if h.opts.ContextKey != nil {
285+
if v, ok := ctx.Value(h.opts.ContextKey).(slog.Value); ok {
286+
for _, a := range v.Group() {
287+
h.appendAttr(b, s, a)
288+
}
357289
}
358290
}
359291
r.Attrs(func(a slog.Attr) bool {

0 commit comments

Comments
 (0)