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