go/logging: add backends

These are (for now unused) backends for the common Leveled interface.

Change-Id: Ic45e7ea26ad0143500489b05ac6bb20aa7fdbcfb
Reviewed-on: https://review.monogon.dev/c/monogon/+/3431
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/go/logging/backend.go b/go/logging/backend.go
new file mode 100644
index 0000000..2fe4556
--- /dev/null
+++ b/go/logging/backend.go
@@ -0,0 +1,144 @@
+package logging
+
+import (
+	"fmt"
+	"io"
+	"runtime"
+	"strings"
+	"time"
+)
+
+// FunctionBackend is the simplest Backend (Leveled implementation). It
+// synchronously forwards logged entries to a function.
+type FunctionBackend struct {
+	depth     int
+	fn        func(severity Severity, msg string)
+	verbosity VerbosityLevel
+}
+
+// NewFunctionBackend returns a FunctionBackend (Leveled implementation) which
+// will call the given function on every log entry synchronously.
+func NewFunctionBackend(fn func(severity Severity, msg string)) *FunctionBackend {
+	return &FunctionBackend{
+		fn: fn,
+	}
+}
+
+func (w *FunctionBackend) Info(args ...any) {
+	w.fn(INFO, fmt.Sprint(args...))
+}
+func (w FunctionBackend) Infof(format string, args ...any) {
+	w.fn(INFO, fmt.Sprintf(format, args...))
+}
+func (w *FunctionBackend) Warning(args ...any) {
+	w.fn(WARNING, fmt.Sprint(args...))
+}
+func (w *FunctionBackend) Warningf(format string, args ...any) {
+	w.fn(WARNING, fmt.Sprintf(format, args...))
+}
+func (w *FunctionBackend) Error(args ...any) {
+	w.fn(ERROR, fmt.Sprint(args...))
+}
+func (w *FunctionBackend) Errorf(format string, args ...any) {
+	w.fn(ERROR, fmt.Sprintf(format, args...))
+}
+func (w *FunctionBackend) Fatal(args ...any) {
+	w.fn(FATAL, fmt.Sprint(args...))
+}
+func (w *FunctionBackend) Fatalf(format string, args ...any) {
+	w.fn(FATAL, fmt.Sprintf(format, args...))
+}
+
+type verboseFunctionBackend struct {
+	backend *FunctionBackend
+	enabled bool
+}
+
+func (w *FunctionBackend) V(level VerbosityLevel) VerboseLeveled {
+	return &verboseFunctionBackend{
+		backend: w,
+		enabled: level > w.verbosity,
+	}
+}
+
+func (w *FunctionBackend) WithAddedStackDepth(depth int) Leveled {
+	w2 := *w
+	w.depth += depth
+	return &w2
+}
+
+func (v *verboseFunctionBackend) Enabled() bool {
+	return v.enabled
+}
+
+func (v *verboseFunctionBackend) Info(args ...any) {
+	if !v.enabled {
+		return
+	}
+	v.backend.fn(INFO, fmt.Sprint(args...))
+}
+
+func (v *verboseFunctionBackend) Infof(format string, args ...any) {
+	if !v.enabled {
+		return
+	}
+	v.backend.fn(INFO, fmt.Sprintf(format, args...))
+}
+
+// WriterBackend is a Backend (Leveled implementation) which outputs log entries
+// to a writer using a given Formatter.
+type WriterBackend struct {
+	FunctionBackend
+	// Formatter is used to turn a log entry alongside metadata into a string. By
+	// default, it's set to LeveledFormatterGlog.
+	Formatter       LeveledFormatter
+	out             io.Writer
+	MinimumSeverity Severity
+}
+
+// LeveledFormatter is a function to turn a leveled log entry into a string which
+// can be output to a user.
+type LeveledFormatter func(file string, line int, time time.Time, severity Severity, msg string) string
+
+// LeveledFormatterGlog implements LeveledFormatter in a glog/klog-compatible
+// way.
+func LeveledFormatterGlog(file string, line int, ts time.Time, severity Severity, msg string) string {
+	// TODO(q3k): unify with //osbase/logtree.LeveledPayload.String.
+	_, month, day := ts.Date()
+	hour, minute, second := ts.Clock()
+	nsec := ts.Nanosecond() / 1000
+
+	res := fmt.Sprintf("%s%02d%02d %02d:%02d:%02d.%06d %s:%d] ", severity, month, day, hour, minute, second, nsec, file, line)
+	res += msg
+	return res
+}
+
+// NewWriterBackend constructs a WriterBackend (Leveled implementation) which
+// writes glog/klog-style entries to the given Writer.
+func NewWriterBackend(w io.Writer) *WriterBackend {
+	res := &WriterBackend{
+		Formatter:       LeveledFormatterGlog,
+		out:             w,
+		MinimumSeverity: INFO,
+	}
+	res.FunctionBackend.fn = res.log
+	return res
+}
+
+func (w *WriterBackend) log(severity Severity, msg string) {
+	if !severity.AtLeast(w.MinimumSeverity) {
+		return
+	}
+	_, file, line, ok := runtime.Caller(2 + w.depth)
+	if !ok {
+		file = "???"
+		line = 1
+	} else {
+		slash := strings.LastIndex(file, "/")
+		if slash >= 0 {
+			file = file[slash+1:]
+		}
+	}
+	res := w.Formatter(file, line, time.Now(), severity, msg)
+	w.out.Write([]byte(res + "\n"))
+}