handler.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package logger
  2. import (
  3. "context"
  4. "errors"
  5. "io"
  6. "log/slog"
  7. "os"
  8. "slices"
  9. "sync"
  10. "time"
  11. )
  12. // LevelCritical is a log level for fatal errors
  13. const LevelCritical = slog.LevelError + 8
  14. // Format represents the log format
  15. type Format int
  16. const (
  17. // FormatStructured is a key=value structured format
  18. FormatStructured Format = iota
  19. // FormatPretty is a human-readable format with colorization
  20. FormatPretty
  21. // FormatJSON is a JSON format
  22. FormatJSON
  23. // FormatGCP is a JSON format for Google Cloud Platform
  24. FormatGCP
  25. )
  26. // attrGroup represents a named group of attributes.
  27. //
  28. // Both the group name and the attributes are optional.
  29. // Non-empty name means new nested group.
  30. type attrGroup struct {
  31. name string
  32. attrs []slog.Attr
  33. }
  34. // Hook is an interface that defines a log hook.
  35. type Hook interface {
  36. // Enabled checks if the hook is enabled for the given log level.
  37. Enabled(lvl slog.Level) bool
  38. // Fire is a function that gets called on log events.
  39. //
  40. // The slice provided in the msg parameter contains the formatted log message,
  41. // followed by a newline character.
  42. // It is guaranteed to be available for the duration of the hook call.
  43. // The hook should not modify the contents of the msg slice except for appending.
  44. Fire(time time.Time, lvl slog.Level, msg []byte) error
  45. }
  46. // Handler is an implementation of [slog.Handler] with support for hooks.
  47. type Handler struct {
  48. out io.Writer
  49. config *Config
  50. mu *sync.Mutex // Mutex is shared between all instances
  51. groups []attrGroup
  52. hooks []Hook
  53. }
  54. // NewHandler creates a new [Handler] instance.
  55. func NewHandler(out io.Writer, config *Config) *Handler {
  56. return &Handler{
  57. out: out,
  58. config: config,
  59. mu: new(sync.Mutex),
  60. }
  61. }
  62. // AddHook adds a new hook to the handler.
  63. func (h *Handler) AddHook(hook Hook) {
  64. if hook == nil {
  65. return
  66. }
  67. h.mu.Lock()
  68. defer h.mu.Unlock()
  69. h.hooks = append(h.hooks, hook)
  70. }
  71. // Enabled checks if the given log level is enabled.
  72. func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
  73. if level >= h.config.Level.Level() {
  74. return true
  75. }
  76. for _, hook := range h.hooks {
  77. if hook.Enabled(level) {
  78. return true
  79. }
  80. }
  81. return false
  82. }
  83. // withGroup returns a new handler with the given attribute group added.
  84. func (h *Handler) withGroup(group attrGroup) *Handler {
  85. h2 := *h
  86. h2.groups = append(slices.Clip(h.groups), group)
  87. h2.hooks = slices.Clip(h.hooks)
  88. return &h2
  89. }
  90. // WithAttrs returns a new handler with the given attributes added.
  91. func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
  92. if len(attrs) == 0 {
  93. return h
  94. }
  95. return h.withGroup(attrGroup{
  96. name: "",
  97. attrs: attrs,
  98. })
  99. }
  100. // WithGroup returns a new handler with the given group name added.
  101. func (h *Handler) WithGroup(name string) slog.Handler {
  102. if name == "" {
  103. return h
  104. }
  105. return h.withGroup(attrGroup{
  106. name: name,
  107. attrs: nil,
  108. })
  109. }
  110. // Handle processes a log record.
  111. func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
  112. buf := newBuffer()
  113. defer func() {
  114. buf.free()
  115. }()
  116. h.format(r, buf)
  117. h.mu.Lock()
  118. defer h.mu.Unlock()
  119. var errs []error
  120. // Write log entry to output
  121. _, err := h.out.Write(*buf)
  122. if err != nil {
  123. errs = append(errs, err)
  124. }
  125. // Fire hooks
  126. for _, hook := range h.hooks {
  127. if !hook.Enabled(r.Level) {
  128. continue
  129. }
  130. if err = hook.Fire(r.Time, r.Level, slices.Clip(*buf)); err != nil {
  131. errs = append(errs, err)
  132. }
  133. }
  134. // If writing to output or firing hooks returned errors,
  135. // join them, write to STDERR, and return
  136. if err = h.joinErrors(errs); err != nil {
  137. h.writeError(err)
  138. return err
  139. }
  140. return nil
  141. }
  142. // format formats a log record and writes it to the buffer.
  143. func (h *Handler) format(r slog.Record, buf *buffer) {
  144. groups := h.groups
  145. // If there are no attributes in the record itself,
  146. // remove empty groups from the end
  147. if r.NumAttrs() == 0 {
  148. for len(groups) > 0 && len(groups[len(groups)-1].attrs) == 0 {
  149. groups = groups[:len(groups)-1]
  150. }
  151. }
  152. // Format the log record according to the format specified in options
  153. switch h.config.Format {
  154. case FormatPretty:
  155. newFormatterPretty(groups, buf).format(r)
  156. case FormatJSON:
  157. newFormatterJSON(groups, buf, false).format(r)
  158. case FormatGCP:
  159. newFormatterJSON(groups, buf, true).format(r)
  160. default:
  161. newFormatterStructured(groups, buf).format(r)
  162. }
  163. // Add line break after each log entry
  164. buf.append('\n')
  165. }
  166. func (h *Handler) joinErrors(errs []error) error {
  167. if len(errs) == 0 {
  168. return nil
  169. }
  170. if len(errs) == 1 {
  171. return errs[0]
  172. }
  173. return errors.Join(errs...)
  174. }
  175. // writeError writes a logging error message to STDERR.
  176. func (h *Handler) writeError(err error) {
  177. buf := newBuffer()
  178. defer func() {
  179. buf.free()
  180. }()
  181. r := slog.NewRecord(time.Now(), slog.LevelError, "An error occurred during logging", 0)
  182. r.Add("error", err)
  183. h.format(r, buf)
  184. _, _ = os.Stderr.Write(*buf)
  185. }