m/p/logtree: implement WithAddedStackDepth

This is a prerequisite to easily pass over trace-based events into
logtree. It allows a testing/Test.Helper()-like mechanism to skip some
stackframes within a call tree to the logger in order to log pertinent
log origins instead of a wrapper.

Change-Id: Ida9732f8505ff4a400e689045bea318a185f7983
Reviewed-on: https://review.monogon.dev/c/monogon/+/538
Reviewed-by: Leopold Schabel <leo@nexantic.com>
diff --git a/metropolis/pkg/logtree/klog.go b/metropolis/pkg/logtree/klog.go
index 07185ed..1c866f4 100644
--- a/metropolis/pkg/logtree/klog.go
+++ b/metropolis/pkg/logtree/klog.go
@@ -48,14 +48,14 @@
 // 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 {
-	n, ok := logger.(*node)
+	p, ok := logger.(*leveledPublisher)
 	if !ok {
 		// Fail fast, as this is a programming error.
-		panic("Expected *node in LeveledLogger from supervisor")
+		panic("Expected *leveledPublisher in LeveledLogger from supervisor")
 	}
 
 	k := &klogParser{
-		n: n,
+		publisher: p,
 	}
 	// klog seems to have no line length limit. Let's assume some sane sort of default.
 	k.buffer = logbuffer.NewLineBuffer(1024, k.consumeLine)
@@ -63,8 +63,8 @@
 }
 
 type klogParser struct {
-	n      *node
-	buffer *logbuffer.LineBuffer
+	publisher *leveledPublisher
+	buffer    *logbuffer.LineBuffer
 }
 
 func (k *klogParser) Write(p []byte) (n int, err error) {
@@ -84,17 +84,17 @@
 	if p == nil {
 		// We could instead emit that line as a raw log - however, this would lead to
 		// interleaving raw logging and leveled logging.
-		k.n.Errorf("Invalid klog line: %s", l.Data)
+		k.publisher.Errorf("Invalid klog line: %s", l.Data)
 	}
 	// TODO(q3k): should this be exposed as an API on LeveledLogger? How much should
 	// we permit library users to 'fake' logs? This would also permit us to get rid
 	// of the type assertion in KLogParser().
 	e := &entry{
-		origin:  k.n.dn,
+		origin:  k.publisher.node.dn,
 		leveled: p,
 	}
-	k.n.tree.journal.append(e)
-	k.n.tree.journal.notify(e)
+	k.publisher.node.tree.journal.append(e)
+	k.publisher.node.tree.journal.notify(e)
 }
 
 var (
diff --git a/metropolis/pkg/logtree/leveled.go b/metropolis/pkg/logtree/leveled.go
index 9590bc3..6e44ef4 100644
--- a/metropolis/pkg/logtree/leveled.go
+++ b/metropolis/pkg/logtree/leveled.go
@@ -62,6 +62,19 @@
 	// 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
diff --git a/metropolis/pkg/logtree/logtree_publisher.go b/metropolis/pkg/logtree/logtree_publisher.go
index 25dfc5a..0b945e3 100644
--- a/metropolis/pkg/logtree/logtree_publisher.go
+++ b/metropolis/pkg/logtree/logtree_publisher.go
@@ -26,10 +26,22 @@
 	"source.monogon.dev/metropolis/pkg/logbuffer"
 )
 
+type leveledPublisher struct {
+	node  *node
+	depth int
+}
+
 // 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) {
