github.com/richardwilkes/toolbox@v1.121.0/log/tracelog/tracelog.go (about) 1 // Copyright (c) 2016-2024 by Richard A. Wilkes. All rights reserved. 2 // 3 // This Source Code Form is subject to the terms of the Mozilla Public 4 // License, version 2.0. If a copy of the MPL was not distributed with 5 // this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 // 7 // This Source Code Form is "Incompatible With Secondary Licenses", as 8 // defined by the Mozilla Public License, version 2.0. 9 10 package tracelog 11 12 import ( 13 "bytes" 14 "context" 15 "fmt" 16 "io" 17 "log/slog" 18 "os" 19 "sync" 20 "time" 21 22 "github.com/richardwilkes/toolbox" 23 "github.com/richardwilkes/toolbox/errs" 24 ) 25 26 var _ slog.Handler = &Handler{} 27 28 // Config is used to configure a Handler. 29 type Config struct { 30 // Level is the minimum log level that will be emitted. Defaults to slog.LevelInfo if not set. 31 Level slog.Leveler 32 // LevelNames is an optional map of slog.Level to string that will be used to format the log level. If not set, the 33 // default slog level names will be used. 34 LevelNames map[slog.Level]string 35 // Sink is the io.Writer that will receive the formatted log output. Defaults to os.Stderr if not set. 36 Sink io.Writer 37 // BufferDepth greater than 0 will enable asynchronous delivery of log messages to the sink. When enabled, if there 38 // is no room remaining in the buffer, the message will be discarded rather than waiting for room to become 39 // available. Defaults to 0 for synchronous delivery. 40 BufferDepth int 41 } 42 43 // Normalize ensures that the Config is valid. 44 func (c *Config) Normalize() { 45 if toolbox.IsNil(c.Level) { 46 c.Level = slog.LevelInfo 47 } 48 if c.Sink == nil { 49 c.Sink = os.Stderr 50 } 51 if c.BufferDepth < 0 { 52 c.BufferDepth = 0 53 } 54 } 55 56 // Handler provides a formatted text output that may include a stack trace on separate lines. The stack trace is 57 // formatted such that most IDEs will auto-generate links for it within their consoles. Note that this slog.Handler is 58 // not optimized for performance, as I expect those that need to run this is environments where that matters will use 59 // one of the implementations provided by slog itself. 60 type Handler struct { 61 level slog.Leveler 62 levelNames map[slog.Level]string 63 delivery chan []byte 64 lock *sync.Mutex 65 sink io.Writer 66 list []entry 67 } 68 69 type entry struct { 70 group string 71 attrs []slog.Attr 72 } 73 74 // New creates a new Handler. May pass nil for cfg to use the defaults. 75 func New(cfg *Config) *Handler { 76 if cfg == nil { 77 cfg = &Config{} 78 } 79 cfg.Normalize() 80 h := Handler{ 81 level: cfg.Level, 82 levelNames: cfg.LevelNames, 83 lock: &sync.Mutex{}, 84 sink: cfg.Sink, 85 } 86 if cfg.BufferDepth > 0 { 87 h.delivery = make(chan []byte, cfg.BufferDepth) 88 go h.backgroundDelivery() 89 } 90 return &h 91 } 92 93 // Enabled implements slog.Handler. 94 func (h *Handler) Enabled(_ context.Context, level slog.Level) bool { 95 return level >= h.level.Level() 96 } 97 98 // WithGroup implements slog.Handler. 99 func (h *Handler) WithGroup(name string) slog.Handler { 100 if name == "" { 101 return h 102 } 103 return h.withGroupOrAttrs(entry{group: name}) 104 } 105 106 // WithAttrs implements slog.Handler. 107 func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { 108 if len(attrs) == 0 { 109 return h 110 } 111 return h.withGroupOrAttrs(entry{attrs: attrs}) 112 } 113 114 func (h *Handler) withGroupOrAttrs(ga entry) *Handler { 115 other := *h 116 other.list = make([]entry, len(h.list)+1) 117 copy(other.list, h.list) 118 other.list[len(other.list)-1] = ga 119 return &other 120 } 121 122 // Handle implements slog.Handler. 123 func (h *Handler) Handle(_ context.Context, r slog.Record) error { //nolint:gocritic // Must use defined API 124 var buffer bytes.Buffer 125 if name, ok := h.levelNames[r.Level]; ok { 126 buffer.WriteString(name) 127 } else { 128 switch r.Level { 129 case slog.LevelDebug: 130 buffer.WriteString("DBG") 131 case slog.LevelInfo: 132 buffer.WriteString("INF") 133 case slog.LevelWarn: 134 buffer.WriteString("WRN") 135 case slog.LevelError: 136 buffer.WriteString("ERR") 137 default: 138 fmt.Fprintf(&buffer, "%3d", r.Level) 139 } 140 } 141 buffer.WriteString(r.Time.Round(0).Format(" | 2006-01-02 | 15:04:05.000 | ")) 142 buffer.WriteString(r.Message) 143 144 s := &state{buffer: &buffer, needBar: true} 145 for _, ga := range h.list { 146 s.append(ga) 147 } 148 r.Attrs(func(attr slog.Attr) bool { 149 s.appendAttr(attr) 150 return true 151 }) 152 buffer.WriteByte('\n') 153 if s.stackErr != nil { 154 buffer.WriteString(s.stackErr.StackTrace(true)) 155 buffer.WriteByte('\n') 156 } 157 if h.delivery != nil { 158 select { 159 case h.delivery <- buffer.Bytes(): 160 default: 161 } 162 return nil 163 } 164 h.lock.Lock() 165 defer h.lock.Unlock() 166 _, err := h.sink.Write(buffer.Bytes()) 167 return err 168 } 169 170 func (h *Handler) backgroundDelivery() { 171 for data := range h.delivery { 172 _, _ = h.sink.Write(data) //nolint:errcheck // We don't care about errors here 173 } 174 } 175 176 type state struct { 177 buffer *bytes.Buffer 178 stackErr errs.StackError 179 group string 180 needBar bool 181 } 182 183 func (s *state) append(ga entry) { 184 if ga.group != "" { 185 s.addGroup(ga.group) 186 return 187 } 188 for _, attr := range ga.attrs { 189 s.appendAttr(attr) 190 } 191 } 192 193 func (s *state) appendAttr(attr slog.Attr) { 194 if s.group == "" && attr.Key == errs.StackTraceKey { 195 if embedded, ok := attr.Value.Any().(interface{ StackError() errs.StackError }); ok { 196 s.stackErr = embedded.StackError() 197 return 198 } 199 } 200 attr.Value = attr.Value.Resolve() 201 if !attr.Equal(slog.Attr{}) { 202 switch attr.Value.Kind() { 203 case slog.KindString: 204 s.addBarIfNeeded() 205 s.writeGroupAndKey(attr.Key) 206 _, _ = fmt.Fprintf(s.buffer, "%q", attr.Value.String()) 207 case slog.KindTime: 208 s.addBarIfNeeded() 209 s.writeGroupAndKey(attr.Key) 210 _, _ = s.buffer.WriteString(attr.Value.Time().Format(time.RFC3339Nano)) 211 case slog.KindGroup: 212 attrs := attr.Value.Group() 213 if len(attrs) == 0 { 214 return 215 } 216 s.addBarIfNeeded() 217 savedPrefix := s.group 218 s.addGroup(attr.Key) 219 for _, attr = range attrs { 220 s.appendAttr(attr) 221 } 222 s.group = savedPrefix 223 default: 224 s.addBarIfNeeded() 225 s.writeGroupAndKey(attr.Key) 226 _, _ = s.buffer.WriteString(attr.Value.String()) 227 } 228 } 229 } 230 231 func (s *state) writeGroupAndKey(key string) { 232 _ = s.buffer.WriteByte(' ') 233 _, _ = s.buffer.WriteString(s.group) 234 _, _ = s.buffer.WriteString(key) 235 _ = s.buffer.WriteByte('=') 236 } 237 238 func (s *state) addGroup(group string) { 239 group += "." 240 if s.group == "" { 241 s.group = group 242 } else { 243 s.group += group 244 } 245 } 246 247 func (s *state) addBarIfNeeded() { 248 if s.needBar { 249 s.buffer.WriteString(" |") 250 s.needBar = false 251 } 252 }