treewide: introduce osbase package and move things around

All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.

Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/osbase/logtree/leveled_payload.go b/osbase/logtree/leveled_payload.go
new file mode 100644
index 0000000..95b9d5c
--- /dev/null
+++ b/osbase/logtree/leveled_payload.go
@@ -0,0 +1,200 @@
+// 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"
+	"strconv"
+	"strings"
+	"time"
+
+	tpb "google.golang.org/protobuf/types/known/timestamppb"
+
+	lpb "source.monogon.dev/osbase/logtree/proto"
+)
+
+// LeveledPayload is a log entry for leveled logs (as per leveled.go). It contains
+// the input to these calls (severity and message split into newline-delimited
+// messages) and additional metadata that would be usually seen in a text
+// representation of a leveled log entry.
+type LeveledPayload struct {
+	// messages is the list of messages contained in this payload. This list is built
+	// from splitting up the given message from the user by newline.
+	messages []string
+	// timestamp is the time at which this message was emitted.
+	timestamp time.Time
+	// severity is the leveled Severity at which this message was emitted.
+	severity Severity
+	// file is the filename of the caller that emitted this message.
+	file string
+	// line is the line number within the file of the caller that emitted this message.
+	line int
+}
+
+// String returns a canonical representation of this payload as a single string
+// prefixed with metadata. If the original message was logged with newlines, this
+// representation will also contain newlines, with each original message part
+// prefixed by the metadata. For an alternative call that will instead return a
+// canonical prefix and a list of lines in the message, see Strings().
+func (p *LeveledPayload) String() string {
+	prefix, lines := p.Strings()
+	res := make([]string, len(p.messages))
+	for i, line := range lines {
+		res[i] = fmt.Sprintf("%s%s", prefix, line)
+	}
+	return strings.Join(res, "\n")
+}
+
+// Strings returns the canonical representation of this payload split into a
+// prefix and all lines that were contained in the original message. This is
+// meant to be displayed to the user by showing the prefix before each line,
+// concatenated together - possibly in a table form with the prefixes all
+// unified with a rowspan- like mechanism.
+//
+// For example, this function can return:
+//
+//	prefix = "I1102 17:20:06.921395 foo.go:42] "
+//	lines = []string{"current tags:", " - one", " - two"}
+//
+// With this data, the result should be presented to users this way in text form:
+// I1102 17:20:06.921395 foo.go:42] current tags:
+// I1102 17:20:06.921395 foo.go:42]  - one
+// I1102 17:20:06.921395 foo.go:42]  - two
+//
+// Or, in a table layout:
+// .-----------------------------------------------------------.
+// | I1102 17:20:06.921395     0 foo.go:42] : current tags:    |
+// |                                        :------------------|
+// |                                        :  - one           |
+// |                                        :------------------|
+// |                                        :  - two           |
+// '-----------------------------------------------------------'
+func (p *LeveledPayload) Strings() (prefix string, lines []string) {
+	_, month, day := p.timestamp.Date()
+	hour, minute, second := p.timestamp.Clock()
+	nsec := p.timestamp.Nanosecond() / 1000
+
+	// Same format as in glog, but without treadid.
+	// Lmmdd hh:mm:ss.uuuuuu file:line]
+	// TODO(q3k): rewrite this to printf-less code.
+	prefix = fmt.Sprintf("%s%02d%02d %02d:%02d:%02d.%06d %s:%d] ", p.severity, month, day, hour, minute, second, nsec, p.file, p.line)
+
+	lines = p.messages
+	return
+}
+
+// Messages returns the inner message lines of this entry, ie. what was passed
+// to the actual logging method, but split by newlines.
+func (p *LeveledPayload) Messages() []string { return p.messages }
+
+func (p *LeveledPayload) MessagesJoined() string { return strings.Join(p.messages, "\n") }
+
+// Timestamp returns the time at which this entry was logged.
+func (p *LeveledPayload) Timestamp() time.Time { return p.timestamp }
+
+// Location returns a string in the form of file_name:line_number that shows the
+// origin of the log entry in the program source.
+func (p *LeveledPayload) Location() string { return fmt.Sprintf("%s:%d", p.file, p.line) }
+
+// Severity returns the Severity with which this entry was logged.
+func (p *LeveledPayload) Severity() Severity { return p.severity }
+
+// Proto converts a LeveledPayload to protobuf format.
+func (p *LeveledPayload) Proto() *lpb.LogEntry_Leveled {
+	return &lpb.LogEntry_Leveled{
+		Lines:     p.Messages(),
+		Timestamp: tpb.New(p.Timestamp()),
+		Severity:  p.Severity().ToProto(),
+		Location:  p.Location(),
+	}
+}
+
+// LeveledPayloadFromProto parses a protobuf message into the internal format.
+func LeveledPayloadFromProto(p *lpb.LogEntry_Leveled) (*LeveledPayload, error) {
+	severity, err := SeverityFromProto(p.Severity)
+	if err != nil {
+		return nil, fmt.Errorf("could not convert severity: %w", err)
+	}
+	parts := strings.Split(p.Location, ":")
+	if len(parts) != 2 {
+		return nil, fmt.Errorf("invalid location, must be two :-delimited parts, is %d parts", len(parts))
+	}
+	file := parts[0]
+	line, err := strconv.Atoi(parts[1])
+	if err != nil {
+		return nil, fmt.Errorf("invalid location line number: %w", err)
+	}
+	return &LeveledPayload{
+		messages:  p.Lines,
+		timestamp: p.Timestamp.AsTime(),
+		severity:  severity,
+		file:      file,
+		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
+}