]> git.giacomolli.uk - onechat/blob - daemon_log.go
Format short daemon events with slog
[onechat] / daemon_log.go
1 package main
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "strings"
9 "sync"
10 )
11
12 // DaemonLogHandler writes short event lines with ordered attributes. Copies
13 // share the writer lock. The err attribute follows all other attributes as a
14 // cause. Journal lines carry a syslog priority instead of a time and level.
15 type DaemonLogHandler struct {
16 out io.Writer
17 prefix string
18 journal bool
19 mutex *sync.Mutex
20 attrs []slog.Attr
21 group string
22 }
23
24 func (h *DaemonLogHandler) Enabled(_ context.Context, _ slog.Level) bool {
25 return true
26 }
27
28 func (h *DaemonLogHandler) Handle(_ context.Context, record slog.Record) error {
29 var line strings.Builder
30
31 if h.journal {
32 priority := 7
33 switch {
34 case record.Level >= slog.LevelError:
35 priority = 3
36 case record.Level >= slog.LevelWarn:
37 priority = 4
38 case record.Level >= slog.LevelInfo:
39 priority = 6
40 }
41
42 fmt.Fprintf(&line, "<%d>", priority)
43 } else {
44 line.WriteString(record.Time.Format("2006/01/02 15:04:05"))
45 line.WriteByte(' ')
46 line.WriteString(record.Level.String())
47 line.WriteByte(' ')
48 }
49
50 line.WriteString(h.prefix)
51 line.WriteString(record.Message)
52
53 var cause string
54
55 for _, attr := range h.attrs {
56 daemonLogAttr(&line, "", attr, &cause)
57 }
58
59 record.Attrs(func(attr slog.Attr) bool {
60 daemonLogAttr(&line, h.group, attr, &cause)
61
62 return true
63 })
64
65 if cause != "" {
66 line.WriteString(": ")
67 line.WriteString(cause)
68 }
69
70 line.WriteByte('\n')
71 h.mutex.Lock()
72 defer h.mutex.Unlock()
73
74 _, err := io.WriteString(h.out, line.String())
75 if err != nil {
76 return fmt.Errorf("write daemon log: %w", err)
77 }
78
79 return nil
80 }
81
82 func (h *DaemonLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
83 clone := *h
84 clone.attrs = append([]slog.Attr(nil), h.attrs...)
85 for _, attr := range attrs {
86 if h.group != "" {
87 attr = slog.Group(strings.TrimSuffix(h.group, "."), attr)
88 }
89
90 clone.attrs = append(clone.attrs, attr)
91 }
92
93 return &clone
94 }
95
96 func (h *DaemonLogHandler) WithGroup(name string) slog.Handler {
97 clone := *h
98 if name != "" {
99 clone.group += name + "."
100 }
101
102 return &clone
103 }
104
105 // daemonLogAttr resolves and writes one attribute, or keeps its cause for the
106 // end of the line. Groups flatten into dotted keys and empty attributes produce
107 // no text.
108 func daemonLogAttr(line *strings.Builder, group string, attr slog.Attr, cause *string) {
109 attr.Value = attr.Value.Resolve()
110 if attr.Equal(slog.Attr{}) {
111 return
112 }
113
114 if attr.Value.Kind() == slog.KindGroup {
115 if attr.Key != "" {
116 group += attr.Key + "."
117 }
118
119 for _, child := range attr.Value.Group() {
120 daemonLogAttr(line, group, child, cause)
121 }
122
123 return
124 }
125
126 if attr.Key == "err" {
127 if attr.Value.Any() != nil {
128 *cause = fmt.Sprint(attr.Value.Any())
129 }
130
131 return
132 }
133
134 line.WriteByte(' ')
135 line.WriteString(group + attr.Key)
136 line.WriteByte('=')
137
138 if attr.Value.Kind() == slog.KindDuration {
139 line.WriteString(durationText(attr.Value.Duration()))
140 } else {
141 fmt.Fprint(line, attr.Value.Any())
142 }
143 }