metropolis/pkg/logtree: allow logging external leveled payloads

This is in preparation for making the mechanism to ingest external
logging more generic (currently we have an ad-hoc solution for klog, but
we now also want to implement one for etcd).

Change-Id: I6e6f656e5d83ad22d67a81fbeb87c8d369796e18
Reviewed-on: https://review.monogon.dev/c/monogon/+/207
Reviewed-by: Leopold Schabel <leo@nexantic.com>
diff --git a/metropolis/pkg/logtree/leveled_payload.go b/metropolis/pkg/logtree/leveled_payload.go
index ed3ed7e..a9ba56d 100644
--- a/metropolis/pkg/logtree/leveled_payload.go
+++ b/metropolis/pkg/logtree/leveled_payload.go
@@ -143,3 +143,55 @@
 		line:      line,
 	}, nil
 }
+
+// ExternalLeveledPayload is a LeveledPayload received from an external source,
+// eg. from parsing the logging output of third-party programs. It can be
+// converted into a LeveledPayload and inserted into a leveled logger, but will
+// be sanitized before that, ensuring that potentially buggy
+// emitters/converters do not end up polluting the leveled logger data.
+//
+// This type should be used only when inserting data from external systems, not
+// by code that just wishes to log things. In the future, data inserted this
+// way might be explicitly marked as tainted so operators can understand that
+// parts of this data might not give the same guarantees as the log entries
+// emitted by the native LeveledLogger API.
+type ExternalLeveledPayload struct {
+	// Log line. If any newlines are found, they will split the message into
+	// multiple messages within LeveledPayload. Empty messages are accepted
+	// verbatim.
+	Message string
+	// Timestamp when this payload was emitted according to its source. If not
+	// given, will default to the time of conversion to LeveledPayload.
+	Timestamp time.Time
+	// Log severity. If invalid or unset will default to INFO.
+	Severity Severity
+	// File name of originating code. Defaults to "unknown" if not set.
+	File string
+	// Line in File. Zero indicates the line is not known.
+	Line int
+}
+
+// sanitize the given ExternalLeveledPayload by creating a corresponding
+// LeveledPayload. The original object is unaltered.
+func (e *ExternalLeveledPayload) sanitize() *LeveledPayload {
+	l := &LeveledPayload{
+		messages:  strings.Split(e.Message, "\n"),
+		timestamp: e.Timestamp,
+		severity:  e.Severity,
+		file:      e.File,
+		line:      e.Line,
+	}
+	if l.timestamp.IsZero() {
+		l.timestamp = time.Now()
+	}
+	if !l.severity.Valid() {
+		l.severity = INFO
+	}
+	if l.file == "" {
+		l.file = "unknown"
+	}
+	if l.line < 0 {
+		l.line = 0
+	}
+	return l
+}