]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/log/slog/handler.go
log/slog: add LogLoggerLevel to enable setting level on the default logger
[gostls13.git] / src / log / slog / handler.go
index dc4c2d92bd7126cfa27cfef7ca8b95ea31e99b20..9f6d88b1696bf71b2f046f8527e4bf1b1c34cc25 100644 (file)
@@ -9,13 +9,14 @@ import (
        "fmt"
        "io"
        "log/slog/internal/buffer"
+       "reflect"
        "slices"
        "strconv"
        "sync"
        "time"
 )
 
-// A Handler handles log records produced by a Logger..
+// A Handler handles log records produced by a Logger.
 //
 // A typical handler may print log records to standard error,
 // or write them to a file or database, or perhaps augment them
@@ -99,7 +100,7 @@ func newDefaultHandler(output func(uintptr, []byte) error) *defaultHandler {
 }
 
 func (*defaultHandler) Enabled(_ context.Context, l Level) bool {
-       return l >= LevelInfo
+       return l >= logLoggerLevel.Level()
 }
 
 // Collect the level, attributes and message in a string and
@@ -124,7 +125,7 @@ func (h *defaultHandler) WithGroup(name string) Handler {
        return &defaultHandler{h.ch.withGroup(name), h.output}
 }
 
-// HandlerOptions are options for a TextHandler or JSONHandler.
+// HandlerOptions are options for a [TextHandler] or [JSONHandler].
 // A zero HandlerOptions consists entirely of default values.
 type HandlerOptions struct {
        // AddSource causes the handler to compute the source code position
@@ -178,7 +179,7 @@ const (
        // message of the log call. The associated value is a string.
        MessageKey = "msg"
        // SourceKey is the key used by the built-in handlers for the source file
-       // and line of the log call. The associated value is a string.
+       // and line of the log call. The associated value is a *[Source].
        SourceKey = "source"
 )
 
@@ -193,7 +194,7 @@ type commonHandler struct {
        groupPrefix string
        groups      []string // all groups started from WithGroup
        nOpenGroups int      // the number of groups opened in preformattedAttrs
-       mu          sync.Mutex
+       mu          *sync.Mutex
        w           io.Writer
 }
 
@@ -207,6 +208,7 @@ func (h *commonHandler) clone() *commonHandler {
                groups:            slices.Clip(h.groups),
                nOpenGroups:       h.nOpenGroups,
                w:                 h.w,
+               mu:                h.mu, // mutex shared among all clones of this handler
        }
 }
 
