osbase/logtree.LeveledLogger -> go/logging.Leveled

This factors out the common leveled logger interface out of the logtree.
We want to use the same interface outside of logtree/supervisor usage
within the resolver code, which will be exposed to clients.

Change-Id: I299e76d91e8cefddf8f36f1e58432418c4694df2
Reviewed-on: https://review.monogon.dev/c/monogon/+/3411
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/agent/BUILD.bazel b/cloud/agent/BUILD.bazel
index db3ee51..7e7614d 100644
--- a/cloud/agent/BUILD.bazel
+++ b/cloud/agent/BUILD.bazel
@@ -15,13 +15,13 @@
     deps = [
         "//cloud/agent/api",
         "//cloud/bmaas/server/api",
+        "//go/logging",
         "//metropolis/node/core/devmgr",
         "//metropolis/node/core/network",
         "//osbase/blockdev",
         "//osbase/bringup",
         "//osbase/build/mkimage/osimage",
         "//osbase/efivarfs",
-        "//osbase/logtree",
         "//osbase/net/proto",
         "//osbase/nvme",
         "//osbase/pki",
diff --git a/cloud/agent/install.go b/cloud/agent/install.go
index 29641bd..932cff8 100644
--- a/cloud/agent/install.go
+++ b/cloud/agent/install.go
@@ -15,10 +15,10 @@
 	"google.golang.org/protobuf/proto"
 
 	bpb "source.monogon.dev/cloud/bmaas/server/api"
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/blockdev"
 	"source.monogon.dev/osbase/build/mkimage/osimage"
 	"source.monogon.dev/osbase/efivarfs"
-	"source.monogon.dev/osbase/logtree"
 	npb "source.monogon.dev/osbase/net/proto"
 )
 
@@ -42,7 +42,7 @@
 
 // install dispatches OSInstallationRequests to the appropriate installer
 // method
-func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger) error {
+func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logging.Leveled) error {
 	switch reqT := req.Type.(type) {
 	case *bpb.OSInstallationRequest_Metropolis:
 		return installMetropolis(reqT.Metropolis, netConfig, l)
@@ -51,7 +51,7 @@
 	}
 }
 
-func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger) error {
+func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logging.Leveled) error {
 	// Validate we are running via EFI.
 	if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
 		//nolint:ST1005
diff --git a/go/logging/BUILD.bazel b/go/logging/BUILD.bazel
new file mode 100644
index 0000000..4a5feae
--- /dev/null
+++ b/go/logging/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "logging",
+    srcs = ["leveled.go"],
+    importpath = "source.monogon.dev/go/logging",
+    visibility = ["//visibility:public"],
+)
diff --git a/go/logging/leveled.go b/go/logging/leveled.go
new file mode 100644
index 0000000..a32b62d
--- /dev/null
+++ b/go/logging/leveled.go
@@ -0,0 +1,123 @@
+package logging
+
+// Leveled is a generic interface for glog-style logging. There are four
+// hardcoded log severities, in increasing order: INFO, WARNING, ERROR, FATAL.
+// Logging at a certain severity level logs not only to consumers expecting data
+// at that severity level, but also all lower severity levels. For example, an
+// ERROR log will also be passed to consumers looking at INFO or WARNING logs.
+type Leveled interface {
+	// Info logs at the INFO severity. Arguments are handled in the manner of
+	// fmt.Print, a terminating newline is added if missing.
+	Info(args ...any)
+	// Infof logs at the INFO severity. Arguments are handled in the manner of
+	// fmt.Printf, a terminating newline is added if missing.
+	Infof(format string, args ...any)
+
+	// Warning logs at the WARNING severity. Arguments are handled in the manner of
+	// fmt.Print, a terminating newline is added if missing.
+	Warning(args ...any)
+	// Warningf logs at the WARNING severity. Arguments are handled in the manner of
+	// fmt.Printf, a terminating newline is added if missing.
+	Warningf(format string, args ...any)
+
+	// Error logs at the ERROR severity. Arguments are handled in the manner of
+	// fmt.Print, a terminating newline is added if missing.
+	Error(args ...any)
+	// Errorf logs at the ERROR severity. Arguments are handled in the manner of
+	// fmt.Printf, a terminating newline is added if missing.
+	Errorf(format string, args ...any)
+
+	// Fatal logs at the FATAL severity and aborts the current program. Arguments are
+	// handled in the manner of fmt.Print, a terminating newline is added if missing.
+	Fatal(args ...any)
+	// Fatalf logs at the FATAL severity and aborts the current program. Arguments are
+	// handled in the manner of fmt.Printf, a terminating newline is added if missing.
+	Fatalf(format string, args ...any)
+
+	// V returns a VerboseLeveledLogger at a given verbosity level. These verbosity
+	// levels can be dynamically set and unset on a package-granular level by consumers
+	// of the LeveledLogger logs. The returned value represents whether logging at the
+	// given verbosity level was active at that time, and as such should not be a long-
+	// lived object in programs. This construct is further refered to as 'V-logs'.
+	V(level VerbosityLevel) VerboseLeveled
+
+	// WithAddedStackDepth returns the same LeveledLogger, but adjusted with an
+	// additional 'extra stack depth' which will be used to skip a given number of
+	// stack/call frames when determining the location where the error originated.
+	// For example, WithStackDepth(1) will return a logger that will skip one
+	// stack/call frame. Then, with function foo() calling function helper() which
+	// in turns call l.Infof(), the log line will be emitted with the call site of
+	// helper() within foo(), instead of the default behaviour of logging the
+	// call site of Infof() within helper().
+	//
+	// This is useful for functions which somehow wrap loggers in helper functions,
+	// for example to expose a slightly different API.
+	WithAddedStackDepth(depth int) Leveled
+}
+
+// VerbosityLevel is a verbosity level defined for V-logs. This can be changed
+// programmatically per Go package. When logging at a given VerbosityLevel V, the
+// current level must be equal or higher to V for the logs to be recorded.
+// Conversely, enabling a V-logging at a VerbosityLevel V also enables all logging
+// at lower levels [Int32Min .. (V-1)].
+type VerbosityLevel int32
+
+type VerboseLeveled interface {
+	// Enabled returns if this level was enabled. If not enabled, all logging into this
+	// logger will be discarded immediately. Thus, Enabled() can be used to check the
+	// verbosity level before performing any logging:
+	//    if l.V(3).Enabled() { l.Info("V3 is enabled") }
+	// or, in simple cases, the convenience function .Info can be used:
+	//    l.V(3).Info("V3 is enabled")
+	// The second form is shorter and more convenient, but more expensive, as its
+	// arguments are always evaluated.
+	Enabled() bool
+	// Info is the equivalent of a LeveledLogger's Info call, guarded by whether this
+	// VerboseLeveledLogger is enabled.
+	Info(args ...any)
+	// Infof is the equivalent of a LeveledLogger's Infof call, guarded by whether this
+	// VerboseLeveledLogger is enabled.
+	Infof(format string, args ...any)
+}
+
+// Severity is one of the severities as described in LeveledLogger.
+type Severity string
+
+const (
+	INFO    Severity = "I"
+	WARNING Severity = "W"
+	ERROR   Severity = "E"
+	FATAL   Severity = "F"
+)
+
+var (
+	// SeverityAtLeast maps a given severity to a list of severities that at that
+	// severity or higher. In other words, SeverityAtLeast[X] returns a list of
+	// severities that might be seen in a log at severity X.
+	SeverityAtLeast = map[Severity][]Severity{
+		INFO:    {INFO, WARNING, ERROR, FATAL},
+		WARNING: {WARNING, ERROR, FATAL},
+		ERROR:   {ERROR, FATAL},
+		FATAL:   {FATAL},
+	}
+)
+
+func (s Severity) AtLeast(other Severity) bool {
+	for _, el := range SeverityAtLeast[other] {
+		if el == s {
+			return true
+		}
+	}
+	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
+	}
+}
diff --git a/metropolis/node/core/BUILD.bazel b/metropolis/node/core/BUILD.bazel
index b511bb9..667f7f3 100644
--- a/metropolis/node/core/BUILD.bazel
+++ b/metropolis/node/core/BUILD.bazel
@@ -23,6 +23,7 @@
     importpath = "source.monogon.dev/metropolis/node/core",
     visibility = ["//visibility:private"],
     deps = [
+        "//go/logging",
         "//metropolis/node",
         "//metropolis/node/core/cluster",
         "//metropolis/node/core/devmgr",
diff --git a/metropolis/node/core/consensus/BUILD.bazel b/metropolis/node/core/consensus/BUILD.bazel
index b68c7f9..ac048d6 100644
--- a/metropolis/node/core/consensus/BUILD.bazel
+++ b/metropolis/node/core/consensus/BUILD.bazel
@@ -12,6 +12,7 @@
     importpath = "source.monogon.dev/metropolis/node/core/consensus",
     visibility = ["//:__subpackages__"],
     deps = [
+        "//go/logging",
         "//metropolis/node",
         "//metropolis/node/core/consensus/client",
         "//metropolis/node/core/identity",
@@ -44,6 +45,7 @@
         "block-network",
     ],
     deps = [
+        "//go/logging",
         "//metropolis/node/core/localstorage",
         "//metropolis/node/core/localstorage/declarative",
         "//metropolis/test/util",
diff --git a/metropolis/node/core/consensus/logparser.go b/metropolis/node/core/consensus/logparser.go
index b403423..f825431 100644
--- a/metropolis/node/core/consensus/logparser.go
+++ b/metropolis/node/core/consensus/logparser.go
@@ -8,6 +8,7 @@
 	"strings"
 	"time"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/logtree/unraw"
@@ -80,13 +81,13 @@
 	// Convert zap level into logtree severity.
 	switch e.Level {
 	case "info":
-		out.Severity = logtree.INFO
+		out.Severity = logging.INFO
 	case "warn":
-		out.Severity = logtree.WARNING
+		out.Severity = logging.WARNING
 	case "error":
-		out.Severity = logtree.ERROR
+		out.Severity = logging.ERROR
 	case "fatal", "panic", "dpanic":
-		out.Severity = logtree.FATAL
+		out.Severity = logging.FATAL
 	}
 
 	// Sort extra keys alphabetically.
diff --git a/metropolis/node/core/consensus/logparser_test.go b/metropolis/node/core/consensus/logparser_test.go
index cfe6fea..101211c 100644
--- a/metropolis/node/core/consensus/logparser_test.go
+++ b/metropolis/node/core/consensus/logparser_test.go
@@ -6,6 +6,7 @@
 
 	"github.com/google/go-cmp/cmp"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 	"source.monogon.dev/osbase/logtree"
 )
@@ -35,7 +36,7 @@
 			&logtree.ExternalLeveledPayload{
 				Message:   `configuring peer listeners, listen-peer-urls: ["https://[::]:7834"]`,
 				Timestamp: timeParse("2021-07-06T17:18:24.368Z"),
-				Severity:  logtree.INFO,
+				Severity:  logging.INFO,
 				File:      "etcd.go",
 				Line:      117,
 			},
@@ -46,7 +47,7 @@
 			&logtree.ExternalLeveledPayload{
 				Message:   `added member, added-peer-id: "9642132f5d0d99e2", added-peer-peer-urls: ["https://metropolis-eb8d68cfb52711ad04c339abdeea74ed:7834"], cluster-id: "137c8e19524788c1", local-member-id: "9642132f5d0d99e2"`,
 				Timestamp: timeParse("2021-07-06T17:21:49.462Z"),
-				Severity:  logtree.INFO,
+				Severity:  logging.INFO,
 				File:      "cluster.go",
 				Line:      392,
 			},
diff --git a/metropolis/node/core/main.go b/metropolis/node/core/main.go
index eba6773..e675f87 100644
--- a/metropolis/node/core/main.go
+++ b/metropolis/node/core/main.go
@@ -25,6 +25,7 @@
 
 	"golang.org/x/sys/unix"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/metropolis/node/core/cluster"
 	"source.monogon.dev/metropolis/node/core/devmgr"
 	"source.monogon.dev/metropolis/node/core/localstorage"
@@ -302,23 +303,23 @@
 	}
 	s := string(p.DN)
 	if strings.HasPrefix(s, "root.role.controlplane.launcher.consensus.etcd") {
-		return p.Leveled.Severity().AtLeast(logtree.WARNING)
+		return p.Leveled.Severity().AtLeast(logging.WARNING)
 	}
 	// TODO(q3k): turn off RPC traces instead
 	if strings.HasPrefix(s, "root.role.controlplane.launcher.curator.listener.rpc") {
 		return false
 	}
 	if strings.HasPrefix(s, "root.role.kubernetes.run.kubernetes.networked.kubelet") {
-		return p.Leveled.Severity().AtLeast(logtree.WARNING)
+		return p.Leveled.Severity().AtLeast(logging.WARNING)
 	}
 	if strings.HasPrefix(s, "root.role.kubernetes.run.kubernetes.networked.apiserver") {
-		return p.Leveled.Severity().AtLeast(logtree.WARNING)
+		return p.Leveled.Severity().AtLeast(logging.WARNING)
 	}
 	if strings.HasPrefix(s, "root.role.kubernetes.run.kubernetes.controller-manager") {
-		return p.Leveled.Severity().AtLeast(logtree.WARNING)
+		return p.Leveled.Severity().AtLeast(logging.WARNING)
 	}
 	if strings.HasPrefix(s, "root.role.kubernetes.run.kubernetes.scheduler") {
-		return p.Leveled.Severity().AtLeast(logtree.WARNING)
+		return p.Leveled.Severity().AtLeast(logging.WARNING)
 	}
 	if strings.HasPrefix(s, "root.kernel") {
 		// Linux writes high-severity logs directly to the console anyways and
@@ -326,7 +327,7 @@
 		return false
 	}
 	if strings.HasPrefix(s, "supervisor") {
-		return p.Leveled.Severity().AtLeast(logtree.WARNING)
+		return p.Leveled.Severity().AtLeast(logging.WARNING)
 	}
 	return true
 }
