|
| 1 | +//go:build go1.21 |
| 2 | +// +build go1.21 |
| 3 | + |
| 4 | +/* |
| 5 | +Copyright 2023 The logr 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 logr |
| 21 | + |
| 22 | +import ( |
| 23 | +"bytes" |
| 24 | +"context" |
| 25 | +"log/slog" |
| 26 | +"testing" |
| 27 | +"time" |
| 28 | +) |
| 29 | + |
| 30 | +var _ SlogSink = &testLogSink{} |
| 31 | + |
| 32 | +// testSlogSink gets embedded in testLogSink to add slog-specific fields |
| 33 | +// which are only available when slog is supported by Go. |
| 34 | +type testSlogSink struct { |
| 35 | +attrs []slog.Attr |
| 36 | +groups []string |
| 37 | + |
| 38 | +fnHandle func(l *testLogSink, ctx context.Context, record slog.Record) |
| 39 | +fnWithAttrs func(l *testLogSink, attrs []slog.Attr) |
| 40 | +fnWithGroup func(l *testLogSink, name string) |
| 41 | +} |
| 42 | + |
| 43 | +func (l *testLogSink) Handle(ctx context.Context, record slog.Record) error { |
| 44 | +if l.fnHandle != nil { |
| 45 | +l.fnHandle(l, ctx, record) |
| 46 | +} |
| 47 | +return nil |
| 48 | +} |
| 49 | + |
| 50 | +func (l *testLogSink) WithAttrs(attrs []slog.Attr) SlogSink { |
| 51 | +if l.fnWithAttrs != nil { |
| 52 | +l.fnWithAttrs(l, attrs) |
| 53 | +} |
| 54 | +out := *l |
| 55 | +out.attrs = append(l.attrs[:], attrs...) |
| 56 | +return &out |
| 57 | +} |
| 58 | + |
| 59 | +func (l *testLogSink) WithGroup(name string) SlogSink { |
| 60 | +if l.fnWithGroup != nil { |
| 61 | +l.fnWithGroup(l, name) |
| 62 | +} |
| 63 | +out := *l |
| 64 | +out.groups = append(l.groups[:], name) |
| 65 | +return &out |
| 66 | +} |
| 67 | + |
| 68 | +func withAttrs(record slog.Record, attrs ...slog.Attr) slog.Record { |
| 69 | +record = record.Clone() |
| 70 | +record.AddAttrs(attrs...) |
| 71 | +return record |
| 72 | +} |
| 73 | + |
| 74 | +func toJSON(record slog.Record) string { |
| 75 | +var buffer bytes.Buffer |
| 76 | +record.Time = time.Time{} |
| 77 | +handler := slog.NewJSONHandler(&buffer, nil) |
| 78 | +if err := handler.Handle(context.Background(), record); err != nil { |
| 79 | +return err.Error() |
| 80 | +} |
| 81 | +return buffer.String() |
| 82 | +} |
| 83 | + |
| 84 | +func TestToSlogHandler(t *testing.T) { |
| 85 | +lvlThreshold := 0 |
| 86 | +actualCalledHandle := 0 |
| 87 | +var actualRecord slog.Record |
| 88 | + |
| 89 | +sink := &testLogSink{} |
| 90 | +logger := New(sink) |
| 91 | + |
| 92 | +sink.fnEnabled = func(lvl int) bool { |
| 93 | +return lvl <= lvlThreshold |
| 94 | +} |
| 95 | + |
| 96 | +sink.fnHandle = func(l *testLogSink, ctx context.Context, record slog.Record) { |
| 97 | +actualCalledHandle++ |
| 98 | + |
| 99 | +// Combine attributes from sink and call. Ordering of WithValues and WithAttrs |
| 100 | +// is wrong, but good enough for test cases. |
| 101 | +var values slog.Record |
| 102 | +values.Add(l.withValues...) |
| 103 | +var attrs []any |
| 104 | +add := func(attr slog.Attr) bool { |
| 105 | +attrs = append(attrs, attr) |
| 106 | +return true |
| 107 | +} |
| 108 | +values.Attrs(add) |
| 109 | +record.Attrs(add) |
| 110 | +for _, attr := range l.attrs { |
| 111 | +attrs = append(attrs, attr) |
| 112 | +} |
| 113 | + |
| 114 | +// Wrap them in groups - not quite correct for WithValues that |
| 115 | +// follows WithGroup, but good enough for test cases. |
| 116 | +for i := len(l.groups) - 1; i >= 0; i-- { |
| 117 | +attrs = []any{slog.Group(l.groups[i], attrs...)} |
| 118 | +} |
| 119 | + |
| 120 | +actualRecord = slog.Record{ |
| 121 | +Level: record.Level, |
| 122 | +Message: record.Message, |
| 123 | +} |
| 124 | +actualRecord.Add(attrs...) |
| 125 | +} |
| 126 | + |
| 127 | +verify := func(t *testing.T, expectedRecord slog.Record) { |
| 128 | +actual := toJSON(actualRecord) |
| 129 | +expected := toJSON(expectedRecord) |
| 130 | +if expected != actual { |
| 131 | +t.Errorf("JSON dump did not match, expected:\n%s\nGot:\n%s\n", expected, actual) |
| 132 | +} |
| 133 | +} |
| 134 | + |
| 135 | +reset := func() { |
| 136 | +lvlThreshold = 0 |
| 137 | +actualCalledHandle = 0 |
| 138 | +actualRecord = slog.Record{} |
| 139 | +} |
| 140 | + |
| 141 | +testcases := map[string]struct { |
| 142 | +run func() |
| 143 | +expectedRecord slog.Record |
| 144 | +}{ |
| 145 | +"simple": { |
| 146 | +func() { slog.New(ToSlogHandler(logger)).Info("simple") }, |
| 147 | +slog.Record{Message: "simple"}, |
| 148 | +}, |
| 149 | + |
| 150 | +"disabled": { |
| 151 | +func() { slog.New(ToSlogHandler(logger.V(1))).Info("") }, |
| 152 | +slog.Record{}, |
| 153 | +}, |
| 154 | + |
| 155 | +"enabled": { |
| 156 | +func() { |
| 157 | +lvlThreshold = 1 |
| 158 | +slog.New(ToSlogHandler(logger.V(1))).Info("enabled") |
| 159 | +}, |
| 160 | +slog.Record{Level: -1, Message: "enabled"}, |
| 161 | +}, |
| 162 | + |
| 163 | +"error": { |
| 164 | +func() { slog.New(ToSlogHandler(logger.V(100))).Error("error") }, |
| 165 | +slog.Record{Level: slog.LevelError, Message: "error"}, |
| 166 | +}, |
| 167 | + |
| 168 | +"with-parameters": { |
| 169 | +func() { slog.New(ToSlogHandler(logger)).Info("", "answer", 42, "foo", "bar") }, |
| 170 | +withAttrs(slog.Record{}, slog.Int("answer", 42), slog.String("foo", "bar")), |
| 171 | +}, |
| 172 | + |
| 173 | +"with-values": { |
| 174 | +func() { slog.New(ToSlogHandler(logger.WithValues("answer", 42, "foo", "bar"))).Info("") }, |
| 175 | +withAttrs(slog.Record{}, slog.Int("answer", 42), slog.String("foo", "bar")), |
| 176 | +}, |
| 177 | + |
| 178 | +"with-group": { |
| 179 | +func() { slog.New(ToSlogHandler(logger)).WithGroup("group").Info("", "answer", 42, "foo", "bar") }, |
| 180 | +withAttrs(slog.Record{}, slog.Group("group", slog.Int("answer", 42), slog.String("foo", "bar"))), |
| 181 | +}, |
| 182 | + |
| 183 | +"with-values-and-group": { |
| 184 | +func() { |
| 185 | +slog.New(ToSlogHandler(logger.WithValues("answer", 42, "foo", "bar"))).WithGroup("group").Info("") |
| 186 | +}, |
| 187 | +// Behavior of testLogSink is not quite correct here. |
| 188 | +withAttrs(slog.Record{}, slog.Group("group", slog.Int("answer", 42), slog.String("foo", "bar"))), |
| 189 | +}, |
| 190 | + |
| 191 | +"with-group-and-values": { |
| 192 | +func() { |
| 193 | +slog.New(ToSlogHandler(logger)).WithGroup("group").With("answer", 42, "foo", "bar").Info("") |
| 194 | +}, |
| 195 | +withAttrs(slog.Record{}, slog.Group("group", slog.Int("answer", 42), slog.String("foo", "bar"))), |
| 196 | +}, |
| 197 | + |
| 198 | +"with-group-and-logr-values": { |
| 199 | +func() { |
| 200 | +slogLogger := slog.New(ToSlogHandler(logger)).WithGroup("group") |
| 201 | +logrLogger := FromSlogHandler(slogLogger.Handler()).WithValues("answer", 42, "foo", "bar") |
| 202 | +slogLogger = slog.New(ToSlogHandler(logrLogger)) |
| 203 | +slogLogger.Info("") |
| 204 | +}, |
| 205 | +withAttrs(slog.Record{}, slog.Group("group", slog.Int("answer", 42), slog.String("foo", "bar"))), |
| 206 | +}, |
| 207 | +} |
| 208 | + |
| 209 | +for name, tc := range testcases { |
| 210 | +t.Run(name, func(t *testing.T) { |
| 211 | +tc.run() |
| 212 | +verify(t, tc.expectedRecord) |
| 213 | +reset() |
| 214 | +}) |
| 215 | +} |
| 216 | +} |
0 commit comments