github.com/searKing/golang/go@v1.2.117/log/slog/handler.go (about) 1 // Copyright 2023 The searKing Author. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package slog 6 7 import ( 8 "io" 9 "log/slog" 10 "os" 11 "runtime" 12 "slices" 13 "sync" 14 "time" 15 "unicode/utf8" 16 17 "github.com/searKing/golang/go/log/slog/internal/buffer" 18 "golang.org/x/term" 19 ) 20 21 var ( 22 timeNow = time.Now // Stubbed out for testing. 23 baseTimestamp time.Time 24 getPid = os.Getpid // Stubbed out for testing. 25 ) 26 27 func init() { 28 baseTimestamp = timeNow() 29 } 30 31 // Keys for "built-in" attributes. 32 const ( 33 // ErrorKey is the key used by the handlers for the error 34 // when the log method is called. The associated Value is an [error]. 35 ErrorKey = "error" 36 ) 37 38 type TimestampMode int 39 40 const ( 41 _ TimestampMode = iota 42 43 // DisableTimestamp disable timestamp logging. useful when output is redirected to logging 44 // system that already adds timestamps. 45 DisableTimestamp 46 47 // SinceStartTimestamp enable the time passed since beginning of execution instead of 48 // logging the full timestamp when a TTY is attached. 49 SinceStartTimestamp 50 ) 51 52 // sharedVar shared const expvar among handler and children handler... 53 type sharedVar struct { 54 once *sync.Once 55 56 // Whether the logger's out is to a terminal 57 isTerminal bool 58 // The max length of the level text, generated dynamically on init 59 maxLevelText int 60 // The process id of the caller. 61 pid int 62 } 63 64 func (h *sharedVar) init(w io.Writer) { 65 h.once.Do(func() { 66 if f, ok := w.(*os.File); ok { 67 h.isTerminal = term.IsTerminal(int(f.Fd())) 68 } 69 // Get the max length of the level text 70 for _, level := range []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError} { 71 levelTextLength := utf8.RuneCount([]byte(level.String())) 72 if levelTextLength > h.maxLevelText { 73 h.maxLevelText = levelTextLength 74 } 75 } 76 h.pid = getPid() 77 }) 78 } 79 80 type commonHandler struct { 81 // replace level.String() 82 ReplaceLevelString func(l slog.Level) string 83 84 // the separator between attributes. 85 AttrSep string 86 87 // Set to true to bypass checking for a TTY before outputting colors. 88 ForceColors bool 89 90 // Force disabling colors. 91 DisableColors bool 92 93 // Force quoting of all values 94 ForceQuote bool 95 96 // DisableQuote disables quoting for all values. 97 // DisableQuote will have a lower priority than ForceQuote. 98 // If both of them are set to true, quote will be forced on all values. 99 DisableQuote bool 100 101 // ForceGoroutineId enables goroutine id instead of pid. 102 ForceGoroutineId bool 103 104 TimestampMode TimestampMode 105 106 // TimestampFormat to use for display when a full timestamp is printed 107 TimestampFormat string 108 109 // Disables the glog style :[IWEF]yyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg msg... 110 // replace with :[IWEF] [yyyymmdd] [hh:mm:ss.uuuuuu] [threadid] [file:line] msg msg... 111 HumanReadable bool 112 113 // PadLevelText Adds padding the level text so that all the levels output at the same length 114 // PadLevelText is a superset of the DisableLevelTruncation option 115 PadLevelText bool 116 117 // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ 118 EnvironmentOverrideColors bool 119 120 // SourcePrettier can be set by the user to modify the content 121 // of the file, function and file keys when AddSource is 122 // activated. If any of the returned value is the empty string the 123 // corresponding key will be removed from slog attrs. 124 SourcePrettier func(r slog.Record) *slog.Source 125 126 // WithFuncName append Caller's func name 127 WithFuncName bool 128 129 opts slog.HandlerOptions 130 131 sharedVar *sharedVar 132 133 preformattedAttrs []byte 134 // groupPrefix is for the text handler only. 135 // It holds the prefix for groups that were already pre-formatted. 136 // A group will appear here when a call to WithGroup is followed by 137 // a call to WithAttrs. 138 groupPrefix string 139 groups []string // all groups started from WithGroup 140 nOpenGroups int // the number of groups opened in preformattedAttrs 141 mu *sync.Mutex // mutex shared among all clones of this handler 142 w io.Writer 143 } 144 145 // NewCommonHandler creates a CommonHandler that writes to w, 146 // using the given options. 147 // If opts is nil, the default options are used. 148 // A [CommonHandler] is a low-level primitive for making structured log. 149 // [NewGlogHandler] or [NewGlogHumanHandler] recommended. 150 func NewCommonHandler(w io.Writer, opts *slog.HandlerOptions) *commonHandler { 151 if opts == nil { 152 opts = &slog.HandlerOptions{} 153 } 154 return &commonHandler{ 155 opts: *opts, 156 sharedVar: &sharedVar{once: &sync.Once{}}, 157 mu: &sync.Mutex{}, 158 w: w, 159 } 160 } 161 162 func (h *commonHandler) clone() *commonHandler { 163 // We can't use assignment because we can't copy the mutex. 164 h2 := *h 165 h2.preformattedAttrs = slices.Clip(h.preformattedAttrs) 166 h2.groups = slices.Clip(h.groups) 167 return &h2 168 } 169 170 // enabled reports whether l is greater than or equal to the 171 // minimum level. 172 func (h *commonHandler) enabled(l slog.Level) bool { 173 minLevel := slog.LevelInfo 174 if h.opts.Level != nil { 175 minLevel = h.opts.Level.Level() 176 } 177 return l >= minLevel 178 } 179 180 func (h *commonHandler) withAttrs(as []slog.Attr) *commonHandler { 181 // We are going to ignore empty groups, so if the entire slice consists of 182 // them, there is nothing to do. 183 if countEmptyGroups(as) == len(as) { 184 return h 185 } 186 h2 := h.clone() 187 // Pre-format the attributes as an optimization. 188 buf := buffer.New() 189 buf.Write(h2.preformattedAttrs) 190 state := h2.newHandleState(buf, false, "") 191 defer state.free() 192 defer func() { 193 h2.preformattedAttrs = buf.Bytes() 194 }() 195 state.prefix.WriteString(h.groupPrefix) 196 if len(h2.preformattedAttrs) > 0 { 197 state.sep = h.attrSep() 198 } 199 state.openGroups() 200 for _, a := range as { 201 state.appendAttr(a) 202 } 203 // Remember the new prefix for later keys. 204 h2.groupPrefix = state.prefix.String() 205 // Remember how many opened groups are in preformattedAttrs, 206 // so we don't open them again when we handle a Record. 207 h2.nOpenGroups = len(h2.groups) 208 return h2 209 } 210 211 func (h *commonHandler) withGroup(name string) *commonHandler { 212 h2 := h.clone() 213 h2.groups = append(h2.groups, name) 214 return h2 215 } 216 217 // handle is the internal implementation of Handler.Handle 218 // used by GlogHandler and HumanGlogHandler. 219 // header formats a log header as defined by the C++ implementation. 220 // It returns a buffer containing the formatted header and the user's file and line number. 221 // The depth specifies how many stack frames above lives the source line to be identified in the log message. 222 // 223 // # LOG LINE PREFIX FORMAT 224 // 225 // Log lines have this form: 226 // 227 // Lyyyymmdd hh:mm:ss.uuuuuu threadid file:line] msg... 228 // 229 // where the fields are defined as follows: 230 // 231 // L A single character, representing the log level 232 // (eg 'I' for INFO) 233 // yyyy The year 234 // mm The month (zero padded; ie May is '05') 235 // dd The day (zero padded) 236 // hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds 237 // threadid The space-padded thread ID as returned by GetTID() 238 // (this matches the PID on Linux) 239 // file The file name 240 // line The line number 241 // msg The user-supplied message 242 // 243 // Example: 244 // 245 // I1103 11:57:31.739339 24395 google.cc:2341] Command line: ./some_prog 246 // I1103 11:57:31.739403 24395 google.cc:2342] Process id 24395 247 // 248 // NOTE: although the microseconds are useful for comparing events on 249 // a single machine, clocks on different machines may not be well 250 // synchronized. Hence, use caution when comparing the low bits of 251 // timestamps from different machines. 252 func (h *commonHandler) handle(r slog.Record) error { 253 h.sharedVar.init(h.w) 254 255 state := h.newHandleState(buffer.New(), true, "") 256 defer state.free() 257 // Built-in attributes. They are not in a group. 258 stateGroups := state.groups 259 state.groups = nil // So ReplaceAttrs sees no groups instead of the pre groups. 260 rep := h.opts.ReplaceAttr 261 if h.isColored() { 262 state.color = levelColor(r.Level) 263 } 264 // level 265 state.appendLevel(r.Level, h.PadLevelText, h.sharedVar.maxLevelText, h.HumanReadable) 266 // time 267 t := r.Time // strip monotonic to match Attr behavior 268 mode := h.TimestampMode 269 if rep != nil { 270 a := rep(nil, slog.Time(slog.TimeKey, r.Time)) 271 if a.Equal(slog.Attr{}) { 272 // disable timestamp logging if time is removed. 273 t = time.Time{} 274 mode = DisableTimestamp 275 } else if a.Value.Kind() == slog.KindTime { 276 t = a.Value.Time() 277 } 278 } 279 state.appendGlogTime(t, h.TimestampFormat, mode, h.HumanReadable) 280 state.appendPid(h.ForceGoroutineId, h.HumanReadable) 281 // source 282 if h.opts.AddSource { 283 if h.SourcePrettier != nil { 284 state.appendSource(h.SourcePrettier(r), h.WithFuncName, h.HumanReadable) 285 } else { 286 state.appendSource(source(r), h.WithFuncName, h.HumanReadable) 287 } 288 } else { 289 if !h.HumanReadable { 290 state.buf.WriteString("]") 291 } 292 } 293 294 var hasMessage bool 295 if rep != nil { 296 a := rep(nil, slog.String(slog.MessageKey, r.Message)) 297 if !isEmptyAttr(a) { 298 state.buf.WriteString(" ") 299 state.appendValue(a.Value) 300 hasMessage = true 301 } 302 } else if r.Message != "" { 303 state.buf.WriteString(" ") 304 // take message as well formatted raw string, may be with color and so on, disable quote 305 state.appendString(r.Message) 306 hasMessage = true 307 } 308 if !hasMessage { 309 state.sep = " " 310 } else { 311 state.sep = h.attrSep() 312 } 313 314 state.groups = stateGroups // Restore groups passed to ReplaceAttrs. 315 state.appendNonBuiltIns(r) 316 state.buf.WriteByte('\n') 317 318 h.mu.Lock() 319 defer h.mu.Unlock() 320 _, err := h.w.Write(state.buf.Bytes()) 321 return err 322 } 323 324 func (s *handleState) appendNonBuiltIns(r slog.Record) { 325 // preformatted Attrs 326 if len(s.h.preformattedAttrs) > 0 { 327 s.buf.WriteString(s.sep) 328 s.buf.Write(s.h.preformattedAttrs) 329 s.sep = s.h.attrSep() 330 } 331 // Attrs in Record -- unlike the built-in ones, they are in groups started 332 // from WithGroup. 333 // If the record has no Attrs, don't output any groups. 334 if r.NumAttrs() > 0 { 335 s.prefix.WriteString(s.h.groupPrefix) 336 s.openGroups() 337 r.Attrs(func(a slog.Attr) bool { 338 s.appendAttr(a) 339 return true 340 }) 341 } 342 } 343 344 // attrSep returns the separator between attributes. 345 func (h *commonHandler) attrSep() string { 346 if h.AttrSep != "" { 347 return h.AttrSep 348 } 349 return " " 350 } 351 352 var groupPool = sync.Pool{New: func() any { 353 s := make([]string, 0, 10) 354 return &s 355 }} 356 357 func (h *commonHandler) newHandleState(buf *buffer.Buffer, freeBuf bool, sep string) handleState { 358 s := handleState{ 359 h: h, 360 buf: buf, 361 freeBuf: freeBuf, 362 sep: sep, 363 prefix: buffer.New(), 364 } 365 if h.opts.ReplaceAttr != nil { 366 s.groups = groupPool.Get().(*[]string) 367 *s.groups = append(*s.groups, h.groups[:h.nOpenGroups]...) 368 } 369 return s 370 } 371 372 func (h *commonHandler) isColored() bool { 373 isColored := h.ForceColors || (h.sharedVar.isTerminal && (runtime.GOOS != "windows")) 374 375 if h.EnvironmentOverrideColors { 376 switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); { 377 case ok && force != "0": 378 isColored = true 379 case ok && force == "0", os.Getenv("CLICOLOR") == "0": 380 isColored = false 381 } 382 } 383 384 return isColored && !h.DisableColors 385 }