diff --git a/metropolis/node/core/network/BUILD.bazel b/metropolis/node/core/network/BUILD.bazel
index 8ef2e6c..6809c58 100644
--- a/metropolis/node/core/network/BUILD.bazel
+++ b/metropolis/node/core/network/BUILD.bazel
@@ -12,10 +12,10 @@
     visibility = ["//:__subpackages__"],
     deps = [
         "//go/algorithm/toposort",
+        "//go/logging",
         "//metropolis/node/core/network/dhcp4c",
         "//metropolis/node/core/network/dhcp4c/callback",
         "//osbase/event/memory",
-        "//osbase/logtree",
         "//osbase/net/dns",
         "//osbase/net/dns/forward",
         "//osbase/net/proto",
diff --git a/metropolis/node/core/network/quirks.go b/metropolis/node/core/network/quirks.go
index 6a3f5cc..547d91e 100644
--- a/metropolis/node/core/network/quirks.go
+++ b/metropolis/node/core/network/quirks.go
@@ -9,12 +9,12 @@
 	"github.com/vishvananda/netlink"
 	"golang.org/x/sys/unix"
 
-	"source.monogon.dev/osbase/logtree"
+	"source.monogon.dev/go/logging"
 )
 
 // applyQuirks applies settings to drivers and/or hardware to make it work
 // better (i.e. with less crashes or faster).
