123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- // Copyright (C) 2025 The Syncthing Authors.
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this file,
- // You can obtain one at https://mozilla.org/MPL/2.0/.
- package slogutil
- import (
- "cmp"
- "context"
- "io"
- "log/slog"
- "path"
- "runtime"
- "strconv"
- "strings"
- "time"
- )
- type LineFormat struct {
- TimestampFormat string
- LevelString bool
- LevelSyslog bool
- }
- type formattingOptions struct {
- LineFormat
- out io.Writer
- recs []*lineRecorder
- timeOverride time.Time
- }
- type formattingHandler struct {
- attrs []slog.Attr
- groups []string
- opts *formattingOptions
- }
- func SetLineFormat(f LineFormat) {
- globalFormatter.LineFormat = f
- }
- var _ slog.Handler = (*formattingHandler)(nil)
- func (h *formattingHandler) Enabled(context.Context, slog.Level) bool {
- return true
- }
- func (h *formattingHandler) Handle(_ context.Context, rec slog.Record) error {
- fr := runtime.CallersFrames([]uintptr{rec.PC})
- var logAttrs []any
- if fram, _ := fr.Next(); fram.Function != "" {
- pkgName, typeName := funcNameToPkg(fram.Function)
- lvl := globalLevels.Get(pkgName)
- if lvl > rec.Level {
- // Logging not enabled at the record's level
- return nil
- }
- logAttrs = append(logAttrs, slog.String("pkg", pkgName))
- if lvl <= slog.LevelDebug {
- // We are debugging, add additional source line data
- if typeName != "" {
- logAttrs = append(logAttrs, slog.String("type", typeName))
- }
- logAttrs = append(logAttrs, slog.Group("src", slog.String("file", path.Base(fram.File)), slog.Int("line", fram.Line)))
- }
- }
- var prefix string
- if len(h.groups) > 0 {
- prefix = strings.Join(h.groups, ".") + "."
- }
- // Build the message string.
- var sb strings.Builder
- sb.WriteString(rec.Message)
- // Collect all the attributes, adding the handler prefix.
- attrs := make([]slog.Attr, 0, rec.NumAttrs()+len(h.attrs)+1)
- rec.Attrs(func(attr slog.Attr) bool {
- attr.Key = prefix + attr.Key
- attrs = append(attrs, attr)
- return true
- })
- attrs = append(attrs, h.attrs...)
- attrs = append(attrs, slog.Group("log", logAttrs...))
- // Expand and format attributes
- var attrCount int
- for _, attr := range attrs {
- for _, attr := range expandAttrs("", attr) {
- appendAttr(&sb, "", attr, &attrCount)
- }
- }
- if attrCount > 0 {
- sb.WriteRune(')')
- }
- line := Line{
- When: cmp.Or(h.opts.timeOverride, rec.Time),
- Message: sb.String(),
- Level: rec.Level,
- }
- // If there is a recorder, record the line.
- for _, rec := range h.opts.recs {
- rec.record(line)
- }
- // If there's an output, print the line.
- if h.opts.out != nil {
- _, _ = line.WriteTo(h.opts.out, h.opts.LineFormat)
- }
- return nil
- }
- func expandAttrs(prefix string, a slog.Attr) []slog.Attr {
- if prefix != "" {
- a.Key = prefix + "." + a.Key
- }
- val := a.Value.Resolve()
- if val.Kind() != slog.KindGroup {
- return []slog.Attr{a}
- }
- var attrs []slog.Attr
- for _, attr := range val.Group() {
- attrs = append(attrs, expandAttrs(a.Key, attr)...)
- }
- return attrs
- }
- func appendAttr(sb *strings.Builder, prefix string, a slog.Attr, attrCount *int) {
- const confusables = ` "()[]{},`
- if a.Key == "" {
- return
- }
- sb.WriteRune(' ')
- if *attrCount == 0 {
- sb.WriteRune('(')
- }
- sb.WriteString(prefix)
- sb.WriteString(a.Key)
- sb.WriteRune('=')
- v := a.Value.Resolve().String()
- if v == "" || strings.ContainsAny(v, confusables) {
- v = strconv.Quote(v)
- }
- sb.WriteString(v)
- *attrCount++
- }
- func (h *formattingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
- if len(h.groups) > 0 {
- prefix := strings.Join(h.groups, ".") + "."
- for i := range attrs {
- attrs[i].Key = prefix + attrs[i].Key
- }
- }
- return &formattingHandler{
- attrs: append(h.attrs, attrs...),
- groups: h.groups,
- opts: h.opts,
- }
- }
- func (h *formattingHandler) WithGroup(name string) slog.Handler {
- if name == "" {
- return h
- }
- return &formattingHandler{
- attrs: h.attrs,
- groups: append([]string{name}, h.groups...),
- opts: h.opts,
- }
- }
- func funcNameToPkg(fn string) (string, string) {
- fn = strings.ToLower(fn)
- fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/lib/")
- fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/internal/")
- pkgTypFn := strings.Split(fn, ".") // [package, type, method] or [package, function]
- if len(pkgTypFn) <= 2 {
- return pkgTypFn[0], ""
- }
- pkg := pkgTypFn[0]
- // Remove parenthesis and asterisk from the type name
- typ := strings.TrimLeft(strings.TrimRight(pkgTypFn[1], ")"), "(*")
- // Skip certain type names that add no value
- typ = strings.TrimSuffix(typ, "service")
- switch typ {
- case pkg, "", "serveparams":
- return pkg, ""
- default:
- return pkg, typ
- }
- }
|