Added logbuffer helper package
This adds a small package which is used to store logs for other
binaries we're calling. It's an in-memory non-depleting ring buffer
capable of directly taking in a stream (for example from stdout).
For reliability it has bounded total memory consumption.
It offers a simple interface to get the last n log lines out.
Test Plan: Has 100% test coverage built-in
Bug: T667
X-Origin-Diff: phab/D442
GitOrigin-RevId: 32d5944650793b6cea8ec48a40ea4abb3944ad21
diff --git a/core/pkg/logbuffer/logbuffer.go b/core/pkg/logbuffer/logbuffer.go
new file mode 100644
index 0000000..8298deb
--- /dev/null
+++ b/core/pkg/logbuffer/logbuffer.go
@@ -0,0 +1,146 @@
+// 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 logbuffer implements a fixed-size in-memory ring buffer for line-separated logs.
+// It implements io.Writer and splits the data into lines. The lines are kept in a ring where the
+// oldest are overwritten once it's full. It allows retrieval of the last n lines. There is a built-in
+// line length limit to bound the memory usage at maxLineLength * size.
+package logbuffer
+
+import (
+ "bytes"
+ "strings"
+ "sync"
+)
+
+// LogBuffer implements a fixed-size in-memory ring buffer for line-separated logs
+type LogBuffer struct {
+ mu sync.RWMutex
+ maxLineLength int
+ content []Line
+ length int
+
+ currentLineBuilder strings.Builder
+ currentLineWrittenLength int
+}
+
+type Line struct {
+ Data string
+ OriginalLength int
+}
+
+func New(size, maxLineLength int) *LogBuffer {
+ return &LogBuffer{
+ content: make([]Line, size),
+ maxLineLength: maxLineLength,
+ }
+}
+
+func (b *LogBuffer) writeLimited(newData []byte) {
+ builder := &b.currentLineBuilder
+ b.currentLineWrittenLength += len(newData)
+ if builder.Len()+len(newData) > b.maxLineLength {
+ builder.Write(newData[:b.maxLineLength-builder.Len()])
+ } else {
+ builder.Write(newData)
+ }
+}
+
+func (b *LogBuffer) commitLine() {
+ b.content[b.length%len(b.content)] = Line{
+ Data: b.currentLineBuilder.String(),
+ OriginalLength: b.currentLineWrittenLength}
+ b.length++
+ b.currentLineBuilder.Reset()
+ b.currentLineWrittenLength = 0
+}
+
+func (b *LogBuffer) Write(data []byte) (int, error) {
+ var pos int = 0
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ for {
+ nextNewline := bytes.IndexRune(data[pos:], '\n')
+
+ // No newline in the data, write everything to the current line
+ if nextNewline == -1 {
+ b.writeLimited(data[pos:])
+ break
+ }
+
+ // Write this line and update position
+ b.writeLimited(data[pos : pos+nextNewline])
+ b.commitLine()
+ pos += nextNewline + 1
+
+ // Data ends with a newline, stop now without writing an empty line
+ if nextNewline == len(data)-1 {
+ break
+ }
+ }
+ return len(data), nil
+}
+
+// capToContentLength caps the number of requested lines to what is actually available
+func (b *LogBuffer) capToContentLength(n int) int {
+ // If there aren't enough lines to read, reduce the request size
+ if n > b.length {
+ n = b.length
+ }
+ // If there isn't enough ringbuffer space, reduce the request size
+ if n > len(b.content) {
+ n = len(b.content)
+ }
+ return n
+}
+
+// ReadLines reads the last n lines from the buffer in chronological order. If n is bigger than the
+// ring buffer or the number of available lines only the number of stored lines are returned.
+func (b *LogBuffer) ReadLines(n int) []Line {
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ n = b.capToContentLength(n)
+
+ // Copy references out to keep them around
+ outArray := make([]Line, n)
+ for i := 1; i <= n; i++ {
+ outArray[n-i] = b.content[(b.length-i)%len(b.content)]
+ }
+ return outArray
+}
+
+// ReadLinesTruncated works exactly the same as ReadLines, but adds an ellipsis at the end of every
+// line that was truncated because it was over MaxLineLength
+func (b *LogBuffer) ReadLinesTruncated(n int, ellipsis string) []string {
+ // This does not use ReadLines() to prevent excessive reference copying and associated GC pressure
+ // since it could process a lot of lines.
+
+ n = b.capToContentLength(n)
+
+ outArray := make([]string, n)
+ for i := 1; i <= n; i++ {
+ line := b.content[(b.length-i)%len(b.content)]
+ if line.OriginalLength > b.maxLineLength {
+ outArray[n-i] = line.Data + ellipsis
+ } else {
+ outArray[n-i] = line.Data
+ }
+ }
+ return outArray
+}