]> git.giacomolli.uk - onechat/blob - daemon_log.go
Describe child agent directories and limits
[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 }