| // 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)) | 
 | } |