blob: 54e7f6854258d840606c26063da1923588cf87e1 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanski0ab4eda2021-03-12 17:43:57 +01002// SPDX-License-Identifier: Apache-2.0
Serge Bazanski0ab4eda2021-03-12 17:43:57 +01003
4package logtree
5
6import (
7 "fmt"
8 "io"
9 "regexp"
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010010 "strconv"
11 "strings"
12 "time"
Serge Bazanski96043bc2021-10-05 12:10:13 +020013
Serge Bazanski3c5d0632024-09-12 10:49:12 +000014 "source.monogon.dev/go/logging"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020015 "source.monogon.dev/osbase/logbuffer"
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010016)
17
18// KLogParser returns an io.WriteCloser to which raw logging from a klog emitter
19// can be piped. It will attempt to parse all lines from this log as
20// glog/klog-style entries, and pass them over to a LeveledLogger as if they were
21// emitted locally.
22//
23// This allows for piping in external processes that emit klog logging into a
24// logtree, leading to niceties like transparently exposing the log severity or
25// source file/line.
26//
27// One caveat, however, is that V-leveled logs will not be translated
28// appropriately - anything that the klog-emitter pumps out as Info will be
29// directly ingested as Info logging. There is no way to work around this.
30//
31// Another important limitation is that any written line is interpreted as having
32// happened recently (ie. within one hour of the time of execution of this
33// function). This is important as klog/glog-formatted loglines don't have a year
34// attached, so we have to infer it based on the current timestamp (note: parsed
35// lines do not necessarily have their year aleays equal to the current year, as
36// the code handles the edge case of parsing a line from the end of a previous
37// year at the beginning of the next).
Serge Bazanski3c5d0632024-09-12 10:49:12 +000038func KLogParser(logger logging.Leveled) io.WriteCloser {
Serge Bazanskid9775a62022-02-15 13:28:55 +010039 p, ok := logger.(*leveledPublisher)
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010040 if !ok {
41 // Fail fast, as this is a programming error.
Serge Bazanskid9775a62022-02-15 13:28:55 +010042 panic("Expected *leveledPublisher in LeveledLogger from supervisor")
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010043 }
44
45 k := &klogParser{
Serge Bazanskid9775a62022-02-15 13:28:55 +010046 publisher: p,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010047 }
48 // klog seems to have no line length limit. Let's assume some sane sort of default.
49 k.buffer = logbuffer.NewLineBuffer(1024, k.consumeLine)
50 return k
51}
52
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010053type klogParser struct {
Serge Bazanskid9775a62022-02-15 13:28:55 +010054 publisher *leveledPublisher
55 buffer *logbuffer.LineBuffer
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010056}
57
58func (k *klogParser) Write(p []byte) (n int, err error) {
59 return k.buffer.Write(p)
60}
61
62// Close must be called exactly once after the parser is done being used. It will
63// pipe any leftover data in its write buffer as one last line to parse.
64func (k *klogParser) Close() error {
65 return k.buffer.Close()
66}
67
68// consumeLine is called by the internal LineBuffer any time a new line is fully
69// written.
70func (k *klogParser) consumeLine(l *logbuffer.Line) {
71 p := parse(time.Now(), l.Data)
72 if p == nil {
73 // We could instead emit that line as a raw log - however, this would lead to
74 // interleaving raw logging and leveled logging.
Serge Bazanskid9775a62022-02-15 13:28:55 +010075 k.publisher.Errorf("Invalid klog line: %s", l.Data)
Serge Bazanski5456a3c2023-03-16 00:29:17 +010076 return
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010077 }
78 // TODO(q3k): should this be exposed as an API on LeveledLogger? How much should
79 // we permit library users to 'fake' logs? This would also permit us to get rid
80 // of the type assertion in KLogParser().
81 e := &entry{
Serge Bazanskid9775a62022-02-15 13:28:55 +010082 origin: k.publisher.node.dn,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010083 leveled: p,
84 }
Serge Bazanskid9775a62022-02-15 13:28:55 +010085 k.publisher.node.tree.journal.append(e)
86 k.publisher.node.tree.journal.notify(e)
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010087}
88
89var (
Serge Bazanski216fe7b2021-05-21 18:36:16 +020090 // reKLog matches and parses klog/glog-formatted log lines. Format: I0312
91 // 14:20:04.240540 204 shared_informer.go:247] Caches are synced for attach
92 // detach
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010093 reKLog = regexp.MustCompile(`^([IEWF])(\d{4})\s+(\d{2}:\d{2}:\d{2}(\.\d+)?)\s+(\d+)\s+([^:]+):(\d+)]\s+(.+)$`)
94)
95
96// parse attempts to parse a klog-formatted line. Returns nil if the line
97// couldn't have been parsed successfully.
Serge Bazanski216fe7b2021-05-21 18:36:16 +020098func parse(now time.Time, s string) *LeveledPayload {
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010099 parts := reKLog.FindStringSubmatch(s)
100 if parts == nil {
101 return nil
102 }
103
104 severityS := parts[1]
105 date := parts[2]
106 timestamp := parts[3]
107 pid := parts[5]
108 file := parts[6]
109 lineS := parts[7]
110 message := parts[8]
111
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000112 var severity logging.Severity
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100113 switch severityS {
114 case "I":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000115 severity = logging.INFO
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100116 case "W":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000117 severity = logging.WARNING
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100118 case "E":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000119 severity = logging.ERROR
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100120 case "F":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000121 severity = logging.FATAL
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100122 default:
123 return nil
124 }
125
126 // Possible race due to klog's/glog's format not containing a year.
127 // On 2020/12/31 at 23:59:59.99999 a klog logger emits this line:
128 //
129 // I1231 23:59:59.99999 1 example.go:10] It's almost 2021! Hooray.
130 //
131 // Then, if this library parses that line at 2021/01/01 00:00:00.00001, the
132 // time will be interpreted as:
133 //
134 // 2021/12/31 23:59:59
135 //
136 // So around one year in the future. We attempt to fix this case further down in
137 // this function.
138 year := now.Year()
139 ts, err := parseKLogTime(year, date, timestamp)
140 if err != nil {
141 return nil
142 }
143
144 // Attempt to fix the aforementioned year-in-the-future issue.
145 if ts.After(now) && ts.Sub(now) > time.Hour {
146 // Parsed timestamp is in the future. How close is it to One-Year-From-Now?
147 oyfn := now.Add(time.Hour * 24 * 365)
148 dOyfn := ts.Sub(oyfn)
149 // Let's make sure Duration-To-One-Year-From-Now is always positive. This
150 // simplifies the rest of the checks and papers over some possible edge cases.
151 if dOyfn < 0 {
152 dOyfn = -dOyfn
153 }
154
155 // Okay, is that very close? Then the issue above happened and we should
156 // attempt to reparse it with last year. We can't just manipulate the date we
157 // already have, as it's difficult to 'subtract one year'.
158 if dOyfn < (time.Hour * 24 * 2) {
159 ts, err = parseKLogTime(year-1, date, timestamp)
160 if err != nil {
161 return nil
162 }
163 } else {
164 // Otherwise, we received some seriously time traveling log entry. Abort.
165 return nil
166 }
167 }
168
169 line, err := strconv.Atoi(lineS)
170 if err != nil {
171 return nil
172 }
173
174 // The PID is discarded.
175 _ = pid
176
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200177 // Finally we have extracted all the data from the line. Inject into the log
178 // publisher.
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100179 return &LeveledPayload{
180 timestamp: ts,
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200181 severity: severity,
182 messages: []string{message},
183 file: file,
184 line: line,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100185 }
186}
187
188// parseKLogTime parses a klog date and time (eg. "0314", "12:13:14.12345") into
189// a time.Time happening at a given year.
190func parseKLogTime(year int, d, t string) (time.Time, error) {
191 var layout string
192 if strings.Contains(t, ".") {
193 layout = "2006 0102 15:04:05.000000"
194 } else {
195 layout = "2006 0102 15:04:05"
196 }
197 // Make up a string that contains the current year. This permits us to parse
198 // fully into an actual timestamp.
199 // TODO(q3k): add a timezone? This currently behaves as UTC, which is probably
200 // what we want, but we should formalize this.
201 return time.Parse(layout, fmt.Sprintf("%d %s %s", year, d, t))
202}