blob: 3dd040e088842f48345c41490509417d0261463b [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"
23 "source.monogon.dev/metropolis/pkg/logbuffer"
24 "strconv"
25 "strings"
26 "time"
27)
28
29// KLogParser returns an io.WriteCloser to which raw logging from a klog emitter
30// can be piped. It will attempt to parse all lines from this log as
31// glog/klog-style entries, and pass them over to a LeveledLogger as if they were
32// emitted locally.
33//
34// This allows for piping in external processes that emit klog logging into a
35// logtree, leading to niceties like transparently exposing the log severity or
36// source file/line.
37//
38// One caveat, however, is that V-leveled logs will not be translated
39// appropriately - anything that the klog-emitter pumps out as Info will be
40// directly ingested as Info logging. There is no way to work around this.
41//
42// Another important limitation is that any written line is interpreted as having
43// happened recently (ie. within one hour of the time of execution of this
44// function). This is important as klog/glog-formatted loglines don't have a year
45// attached, so we have to infer it based on the current timestamp (note: parsed
46// lines do not necessarily have their year aleays equal to the current year, as
47// the code handles the edge case of parsing a line from the end of a previous
48// year at the beginning of the next).
49func KLogParser(logger LeveledLogger) io.WriteCloser {
50 n, ok := logger.(*node)
51 if !ok {
52 // Fail fast, as this is a programming error.
53 panic("Expected *node in LeveledLogger from supervisor")
54 }
55
56 k := &klogParser{
57 n: n,
58 }
59 // klog seems to have no line length limit. Let's assume some sane sort of default.
60 k.buffer = logbuffer.NewLineBuffer(1024, k.consumeLine)
61 return k
62}
63
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010064type klogParser struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020065 n *node
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010066 buffer *logbuffer.LineBuffer
67}
68
69func (k *klogParser) Write(p []byte) (n int, err error) {
70 return k.buffer.Write(p)
71}
72
73// Close must be called exactly once after the parser is done being used. It will
74// pipe any leftover data in its write buffer as one last line to parse.
75func (k *klogParser) Close() error {
76 return k.buffer.Close()
77}
78
79// consumeLine is called by the internal LineBuffer any time a new line is fully
80// written.
81func (k *klogParser) consumeLine(l *logbuffer.Line) {
82 p := parse(time.Now(), l.Data)
83 if p == nil {
84 // We could instead emit that line as a raw log - however, this would lead to
85 // interleaving raw logging and leveled logging.
86 k.n.Errorf("Invalid klog line: %s", l.Data)
87 }
88 // TODO(q3k): should this be exposed as an API on LeveledLogger? How much should
89 // we permit library users to 'fake' logs? This would also permit us to get rid
90 // of the type assertion in KLogParser().
91 e := &entry{
Serge Bazanski216fe7b2021-05-21 18:36:16 +020092 origin: k.n.dn,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +010093 leveled: p,
94 }
95 k.n.tree.journal.append(e)
96 k.n.tree.journal.notify(e)
97}
98
99var (
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100 // reKLog matches and parses klog/glog-formatted log lines. Format: I0312
101 // 14:20:04.240540 204 shared_informer.go:247] Caches are synced for attach
102 // detach
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100103 reKLog = regexp.MustCompile(`^([IEWF])(\d{4})\s+(\d{2}:\d{2}:\d{2}(\.\d+)?)\s+(\d+)\s+([^:]+):(\d+)]\s+(.+)$`)
104)
105
106// parse attempts to parse a klog-formatted line. Returns nil if the line
107// couldn't have been parsed successfully.
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200108func parse(now time.Time, s string) *LeveledPayload {
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100109 parts := reKLog.FindStringSubmatch(s)
110 if parts == nil {
111 return nil
112 }
113
114 severityS := parts[1]
115 date := parts[2]
116 timestamp := parts[3]
117 pid := parts[5]
118 file := parts[6]
119 lineS := parts[7]
120 message := parts[8]
121
122 var severity Severity
123 switch severityS {
124 case "I":
125 severity = INFO
126 case "W":
127 severity = WARNING
128 case "E":
129 severity = ERROR
130 case "F":
131 severity = FATAL
132 default:
133 return nil
134 }
135
136 // Possible race due to klog's/glog's format not containing a year.
137 // On 2020/12/31 at 23:59:59.99999 a klog logger emits this line:
138 //
139 // I1231 23:59:59.99999 1 example.go:10] It's almost 2021! Hooray.
140 //
141 // Then, if this library parses that line at 2021/01/01 00:00:00.00001, the
142 // time will be interpreted as:
143 //
144 // 2021/12/31 23:59:59
145 //
146 // So around one year in the future. We attempt to fix this case further down in
147 // this function.
148 year := now.Year()
149 ts, err := parseKLogTime(year, date, timestamp)
150 if err != nil {
151 return nil
152 }
153
154 // Attempt to fix the aforementioned year-in-the-future issue.
155 if ts.After(now) && ts.Sub(now) > time.Hour {
156 // Parsed timestamp is in the future. How close is it to One-Year-From-Now?
157 oyfn := now.Add(time.Hour * 24 * 365)
158 dOyfn := ts.Sub(oyfn)
159 // Let's make sure Duration-To-One-Year-From-Now is always positive. This
160 // simplifies the rest of the checks and papers over some possible edge cases.
161 if dOyfn < 0 {
162 dOyfn = -dOyfn
163 }
164
165 // Okay, is that very close? Then the issue above happened and we should
166 // attempt to reparse it with last year. We can't just manipulate the date we
167 // already have, as it's difficult to 'subtract one year'.
168 if dOyfn < (time.Hour * 24 * 2) {
169 ts, err = parseKLogTime(year-1, date, timestamp)
170 if err != nil {
171 return nil
172 }
173 } else {
174 // Otherwise, we received some seriously time traveling log entry. Abort.
175 return nil
176 }
177 }
178
179 line, err := strconv.Atoi(lineS)
180 if err != nil {
181 return nil
182 }
183
184 // The PID is discarded.
185 _ = pid
186
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200187 // Finally we have extracted all the data from the line. Inject into the log
188 // publisher.
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100189 return &LeveledPayload{
190 timestamp: ts,
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200191 severity: severity,
192 messages: []string{message},
193 file: file,
194 line: line,
Serge Bazanski0ab4eda2021-03-12 17:43:57 +0100195 }
196}
197
198// parseKLogTime parses a klog date and time (eg. "0314", "12:13:14.12345") into
199// a time.Time happening at a given year.
200func parseKLogTime(year int, d, t string) (time.Time, error) {
201 var layout string
202 if strings.Contains(t, ".") {
203 layout = "2006 0102 15:04:05.000000"
204 } else {
205 layout = "2006 0102 15:04:05"
206 }
207 // Make up a string that contains the current year. This permits us to parse
208 // fully into an actual timestamp.
209 // TODO(q3k): add a timezone? This currently behaves as UTC, which is probably
210 // what we want, but we should formalize this.
211 return time.Parse(layout, fmt.Sprintf("%d %s %s", year, d, t))
212}