]> git.giacomolli.uk - onechat/blob - daemon_log.go
Carry new images through model request chains
[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 // daemonLogger returns a logger for one daemon part. JOURNAL_STREAM selects
15 // syslog priorities without a time stamp. Other output carries the standard
16 // time stamp and level.
17 func daemonLogger(out io.Writer, prefix string) *slog.Logger {
18 return slog.New(&DaemonLogHandler{
19 out: out,
20 prefix: prefix,
21 journal: os.Getenv("JOURNAL_STREAM") != "",
22 mutex: &sync.Mutex{},
23 })
24 }
25
26 // DaemonLogHandler writes short event lines with ordered attributes. Copies
27 // share the writer lock. The err attribute follows all other attributes as a
28 // cause. Journal lines carry a syslog priority instead of a time and level.
29 type DaemonLogHandler struct {
30 out io.Writer
31 prefix string
32 journal bool
33 mutex *sync.Mutex
34 attrs []slog.Attr
35 group string
36 }
37
38 func (h *DaemonLogHandler) Enabled(_ context.Context, _ slog.Level) bool {
39 return true
40 }
41
42 func (h *DaemonLogHandler) Handle(_ context.Context, record slog.Record) error {
43 var line strings.Builder
44
45 if h.journal {
46 priority := 7
47 switch {
48 case record.Level >= slog.LevelError:
49 priority = 3
50 case record.Level >= slog.LevelWarn:
51 priority = 4
52 case record.Level >= slog.LevelInfo:
53 priority = 6
54 }
55
56 fmt.Fprintf(&line, "<%d>", priority)
57 } else {
58 line.WriteString(record.Time.Format("2006/01/02 15:04:05"))
59 line.WriteByte(' ')
60 line.WriteString(record.Level.String())
61 line.WriteByte(' ')
62 }
63
64 line.WriteString(h.prefix)
65 line.WriteString(record.Message)
66
67 var cause string
68
69 for _, attr := range h.attrs {
70 daemonLogAttr(&line, "", attr, &cause)
71 }
72
73 record.Attrs(func(attr slog.Attr) bool {
74 daemonLogAttr(&line, h.group, attr, &cause)
75
76 return true
77 })
78
79 if cause != "" {
80 line.WriteString(": ")
81 line.WriteString(cause)
82 }
83
84 line.WriteByte('\n')
85 h.mutex.Lock()
86 defer h.mutex.Unlock()
87
88 _, err := io.WriteString(h.out, line.String())
89 if err != nil {
90 return fmt.Errorf("write daemon log: %w", err)
91 }
92
93 return nil
94 }
95
96 func (h *DaemonLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
97 clone := *h
98 clone.attrs = append([]slog.Attr(nil), h.attrs...)
99 for _, attr := range attrs {
100 if h.group != "" {
101 attr = slog.Group(strings.TrimSuffix(h.group, "."), attr)
102 }
103
104 clone.attrs = append(clone.attrs, attr)
105 }
106
107 return &clone
108 }
109
110 func (h *DaemonLogHandler) WithGroup(name string) slog.Handler {
111 clone := *h
112 if name != "" {
113 clone.group += name + "."
114 }
115
116 return &clone
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 }