]> git.giacomolli.uk - onechat/blob - daemon_log.go
Separate multiline Go statements
[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 // daemonLogger returns a logger for one daemon part. JOURNAL_STREAM selects
108 // syslog priorities without a time stamp. Other output carries the standard
109 // time stamp and level.
110 func daemonLogger(out io.Writer, prefix string) *slog.Logger {
111 return slog.New(&DaemonLogHandler{
112 out: out,
113 prefix: prefix,
114 journal: os.Getenv("JOURNAL_STREAM") != "",
115 mutex: &sync.Mutex{},
116 })
117 }
118
119 // daemonLogAttr resolves and writes one attribute, or keeps its cause for the
120 // end of the line. Groups flatten into dotted keys and empty attributes produce
121 // no text.
122 func daemonLogAttr(line *strings.Builder, group string, attr slog.Attr, cause *string) {
123 attr.Value = attr.Value.Resolve()
124 if attr.Equal(slog.Attr{}) {
125 return
126 }
127
128 if attr.Value.Kind() == slog.KindGroup {
129 if attr.Key != "" {
130 group += attr.Key + "."
131 }
132
133 for _, child := range attr.Value.Group() {
134 daemonLogAttr(line, group, child, cause)
135 }
136
137 return
138 }
139
140 if attr.Key == "err" {
141 if attr.Value.Any() != nil {
142 *cause = fmt.Sprint(attr.Value.Any())
143 }
144
145 return
146 }
147
148 line.WriteByte(' ')
149 line.WriteString(group + attr.Key)
150 line.WriteByte('=')
151
152 if attr.Value.Kind() == slog.KindDuration {
153 line.WriteString(durationText(attr.Value.Duration()))
154 } else {
155 fmt.Fprint(line, attr.Value.Any())
156 }
157 }
158
159 // durationText returns d for a log line: rounded to the second, or to the
160 // millisecond when it is shorter than a second.
161 func durationText(d time.Duration) string {
162 if d < time.Second {
163 return d.Round(time.Millisecond).String()
164 }
165
166 return d.Round(time.Second).String()
167 }