blob: 6dd60e73189421fed3067bd503415d65cd148bfb [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
Tim Windelschmidtcfbc9032025-07-15 14:18:45 +020014 "k8s.io/klog/v2"
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 Bazanski0ab4eda2021-03-12 17:43:57 +010018)
19
20// KLogParser returns an io.WriteCloser to which raw logging from a klog emitter
21// can be piped. It will attempt to parse all lines from this log as
22// glog/klog-style entries, and pass them over to a LeveledLogger as if they were
23// emitted locally.
24//
25// This allows for piping in external processes that emit klog logging into a
26// logtree, leading to niceties like transparently exposing the log severity or
27// source file/line.
28//
29// One caveat, however, is that V-leveled logs will not be translated
30// appropriately - anything that the klog-emitter pumps out as Info will be
31// directly ingested as Info logging. There is no way to work around this.
32//
33// Another important limitation is that any written line is interpreted as having
34// happened recently (ie. within one hour of the time of execution of this
35// function). This is important as klog/glog-formatted loglines don't have a year
36// attached, so we have to infer it based on the current timestamp (note: parsed
37// lines do not necessarily have their year aleays equal to the current year, as
38// the code handles the edge case of parsing a line from the end of a previous
39// year at the beginning of the next).
Serge Bazanski3c5d0632024-09-12 10:49:12 +000040func KLogParser(logger logging.Leveled) io.WriteCloser {
Serge Bazanskid9775a62022-02-15 13:28:55 +010041 p, ok := logger.(*leveledPublisher)
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010042 if !ok {
43 // Fail fast, as this is a programming error.
Serge Bazanskid9775a62022-02-15 13:28:55 +010044 panic("Expected *leveledPublisher in LeveledLogger from supervisor")
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010045 }
46
47 k := &klogParser{
Serge Bazanskid9775a62022-02-15 13:28:55 +010048 publisher: p,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010049 }
50 // klog seems to have no line length limit. Let's assume some sane sort of default.
51 k.buffer = logbuffer.NewLineBuffer(1024, k.consumeLine)
52 return k
53}
54
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010055type klogParser struct {
Serge Bazanskid9775a62022-02-15 13:28:55 +010056 publisher *leveledPublisher
57 buffer *logbuffer.LineBuffer
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010058}
59
60func (k *klogParser) Write(p []byte) (n int, err error) {
61 return k.buffer.Write(p)
62}
63
64// Close must be called exactly once after the parser is done being used. It will
65// pipe any leftover data in its write buffer as one last line to parse.
66func (k *klogParser) Close() error {
67 return k.buffer.Close()
68}
69
70// consumeLine is called by the internal LineBuffer any time a new line is fully
71// written.
72func (k *klogParser) consumeLine(l *logbuffer.Line) {
73 p := parse(time.Now(), l.Data)
74 if p == nil {
75 // We could instead emit that line as a raw log - however, this would lead to
76 // interleaving raw logging and leveled logging.
Serge Bazanskid9775a62022-02-15 13:28:55 +010077 k.publisher.Errorf("Invalid klog line: %s", l.Data)
Serge Bazanski5456a3c2023-03-16 00:29:17 +010078 return
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010079 }
80 // TODO(q3k): should this be exposed as an API on LeveledLogger? How much should
81 // we permit library users to 'fake' logs? This would also permit us to get rid
82 // of the type assertion in KLogParser().
83 e := &entry{
Serge Bazanskid9775a62022-02-15 13:28:55 +010084 origin: k.publisher.node.dn,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010085 leveled: p,
86 }
Serge Bazanskid9775a62022-02-15 13:28:55 +010087 k.publisher.node.tree.journal.append(e)
88 k.publisher.node.tree.journal.notify(e)
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010089}
90
91var (
Serge Bazanski216fe7b2021-05-21 18:36:16 +020092 // reKLog matches and parses klog/glog-formatted log lines. Format: I0312
93 // 14:20:04.240540 204 shared_informer.go:247] Caches are synced for attach
94 // detach
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010095 reKLog = regexp.MustCompile(`^([IEWF])(\d{4})\s+(\d{2}:\d{2}:\d{2}(\.\d+)?)\s+(\d+)\s+([^:]+):(\d+)]\s+(.+)$`)
96)
97
98// parse attempts to parse a klog-formatted line. Returns nil if the line
99// couldn't have been parsed successfully.
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100func parse(now time.Time, s string) *LeveledPayload {
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100101 parts := reKLog.FindStringSubmatch(s)
102 if parts == nil {
103 return nil
104 }
105
106 severityS := parts[1]
107 date := parts[2]
108 timestamp := parts[3]
109 pid := parts[5]
110 file := parts[6]
111 lineS := parts[7]
112 message := parts[8]
113
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000114 var severity logging.Severity
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100115 switch severityS {
116 case "I":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000117 severity = logging.INFO
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100118 case "W":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000119 severity = logging.WARNING
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100120 case "E":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000121 severity = logging.ERROR
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100122 case "F":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000123 severity = logging.FATAL
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100124 default:
125 return nil
126 }
127
128 // Possible race due to klog's/glog's format not containing a year.
129 // On 2020/12/31 at 23:59:59.99999 a klog logger emits this line:
130 //
131 // I1231 23:59:59.99999 1 example.go:10] It's almost 2021! Hooray.
132 //
133 // Then, if this library parses that line at 2021/01/01 00:00:00.00001, the
134 // time will be interpreted as:
135 //
136 // 2021/12/31 23:59:59
137 //
138 // So around one year in the future. We attempt to fix this case further down in
139 // this function.
140 year := now.Year()
141 ts, err := parseKLogTime(year, date, timestamp)
142 if err != nil {
143 return nil
144 }
145
146 // Attempt to fix the aforementioned year-in-the-future issue.
147 if ts.After(now) && ts.Sub(now) > time.Hour {
148 // Parsed timestamp is in the future. How close is it to One-Year-From-Now?
149 oyfn := now.Add(time.Hour * 24 * 365)
150 dOyfn := ts.Sub(oyfn)
151 // Let's make sure Duration-To-One-Year-From-Now is always positive. This
152 // simplifies the rest of the checks and papers over some possible edge cases.
153 if dOyfn < 0 {
154 dOyfn = -dOyfn
155 }
156
157 // Okay, is that very close? Then the issue above happened and we should
158 // attempt to reparse it with last year. We can't just manipulate the date we
159 // already have, as it's difficult to 'subtract one year'.
160 if dOyfn < (time.Hour * 24 * 2) {
161 ts, err = parseKLogTime(year-1, date, timestamp)
162 if err != nil {
163 return nil
164 }
165 } else {
166 // Otherwise, we received some seriously time traveling log entry. Abort.
167 return nil
168 }
169 }
170
171 line, err := strconv.Atoi(lineS)
172 if err != nil {
173 return nil
174 }
175
176 // The PID is discarded.
177 _ = pid
178
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200179 // Finally we have extracted all the data from the line. Inject into the log
180 // publisher.
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100181 return &LeveledPayload{
182 timestamp: ts,
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200183 severity: severity,
184 messages: []string{message},
185 file: file,
186 line: line,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100187 }
188}
189
190// parseKLogTime parses a klog date and time (eg. "0314", "12:13:14.12345") into
191// a time.Time happening at a given year.
192func parseKLogTime(year int, d, t string) (time.Time, error) {
193 var layout string
194 if strings.Contains(t, ".") {
195 layout = "2006 0102 15:04:05.000000"
196 } else {
197 layout = "2006 0102 15:04:05"
198 }
199 // Make up a string that contains the current year. This permits us to parse
200 // fully into an actual timestamp.
201 // TODO(q3k): add a timezone? This currently behaves as UTC, which is probably
202 // what we want, but we should formalize this.
203 return time.Parse(layout, fmt.Sprintf("%d %s %s", year, d, t))
204}
Tim Windelschmidtcfbc9032025-07-15 14:18:45 +0200205
206// klogSink implements klog.LogSink on a logging.Leveld logger.
207type klogSink struct {
208 logging.Leveled
209}
210
211func (k *klogSink) Enabled(level int) bool {
212 return k.V(logging.VerbosityLevel(level)).Enabled()
213}
214
215func (k *klogSink) Error(err error, msg string, keysAndValues ...any) {
216 if err != nil {
217 k.Errorf("%s: %w - %v", msg, err, keysAndValues)
218 } else {
219 k.Errorf("%s: %v", msg, keysAndValues)
220 }
221}
222
223func (k *klogSink) Info(level int, msg string, keysAndValues ...any) {
224 k.Infof("%s: %v", msg, keysAndValues)
225}
226
227func (k *klogSink) Init(info klog.RuntimeInfo) {
228}
229
230// WithName is only need if we would want to wrap the logger with a new
231// name, but we don't need that.
232func (k *klogSink) WithName(name string) klog.LogSink {
233 k.Warning("logtree.klogSink: WithName() called but not implemented. This is a bug!")
234 return k
235}
236
237// WithValues is only need if we would want to wrap the logger with a list
238// of static keys/values, but we don't need that.
239func (k *klogSink) WithValues(keysAndValues ...any) klog.LogSink {
240 k.Warning("logtree.klogSink: WithValues() called but not implemented. This is a bug!")
241 return k
242}
243
244func NewKlogLogger(logger logging.Leveled) klog.Logger {
245 return klog.New(&klogSink{logger})
246}