sloghandler.go

  1//go:build go1.21
  2// +build go1.21
  3
  4/*
  5Copyright 2023 The logr Authors.
  6
  7Licensed under the Apache License, Version 2.0 (the "License");
  8you may not use this file except in compliance with the License.
  9You may obtain a copy of the License at
 10
 11    http://www.apache.org/licenses/LICENSE-2.0
 12
 13Unless required by applicable law or agreed to in writing, software
 14distributed under the License is distributed on an "AS IS" BASIS,
 15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 16See the License for the specific language governing permissions and
 17limitations under the License.
 18*/
 19
 20package logr
 21
 22import (
 23	"context"
 24	"log/slog"
 25)
 26
 27type slogHandler struct {
 28	// May be nil, in which case all logs get discarded.
 29	sink LogSink
 30	// Non-nil if sink is non-nil and implements SlogSink.
 31	slogSink SlogSink
 32
 33	// groupPrefix collects values from WithGroup calls. It gets added as
 34	// prefix to value keys when handling a log record.
 35	groupPrefix string
 36
 37	// levelBias can be set when constructing the handler to influence the
 38	// slog.Level of log records. A positive levelBias reduces the
 39	// slog.Level value. slog has no API to influence this value after the
 40	// handler got created, so it can only be set indirectly through
 41	// Logger.V.
 42	levelBias slog.Level
 43}
 44
 45var _ slog.Handler = &slogHandler{}
 46
 47// groupSeparator is used to concatenate WithGroup names and attribute keys.
 48const groupSeparator = "."
 49
 50// GetLevel is used for black box unit testing.
 51func (l *slogHandler) GetLevel() slog.Level {
 52	return l.levelBias
 53}
 54
 55func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool {
 56	return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level)))
 57}
 58
 59func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error {
 60	if l.slogSink != nil {
 61		// Only adjust verbosity level of log entries < slog.LevelError.
 62		if record.Level < slog.LevelError {
 63			record.Level -= l.levelBias
 64		}
 65		return l.slogSink.Handle(ctx, record)
 66	}
 67
 68	// No need to check for nil sink here because Handle will only be called
 69	// when Enabled returned true.
 70
 71	kvList := make([]any, 0, 2*record.NumAttrs())
 72	record.Attrs(func(attr slog.Attr) bool {
 73		kvList = attrToKVs(attr, l.groupPrefix, kvList)
 74		return true
 75	})
 76	if record.Level >= slog.LevelError {
 77		l.sinkWithCallDepth().Error(nil, record.Message, kvList...)
 78	} else {
 79		level := l.levelFromSlog(record.Level)
 80		l.sinkWithCallDepth().Info(level, record.Message, kvList...)
 81	}
 82	return nil
 83}
 84
 85// sinkWithCallDepth adjusts the stack unwinding so that when Error or Info
 86// are called by Handle, code in slog gets skipped.
 87//
 88// This offset currently (Go 1.21.0) works for calls through
 89// slog.New(ToSlogHandler(...)).  There's no guarantee that the call
 90// chain won't change. Wrapping the handler will also break unwinding. It's
 91// still better than not adjusting at all....
 92//
 93// This cannot be done when constructing the handler because FromSlogHandler needs
 94// access to the original sink without this adjustment. A second copy would
 95// work, but then WithAttrs would have to be called for both of them.
 96func (l *slogHandler) sinkWithCallDepth() LogSink {
 97	if sink, ok := l.sink.(CallDepthLogSink); ok {
 98		return sink.WithCallDepth(2)
 99	}
100	return l.sink
101}
102
103func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
104	if l.sink == nil || len(attrs) == 0 {
105		return l
106	}
107
108	clone := *l
109	if l.slogSink != nil {
110		clone.slogSink = l.slogSink.WithAttrs(attrs)
111		clone.sink = clone.slogSink
112	} else {
113		kvList := make([]any, 0, 2*len(attrs))
114		for _, attr := range attrs {
115			kvList = attrToKVs(attr, l.groupPrefix, kvList)
116		}
117		clone.sink = l.sink.WithValues(kvList...)
118	}
119	return &clone
120}
121
122func (l *slogHandler) WithGroup(name string) slog.Handler {
123	if l.sink == nil {
124		return l
125	}
126	if name == "" {
127		// slog says to inline empty groups
128		return l
129	}
130	clone := *l
131	if l.slogSink != nil {
132		clone.slogSink = l.slogSink.WithGroup(name)
133		clone.sink = clone.slogSink
134	} else {
135		clone.groupPrefix = addPrefix(clone.groupPrefix, name)
136	}
137	return &clone
138}
139
140// attrToKVs appends a slog.Attr to a logr-style kvList.  It handle slog Groups
141// and other details of slog.
142func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any {
143	attrVal := attr.Value.Resolve()
144	if attrVal.Kind() == slog.KindGroup {
145		groupVal := attrVal.Group()
146		grpKVs := make([]any, 0, 2*len(groupVal))
147		prefix := groupPrefix
148		if attr.Key != "" {
149			prefix = addPrefix(groupPrefix, attr.Key)
150		}
151		for _, attr := range groupVal {
152			grpKVs = attrToKVs(attr, prefix, grpKVs)
153		}
154		kvList = append(kvList, grpKVs...)
155	} else if attr.Key != "" {
156		kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any())
157	}
158
159	return kvList
160}
161
162func addPrefix(prefix, name string) string {
163	if prefix == "" {
164		return name
165	}
166	if name == "" {
167		return prefix
168	}
169	return prefix + groupSeparator + name
170}
171
172// levelFromSlog adjusts the level by the logger's verbosity and negates it.
173// It ensures that the result is >= 0. This is necessary because the result is
174// passed to a LogSink and that API did not historically document whether
175// levels could be negative or what that meant.
176//
177// Some example usage:
178//
179//	logrV0 := getMyLogger()
180//	logrV2 := logrV0.V(2)
181//	slogV2 := slog.New(logr.ToSlogHandler(logrV2))
182//	slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6)
183//	slogV2.Info("msg")  // =~  logrV2.V(0) =~ logrV0.V(2)
184//	slogv2.Warn("msg")  // =~ logrV2.V(-4) =~ logrV0.V(0)
185func (l *slogHandler) levelFromSlog(level slog.Level) int {
186	result := -level
187	result += l.levelBias // in case the original Logger had a V level
188	if result < 0 {
189		result = 0 // because LogSink doesn't expect negative V levels
190	}
191	return int(result)
192}