blob: dca5a8229d122258fa87ae14290c785926703abb [file] [log] [blame]
Serge Bazanski0ab4eda2021-03-12 17:43:57 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package logtree
18
19import (
20 "fmt"
21 "io"
22 "regexp"
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010023 "strconv"
24 "strings"
25 "time"
Serge Bazanski96043bc2021-10-05 12:10:13 +020026
Serge Bazanski3c5d0632024-09-12 10:49:12 +000027 "source.monogon.dev/go/logging"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020028 "source.monogon.dev/osbase/logbuffer"
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010029)
30
31// KLogParser returns an io.WriteCloser to which raw logging from a klog emitter
32// can be piped. It will attempt to parse all lines from this log as
33// glog/klog-style entries, and pass them over to a LeveledLogger as if they were
34// emitted locally.
35//
36// This allows for piping in external processes that emit klog logging into a
37// logtree, leading to niceties like transparently exposing the log severity or
38// source file/line.
39//
40// One caveat, however, is that V-leveled logs will not be translated
41// appropriately - anything that the klog-emitter pumps out as Info will be
42// directly ingested as Info logging. There is no way to work around this.
43//
44// Another important limitation is that any written line is interpreted as having
45// happened recently (ie. within one hour of the time of execution of this
46// function). This is important as klog/glog-formatted loglines don't have a year
47// attached, so we have to infer it based on the current timestamp (note: parsed
48// lines do not necessarily have their year aleays equal to the current year, as
49// the code handles the edge case of parsing a line from the end of a previous
50// year at the beginning of the next).
Serge Bazanski3c5d0632024-09-12 10:49:12 +000051func KLogParser(logger logging.Leveled) io.WriteCloser {
Serge Bazanskid9775a62022-02-15 13:28:55 +010052 p, ok := logger.(*leveledPublisher)
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010053 if !ok {
54 // Fail fast, as this is a programming error.
Serge Bazanskid9775a62022-02-15 13:28:55 +010055 panic("Expected *leveledPublisher in LeveledLogger from supervisor")
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010056 }
57
58 k := &klogParser{
Serge Bazanskid9775a62022-02-15 13:28:55 +010059 publisher: p,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010060 }
61 // klog seems to have no line length limit. Let's assume some sane sort of default.
62 k.buffer = logbuffer.NewLineBuffer(1024, k.consumeLine)
63 return k
64}
65
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010066type klogParser struct {
Serge Bazanskid9775a62022-02-15 13:28:55 +010067 publisher *leveledPublisher
68 buffer *logbuffer.LineBuffer
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010069}
70
71func (k *klogParser) Write(p []byte) (n int, err error) {
72 return k.buffer.Write(p)
73}
74
75// Close must be called exactly once after the parser is done being used. It will
76// pipe any leftover data in its write buffer as one last line to parse.
77func (k *klogParser) Close() error {
78 return k.buffer.Close()
79}
80
81// consumeLine is called by the internal LineBuffer any time a new line is fully
82// written.
83func (k *klogParser) consumeLine(l *logbuffer.Line) {
84 p := parse(time.Now(), l.Data)
85 if p == nil {
86 // We could instead emit that line as a raw log - however, this would lead to
87 // interleaving raw logging and leveled logging.
Serge Bazanskid9775a62022-02-15 13:28:55 +010088 k.publisher.Errorf("Invalid klog line: %s", l.Data)
Serge Bazanski5456a3c2023-03-16 00:29:17 +010089 return
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010090 }
91 // TODO(q3k): should this be exposed as an API on LeveledLogger? How much should
92 // we permit library users to 'fake' logs? This would also permit us to get rid
93 // of the type assertion in KLogParser().
94 e := &entry{
Serge Bazanskid9775a62022-02-15 13:28:55 +010095 origin: k.publisher.node.dn,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010096 leveled: p,
97 }
Serge Bazanskid9775a62022-02-15 13:28:55 +010098 k.publisher.node.tree.journal.append(e)
99 k.publisher.node.tree.journal.notify(e)
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100100}
101
102var (
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200103 // reKLog matches and parses klog/glog-formatted log lines. Format: I0312
104 // 14:20:04.240540 204 shared_informer.go:247] Caches are synced for attach
105 // detach
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100106 reKLog = regexp.MustCompile(`^([IEWF])(\d{4})\s+(\d{2}:\d{2}:\d{2}(\.\d+)?)\s+(\d+)\s+([^:]+):(\d+)]\s+(.+)$`)
107)
108
109// parse attempts to parse a klog-formatted line. Returns nil if the line
110// couldn't have been parsed successfully.
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200111func parse(now time.Time, s string) *LeveledPayload {
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100112 parts := reKLog.FindStringSubmatch(s)
113 if parts == nil {
114 return nil
115 }
116
117 severityS := parts[1]
118 date := parts[2]
119 timestamp := parts[3]
120 pid := parts[5]
121 file := parts[6]
122 lineS := parts[7]
123 message := parts[8]
124
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000125 var severity logging.Severity
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100126 switch severityS {
127 case "I":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000128 severity = logging.INFO
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100129 case "W":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000130 severity = logging.WARNING
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100131 case "E":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000132 severity = logging.ERROR
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100133 case "F":
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000134 severity = logging.FATAL
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100135 default:
136 return nil
137 }
138
139 // Possible race due to klog's/glog's format not containing a year.
140 // On 2020/12/31 at 23:59:59.99999 a klog logger emits this line:
141 //
142 // I1231 23:59:59.99999 1 example.go:10] It's almost 2021! Hooray.
143 //
144 // Then, if this library parses that line at 2021/01/01 00:00:00.00001, the
145 // time will be interpreted as:
146 //
147 // 2021/12/31 23:59:59
148 //
149 // So around one year in the future. We attempt to fix this case further down in
150 // this function.
151 year := now.Year()
152 ts, err := parseKLogTime(year, date, timestamp)
153 if err != nil {
154 return nil
155 }
156
157 // Attempt to fix the aforementioned year-in-the-future issue.
158 if ts.After(now) && ts.Sub(now) > time.Hour {
159 // Parsed timestamp is in the future. How close is it to One-Year-From-Now?
160 oyfn := now.Add(time.Hour * 24 * 365)
161 dOyfn := ts.Sub(oyfn)
162 // Let's make sure Duration-To-One-Year-From-Now is always positive. This
163 // simplifies the rest of the checks and papers over some possible edge cases.
164 if dOyfn < 0 {
165 dOyfn = -dOyfn
166 }
167
168 // Okay, is that very close? Then the issue above happened and we should
169 // attempt to reparse it with last year. We can't just manipulate the date we
170 // already have, as it's difficult to 'subtract one year'.
171 if dOyfn < (time.Hour * 24 * 2) {
172 ts, err = parseKLogTime(year-1, date, timestamp)
173 if err != nil {
174 return nil
175 }
176 } else {
177 // Otherwise, we received some seriously time traveling log entry. Abort.
178 return nil
179 }
180 }
181
182 line, err := strconv.Atoi(lineS)
183 if err != nil {
184 return nil
185 }
186
187 // The PID is discarded.
188 _ = pid
189
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200190 // Finally we have extracted all the data from the line. Inject into the log
191 // publisher.
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100192 return &LeveledPayload{
193 timestamp: ts,
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200194 severity: severity,
195 messages: []string{message},
196 file: file,
197 line: line,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100198 }
199}
200
201// parseKLogTime parses a klog date and time (eg. "0314", "12:13:14.12345") into
202// a time.Time happening at a given year.
203func parseKLogTime(year int, d, t string) (time.Time, error) {
204 var layout string
205 if strings.Contains(t, ".") {
206 layout = "2006 0102 15:04:05.000000"
207 } else {
208 layout = "2006 0102 15:04:05"
209 }
210 // Make up a string that contains the current year. This permits us to parse
211 // fully into an actual timestamp.
212 // TODO(q3k): add a timezone? This currently behaves as UTC, which is probably
213 // what we want, but we should formalize this.
214 return time.Parse(layout, fmt.Sprintf("%d %s %s", year, d, t))
215}