blob: 8298debdc0a18857c96af0007eea4e73be723b3e [file] [log] [blame]
Lorenz Brun25b82a82020-03-23 20:27:51 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// Package logbuffer implements a fixed-size in-memory ring buffer for line-separated logs.
18// It implements io.Writer and splits the data into lines. The lines are kept in a ring where the
19// oldest are overwritten once it's full. It allows retrieval of the last n lines. There is a built-in
20// line length limit to bound the memory usage at maxLineLength * size.
21package logbuffer
22
23import (
24 "bytes"
25 "strings"
26 "sync"
27)
28
29// LogBuffer implements a fixed-size in-memory ring buffer for line-separated logs
30type LogBuffer struct {
31 mu sync.RWMutex
32 maxLineLength int
33 content []Line
34 length int
35
36 currentLineBuilder strings.Builder
37 currentLineWrittenLength int
38}
39
40type Line struct {
41 Data string
42 OriginalLength int
43}
44
45func New(size, maxLineLength int) *LogBuffer {
46 return &LogBuffer{
47 content: make([]Line, size),
48 maxLineLength: maxLineLength,
49 }
50}
51
52func (b *LogBuffer) writeLimited(newData []byte) {
53 builder := &b.currentLineBuilder
54 b.currentLineWrittenLength += len(newData)
55 if builder.Len()+len(newData) > b.maxLineLength {
56 builder.Write(newData[:b.maxLineLength-builder.Len()])
57 } else {
58 builder.Write(newData)
59 }
60}
61
62func (b *LogBuffer) commitLine() {
63 b.content[b.length%len(b.content)] = Line{
64 Data: b.currentLineBuilder.String(),
65 OriginalLength: b.currentLineWrittenLength}
66 b.length++
67 b.currentLineBuilder.Reset()
68 b.currentLineWrittenLength = 0
69}
70
71func (b *LogBuffer) Write(data []byte) (int, error) {
72 var pos int = 0
73
74 b.mu.Lock()
75 defer b.mu.Unlock()
76
77 for {
78 nextNewline := bytes.IndexRune(data[pos:], '\n')
79
80 // No newline in the data, write everything to the current line
81 if nextNewline == -1 {
82 b.writeLimited(data[pos:])
83 break
84 }
85
86 // Write this line and update position
87 b.writeLimited(data[pos : pos+nextNewline])
88 b.commitLine()
89 pos += nextNewline + 1
90
91 // Data ends with a newline, stop now without writing an empty line
92 if nextNewline == len(data)-1 {
93 break
94 }
95 }
96 return len(data), nil
97}
98
99// capToContentLength caps the number of requested lines to what is actually available
100func (b *LogBuffer) capToContentLength(n int) int {
101 // If there aren't enough lines to read, reduce the request size
102 if n > b.length {
103 n = b.length
104 }
105 // If there isn't enough ringbuffer space, reduce the request size
106 if n > len(b.content) {
107 n = len(b.content)
108 }
109 return n
110}
111
112// ReadLines reads the last n lines from the buffer in chronological order. If n is bigger than the
113// ring buffer or the number of available lines only the number of stored lines are returned.
114func (b *LogBuffer) ReadLines(n int) []Line {
115 b.mu.RLock()
116 defer b.mu.RUnlock()
117
118 n = b.capToContentLength(n)
119
120 // Copy references out to keep them around
121 outArray := make([]Line, n)
122 for i := 1; i <= n; i++ {
123 outArray[n-i] = b.content[(b.length-i)%len(b.content)]
124 }
125 return outArray
126}
127
128// ReadLinesTruncated works exactly the same as ReadLines, but adds an ellipsis at the end of every
129// line that was truncated because it was over MaxLineLength
130func (b *LogBuffer) ReadLinesTruncated(n int, ellipsis string) []string {
131 // This does not use ReadLines() to prevent excessive reference copying and associated GC pressure
132 // since it could process a lot of lines.
133
134 n = b.capToContentLength(n)
135
136 outArray := make([]string, n)
137 for i := 1; i <= n; i++ {
138 line := b.content[(b.length-i)%len(b.content)]
139 if line.OriginalLength > b.maxLineLength {
140 outArray[n-i] = line.Data + ellipsis
141 } else {
142 outArray[n-i] = line.Data
143 }
144 }
145 return outArray
146}