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{
21 journal: os.Getenv("JOURNAL_STREAM") != "",
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 {
38 func (h *DaemonLogHandler) Enabled(_ context.Context, _ slog.Level) bool {
42 func (h *DaemonLogHandler) Handle(_ context.Context, record slog.Record) error {
43 var line strings.Builder
48 case record.Level >= slog.LevelError:
50 case record.Level >= slog.LevelWarn:
52 case record.Level >= slog.LevelInfo:
56 fmt.Fprintf(&line, "<%d>", priority)
58 line.WriteString(record.Time.Format("2006/01/02 15:04:05"))
60 line.WriteString(record.Level.String())
64 line.WriteString(h.prefix)
65 line.WriteString(record.Message)
69 for _, attr := range h.attrs {
70 daemonLogAttr(&line, "", attr, &cause)
73 record.Attrs(func(attr slog.Attr) bool {
74 daemonLogAttr(&line, h.group, attr, &cause)
80 line.WriteString(": ")
81 line.WriteString(cause)
86 defer h.mutex.Unlock()
88 _, err := io.WriteString(h.out, line.String())
90 return fmt.Errorf("write daemon log: %w", err)
96 func (h *DaemonLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
98 clone.attrs = append([]slog.Attr(nil), h.attrs...)
99 for _, attr := range attrs {
101 attr = slog.Group(strings.TrimSuffix(h.group, "."), attr)
104 clone.attrs = append(clone.attrs, attr)
110 func (h *DaemonLogHandler) WithGroup(name string) slog.Handler {
113 clone.group += name + "."
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
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{}) {
128 if attr.Value.Kind() == slog.KindGroup {
130 group += attr.Key + "."
133 for _, child := range attr.Value.Group() {
134 daemonLogAttr(line, group, child, cause)
140 if attr.Key == "err" {
141 if attr.Value.Any() != nil {
142 *cause = fmt.Sprint(attr.Value.Any())
149 line.WriteString(group + attr.Key)
152 if attr.Value.Kind() == slog.KindDuration {
153 line.WriteString(durationText(attr.Value.Duration()))
155 fmt.Fprint(line, attr.Value.Any())
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 {
163 return d.Round(time.Millisecond).String()
166 return d.Round(time.Second).String()