| // Copyright 2020 The Monogon Project Authors. |
| // |
| // SPDX-License-Identifier: Apache-2.0 |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package logtree |
| |
| import ( |
| "fmt" |
| "io" |
| "regexp" |
| "source.monogon.dev/metropolis/pkg/logbuffer" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| // KLogParser returns an io.WriteCloser to which raw logging from a klog emitter |
| // can be piped. It will attempt to parse all lines from this log as |
| // glog/klog-style entries, and pass them over to a LeveledLogger as if they were |
| // emitted locally. |
| // |
| // This allows for piping in external processes that emit klog logging into a |
| // logtree, leading to niceties like transparently exposing the log severity or |
| // source file/line. |
| // |
| // One caveat, however, is that V-leveled logs will not be translated |
| // appropriately - anything that the klog-emitter pumps out as Info will be |
| // directly ingested as Info logging. There is no way to work around this. |
| // |
| // Another important limitation is that any written line is interpreted as having |
| // happened recently (ie. within one hour of the time of execution of this |
| // function). This is important as klog/glog-formatted loglines don't have a year |
| // attached, so we have to infer it based on the current timestamp (note: parsed |
| // lines do not necessarily have their year aleays equal to the current year, as |
| // the code handles the edge case of parsing a line from the end of a previous |
| // year at the beginning of the next). |
| func KLogParser(logger LeveledLogger) io.WriteCloser { |
| n, ok := logger.(*node) |
| if !ok { |
| // Fail fast, as this is a programming error. |
| panic("Expected *node in LeveledLogger from supervisor") |
| } |
| |
| k := &klogParser{ |
| n: n, |
| } |
| // klog seems to have no line length limit. Let's assume some sane sort of default. |
| k.buffer = logbuffer.NewLineBuffer(1024, k.consumeLine) |
| return k |
| } |
| |
| |
| type klogParser struct { |
| n *node |
| buffer *logbuffer.LineBuffer |
| } |
| |
| func (k *klogParser) Write(p []byte) (n int, err error) { |
| return k.buffer.Write(p) |
| } |
| |
| // Close must be called exactly once after the parser is done being used. It will |
| // pipe any leftover data in its write buffer as one last line to parse. |
| func (k *klogParser) Close() error { |
| return k.buffer.Close() |
| } |
| |
| // consumeLine is called by the internal LineBuffer any time a new line is fully |
| // written. |
| func (k *klogParser) consumeLine(l *logbuffer.Line) { |
| p := parse(time.Now(), l.Data) |
| if p == nil { |
| // We could instead emit that line as a raw log - however, this would lead to |
| // interleaving raw logging and leveled logging. |
| k.n.Errorf("Invalid klog line: %s", l.Data) |
| } |
| // TODO(q3k): should this be exposed as an API on LeveledLogger? How much should |
| // we permit library users to 'fake' logs? This would also permit us to get rid |
| // of the type assertion in KLogParser(). |
| e := &entry{ |
| origin: k.n.dn, |
| leveled: p, |
| } |
| k.n.tree.journal.append(e) |
| k.n.tree.journal.notify(e) |
| } |
| |
| var ( |
| // reKLog matches and parses klog/glog-formatted log lines. |
| // Format: I0312 14:20:04.240540 204 shared_informer.go:247] Caches are synced for attach detach |
| reKLog = regexp.MustCompile(`^([IEWF])(\d{4})\s+(\d{2}:\d{2}:\d{2}(\.\d+)?)\s+(\d+)\s+([^:]+):(\d+)]\s+(.+)$`) |
| ) |
| |
| // parse attempts to parse a klog-formatted line. Returns nil if the line |
| // couldn't have been parsed successfully. |
| func parse(now time.Time, s string) (*LeveledPayload) { |
| parts := reKLog.FindStringSubmatch(s) |
| if parts == nil { |
| return nil |
| } |
| |
| severityS := parts[1] |
| date := parts[2] |
| timestamp := parts[3] |
| pid := parts[5] |
| file := parts[6] |
| lineS := parts[7] |
| message := parts[8] |
| |
| var severity Severity |
| switch severityS { |
| case "I": |
| severity = INFO |
| case "W": |
| severity = WARNING |
| case "E": |
| severity = ERROR |
| case "F": |
| severity = FATAL |
| default: |
| return nil |
| } |
| |
| // Possible race due to klog's/glog's format not containing a year. |
| // On 2020/12/31 at 23:59:59.99999 a klog logger emits this line: |
| // |
| // I1231 23:59:59.99999 1 example.go:10] It's almost 2021! Hooray. |
| // |
| // Then, if this library parses that line at 2021/01/01 00:00:00.00001, the |
| // time will be interpreted as: |
| // |
| // 2021/12/31 23:59:59 |
| // |
| // So around one year in the future. We attempt to fix this case further down in |
| // this function. |
| year := now.Year() |
| ts, err := parseKLogTime(year, date, timestamp) |
| if err != nil { |
| return nil |
| } |
| |
| // Attempt to fix the aforementioned year-in-the-future issue. |
| if ts.After(now) && ts.Sub(now) > time.Hour { |
| // Parsed timestamp is in the future. How close is it to One-Year-From-Now? |
| oyfn := now.Add(time.Hour * 24 * 365) |
| dOyfn := ts.Sub(oyfn) |
| // Let's make sure Duration-To-One-Year-From-Now is always positive. This |
| // simplifies the rest of the checks and papers over some possible edge cases. |
| if dOyfn < 0 { |
| dOyfn = -dOyfn |
| } |
| |
| // Okay, is that very close? Then the issue above happened and we should |
| // attempt to reparse it with last year. We can't just manipulate the date we |
| // already have, as it's difficult to 'subtract one year'. |
| if dOyfn < (time.Hour * 24 * 2) { |
| ts, err = parseKLogTime(year-1, date, timestamp) |
| if err != nil { |
| return nil |
| } |
| } else { |
| // Otherwise, we received some seriously time traveling log entry. Abort. |
| return nil |
| } |
| } |
| |
| line, err := strconv.Atoi(lineS) |
| if err != nil { |
| return nil |
| } |
| |
| // The PID is discarded. |
| _ = pid |
| |
| // Finally we have extracted all the data from the line. Inject into the log publisher. |
| return &LeveledPayload{ |
| timestamp: ts, |
| severity: severity, |
| messages: []string{message}, |
| file: file, |
| line: line, |
| } |
| } |
| |
| // parseKLogTime parses a klog date and time (eg. "0314", "12:13:14.12345") into |
| // a time.Time happening at a given year. |
| func parseKLogTime(year int, d, t string) (time.Time, error) { |
| var layout string |
| if strings.Contains(t, ".") { |
| layout = "2006 0102 15:04:05.000000" |
| } else { |
| layout = "2006 0102 15:04:05" |
| } |
| // Make up a string that contains the current year. This permits us to parse |
| // fully into an actual timestamp. |
| // TODO(q3k): add a timezone? This currently behaves as UTC, which is probably |
| // what we want, but we should formalize this. |
| return time.Parse(layout, fmt.Sprintf("%d %s %s", year, d, t)) |
| } |