blob: f38a092310560ef596f14115a298a953071da966 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +02004package logtree
5
6import (
7 "encoding/json"
8 "fmt"
9 "strconv"
10 "strings"
11 "time"
12
13 "go.uber.org/zap"
14 "go.uber.org/zap/zapcore"
15
Serge Bazanski3c5d0632024-09-12 10:49:12 +000016 "source.monogon.dev/go/logging"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020017 "source.monogon.dev/osbase/logbuffer"
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +020018)
19
20// Zapify turns a LeveledLogger into a zap.Logger which pipes its output into the
21// LeveledLogger. The message, severity and caller are carried over. Extra fields
22// are appended as JSON to the end of the log line.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000023func Zapify(logger logging.Leveled, minimumLevel zapcore.Level) *zap.Logger {
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +020024 p, ok := logger.(*leveledPublisher)
25 if !ok {
26 // Fail fast, as this is a programming error.
27 panic("Expected *leveledPublisher in LeveledLogger from supervisor")
28 }
29
30 ec := zapcore.EncoderConfig{
31 MessageKey: "message",
32 LevelKey: "level",
33 TimeKey: "time",
34 CallerKey: "caller",
35 EncodeLevel: zapcore.LowercaseLevelEncoder,
36 EncodeTime: zapcore.EpochTimeEncoder,
37 EncodeCaller: zapcore.ShortCallerEncoder,
38 }
39 s := zapSink{
40 publisher: p,
41 }
42 s.buffer = logbuffer.NewLineBuffer(4096, s.consumeLine)
43 zc := zapcore.NewCore(zapcore.NewJSONEncoder(ec), s.buffer, minimumLevel)
44 return zap.New(zc, zap.AddCaller())
45}
46
47type zapSink struct {
48 publisher *leveledPublisher
49 buffer *logbuffer.LineBuffer
50}
51
52func (z *zapSink) consumeLine(l *logbuffer.Line) {
53 ze, err := parseZapJSON(l.Data)
54 if err != nil {
55 z.publisher.Warningf("failed to parse zap JSON: %v: %q", err, l.Data)
56 return
57 }
58 message := ze.message
59 if len(ze.extra) > 0 {
60 message += " " + ze.extra
61 }
62 e := &entry{
63 origin: z.publisher.node.dn,
64 leveled: &LeveledPayload{
65 timestamp: ze.time,
66 severity: ze.severity,
67 messages: []string{message},
68 file: ze.file,
69 line: ze.line,
70 },
71 }
72 z.publisher.node.tree.journal.append(e)
73 z.publisher.node.tree.journal.notify(e)
74}
75
76type zapEntry struct {
77 message string
Serge Bazanski3c5d0632024-09-12 10:49:12 +000078 severity logging.Severity
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +020079 time time.Time
80 file string
81 line int
82 extra string
83}
84
85func parseZapJSON(s string) (*zapEntry, error) {
86 entry := make(map[string]any)
87 if err := json.Unmarshal([]byte(s), &entry); err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +020088 return nil, fmt.Errorf("invalid JSON: %w", err)
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +020089 }
90 message, ok := entry["message"].(string)
91 if !ok {
92 return nil, fmt.Errorf("no message field")
93 }
94 level, ok := entry["level"].(string)
95 if !ok {
96 return nil, fmt.Errorf("no level field")
97 }
98 t, ok := entry["time"].(float64)
99 if !ok {
100 return nil, fmt.Errorf("no time field")
101 }
102 caller, ok := entry["caller"].(string)
103 if !ok {
104 return nil, fmt.Errorf("no caller field")
105 }
106
107 callerParts := strings.Split(caller, ":")
108 if len(callerParts) != 2 {
109 return nil, fmt.Errorf("invalid caller")
110 }
111 callerDirFile := strings.Split(callerParts[0], "/")
112 callerFile := callerDirFile[len(callerDirFile)-1]
113 callerLineS := callerParts[1]
114 callerLine, _ := strconv.Atoi(callerLineS)
115
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000116 var severity logging.Severity
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200117 switch level {
118 case "warn":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000119 severity = logging.WARNING
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200120 case "error", "dpanic", "panic", "fatal":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000121 severity = logging.ERROR
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200122 default:
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000123 severity = logging.INFO
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200124 }
125
126 secs := int64(t)
127 nsecs := int64((t - float64(secs)) * 1e9)
128
129 delete(entry, "message")
130 delete(entry, "level")
131 delete(entry, "time")
132 delete(entry, "caller")
Tim Windelschmidtbda384c2024-04-11 01:41:57 +0200133 var extra []byte
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200134 if len(entry) > 0 {
135 extra, _ = json.Marshal(entry)
136 }
137 return &zapEntry{
138 message: message,
139 severity: severity,
140 time: time.Unix(secs, nsecs),
141 file: callerFile,
142 line: callerLine,
143 extra: string(extra),
144 }, nil
145}