osbase/logtree: add WithStartPosition option

To allow users to not always request all messages,
we introduce another option to the logtree.LogReader
which allows for starting at a specific global log id.
This, for example, makes implementing scrollback easier.

Change-Id: I1773288f670f476706d94baf3f052fe1e5da9eb0
Reviewed-on: https://review.monogon.dev/c/monogon/+/4452
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/osbase/logtree/logtree_access.go b/osbase/logtree/logtree_access.go
index 1582a8f..1b4f90d 100644
--- a/osbase/logtree/logtree_access.go
+++ b/osbase/logtree/logtree_access.go
@@ -10,6 +10,13 @@
 	"source.monogon.dev/go/logging"
 )
 
+type ReadDirection int
+
+const (
+	ReadDirectionAfter ReadDirection = iota
+	ReadDirectionBefore
+)
+
 // LogReadOption describes options for the LogTree.Read call.
 type LogReadOption func(*logReaderOptions)
 
@@ -21,6 +28,8 @@
 	onlyRaw                    bool
 	leveledWithMinimumSeverity logging.Severity
 	withStreamBufferSize       int
+	withStartPosition          int
+	startPositionReadDirection ReadDirection
 }
 
 // WithChildren makes Read return/stream data for both a given DN and all its
@@ -54,6 +63,20 @@
 	return func(lro *logReaderOptions) { lro.withBacklog = count }
 }
 
+// WithStartPosition makes Read return log entries from the given position.
+// It requires WithBacklog to be provided.
+//
+// The Journal keeps a global counter for all logs, starting at 0 for the
+// first message. Based on this the user can read entries
+// (based on the ReadDirection option) either after or before the given
+// position.
+func WithStartPosition(pos int, direction ReadDirection) LogReadOption {
+	return func(lro *logReaderOptions) {
+		lro.withStartPosition = pos
+		lro.startPositionReadDirection = direction
+	}
+}
+
 // BacklogAllAvailable makes WithBacklog return all backlogged log data that
 // logtree possesses.
 const BacklogAllAvailable int = -1
@@ -107,7 +130,8 @@
 }
 
 var (
-	ErrRawAndLeveled = errors.New("cannot return logs that are simultaneously OnlyRaw and OnlyLeveled")
+	ErrRawAndLeveled               = errors.New("cannot return logs that are simultaneously OnlyRaw and OnlyLeveled")
+	ErrStartPositionWithoutBacklog = errors.New("cannot return logs that are WithStartingPosition and missing WithBacklog")
 )
 
 // Read and/or stream entries from a LogTree. The returned LogReader is influenced
@@ -121,6 +145,7 @@
 
 	lro := logReaderOptions{
 		withStreamBufferSize: 128,
+		withStartPosition:    -1,
 	}
 
 	for _, opt := range opts {
@@ -131,7 +156,15 @@
 		return nil, ErrRawAndLeveled
 	}
 
+	isWithBacklog := lro.withBacklog > 0 || lro.withBacklog == BacklogAllAvailable
+	if lro.withStartPosition != -1 && !isWithBacklog {
+		return nil, ErrStartPositionWithoutBacklog
+	}
+
 	var filters []filter
+	if lro.withStartPosition != -1 {
+		filters = append(filters, filterStartPosition(lro.withBacklog, lro.withStartPosition, lro.startPositionReadDirection))
+	}
 	if lro.onlyLeveled {
 		filters = append(filters, filterOnlyLeveled)
 	}
@@ -148,7 +181,7 @@
 	}
 
 	var entries []*entry
-	if lro.withBacklog > 0 || lro.withBacklog == BacklogAllAvailable {
+	if isWithBacklog {
 		if lro.withChildren {
 			entries = l.journal.scanEntries(lro.withBacklog, filters...)
 		} else {