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.go b/metropolis/pkg/logtree/leveled.go
index a4220f9..9590bc3 100644
--- a/metropolis/pkg/logtree/leveled.go
+++ b/metropolis/pkg/logtree/leveled.go
@@ -120,6 +120,17 @@
 	return false
 }
 
+// Valid returns whether true if this severity is one of the known levels
+// (INFO, WARNING, ERROR or FATAL), false otherwise.
+func (s Severity) Valid() bool {
+	switch s {
+	case INFO, WARNING, ERROR, FATAL:
+		return true
+	default:
+		return false
+	}
+}
+
 func SeverityFromProto(s apb.LeveledLogSeverity) (Severity, error) {
 	switch s {
 	case apb.LeveledLogSeverity_INFO:
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
+}
diff --git a/metropolis/pkg/logtree/logtree_publisher.go b/metropolis/pkg/logtree/logtree_publisher.go
index 6106b19..25dfc5a 100644
--- a/metropolis/pkg/logtree/logtree_publisher.go
+++ b/metropolis/pkg/logtree/logtree_publisher.go
@@ -81,6 +81,25 @@
 	n.tree.journal.notify(e)
 }
 
+// LogExternalLeveled injects a ExternalLeveledPayload into a given
+// LeveledLogger. This should only be used by systems which translate external
+// data sources into leveled logging - see ExternelLeveledPayload for more
+// information.
+func LogExternalLeveled(l LeveledLogger, e *ExternalLeveledPayload) error {
+	n, ok := l.(*node)
+	if !ok {
+		return fmt.Errorf("the given LeveledLogger is not a logtree node")
+	}
+	p := e.sanitize()
+	entry := &entry{
+		origin:  n.dn,
+		leveled: p,
+	}
+	n.tree.journal.append(entry)
+	n.tree.journal.notify(entry)
+	return nil
+}
+
 // log builds a LeveledPayload and entry for a given message, including all related
 // metadata. It will create a new entry append it to the journal, and notify all
 // pertinent subscribers.