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