@@ -221,23 +223,34 @@ func (h *commonHandler) enabled(l Level) bool {
 }
 
 func (h *commonHandler) withAttrs(as []Attr) *commonHandler {
+       // We are going to ignore empty groups, so if the entire slice consists of
+       // them, there is nothing to do.
+       if countEmptyGroups(as) == len(as) {
+               return h
+       }
        h2 := h.clone()
        // Pre-format the attributes as an optimization.
        state := h2.newHandleState((*buffer.Buffer)(&h2.preformattedAttrs), false, "")
        defer state.free()
        state.prefix.WriteString(h.groupPrefix)
-       if len(h2.preformattedAttrs) > 0 {
+       if pfa := h2.preformattedAttrs; len(pfa) > 0 {
                state.sep = h.attrSep()
+               if h2.json && pfa[len(pfa)-1] == '{' {
+                       state.sep = ""
+               }
        }
+       // Remember the position in the buffer, in case all attrs are empty.
+       pos := state.buf.Len()
        state.openGroups()
-       for _, a := range as {
-               state.appendAttr(a)
+       if !state.appendAttrs(as) {
+               state.buf.SetLen(pos)
+       } else {
+               // Remember the new prefix for later keys.
+               h2.groupPrefix = state.prefix.String()
+               // Remember how many opened groups are in preformattedAttrs,
+               // so we don't open them again when we handle a Record.
+               h2.nOpenGroups = len(h2.groups)
        }
-       // Remember the new prefix for later keys.
-       h2.groupPrefix = state.prefix.String()
-       // Remember how many opened groups are in preformattedAttrs,
-       // so we don't open them again when we handle a Record.
-       h2.nOpenGroups = len(h2.groups)
        return h2
 }
 
@@ -247,6 +260,8 @@ func (h *commonHandler) withGroup(name string) *commonHandler {
        return h2
 }
 
+// handle is the internal implementation of Handler.Handle
+// used by TextHandler and JSONHandler.
 func (h *commonHandler) handle(r Record) error {
        state := h.newHandleState(buffer.New(), true, "")
        defer state.free()
@@ -301,22 +316,42 @@ func (h *commonHandler) handle(r Record) error {
 
 func (s *handleState) appendNonBuiltIns(r Record) {
        // preformatted Attrs
-       if len(s.h.preformattedAttrs) > 0 {
+       if pfa := s.h.preformattedAttrs; len(pfa) > 0 {
                s.buf.WriteString(s.sep)
-               s.buf.Write(s.h.preformattedAttrs)
+               s.buf.Write(pfa)
                s.sep = s.h.attrSep()
+               if s.h.json && pfa[len(pfa)-1] == '{' {
+                       s.sep = ""
+               }
        }
        // Attrs in Record -- unlike the built-in ones, they are in groups started
        // from WithGroup.
-       s.prefix.WriteString(s.h.groupPrefix)
-       s.openGroups()
-       r.Attrs(func(a Attr) bool {
-               s.appendAttr(a)
-               return true
-       })
+       // If the record has no Attrs, don't output any groups.
+       nOpenGroups := s.h.nOpenGroups
+       if r.NumAttrs() > 0 {
+               s.prefix.WriteString(s.h.groupPrefix)
+               // The group may turn out to be empty even though it has attrs (for
+               // example, ReplaceAttr may delete all the attrs).
+               // So remember where we are in the buffer, to restore the position
+               // later if necessary.
+               pos := s.buf.Len()
+               s.openGroups()
+               nOpenGroups = len(s.h.groups)
+               empty := true
+               r.Attrs(func(a Attr) bool {
+                       if s.appendAttr(a) {
+                               empty = false
+                       }
+                       return true
+               })
+               if empty {
+                       s.buf.SetLen(pos)
+                       nOpenGroups = s.h.nOpenGroups
+               }
+       }
        if s.h.json {
                // Close all open groups.
-               for range s.h.groups {
+               for range s.h.groups[:nOpenGroups] {
                        s.buf.WriteByte('}')
                }
                // Close the top-level object.
@@ -414,23 +449,37 @@ func (s *handleState) closeGroup(name string) {
        }
 }
 
+// appendAttrs appends the slice of Attrs.
+// It reports whether something was appended.
+func (s *handleState) appendAttrs(as []Attr) bool {
+       nonEmpty := false
+       for _, a := range as {
+               if s.appendAttr(a) {
+                       nonEmpty = true
+               }
+       }
+       return nonEmpty
+}
+
 // appendAttr appends the Attr's key and value using app.
 // It handles replacement and checking for an empty key.
 // after replacement).
-func (s *handleState) appendAttr(a Attr) {
+// It reports whether something was appended.
+func (s *handleState) appendAttr(a Attr) bool {
+       a.Value = a.Value.Resolve()
        if rep := s.h.opts.ReplaceAttr; rep != nil && a.Value.Kind() != KindGroup {
                var gs []string
                if s.groups != nil {
                        gs = *s.groups
                }
-               // Resolve before calling ReplaceAttr, so the user doesn't have to.
-               a.Value = a.Value.Resolve()
+               // a.Value is resolved before calling ReplaceAttr, so the user doesn't have to.
                a = rep(gs, a)
+               // The ReplaceAttr function may return an unresolved Attr.
+               a.Value = a.Value.Resolve()
        }
-       a.Value = a.Value.Resolve()
        // Elide empty Attrs.
        if a.isEmpty() {
-               return
+               return false
        }
        // Special case: Source.
        if v := a.Value; v.Kind() == KindAny {
@@ -446,12 +495,18 @@ func (s *handleState) appendAttr(a Attr) {
                attrs := a.Value.Group()
                // Output only non-empty groups.
                if len(attrs) > 0 {
+                       // The group may turn out to be empty even though it has attrs (for
+                       // example, ReplaceAttr may delete all the attrs).
+                       // So remember where we are in the buffer, to restore the position
+                       // later if necessary.
+                       pos := s.buf.Len()
                        // Inline a group with an empty key.
                        if a.Key != "" {
                                s.openGroup(a.Key)
                        }
-                       for _, aa := range attrs {
-                               s.appendAttr(aa)
+                       if !s.appendAttrs(attrs) {
+                               s.buf.SetLen(pos)
+                               return false
                        }
                        if a.Key != "" {
                                s.closeGroup(a.Key)
@@ -461,6 +516,7 @@ func (s *handleState) appendAttr(a Attr) {
                s.appendKey(a.Key)
                s.appendValue(a.Value)
        }
+       return true
 }
 
 func (s *handleState) appendError(err error) {
@@ -499,6 +555,23 @@ func (s *handleState) appendString(str string) {
 }
 
 func (s *handleState) appendValue(v Value) {
+       defer func() {
+               if r := recover(); r != nil {
+                       // If it panics with a nil pointer, the most likely cases are
+                       // an encoding.TextMarshaler or error fails to guard against nil,
+                       // in which case "<nil>" seems to be the feasible choice.
+                       //
+                       // Adapted from the code in fmt/print.go.
+                       if v := reflect.ValueOf(v.any); v.Kind() == reflect.Pointer && v.IsNil() {
+                               s.appendString("<nil>")
+                               return
+                       }
+
+                       // Otherwise just print the original panic message.
+                       s.appendString(fmt.Sprintf("!PANIC: %v", r))
+               }
+       }()
+
        var err error
        if s.h.json {
                err = appendJSONValue(s, v)
@@ -514,41 +587,19 @@ func (s *handleState) appendTime(t time.Time) {
        if s.h.json {
                appendJSONTime(s, t)
        } else {
-               writeTimeRFC3339Millis(s.buf, t)
+               *s.buf = appendRFC3339Millis(*s.buf, t)
        }
 }
 
-// This takes half the time of Time.AppendFormat.
-func writeTimeRFC3339Millis(buf *buffer.Buffer, t time.Time) {
-       year, month, day := t.Date()
-       buf.WritePosIntWidth(year, 4)
-       buf.WriteByte('-')
-       buf.WritePosIntWidth(int(month), 2)
-       buf.WriteByte('-')
-       buf.WritePosIntWidth(day, 2)
-       buf.WriteByte('T')
-       hour, min, sec := t.Clock()
-       buf.WritePosIntWidth(hour, 2)
-       buf.WriteByte(':')
-       buf.WritePosIntWidth(min, 2)
-       buf.WriteByte(':')
-       buf.WritePosIntWidth(sec, 2)
-       ns := t.Nanosecond()
-       buf.WriteByte('.')
-       buf.WritePosIntWidth(ns/1e6, 3)
-       _, offsetSeconds := t.Zone()
-       if offsetSeconds == 0 {
-               buf.WriteByte('Z')
-       } else {
-               offsetMinutes := offsetSeconds / 60
-               if offsetMinutes < 0 {
-                       buf.WriteByte('-')
-                       offsetMinutes = -offsetMinutes
-               } else {
-                       buf.WriteByte('+')
-               }
-               buf.WritePosIntWidth(offsetMinutes/60, 2)
-               buf.WriteByte(':')
-               buf.WritePosIntWidth(offsetMinutes%60, 2)
-       }
+func appendRFC3339Millis(b []byte, t time.Time) []byte {
+       // Format according to time.RFC3339Nano since it is highly optimized,
+       // but truncate it to use millisecond resolution.
+       // Unfortunately, that format trims trailing 0s, so add 1/10 millisecond
+       // to guarantee that there are exactly 4 digits after the period.
+       const prefixLen = len("2006-01-02T15:04:05.000")
+       n := len(b)
+       t = t.Truncate(time.Millisecond).Add(time.Millisecond / 10)
+       b = t.AppendFormat(b, time.RFC3339Nano)
+       b = append(b[:n+prefixLen], b[n+prefixLen+1:]...) // drop the 4th digit
+       return b
 }