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}