-	return l.nodeByDN(dn)
+	node, err := l.nodeByDN(dn)
+	if err != nil {
+		return nil, err
+	}
+	return &leveledPublisher{
+		node:  node,
+		depth: 0,
+	}, nil
 }
 
 func (l *LogTree) RawFor(dn DN) (io.Writer, error) {
@@ -86,24 +98,24 @@
 // data sources into leveled logging - see ExternelLeveledPayload for more
 // information.
 func LogExternalLeveled(l LeveledLogger, e *ExternalLeveledPayload) error {
-	n, ok := l.(*node)
+	publisher, ok := l.(*leveledPublisher)
 	if !ok {
-		return fmt.Errorf("the given LeveledLogger is not a logtree node")
+		return fmt.Errorf("the given LeveledLogger is not a *leveledPublisher")
 	}
 	p := e.sanitize()
 	entry := &entry{
-		origin:  n.dn,
+		origin:  publisher.node.dn,
 		leveled: p,
 	}
-	n.tree.journal.append(entry)
-	n.tree.journal.notify(entry)
+	publisher.node.tree.journal.append(entry)
+	publisher.node.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.
-func (n *node) logLeveled(depth int, severity Severity, msg string) {
+func (l *leveledPublisher) logLeveled(depth int, severity Severity, msg string) {
 	_, file, line, ok := runtime.Caller(2 + depth)
 	if !ok {
 		file = "???"
@@ -126,58 +138,65 @@
 		line:      line,
 	}
 	e := &entry{
-		origin:  n.dn,
+		origin:  l.node.dn,
 		leveled: p,
 	}
-	n.tree.journal.append(e)
-	n.tree.journal.notify(e)
+	l.node.tree.journal.append(e)
+	l.node.tree.journal.notify(e)
 }
 
 // Info implements the LeveledLogger interface.
-func (n *node) Info(args ...interface{}) {
-	n.logLeveled(0, INFO, fmt.Sprint(args...))
+func (l *leveledPublisher) Info(args ...interface{}) {
+	l.logLeveled(l.depth, INFO, fmt.Sprint(args...))
 }
 
 // Infof implements the LeveledLogger interface.
-func (n *node) Infof(format string, args ...interface{}) {
-	n.logLeveled(0, INFO, fmt.Sprintf(format, args...))
+func (l *leveledPublisher) Infof(format string, args ...interface{}) {
+	l.logLeveled(l.depth, INFO, fmt.Sprintf(format, args...))
 }
 
 // Warning implements the LeveledLogger interface.
-func (n *node) Warning(args ...interface{}) {
-	n.logLeveled(0, WARNING, fmt.Sprint(args...))
+func (l *leveledPublisher) Warning(args ...interface{}) {
+	l.logLeveled(l.depth, WARNING, fmt.Sprint(args...))
 }
 
 // Warningf implements the LeveledLogger interface.
-func (n *node) Warningf(format string, args ...interface{}) {
-	n.logLeveled(0, WARNING, fmt.Sprintf(format, args...))
+func (l *leveledPublisher) Warningf(format string, args ...interface{}) {
+	l.logLeveled(l.depth, WARNING, fmt.Sprintf(format, args...))
 }
 
 // Error implements the LeveledLogger interface.
-func (n *node) Error(args ...interface{}) {
-	n.logLeveled(0, ERROR, fmt.Sprint(args...))
+func (l *leveledPublisher) Error(args ...interface{}) {
+	l.logLeveled(l.depth, ERROR, fmt.Sprint(args...))
 }
 
 // Errorf implements the LeveledLogger interface.
-func (n *node) Errorf(format string, args ...interface{}) {
-	n.logLeveled(0, ERROR, fmt.Sprintf(format, args...))
+func (l *leveledPublisher) Errorf(format string, args ...interface{}) {
+	l.logLeveled(l.depth, ERROR, fmt.Sprintf(format, args...))
 }
 
 // Fatal implements the LeveledLogger interface.
-func (n *node) Fatal(args ...interface{}) {
-	n.logLeveled(0, FATAL, fmt.Sprint(args...))
+func (l *leveledPublisher) Fatal(args ...interface{}) {
+	l.logLeveled(l.depth, FATAL, fmt.Sprint(args...))
 }
 
 // Fatalf implements the LeveledLogger interface.
-func (n *node) Fatalf(format string, args ...interface{}) {
-	n.logLeveled(0, FATAL, fmt.Sprintf(format, args...))
+func (l *leveledPublisher) Fatalf(format string, args ...interface{}) {
+	l.logLeveled(l.depth, FATAL, fmt.Sprintf(format, args...))
+}
+
+// WithAddedStackDepth impleemnts the LeveledLogger interface.
+func (l *leveledPublisher) WithAddedStackDepth(depth int) LeveledLogger {
+	l2 := *l
+	l2.depth += depth
+	return &l2
 }
 
 // V implements the LeveledLogger interface.
-func (n *node) V(v VerbosityLevel) VerboseLeveledLogger {
+func (l *leveledPublisher) V(v VerbosityLevel) VerboseLeveledLogger {
 	return &verbose{
-		node:    n,
-		enabled: n.verbosity >= v,
+		publisher: l,
+		enabled:   l.node.verbosity >= v,
 	}
 }
 
@@ -186,8 +205,9 @@
 // VerboseLeveledLoggers must be short lived, as a changed in verbosity will not
 // affect all already existing VerboseLeveledLoggers.
 type verbose struct {
-	node    *node
-	enabled bool
+	publisher *leveledPublisher
+	node      *node
+	enabled   bool
 }
 
 func (v *verbose) Enabled() bool {
@@ -198,12 +218,12 @@
 	if !v.enabled {
 		return
 	}
-	v.node.logLeveled(0, INFO, fmt.Sprint(args...))
+	v.publisher.logLeveled(v.publisher.depth, INFO, fmt.Sprint(args...))
 }
 
 func (v *verbose) Infof(format string, args ...interface{}) {
 	if !v.enabled {
 		return
 	}
-	v.node.logLeveled(0, INFO, fmt.Sprintf(format, args...))
+	v.publisher.logLeveled(v.publisher.depth, INFO, fmt.Sprintf(format, args...))
 }
diff --git a/metropolis/pkg/logtree/logtree_test.go b/metropolis/pkg/logtree/logtree_test.go
index b900201..315dbc3 100644
--- a/metropolis/pkg/logtree/logtree_test.go
+++ b/metropolis/pkg/logtree/logtree_test.go
@@ -209,3 +209,31 @@
 		t.Fatalf("wanted entry %q, got %q", want, got)
 	}
 }
+
+func TestAddedStackDepth(t *testing.T) {
+	tree := New()
+	helper := func(msg string) {
+		tree.MustLeveledFor("main").WithAddedStackDepth(1).Infof("oh no: %s", msg)
+	}
+
+	// The next three lines are tested to be next to each other.
+	helper("it failed")
+	tree.MustLeveledFor("main").Infof("something else")
+
+	reader, err := tree.Read("main", WithBacklog(BacklogAllAvailable))
+	if err != nil {
+		t.Fatalf("Read: %v", err)
+	}
+	if want, got := 2, len(reader.Backlog); want != got {
+		t.Fatalf("wanted %d entries, got %d", want, got)
+	}
+	if want, got := "oh no: it failed", reader.Backlog[0].Leveled.MessagesJoined(); want != got {
+		t.Errorf("wanted entry %q, got %q", want, got)
+	}
+	if want, got := "something else", reader.Backlog[1].Leveled.MessagesJoined(); want != got {
+		t.Errorf("wanted entry %q, got %q", want, got)
+	}
+	if first, second := reader.Backlog[0].Leveled.line, reader.Backlog[1].Leveled.line; first+1 != second {
+		t.Errorf("first entry at %d, second at %d, wanted one after the other", first, second)
+	}
+}