go.temporal.io/server@v1.23.0/common/log/slog.go (about) 1 // The MIT License 2 // 3 // Copyright (c) 2023 Temporal Technologies Inc. All rights reserved. 4 // 5 // Copyright (c) 2023 Uber Technologies, Inc. 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 // THE SOFTWARE. 24 25 // Part of this implementation was taken from https://github.com/uber-go/zap/blob/99f1811d5d2a52264a9c82505a74c2709b077a73/exp/zapslog/handler.go 26 27 package log 28 29 import ( 30 "context" 31 "log/slog" 32 33 "go.temporal.io/server/common/log/tag" 34 "go.uber.org/zap" 35 "go.uber.org/zap/zapcore" 36 ) 37 38 type handler struct { 39 zapLogger *zap.Logger 40 logger Logger 41 tags []tag.Tag 42 group string 43 } 44 45 var _ slog.Handler = (*handler)(nil) 46 47 // NewSlogLogger creates an slog.Logger from a given logger. 48 func NewSlogLogger(logger Logger) *slog.Logger { 49 logger = withIncreasedSkip(logger, 3) 50 return slog.New(&handler{logger: logger, zapLogger: extractZapLogger(logger), group: "", tags: nil}) 51 } 52 53 // Enabled reports whether the handler handles records at the given level. 54 func (h *handler) Enabled(_ context.Context, level slog.Level) bool { 55 if h.zapLogger == nil { 56 return true 57 } 58 return h.zapLogger.Core().Enabled(convertSlogToZapLevel(level)) 59 } 60 61 // Handle implements slog.Handler. 62 func (h *handler) Handle(_ context.Context, record slog.Record) error { 63 tags := make([]tag.Tag, len(h.tags), len(h.tags)+record.NumAttrs()) 64 copy(tags, h.tags) 65 record.Attrs(func(attr slog.Attr) bool { 66 tags = append(tags, tag.NewZapTag(convertAttrToField(h.prependGroup(attr)))) 67 return true 68 }) 69 // Not capturing the log location and stack trace here. We seem to not need this functionality since our zapLogger 70 // adds the logging-call-at tag. 71 switch record.Level { 72 case slog.LevelDebug: 73 h.logger.Debug(record.Message, tags...) 74 case slog.LevelInfo: 75 h.logger.Info(record.Message, tags...) 76 case slog.LevelWarn: 77 h.logger.Warn(record.Message, tags...) 78 case slog.LevelError: 79 h.logger.Error(record.Message, tags...) 80 default: 81 } 82 return nil 83 } 84 85 // WithAttrs implements slog.Handler. 86 func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler { 87 tags := make([]tag.Tag, len(h.tags), len(h.tags)+len(attrs)) 88 copy(tags, h.tags) 89 for _, attr := range attrs { 90 tags = append(tags, tag.NewZapTag(convertAttrToField(h.prependGroup(attr)))) 91 } 92 return &handler{logger: h.logger, tags: tags, group: h.group} 93 } 94 95 // WithGroup implements slog.Handler. 96 func (h *handler) WithGroup(name string) slog.Handler { 97 group := name 98 if h.group != "" { 99 group = h.group + "." + name 100 } 101 return &handler{logger: h.logger, tags: h.tags, group: group} 102 } 103 104 func (h *handler) prependGroup(attr slog.Attr) slog.Attr { 105 if h.group == "" { 106 return attr 107 } 108 return slog.Attr{Key: h.group + "." + attr.Key, Value: attr.Value} 109 } 110 111 func extractZapLogger(logger Logger) *zap.Logger { 112 switch l := logger.(type) { 113 case *zapLogger: 114 return l.zl 115 case *throttledLogger: 116 return extractZapLogger(l.logger) 117 case *withLogger: 118 return extractZapLogger(l.logger) 119 } 120 return nil 121 } 122 123 // withIncreasedSkip increases the skip level for the given logger if it embeds a zapLogger. 124 func withIncreasedSkip(logger Logger, skip int) Logger { 125 switch l := logger.(type) { 126 case *zapLogger: 127 return l.Skip(skip) 128 case *throttledLogger: 129 return &throttledLogger{ 130 limiter: l.limiter, 131 logger: withIncreasedSkip(l.logger, skip), 132 } 133 case *withLogger: 134 return &withLogger{ 135 tags: l.tags, 136 logger: withIncreasedSkip(l.logger, skip), 137 } 138 } 139 return nil 140 } 141 142 // convertSlogToZapLevel maps slog Levels to zap Levels. 143 // Note that there is some room between slog levels while zap levels are continuous, so we can't 1:1 map them. 144 // See also https://go.googlesource.com/proposal/+/master/design/56345-structured-logging.md?pli=1#levels 145 func convertSlogToZapLevel(l slog.Level) zapcore.Level { 146 switch { 147 case l >= slog.LevelError: 148 return zapcore.ErrorLevel 149 case l >= slog.LevelWarn: 150 return zapcore.WarnLevel 151 case l >= slog.LevelInfo: 152 return zapcore.InfoLevel 153 default: 154 return zapcore.DebugLevel 155 } 156 } 157 158 // groupObject holds all the Attrs saved in a slog.GroupValue. 159 type groupObject []slog.Attr 160 161 func (gs groupObject) MarshalLogObject(enc zapcore.ObjectEncoder) error { 162 for _, attr := range gs { 163 convertAttrToField(attr).AddTo(enc) 164 } 165 return nil 166 } 167 168 func convertAttrToField(attr slog.Attr) zapcore.Field { 169 if attr.Equal(slog.Attr{}) { 170 // Ignore empty attrs. 171 return zap.Skip() 172 } 173 174 switch attr.Value.Kind() { 175 case slog.KindBool: 176 return zap.Bool(attr.Key, attr.Value.Bool()) 177 case slog.KindDuration: 178 return zap.Duration(attr.Key, attr.Value.Duration()) 179 case slog.KindFloat64: 180 return zap.Float64(attr.Key, attr.Value.Float64()) 181 case slog.KindInt64: 182 return zap.Int64(attr.Key, attr.Value.Int64()) 183 case slog.KindString: 184 return zap.String(attr.Key, attr.Value.String()) 185 case slog.KindTime: 186 return zap.Time(attr.Key, attr.Value.Time()) 187 case slog.KindUint64: 188 return zap.Uint64(attr.Key, attr.Value.Uint64()) 189 case slog.KindGroup: 190 return zap.Object(attr.Key, groupObject(attr.Value.Group())) 191 case slog.KindLogValuer: 192 return convertAttrToField(slog.Attr{ 193 Key: attr.Key, 194 // TODO: resolve the value in a lazy way. 195 // This probably needs a new Zap field type 196 // that can be resolved lazily. 197 Value: attr.Value.Resolve(), 198 }) 199 default: 200 return zap.Any(attr.Key, attr.Value.Any()) 201 } 202 }