formatting.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. // Copyright (C) 2025 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package slogutil
  7. import (
  8. "cmp"
  9. "context"
  10. "io"
  11. "log/slog"
  12. "path"
  13. "runtime"
  14. "strconv"
  15. "strings"
  16. "time"
  17. )
  18. type formattingHandler struct {
  19. attrs []slog.Attr
  20. groups []string
  21. out io.Writer
  22. recs []*lineRecorder
  23. timeOverride time.Time
  24. }
  25. var _ slog.Handler = (*formattingHandler)(nil)
  26. func (h *formattingHandler) Enabled(context.Context, slog.Level) bool {
  27. return true
  28. }
  29. func (h *formattingHandler) Handle(_ context.Context, rec slog.Record) error {
  30. fr := runtime.CallersFrames([]uintptr{rec.PC})
  31. var logAttrs []any
  32. if fram, _ := fr.Next(); fram.Function != "" {
  33. pkgName, typeName := funcNameToPkg(fram.Function)
  34. lvl := globalLevels.Get(pkgName)
  35. if lvl > rec.Level {
  36. // Logging not enabled at the record's level
  37. return nil
  38. }
  39. logAttrs = append(logAttrs, slog.String("pkg", pkgName))
  40. if lvl <= slog.LevelDebug {
  41. // We are debugging, add additional source line data
  42. if typeName != "" {
  43. logAttrs = append(logAttrs, slog.String("type", typeName))
  44. }
  45. logAttrs = append(logAttrs, slog.Group("src", slog.String("file", path.Base(fram.File)), slog.Int("line", fram.Line)))
  46. }
  47. }
  48. var prefix string
  49. if len(h.groups) > 0 {
  50. prefix = strings.Join(h.groups, ".") + "."
  51. }
  52. // Build the message string.
  53. var sb strings.Builder
  54. sb.WriteString(rec.Message)
  55. // Collect all the attributes, adding the handler prefix.
  56. attrs := make([]slog.Attr, 0, rec.NumAttrs()+len(h.attrs)+1)
  57. rec.Attrs(func(attr slog.Attr) bool {
  58. attr.Key = prefix + attr.Key
  59. attrs = append(attrs, attr)
  60. return true
  61. })
  62. attrs = append(attrs, h.attrs...)
  63. attrs = append(attrs, slog.Group("log", logAttrs...))
  64. // Expand and format attributes
  65. var attrCount int
  66. for _, attr := range attrs {
  67. for _, attr := range expandAttrs("", attr) {
  68. appendAttr(&sb, "", attr, &attrCount)
  69. }
  70. }
  71. if attrCount > 0 {
  72. sb.WriteRune(')')
  73. }
  74. line := Line{
  75. When: cmp.Or(h.timeOverride, rec.Time),
  76. Message: sb.String(),
  77. Level: rec.Level,
  78. }
  79. // If there is a recorder, record the line.
  80. for _, rec := range h.recs {
  81. rec.record(line)
  82. }
  83. // If there's an output, print the line.
  84. if h.out != nil {
  85. _, _ = line.WriteTo(h.out)
  86. }
  87. return nil
  88. }
  89. func expandAttrs(prefix string, a slog.Attr) []slog.Attr {
  90. if prefix != "" {
  91. a.Key = prefix + "." + a.Key
  92. }
  93. val := a.Value.Resolve()
  94. if val.Kind() != slog.KindGroup {
  95. return []slog.Attr{a}
  96. }
  97. var attrs []slog.Attr
  98. for _, attr := range val.Group() {
  99. attrs = append(attrs, expandAttrs(a.Key, attr)...)
  100. }
  101. return attrs
  102. }
  103. func appendAttr(sb *strings.Builder, prefix string, a slog.Attr, attrCount *int) {
  104. if a.Key == "" {
  105. return
  106. }
  107. sb.WriteRune(' ')
  108. if *attrCount == 0 {
  109. sb.WriteRune('(')
  110. }
  111. sb.WriteString(prefix)
  112. sb.WriteString(a.Key)
  113. sb.WriteRune('=')
  114. v := a.Value.Resolve().String()
  115. if strings.ContainsAny(v, ` "()`) {
  116. v = strconv.Quote(v)
  117. }
  118. sb.WriteString(v)
  119. *attrCount++
  120. }
  121. func (h *formattingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
  122. if len(h.groups) > 0 {
  123. prefix := strings.Join(h.groups, ".") + "."
  124. for i := range attrs {
  125. attrs[i].Key = prefix + attrs[i].Key
  126. }
  127. }
  128. return &formattingHandler{
  129. attrs: append(h.attrs, attrs...),
  130. groups: h.groups,
  131. recs: h.recs,
  132. out: h.out,
  133. timeOverride: h.timeOverride,
  134. }
  135. }
  136. func (h *formattingHandler) WithGroup(name string) slog.Handler {
  137. if name == "" {
  138. return h
  139. }
  140. return &formattingHandler{
  141. attrs: h.attrs,
  142. groups: append([]string{name}, h.groups...),
  143. recs: h.recs,
  144. out: h.out,
  145. timeOverride: h.timeOverride,
  146. }
  147. }
  148. func funcNameToPkg(fn string) (string, string) {
  149. fn = strings.ToLower(fn)
  150. fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/lib/")
  151. fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/internal/")
  152. pkgTypFn := strings.Split(fn, ".") // [package, type, method] or [package, function]
  153. if len(pkgTypFn) <= 2 {
  154. return pkgTypFn[0], ""
  155. }
  156. pkg := pkgTypFn[0]
  157. // Remove parenthesis and asterisk from the type name
  158. typ := strings.TrimLeft(strings.TrimRight(pkgTypFn[1], ")"), "(*")
  159. // Skip certain type names that add no value
  160. typ = strings.TrimSuffix(typ, "service")
  161. switch typ {
  162. case pkg, "", "serveparams":
  163. return pkg, ""
  164. default:
  165. return pkg, typ
  166. }
  167. }