blob: 87552862290c56ea85d4f1e8244f520fb5acfad1 [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
64
65type klogParser struct {
66 n *node
67 buffer *logbuffer.LineBuffer
68}
69
70func (k *klogParser) Write(p []byte) (n int, err error) {
71 return k.buffer.Write(p)
72}
73
74// Close must be called exactly once after the parser is done being used. It will
75// pipe any leftover data in its write buffer as one last line to parse.
76func (k *klogParser) Close() error {
77 return k.buffer.Close()
78}
79
80// consumeLine is called by the internal LineBuffer any time a new line is fully
81// written.
82func (k *klogParser) consumeLine(l *logbuffer.Line) {
83 p := parse(time.Now(), l.Data)
84 if p == nil {
85 // We could instead emit that line as a raw log - however, this would lead to
86 // interleaving raw logging and leveled logging.
87 k.n.Errorf("Invalid klog line: %s", l.Data)
88 }
89 // TODO(q3k): should this be exposed as an API on LeveledLogger? How much should
90 // we permit library users to 'fake' logs? This would also permit us to get rid
91 // of the type assertion in KLogParser().
92 e := &entry{
93 origin: k.n.dn,
94 leveled: p,
95 }
96 k.n.tree.journal.append(e)
97 k.n.tree.journal.notify(e)
98}
99
100var (
101 // reKLog matches and parses klog/glog-formatted log lines.
102 // Format: I0312 14:20:04.240540 204 shared_informer.go:247] Caches are synced for attach detach
103 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.
108func parse(now time.Time, s string) (*LeveledPayload) {
109 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
187 // Finally we have extracted all the data from the line. Inject into the log publisher.
188 return &LeveledPayload{
189 timestamp: ts,
190 severity: severity,
191 messages: []string{message},
192 file: file,
193 line: line,
194 }
195}
196
197// parseKLogTime parses a klog date and time (eg. "0314", "12:13:14.12345") into
198// a time.Time happening at a given year.
199func parseKLogTime(year int, d, t string) (time.Time, error) {
200 var layout string
201 if strings.Contains(t, ".") {
202 layout = "2006 0102 15:04:05.000000"
203 } else {
204 layout = "2006 0102 15:04:05"
205 }
206 // Make up a string that contains the current year. This permits us to parse
207 // fully into an actual timestamp.
208 // TODO(q3k): add a timezone? This currently behaves as UTC, which is probably
209 // what we want, but we should formalize this.
210 return time.Parse(layout, fmt.Sprintf("%d %s %s", year, d, t))
211}