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