-func applyQuirks(l logtree.LeveledLogger) error {
+func applyQuirks(l logging.Leveled) error {
 	ethtoolFd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP)
 	if err != nil {
 		return fmt.Errorf("while creating IP socket for ethtool: %w", err)
diff --git a/metropolis/node/core/network/static.go b/metropolis/node/core/network/static.go
index 86c45f6..fdd00a8 100644
--- a/metropolis/node/core/network/static.go
+++ b/metropolis/node/core/network/static.go
@@ -16,9 +16,9 @@
 	"golang.org/x/sys/unix"
 
 	"source.monogon.dev/go/algorithm/toposort"
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/metropolis/node/core/network/dhcp4c"
 	dhcpcb "source.monogon.dev/metropolis/node/core/network/dhcp4c/callback"
-	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 	"source.monogon.dev/osbase/sysctl"
 
@@ -272,7 +272,7 @@
 	return hostDevices, nil
 }
 
-func deviceIfaceFromSpec(it *netpb.Interface_Device, hostDevices []deviceIfData, l logtree.LeveledLogger) (*netlink.Device, error) {
+func deviceIfaceFromSpec(it *netpb.Interface_Device, hostDevices []deviceIfData, l logging.Leveled) (*netlink.Device, error) {
 	var matchedDevices []*netlink.Device
 	var err error
 	var parsedHWAddr net.HardwareAddr
diff --git a/metropolis/node/core/rpc/BUILD.bazel b/metropolis/node/core/rpc/BUILD.bazel
index e80ded4..fcc68fa 100644
--- a/metropolis/node/core/rpc/BUILD.bazel
+++ b/metropolis/node/core/rpc/BUILD.bazel
@@ -13,10 +13,10 @@
     importpath = "source.monogon.dev/metropolis/node/core/rpc",
     visibility = ["//visibility:public"],
     deps = [
+        "//go/logging",
         "//metropolis/node/core/identity",
         "//metropolis/proto/api",
         "//metropolis/proto/ext",
-        "//osbase/logtree",
         "@org_golang_google_grpc//:grpc",
         "@org_golang_google_grpc//codes",
         "@org_golang_google_grpc//credentials",
diff --git a/metropolis/node/core/rpc/server_authentication.go b/metropolis/node/core/rpc/server_authentication.go
index c7d6e91..eed7dba 100644
--- a/metropolis/node/core/rpc/server_authentication.go
+++ b/metropolis/node/core/rpc/server_authentication.go
@@ -12,8 +12,8 @@
 	"google.golang.org/grpc/peer"
 	"google.golang.org/grpc/status"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/metropolis/node/core/identity"
-	"source.monogon.dev/osbase/logtree"
 )
 
 // ServerSecurity are the security options of a RPC server that will run
@@ -37,7 +37,7 @@
 // metropolis.proto.ext.authorization options and authenticate/authorize
 // incoming connections. It also runs the gRPC server with the correct TLS
 // settings for authenticating itself to callers.
-func (s *ServerSecurity) GRPCOptions(logger logtree.LeveledLogger) []grpc.ServerOption {
+func (s *ServerSecurity) GRPCOptions(logger logging.Leveled) []grpc.ServerOption {
 	externalCreds := credentials.NewTLS(&tls.Config{
 		Certificates: []tls.Certificate{s.NodeCredentials.TLSCredentials()},
 		ClientAuth:   tls.RequestClientCert,
@@ -53,7 +53,7 @@
 // streamInterceptor returns a gRPC StreamInterceptor interface for use with
 // grpc.NewServer. It's applied to gRPC servers started within Metropolis,
 // notably to the Curator.
-func (s *ServerSecurity) streamInterceptor(logger logtree.LeveledLogger) grpc.StreamServerInterceptor {
+func (s *ServerSecurity) streamInterceptor(logger logging.Leveled) grpc.StreamServerInterceptor {
 	return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
 		var span *logtreeSpan
 		// HACK: Do not log any log retrieval methods into the log, otherwise logs blow up
@@ -87,7 +87,7 @@
 // unaryInterceptor returns a gRPC UnaryInterceptor interface for use with
 // grpc.NewServer. It's applied to gRPC servers started within Metropolis,
 // notably to the Curator.
-func (s *ServerSecurity) unaryInterceptor(logger logtree.LeveledLogger) grpc.UnaryServerInterceptor {
+func (s *ServerSecurity) unaryInterceptor(logger logging.Leveled) grpc.UnaryServerInterceptor {
 	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
 		// Inject span if we have a logger.
 		if logger != nil {
diff --git a/metropolis/node/core/rpc/trace.go b/metropolis/node/core/rpc/trace.go
index a686c06..f27a311 100644
--- a/metropolis/node/core/rpc/trace.go
+++ b/metropolis/node/core/rpc/trace.go
@@ -10,7 +10,7 @@
 	"google.golang.org/protobuf/encoding/prototext"
 	"google.golang.org/protobuf/proto"
 
-	"source.monogon.dev/osbase/logtree"
+	"source.monogon.dev/go/logging"
 )
 
 // Span implements a compatible subset of
@@ -64,14 +64,14 @@
 	// logger is the logtree LeveledLogger backing this span. All Events added into
 	// the Span will go straight into that logger. If the logger is nil, all events
 	// will be dropped instead.
-	logger logtree.LeveledLogger
+	logger logging.Leveled
 	// uid is the span ID of this logtreeSpan. Currently this is a monotonic counter
 	// based on the current nanosecond epoch, but this might change in the future.
 	// This field is ignored if logger is nil.
 	uid uint64
 }
 
-func newLogtreeSpan(l logtree.LeveledLogger) *logtreeSpan {
+func newLogtreeSpan(l logging.Leveled) *logtreeSpan {
 	uid := uint64(time.Now().UnixNano())
 	return &logtreeSpan{
 		logger: l,
diff --git a/metropolis/node/core/update/BUILD.bazel b/metropolis/node/core/update/BUILD.bazel
index f60ba40..6b12a94 100644
--- a/metropolis/node/core/update/BUILD.bazel
+++ b/metropolis/node/core/update/BUILD.bazel
@@ -9,13 +9,13 @@
     importpath = "source.monogon.dev/metropolis/node/core/update",
     visibility = ["//visibility:public"],
     deps = [
+        "//go/logging",
         "//metropolis/node/core/abloader/spec",
         "//osbase/blockdev",
         "//osbase/build/mkimage/osimage",
         "//osbase/efivarfs",
         "//osbase/gpt",
         "//osbase/kexec",
-        "//osbase/logtree",
         "@com_github_cenkalti_backoff_v4//:backoff",
         "@org_golang_google_grpc//codes",
         "@org_golang_google_grpc//status",
diff --git a/metropolis/node/core/update/update.go b/metropolis/node/core/update/update.go
index 7c9e1c3..2e30d2a 100644
--- a/metropolis/node/core/update/update.go
+++ b/metropolis/node/core/update/update.go
@@ -23,13 +23,13 @@
 	"google.golang.org/grpc/status"
 	"google.golang.org/protobuf/proto"
 
+	"source.monogon.dev/go/logging"
 	abloaderpb "source.monogon.dev/metropolis/node/core/abloader/spec"
 	"source.monogon.dev/osbase/blockdev"
 	"source.monogon.dev/osbase/build/mkimage/osimage"
 	"source.monogon.dev/osbase/efivarfs"
 	"source.monogon.dev/osbase/gpt"
 	"source.monogon.dev/osbase/kexec"
-	"source.monogon.dev/osbase/logtree"
 )
 
 // Service contains data and functionality to perform A/B updates on a
@@ -43,7 +43,7 @@
 	ESPPartNumber uint32
 
 	// Logger service for the update service.
-	Logger logtree.LeveledLogger
+	Logger logging.Leveled
 }
 
 type Slot int
diff --git a/metropolis/node/kubernetes/BUILD.bazel b/metropolis/node/kubernetes/BUILD.bazel
index f1af467..d4a36dc 100644
--- a/metropolis/node/kubernetes/BUILD.bazel
+++ b/metropolis/node/kubernetes/BUILD.bazel
@@ -16,6 +16,7 @@
     importpath = "source.monogon.dev/metropolis/node/kubernetes",
     visibility = ["//metropolis/node:__subpackages__"],
     deps = [
+        "//go/logging",
         "//go/net/tinylb",
         "//metropolis/node",
         "//metropolis/node/core/clusternet",
@@ -37,7 +38,6 @@
         "//osbase/event/memory",
         "//osbase/fileargs",
         "//osbase/fsquota",
-        "//osbase/logtree",
         "//osbase/loop",
         "//osbase/net/dns/kubernetes",
         "//osbase/supervisor",
diff --git a/metropolis/node/kubernetes/clusternet/BUILD.bazel b/metropolis/node/kubernetes/clusternet/BUILD.bazel
index 6b4d744..79bedbe 100644
--- a/metropolis/node/kubernetes/clusternet/BUILD.bazel
+++ b/metropolis/node/kubernetes/clusternet/BUILD.bazel
@@ -6,9 +6,9 @@
     importpath = "source.monogon.dev/metropolis/node/kubernetes/clusternet",
     visibility = ["//metropolis/node/kubernetes:__subpackages__"],
     deps = [
+        "//go/logging",
         "//metropolis/node/core/clusternet",
         "//osbase/event",
-        "//osbase/logtree",
         "//osbase/supervisor",
         "@io_k8s_api//core/v1:core",
         "@io_k8s_apimachinery//pkg/fields",
diff --git a/metropolis/node/kubernetes/clusternet/clusternet.go b/metropolis/node/kubernetes/clusternet/clusternet.go
index 28e268d..30c15a1 100644
--- a/metropolis/node/kubernetes/clusternet/clusternet.go
+++ b/metropolis/node/kubernetes/clusternet/clusternet.go
@@ -41,9 +41,9 @@
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/tools/cache"
 
+	"source.monogon.dev/go/logging"
 	oclusternet "source.monogon.dev/metropolis/node/core/clusternet"
 	"source.monogon.dev/osbase/event"
-	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -52,7 +52,7 @@
 	Kubernetes kubernetes.Interface
 	Prefixes   event.Value[*oclusternet.Prefixes]
 
-	logger logtree.LeveledLogger
+	logger logging.Leveled
 }
 
 // ensureNode is called any time the node that this Service is running on gets
diff --git a/metropolis/node/kubernetes/csi.go b/metropolis/node/kubernetes/csi.go
index dfbd48c..3236bba 100644
--- a/metropolis/node/kubernetes/csi.go
+++ b/metropolis/node/kubernetes/csi.go
@@ -33,9 +33,9 @@
 	"google.golang.org/protobuf/types/known/wrapperspb"
 	pluginregistration "k8s.io/kubelet/pkg/apis/pluginregistration/v1"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/osbase/fsquota"
-	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/loop"
 	"source.monogon.dev/osbase/supervisor"
 )
@@ -51,7 +51,7 @@
 	KubeletDirectory *localstorage.DataKubernetesKubeletDirectory
 	VolumesDirectory *localstorage.DataVolumesDirectory
 
-	logger logtree.LeveledLogger
+	logger logging.Leveled
 }
 
 func (s *csiPluginServer) Run(ctx context.Context) error {
diff --git a/metropolis/node/kubernetes/plugins/kvmdevice/BUILD.bazel b/metropolis/node/kubernetes/plugins/kvmdevice/BUILD.bazel
index d6afb23..afb236a 100644
--- a/metropolis/node/kubernetes/plugins/kvmdevice/BUILD.bazel
+++ b/metropolis/node/kubernetes/plugins/kvmdevice/BUILD.bazel
@@ -6,8 +6,8 @@
     importpath = "source.monogon.dev/metropolis/node/kubernetes/plugins/kvmdevice",
     visibility = ["//visibility:public"],
     deps = [
+        "//go/logging",
         "//metropolis/node/core/localstorage",
-        "//osbase/logtree",
         "//osbase/supervisor",
         "@io_k8s_api//core/v1:core",
         "@io_k8s_kubelet//pkg/apis/deviceplugin/v1beta1",
diff --git a/metropolis/node/kubernetes/plugins/kvmdevice/kvmdevice.go b/metropolis/node/kubernetes/plugins/kvmdevice/kvmdevice.go
index f285c47..4d3f4a1 100644
--- a/metropolis/node/kubernetes/plugins/kvmdevice/kvmdevice.go
+++ b/metropolis/node/kubernetes/plugins/kvmdevice/kvmdevice.go
@@ -39,8 +39,8 @@
 	deviceplugin "k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1"
 	pluginregistration "k8s.io/kubelet/pkg/apis/pluginregistration/v1"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/metropolis/node/core/localstorage"
-	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -51,7 +51,7 @@
 	*deviceplugin.UnimplementedDevicePluginServer
 	KubeletDirectory *localstorage.DataKubernetesKubeletDirectory
 
-	logger logtree.LeveledLogger
+	logger logging.Leveled
 }
 
 func (k *Plugin) GetInfo(context.Context, *pluginregistration.InfoRequest) (*pluginregistration.PluginInfo, error) {
diff --git a/metropolis/node/kubernetes/provisioner.go b/metropolis/node/kubernetes/provisioner.go
index 46fd908..aacb949 100644
--- a/metropolis/node/kubernetes/provisioner.go
+++ b/metropolis/node/kubernetes/provisioner.go
@@ -39,9 +39,9 @@
 	ref "k8s.io/client-go/tools/reference"
 	"k8s.io/client-go/util/workqueue"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/metropolis/node/core/localstorage"
 	"source.monogon.dev/osbase/fsquota"
-	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
 )
 
@@ -72,7 +72,7 @@
 	pvcInformer          coreinformers.PersistentVolumeClaimInformer
 	pvInformer           coreinformers.PersistentVolumeInformer
 	storageClassInformer storageinformers.StorageClassInformer
-	logger               logtree.LeveledLogger
+	logger               logging.Leveled
 }
 
 // runCSIProvisioner runs the main provisioning machinery. It consists of a
diff --git a/osbase/logtree/BUILD.bazel b/osbase/logtree/BUILD.bazel
index 7c13aeb..60e8743 100644
--- a/osbase/logtree/BUILD.bazel
+++ b/osbase/logtree/BUILD.bazel
@@ -23,6 +23,7 @@
     importpath = "source.monogon.dev/osbase/logtree",
     visibility = ["//visibility:public"],
     deps = [
+        "//go/logging",
         "//osbase/logbuffer",
         "//osbase/logtree/proto",
         "@com_github_mitchellh_go_wordwrap//:go-wordwrap",
@@ -52,6 +53,7 @@
     ],
     embed = [":logtree"],
     deps = [
+        "//go/logging",
         "@com_github_google_go_cmp//cmp",
         "@org_uber_go_zap//:zap",
     ],
diff --git a/osbase/logtree/grpc.go b/osbase/logtree/grpc.go
index 3b2594d..7e17c56 100644
--- a/osbase/logtree/grpc.go
+++ b/osbase/logtree/grpc.go
@@ -1,9 +1,13 @@
 package logtree
 
-import "google.golang.org/grpc/grpclog"
+import (
+	"google.golang.org/grpc/grpclog"
+
+	"source.monogon.dev/go/logging"
+)
 
 // GRPCify turns a LeveledLogger into a go-grpc compatible logger.
-func GRPCify(logger LeveledLogger) grpclog.LoggerV2 {
+func GRPCify(logger logging.Leveled) grpclog.LoggerV2 {
 	lp, ok := logger.(*leveledPublisher)
 	if !ok {
 		// Fail fast, as this is a programming error.
@@ -71,5 +75,5 @@
 }
 
 func (g *leveledGRPCV2) V(l int) bool {
-	return g.lp.V(VerbosityLevel(l)).Enabled()
+	return g.lp.V(logging.VerbosityLevel(l)).Enabled()
 }
diff --git a/osbase/logtree/journal.go b/osbase/logtree/journal.go
index 412c042..d0e8663 100644
--- a/osbase/logtree/journal.go
+++ b/osbase/logtree/journal.go
@@ -21,6 +21,8 @@
 	"sort"
 	"strings"
 	"sync"
+
+	"source.monogon.dev/go/logging"
 )
 
 // DN is the Distinguished Name, a dot-delimited path used to address loggers
@@ -166,7 +168,7 @@
 // filterSeverity returns a filter that accepts log entries at a given severity
 // level or above. See the Severity type for more information about severity
 // levels.
-func filterSeverity(atLeast Severity) filter {
+func filterSeverity(atLeast logging.Severity) filter {
 	return func(e *entry) bool {
 		return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
 	}
diff --git a/osbase/logtree/journal_test.go b/osbase/logtree/journal_test.go
index e9fc3b4..d295732 100644
--- a/osbase/logtree/journal_test.go
+++ b/osbase/logtree/journal_test.go
@@ -21,13 +21,15 @@
 	"strings"
 	"testing"
 	"time"
+
+	"source.monogon.dev/go/logging"
 )
 
 func testPayload(msg string) *LeveledPayload {
 	return &LeveledPayload{
 		messages:  []string{msg},
 		timestamp: time.Now(),
-		severity:  INFO,
+		severity:  logging.INFO,
 		file:      "main.go",
 		line:      1337,
 	}
diff --git a/osbase/logtree/klog.go b/osbase/logtree/klog.go
index ad7e162..dca5a82 100644
--- a/osbase/logtree/klog.go
+++ b/osbase/logtree/klog.go
@@ -24,6 +24,7 @@
 	"strings"
 	"time"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 )
 
@@ -47,7 +48,7 @@
 // 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 {
+func KLogParser(logger logging.Leveled) io.WriteCloser {
 	p, ok := logger.(*leveledPublisher)
 	if !ok {
 		// Fail fast, as this is a programming error.
@@ -121,16 +122,16 @@
 	lineS := parts[7]
 	message := parts[8]
 
-	var severity Severity
+	var severity logging.Severity
 	switch severityS {
 	case "I":
-		severity = INFO
+		severity = logging.INFO
 	case "W":
-		severity = WARNING
+		severity = logging.WARNING
 	case "E":
-		severity = ERROR
+		severity = logging.ERROR
 	case "F":
-		severity = FATAL
+		severity = logging.FATAL
 	default:
 		return nil
 	}
diff --git a/osbase/logtree/klog_test.go b/osbase/logtree/klog_test.go
index d53df3f..788a7eb 100644
--- a/osbase/logtree/klog_test.go
+++ b/osbase/logtree/klog_test.go
@@ -21,6 +21,8 @@
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+
+	"source.monogon.dev/go/logging"
 )
 
 func TestParse(t *testing.T) {
@@ -40,7 +42,7 @@
 		{now, "E0312 14:20:04.240540    204 shared_informer.go:247] Caches are synced for attach detach", &LeveledPayload{
 			messages:  []string{"Caches are synced for attach detach"},
 			timestamp: time.Date(2021, 03, 12, 14, 20, 4, 240540000, time.UTC),
-			severity:  ERROR,
+			severity:  logging.ERROR,
 			file:      "shared_informer.go",
 			line:      247,
 		}},
@@ -56,7 +58,7 @@
 		{nowNewYear, "I1231 23:59:43.123456    123 fry.go:123] Here's to another lousy millenium!", &LeveledPayload{
 			messages:  []string{"Here's to another lousy millenium!"},
 			timestamp: time.Date(1999, 12, 31, 23, 59, 43, 123456000, time.UTC),
-			severity:  INFO,
+			severity:  logging.INFO,
 			file:      "fry.go",
 			line:      123,
 		}},
@@ -68,7 +70,7 @@
 		{now, "E0312 14:20:04 204 shared_informer.go:247] Caches are synced for attach detach", &LeveledPayload{
 			messages:  []string{"Caches are synced for attach detach"},
 			timestamp: time.Date(2021, 03, 12, 14, 20, 4, 0, time.UTC),
-			severity:  ERROR,
+			severity:  logging.ERROR,
 			file:      "shared_informer.go",
 			line:      247,
 		}},
diff --git a/osbase/logtree/kmsg.go b/osbase/logtree/kmsg.go
index 03bb6ff..11e140a 100644
--- a/osbase/logtree/kmsg.go
+++ b/osbase/logtree/kmsg.go
@@ -14,6 +14,8 @@
 	"time"
 
 	"golang.org/x/sys/unix"
+
+	"source.monogon.dev/go/logging"
 )
 
 const (
@@ -29,7 +31,7 @@
 
 // KmsgPipe pipes logs from the kernel kmsg interface at /dev/kmsg into the
 // given logger.
-func KmsgPipe(ctx context.Context, lt LeveledLogger) error {
+func KmsgPipe(ctx context.Context, lt logging.Leveled) error {
 	publisher, ok := lt.(*leveledPublisher)
 	if !ok {
 		// Fail fast, as this is a programming error.
@@ -119,18 +121,18 @@
 
 	monotonicFromNow := monotonic - monotonicSinceBoot
 
-	var severity Severity
+	var severity logging.Severity
 	switch loglevel {
 	case loglevelEmergency, loglevelAlert:
-		severity = FATAL
+		severity = logging.FATAL
 	case loglevelCritical, loglevelError:
-		severity = ERROR
+		severity = logging.ERROR
 	case loglevelWarning:
-		severity = WARNING
+		severity = logging.WARNING
 	case loglevelNotice, loglevelInfo, loglevelDebug:
-		severity = INFO
+		severity = logging.INFO
 	default:
-		severity = INFO
+		severity = logging.INFO
 	}
 
 	return &LeveledPayload{
diff --git a/osbase/logtree/kmsg_test.go b/osbase/logtree/kmsg_test.go
index e2faf82..24f2acf 100644
--- a/osbase/logtree/kmsg_test.go
+++ b/osbase/logtree/kmsg_test.go
@@ -8,6 +8,8 @@
 	"time"
 
 	"github.com/google/go-cmp/cmp"
+
+	"source.monogon.dev/go/logging"
 )
 
 func TestParseKmsg(t *testing.T) {
@@ -26,13 +28,13 @@
 		{"6,30962,1501094342185,-;test\n", &LeveledPayload{
 			messages:  []string{"test"},
 			timestamp: time.Date(2023, 8, 9, 14, 57, 23, 35675222, time.UTC),
-			severity:  INFO,
+			severity:  logging.INFO,
 		}},
 		// With metadata and different severity
 		{"4,30951,1486884175312,-;nvme nvme2: starting error recovery\n SUBSYSTEM=nvme\n DEVICE=c239:2\n", &LeveledPayload{
 			messages:  []string{"nvme nvme2: starting error recovery"},
 			timestamp: time.Date(2023, 8, 9, 11, 00, 32, 868802222, time.UTC),
-			severity:  WARNING,
+			severity:  logging.WARNING,
 		}},
 	} {
 		got := parseKmsg(now, nowMonotonic, []byte(te.line))
diff --git a/osbase/logtree/leveled.go b/osbase/logtree/leveled.go
index 98699b8..701d8d0 100644
--- a/osbase/logtree/leveled.go
+++ b/osbase/logtree/leveled.go
@@ -19,155 +19,34 @@
 import (
 	"fmt"
 
+	"source.monogon.dev/go/logging"
 	lpb "source.monogon.dev/osbase/logtree/proto"
 )
 
-// LeveledLogger is a generic interface for glog-style logging. There are four
-// hardcoded log severities, in increasing order: INFO, WARNING, ERROR, FATAL.
-// Logging at a certain severity level logs not only to consumers expecting data at
-// that severity level, but also all lower severity levels. For example, an ERROR
-// log will also be passed to consumers looking at INFO or WARNING logs.
-type LeveledLogger interface {
-	// Info logs at the INFO severity. Arguments are handled in the manner of
-	// fmt.Print, a terminating newline is added if missing.
-	Info(args ...interface{})
-	// Infof logs at the INFO severity. Arguments are handled in the manner of
-	// fmt.Printf, a terminating newline is added if missing.
-	Infof(format string, args ...interface{})
-
-	// Warning logs at the WARNING severity. Arguments are handled in the manner of
-	// fmt.Print, a terminating newline is added if missing.
-	Warning(args ...interface{})
-	// Warningf logs at the WARNING severity. Arguments are handled in the manner of
-	// fmt.Printf, a terminating newline is added if missing.
-	Warningf(format string, args ...interface{})
-
-	// Error logs at the ERROR severity. Arguments are handled in the manner of
-	// fmt.Print, a terminating newline is added if missing.
-	Error(args ...interface{})
-	// Errorf logs at the ERROR severity. Arguments are handled in the manner of
-	// fmt.Printf, a terminating newline is added if missing.
-	Errorf(format string, args ...interface{})
-
-	// Fatal logs at the FATAL severity and aborts the current program. Arguments are
-	// handled in the manner of fmt.Print, a terminating newline is added if missing.
-	Fatal(args ...interface{})
-	// Fatalf logs at the FATAL severity and aborts the current program. Arguments are
-	// handled in the manner of fmt.Printf, a terminating newline is added if missing.
-	Fatalf(format string, args ...interface{})
-
-	// V returns a VerboseLeveledLogger at a given verbosity level. These verbosity
-	// levels can be dynamically set and unset on a package-granular level by consumers
-	// of the LeveledLogger logs. The returned value represents whether logging at the
-	// given verbosity level was active at that time, and as such should not be a long-
-	// lived object in programs. This construct is further refered to as 'V-logs'.
-	V(level VerbosityLevel) VerboseLeveledLogger
-
-	// WithAddedStackDepth returns the same LeveledLogger, but adjusted with an
-	// additional 'extra stack depth' which will be used to skip a given number of
-	// stack/call frames when determining the location where the error originated.
-	// For example, WithStackDepth(1) will return a logger that will skip one
-	// stack/call frame. Then, with function foo() calling function helper() which
-	// in turns call l.Infof(), the log line will be emitted with the call site of
-	// helper() within foo(), instead of the default behaviour of logging the
-	// call site of Infof() within helper().
-	//
-	// This is useful for functions which somehow wrap loggers in helper functions,
-	// for example to expose a slightly different API.
-	WithAddedStackDepth(depth int) LeveledLogger
-}
-
-// VerbosityLevel is a verbosity level defined for V-logs. This can be changed
-// programmatically per Go package. When logging at a given VerbosityLevel V, the
-// current level must be equal or higher to V for the logs to be recorded.
-// Conversely, enabling a V-logging at a VerbosityLevel V also enables all logging
-// at lower levels [Int32Min .. (V-1)].
-type VerbosityLevel int32
-
-type VerboseLeveledLogger interface {
-	// Enabled returns if this level was enabled. If not enabled, all logging into this
-	// logger will be discarded immediately. Thus, Enabled() can be used to check the
-	// verbosity level before performing any logging:
-	//    if l.V(3).Enabled() { l.Info("V3 is enabled") }
-	// or, in simple cases, the convenience function .Info can be used:
-	//    l.V(3).Info("V3 is enabled")
-	// The second form is shorter and more convenient, but more expensive, as its
-	// arguments are always evaluated.
-	Enabled() bool
-	// Info is the equivalent of a LeveledLogger's Info call, guarded by whether this
-	// VerboseLeveledLogger is enabled.
-	Info(args ...interface{})
-	// Infof is the equivalent of a LeveledLogger's Infof call, guarded by whether this
-	// VerboseLeveledLogger is enabled.
-	Infof(format string, args ...interface{})
-}
-
-// Severity is one of the severities as described in LeveledLogger.
-type Severity string
-
-const (
-	INFO    Severity = "I"
-	WARNING Severity = "W"
-	ERROR   Severity = "E"
-	FATAL   Severity = "F"
-)
-
-var (
-	// SeverityAtLeast maps a given severity to a list of severities that at that
-	// severity or higher. In other words, SeverityAtLeast[X] returns a list of
-	// severities that might be seen in a log at severity X.
-	SeverityAtLeast = map[Severity][]Severity{
-		INFO:    {INFO, WARNING, ERROR, FATAL},
-		WARNING: {WARNING, ERROR, FATAL},
-		ERROR:   {ERROR, FATAL},
-		FATAL:   {FATAL},
-	}
-)
-
-func (s Severity) AtLeast(other Severity) bool {
-	for _, el := range SeverityAtLeast[other] {
-		if el == s {
-			return true
-		}
-	}
-	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 lpb.LeveledLogSeverity) (Severity, error) {
+func SeverityFromProto(s lpb.LeveledLogSeverity) (logging.Severity, error) {
 	switch s {
 	case lpb.LeveledLogSeverity_INFO:
-		return INFO, nil
+		return logging.INFO, nil
 	case lpb.LeveledLogSeverity_WARNING:
-		return WARNING, nil
+		return logging.WARNING, nil
 	case lpb.LeveledLogSeverity_ERROR:
-		return ERROR, nil
+		return logging.ERROR, nil
 	case lpb.LeveledLogSeverity_FATAL:
-		return FATAL, nil
+		return logging.FATAL, nil
 	default:
 		return "", fmt.Errorf("unknown severity value %d", s)
 	}
 }
 
-func (s Severity) ToProto() lpb.LeveledLogSeverity {
+func SeverityToProto(s logging.Severity) lpb.LeveledLogSeverity {
 	switch s {
-	case INFO:
+	case logging.INFO:
 		return lpb.LeveledLogSeverity_INFO
-	case WARNING:
+	case logging.WARNING:
 		return lpb.LeveledLogSeverity_WARNING
-	case ERROR:
+	case logging.ERROR:
 		return lpb.LeveledLogSeverity_ERROR
-	case FATAL:
+	case logging.FATAL:
 		return lpb.LeveledLogSeverity_FATAL
 	default:
 		return lpb.LeveledLogSeverity_INVALID
diff --git a/osbase/logtree/leveled_payload.go b/osbase/logtree/leveled_payload.go
index 95b9d5c..b038848 100644
--- a/osbase/logtree/leveled_payload.go
+++ b/osbase/logtree/leveled_payload.go
@@ -24,6 +24,7 @@
 
 	tpb "google.golang.org/protobuf/types/known/timestamppb"
 
+	"source.monogon.dev/go/logging"
 	lpb "source.monogon.dev/osbase/logtree/proto"
 )
 
@@ -38,7 +39,7 @@
 	// 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
+	severity logging.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.
@@ -111,14 +112,14 @@
 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 }
+func (p *LeveledPayload) Severity() logging.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(),
+		Severity:  SeverityToProto(p.Severity()),
 		Location:  p.Location(),
 	}
 }
@@ -167,7 +168,7 @@
 	// 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
+	Severity logging.Severity
 	// File name of originating code. Defaults to "unknown" if not set.
 	File string
 	// Line in File. Zero indicates the line is not known.
@@ -188,7 +189,7 @@
 		l.timestamp = time.Now()
 	}
 	if !l.severity.Valid() {
-		l.severity = INFO
+		l.severity = logging.INFO
 	}
 	if l.file == "" {
 		l.file = "unknown"
diff --git a/osbase/logtree/logtree.go b/osbase/logtree/logtree.go
index c20681d..b6f8a06 100644
--- a/osbase/logtree/logtree.go
+++ b/osbase/logtree/logtree.go
@@ -21,6 +21,7 @@
 	"strings"
 	"sync"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 )
 
@@ -52,7 +53,7 @@
 	tree *LogTree
 	// verbosity is the current verbosity level of this DN/node, affecting .V(n)
 	// LeveledLogger calls
-	verbosity     VerbosityLevel
+	verbosity     logging.VerbosityLevel
 	rawLineBuffer *logbuffer.LineBuffer
 
 	// mu guards children.
diff --git a/osbase/logtree/logtree_access.go b/osbase/logtree/logtree_access.go
index b601ea4..30ceccf 100644
--- a/osbase/logtree/logtree_access.go
+++ b/osbase/logtree/logtree_access.go
@@ -19,6 +19,8 @@
 import (
 	"errors"
 	"sync/atomic"
+
+	"source.monogon.dev/go/logging"
 )
 
 // LogReadOption describes options for the LogTree.Read call.
@@ -28,7 +30,7 @@
 	withBacklog                int
 	onlyLeveled                bool
 	onlyRaw                    bool
-	leveledWithMinimumSeverity Severity
+	leveledWithMinimumSeverity logging.Severity
 }
 
 // WithChildren makes Read return/stream data for both a given DN and all its
@@ -54,7 +56,7 @@
 // LeveledWithMinimumSeverity makes Read return only log entries that are at least
 // at a given Severity. If only leveled entries are needed, OnlyLeveled must be
 // used. This is a no-op when OnlyRaw is used.
-func LeveledWithMinimumSeverity(s Severity) LogReadOption {
+func LeveledWithMinimumSeverity(s logging.Severity) LogReadOption {
 	return LogReadOption{leveledWithMinimumSeverity: s}
 }
 
@@ -111,7 +113,7 @@
 	var backlog int
 	var stream bool
 	var recursive bool
-	var leveledSeverity Severity
+	var leveledSeverity logging.Severity
 	var onlyRaw, onlyLeveled bool
 
 	for _, opt := range opts {
diff --git a/osbase/logtree/logtree_publisher.go b/osbase/logtree/logtree_publisher.go
index 6c4120a..2df7037 100644
--- a/osbase/logtree/logtree_publisher.go
+++ b/osbase/logtree/logtree_publisher.go
@@ -23,6 +23,7 @@
 	"strings"
 	"time"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 )
 
@@ -33,7 +34,7 @@
 
 // LeveledFor returns a LeveledLogger publishing interface for a given DN. An error
 // may be returned if the DN is malformed.
-func (l *LogTree) LeveledFor(dn DN) (LeveledLogger, error) {
+func (l *LogTree) LeveledFor(dn DN) (logging.Leveled, error) {
 	node, err := l.nodeByDN(dn)
 	if err != nil {
 		return nil, err
@@ -54,7 +55,7 @@
 
 // MustLeveledFor returns a LeveledLogger publishing interface for a given DN, or
 // panics if the given DN is invalid.
-func (l *LogTree) MustLeveledFor(dn DN) LeveledLogger {
+func (l *LogTree) MustLeveledFor(dn DN) logging.Leveled {
 	leveled, err := l.LeveledFor(dn)
 	if err != nil {
 		panic(fmt.Errorf("LeveledFor returned: %w", err))
@@ -72,7 +73,7 @@
 
 // SetVerbosity sets the verbosity for a given DN (non-recursively, ie. for that DN
 // only, not its children).
-func (l *LogTree) SetVerbosity(dn DN, level VerbosityLevel) error {
+func (l *LogTree) SetVerbosity(dn DN, level logging.VerbosityLevel) error {
 	node, err := l.nodeByDN(dn)
 	if err != nil {
 		return err
@@ -97,7 +98,7 @@
 // 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 {
+func LogExternalLeveled(l logging.Leveled, e *ExternalLeveledPayload) error {
 	publisher, ok := l.(*leveledPublisher)
 	if !ok {
 		return fmt.Errorf("the given LeveledLogger is not a *leveledPublisher")
@@ -115,7 +116,7 @@
 // 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.
-func (l *leveledPublisher) logLeveled(depth int, severity Severity, msg string) {
+func (l *leveledPublisher) logLeveled(depth int, severity logging.Severity, msg string) {
 	_, file, line, ok := runtime.Caller(2 + depth)
 	if !ok {
 		file = "???"
@@ -147,53 +148,53 @@
 
 // Info implements the LeveledLogger interface.
 func (l *leveledPublisher) Info(args ...interface{}) {
-	l.logLeveled(l.depth, INFO, fmt.Sprint(args...))
+	l.logLeveled(l.depth, logging.INFO, fmt.Sprint(args...))
 }
 
 // Infof implements the LeveledLogger interface.
 func (l *leveledPublisher) Infof(format string, args ...interface{}) {
-	l.logLeveled(l.depth, INFO, fmt.Sprintf(format, args...))
+	l.logLeveled(l.depth, logging.INFO, fmt.Sprintf(format, args...))
 }
 
 // Warning implements the LeveledLogger interface.
 func (l *leveledPublisher) Warning(args ...interface{}) {
-	l.logLeveled(l.depth, WARNING, fmt.Sprint(args...))
+	l.logLeveled(l.depth, logging.WARNING, fmt.Sprint(args...))
 }
 
 // Warningf implements the LeveledLogger interface.
 func (l *leveledPublisher) Warningf(format string, args ...interface{}) {
-	l.logLeveled(l.depth, WARNING, fmt.Sprintf(format, args...))
+	l.logLeveled(l.depth, logging.WARNING, fmt.Sprintf(format, args...))
 }
 
 // Error implements the LeveledLogger interface.
 func (l *leveledPublisher) Error(args ...interface{}) {
-	l.logLeveled(l.depth, ERROR, fmt.Sprint(args...))
+	l.logLeveled(l.depth, logging.ERROR, fmt.Sprint(args...))
 }
 
 // Errorf implements the LeveledLogger interface.
 func (l *leveledPublisher) Errorf(format string, args ...interface{}) {
-	l.logLeveled(l.depth, ERROR, fmt.Sprintf(format, args...))
+	l.logLeveled(l.depth, logging.ERROR, fmt.Sprintf(format, args...))
 }
 
 // Fatal implements the LeveledLogger interface.
 func (l *leveledPublisher) Fatal(args ...interface{}) {
-	l.logLeveled(l.depth, FATAL, fmt.Sprint(args...))
+	l.logLeveled(l.depth, logging.FATAL, fmt.Sprint(args...))
 }
 
 // Fatalf implements the LeveledLogger interface.
 func (l *leveledPublisher) Fatalf(format string, args ...interface{}) {
-	l.logLeveled(l.depth, FATAL, fmt.Sprintf(format, args...))
+	l.logLeveled(l.depth, logging.FATAL, fmt.Sprintf(format, args...))
 }
 
 // WithAddedStackDepth impleemnts the LeveledLogger interface.
-func (l *leveledPublisher) WithAddedStackDepth(depth int) LeveledLogger {
+func (l *leveledPublisher) WithAddedStackDepth(depth int) logging.Leveled {
 	l2 := *l
 	l2.depth += depth
 	return &l2
 }
 
 // V implements the LeveledLogger interface.
-func (l *leveledPublisher) V(v VerbosityLevel) VerboseLeveledLogger {
+func (l *leveledPublisher) V(v logging.VerbosityLevel) logging.VerboseLeveled {
 	return &verbose{
 		publisher: l,
 		enabled:   l.node.verbosity >= v,
@@ -218,12 +219,12 @@
 	if !v.enabled {
 		return
 	}
-	v.publisher.logLeveled(v.publisher.depth, INFO, fmt.Sprint(args...))
+	v.publisher.logLeveled(v.publisher.depth, logging.INFO, fmt.Sprint(args...))
 }
 
 func (v *verbose) Infof(format string, args ...interface{}) {
 	if !v.enabled {
 		return
 	}
-	v.publisher.logLeveled(v.publisher.depth, INFO, fmt.Sprintf(format, args...))
+	v.publisher.logLeveled(v.publisher.depth, logging.INFO, fmt.Sprintf(format, args...))
 }
diff --git a/osbase/logtree/logtree_test.go b/osbase/logtree/logtree_test.go
index 54eabb7..8ddd3d0 100644
--- a/osbase/logtree/logtree_test.go
+++ b/osbase/logtree/logtree_test.go
@@ -21,6 +21,8 @@
 	"strings"
 	"testing"
 	"time"
+
+	"source.monogon.dev/go/logging"
 )
 
 func expect(tree *LogTree, t *testing.T, dn DN, entries ...string) string {
@@ -227,13 +229,13 @@
 
 	for _, te := range []struct {
 		ix       int
-		severity Severity
+		severity logging.Severity
 		message  string
 	}{
-		{0, ERROR, "i am an error"},
-		{1, WARNING, "i am a warning"},
-		{2, INFO, "i am informative"},
-		{3, INFO, "i am a zero-level debug"},
+		{0, logging.ERROR, "i am an error"},
+		{1, logging.WARNING, "i am a warning"},
+		{2, logging.INFO, "i am informative"},
+		{3, logging.INFO, "i am a zero-level debug"},
 	} {
 		p := reader.Backlog[te.ix]
 		if want, got := te.severity, p.Leveled.Severity(); want != got {
@@ -255,7 +257,7 @@
 	tree.MustLeveledFor("main").Info("i am informative")
 	tree.MustLeveledFor("main").V(0).Info("i am a zero-level debug")
 
-	reader, err := tree.Read("main", WithBacklog(BacklogAllAvailable), LeveledWithMinimumSeverity(WARNING))
+	reader, err := tree.Read("main", WithBacklog(BacklogAllAvailable), LeveledWithMinimumSeverity(logging.WARNING))
 	if err != nil {
 		t.Fatalf("Read: %v", err)
 	}
@@ -311,7 +313,7 @@
 			&LogEntry{
 				Leveled: &LeveledPayload{
 					messages: []string{"Hello there!"},
-					severity: WARNING,
+					severity: logging.WARNING,
 				},
 				DN: "root.role.kubernetes.run.kubernetes.apiserver",
 			},
@@ -322,7 +324,7 @@
 			&LogEntry{
 				Leveled: &LeveledPayload{
 					messages: []string{"Hello there!", "I am multiline."},
-					severity: WARNING,
+					severity: logging.WARNING,
 				},
 				DN: "root.role.kubernetes.run.kubernetes.apiserver",
 			},
@@ -336,7 +338,7 @@
 			&LogEntry{
 				Leveled: &LeveledPayload{
 					messages: []string{"Hello there! I am a very long string, and I will get wrapped to 120 columns because that's just how life is for long strings."},
-					severity: WARNING,
+					severity: logging.WARNING,
 				},
 				DN: "root.role.kubernetes.run.kubernetes.apiserver",
 			},
@@ -350,7 +352,7 @@
 			&LogEntry{
 				Leveled: &LeveledPayload{
 					messages: []string{"Hello there!"},
-					severity: WARNING,
+					severity: logging.WARNING,
 				},
 				DN: "root.role.kubernetes.run.kubernetes.apiserver",
 			},
@@ -363,7 +365,7 @@
 			&LogEntry{
 				Leveled: &LeveledPayload{
 					messages: []string{"Hello there!"},
-					severity: WARNING,
+					severity: logging.WARNING,
 				},
 				DN: "root.role.kubernetes.run.kubernetes.apiserver",
 			},
diff --git a/osbase/logtree/unraw/BUILD.bazel b/osbase/logtree/unraw/BUILD.bazel
index 3ae4da1..9364a30 100644
--- a/osbase/logtree/unraw/BUILD.bazel
+++ b/osbase/logtree/unraw/BUILD.bazel
@@ -6,6 +6,7 @@
     importpath = "source.monogon.dev/osbase/logtree/unraw",
     visibility = ["//visibility:public"],
     deps = [
+        "//go/logging",
         "//osbase/logbuffer",
         "//osbase/logtree",
         "//osbase/supervisor",
diff --git a/osbase/logtree/unraw/unraw.go b/osbase/logtree/unraw/unraw.go
index a1f2624..88ab9d1 100644
--- a/osbase/logtree/unraw/unraw.go
+++ b/osbase/logtree/unraw/unraw.go
@@ -24,6 +24,7 @@
 	"syscall"
 	"time"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 	"source.monogon.dev/osbase/logtree"
 	"source.monogon.dev/osbase/supervisor"
@@ -51,7 +52,7 @@
 	MaximumLineLength int
 	// LeveledLogger is the logtree leveled logger into which events from the
 	// Parser will be sent.
-	LeveledLogger logtree.LeveledLogger
+	LeveledLogger logging.Leveled
 
 	// mu guards lb.
 	mu sync.Mutex
diff --git a/osbase/logtree/zap.go b/osbase/logtree/zap.go
index f3ae6e3..82e6dda 100644
--- a/osbase/logtree/zap.go
+++ b/osbase/logtree/zap.go
@@ -10,13 +10,14 @@
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logbuffer"
 )
 
 // Zapify turns a LeveledLogger into a zap.Logger which pipes its output into the
 // LeveledLogger. The message, severity and caller are carried over. Extra fields
 // are appended as JSON to the end of the log line.
-func Zapify(logger LeveledLogger, minimumLevel zapcore.Level) *zap.Logger {
+func Zapify(logger logging.Leveled, minimumLevel zapcore.Level) *zap.Logger {
 	p, ok := logger.(*leveledPublisher)
 	if !ok {
 		// Fail fast, as this is a programming error.
@@ -71,7 +72,7 @@
 
 type zapEntry struct {
 	message  string
-	severity Severity
+	severity logging.Severity
 	time     time.Time
 	file     string
 	line     int
@@ -109,14 +110,14 @@
 	callerLineS := callerParts[1]
 	callerLine, _ := strconv.Atoi(callerLineS)
 
-	var severity Severity
+	var severity logging.Severity
 	switch level {
 	case "warn":
-		severity = WARNING
+		severity = logging.WARNING
 	case "error", "dpanic", "panic", "fatal":
-		severity = ERROR
+		severity = logging.ERROR
 	default:
-		severity = INFO
+		severity = logging.INFO
 	}
 
 	secs := int64(t)
diff --git a/osbase/logtree/zap_test.go b/osbase/logtree/zap_test.go
index 3917cd8..b03b8cf 100644
--- a/osbase/logtree/zap_test.go
+++ b/osbase/logtree/zap_test.go
@@ -4,6 +4,8 @@
 	"testing"
 
 	"go.uber.org/zap"
+
+	"source.monogon.dev/go/logging"
 )
 
 func TestZapify(t *testing.T) {
@@ -25,11 +27,11 @@
 	} else {
 		for i, te := range []struct {
 			msg string
-			sev Severity
+			sev logging.Severity
 		}{
-			{`foo {"intp":42,"strp":"strv"}`, INFO},
-			{`foo! {"intp":1337,"strp":"strv"}`, WARNING},
-			{`foo!!`, ERROR},
+			{`foo {"intp":42,"strp":"strv"}`, logging.INFO},
+			{`foo! {"intp":1337,"strp":"strv"}`, logging.WARNING},
+			{`foo!!`, logging.ERROR},
 		} {
 			if want, got := te.msg, res.Backlog[i].Leveled.messages[0]; want != got {
 				t.Errorf("Line %d: wanted message %q, got %q", i, want, got)
diff --git a/osbase/supervisor/BUILD.bazel b/osbase/supervisor/BUILD.bazel
index b6b4861..9dfda5a 100644
--- a/osbase/supervisor/BUILD.bazel
+++ b/osbase/supervisor/BUILD.bazel
@@ -15,6 +15,7 @@
     # TODO(#189): move supervisor to //go
     visibility = ["//visibility:public"],
     deps = [
+        "//go/logging",
         "//osbase/logtree",
         "@com_github_cenkalti_backoff_v4//:backoff",
         "@com_github_prometheus_client_golang//prometheus",
diff --git a/osbase/supervisor/supervisor.go b/osbase/supervisor/supervisor.go
index ff87a25..5570945 100644
--- a/osbase/supervisor/supervisor.go
+++ b/osbase/supervisor/supervisor.go
@@ -27,6 +27,7 @@
 	"io"
 	"sync"
 
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/logtree"
 )
 
@@ -94,7 +95,7 @@
 	// logtree is the main logtree exposed to runnables and used internally.
 	logtree *logtree.LogTree
 	// ilogger is the internal logger logging to "supervisor" in the logtree.
-	ilogger logtree.LeveledLogger
+	ilogger logging.Leveled
 
 	// pReq is an interface channel to the lifecycle processor of the
 	// supervisor.
@@ -160,7 +161,7 @@
 	return sup
 }
 
-func Logger(ctx context.Context) logtree.LeveledLogger {
+func Logger(ctx context.Context) logging.Leveled {
 	node, unlock := fromContext(ctx)
 	defer unlock()
 	return node.sup.logtree.MustLeveledFor(logtree.DN(node.dn()))
@@ -182,7 +183,7 @@
 // sub-logger with a given name, that name also becomes unavailable for use as
 // a child runnable (no runnable and sub-logger may ever log into the same
 // logtree DN).
-func SubLogger(ctx context.Context, name string) (logtree.LeveledLogger, error) {
+func SubLogger(ctx context.Context, name string) (logging.Leveled, error) {
 	node, unlock := fromContext(ctx)
 	defer unlock()
 
@@ -201,7 +202,7 @@
 // MustSubLogger is a wrapper around SubLogger which panics on error. Errors
 // should only happen due to invalid names, so as long as the given name is
 // compile-time constant and valid, this function is safe to use.
-func MustSubLogger(ctx context.Context, name string) logtree.LeveledLogger {
+func MustSubLogger(ctx context.Context, name string) logging.Leveled {
 	l, err := SubLogger(ctx, name)
 	if err != nil {
 		panic(err)
diff --git a/osbase/tpm/BUILD.bazel b/osbase/tpm/BUILD.bazel
index d75c9fb..6f33dca 100644
--- a/osbase/tpm/BUILD.bazel
+++ b/osbase/tpm/BUILD.bazel
@@ -9,7 +9,7 @@
     importpath = "source.monogon.dev/osbase/tpm",
     visibility = ["//visibility:public"],
     deps = [
-        "//osbase/logtree",
+        "//go/logging",
         "//osbase/sysfs",
         "//osbase/tpm/proto",
         "@com_github_google_go_tpm//tpm2",
diff --git a/osbase/tpm/tpm.go b/osbase/tpm/tpm.go
index b77c36d..f664c75 100644
--- a/osbase/tpm/tpm.go
+++ b/osbase/tpm/tpm.go
@@ -41,7 +41,7 @@
 
 	tpmpb "source.monogon.dev/osbase/tpm/proto"
 
-	"source.monogon.dev/osbase/logtree"
+	"source.monogon.dev/go/logging"
 	"source.monogon.dev/osbase/sysfs"
 )
 
@@ -126,7 +126,7 @@
 
 // TPM represents a high-level interface to a connected TPM 2.0
 type TPM struct {
-	logger logtree.LeveledLogger
+	logger logging.Leveled
 	device io.ReadWriteCloser
 
 	// We keep the AK loaded since it's used fairly often and deriving it is
@@ -137,7 +137,7 @@
 
 // Initialize finds and opens the TPM (if any). If there is no TPM available it
 // returns ErrNotExists
-func Initialize(logger logtree.LeveledLogger) error {
+func Initialize(logger logging.Leveled) error {
 	lock.Lock()
 	defer lock.Unlock()
 	tpmDir, err := os.Open("/sys/class/tpm")