formatting.go 4.6 KB

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