blob: 6ee7d6b316bf0ae2ae07f9c7b95e53bc5a583cf5 [file] [log] [blame]
Serge Bazanski248b2ec2020-10-26 15:55: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
17package logbuffer
18
19import (
20 "bytes"
21 "fmt"
22 "strings"
23 "sync"
24)
25
26// Line is a line stored in the log buffer - a string, that has been perhaps truncated (due to exceeded limits).
27type Line struct {
28 Data string
29 OriginalLength int
30}
31
32// Truncated returns whether this line has been truncated to fit limits.
33func (l *Line) Truncated() bool {
34 return l.OriginalLength > len(l.Data)
35}
36
37// String returns the line with an ellipsis at the end (...) if the line has been truncated, or the original line
38// otherwise.
39func (l *Line) String() string {
40 if l.Truncated() {
41 return l.Data + "..."
42 }
43 return l.Data
44}
45
46// LineBuffer is a io.WriteCloser that will call a given callback every time a line is completed.
47type LineBuffer struct {
48 maxLineLength int
49 cb LineBufferCallback
50
51 mu sync.Mutex
52 cur strings.Builder
53 // length is the length of the line currently being written - this will continue to increase, even if the string
54 // exceeds maxLineLength.
55 length int
56 closed bool
57}
58
59// LineBufferCallback is a callback that will get called any time the line is completed. The function must not cause another
60// write to the LineBuffer, or the program will deadlock.
61type LineBufferCallback func(*Line)
62
63// NewLineBuffer creates a new LineBuffer with a given line length limit and callback.
64func NewLineBuffer(maxLineLength int, cb LineBufferCallback) *LineBuffer {
65 return &LineBuffer{
66 maxLineLength: maxLineLength,
67 cb: cb,
68 }
69}
70
71// writeLimited writes to the internal buffer, making sure that its size does not exceed the maxLineLength.
72func (l *LineBuffer) writeLimited(data []byte) {
73 l.length += len(data)
74 if l.cur.Len()+len(data) > l.maxLineLength {
75 data = data[:l.maxLineLength-l.cur.Len()]
76 }
77 l.cur.Write(data)
78}
79
80// comitLine calls the callback and resets the builder.
81func (l *LineBuffer) commitLine() {
82 l.cb(&Line{
83 Data: l.cur.String(),
84 OriginalLength: l.length,
85 })
86 l.cur.Reset()
87 l.length = 0
88}
89
90func (l *LineBuffer) Write(data []byte) (int, error) {
91 var pos = 0
92
93 l.mu.Lock()
94 defer l.mu.Unlock()
95
96 if l.closed {
97 return 0, fmt.Errorf("closed")
98 }
99
100 for {
101 nextNewline := bytes.IndexRune(data[pos:], '\n')
102
103 // No newline in the data, write everything to the current line
104 if nextNewline == -1 {
105 l.writeLimited(data[pos:])
106 break
107 }
108
109 // Write this line and update position
110 l.writeLimited(data[pos : pos+nextNewline])
111 l.commitLine()
112 pos += nextNewline + 1
113
114 // Data ends with a newline, stop now without writing an empty line
115 if nextNewline == len(data)-1 {
116 break
117 }
118 }
119 return len(data), nil
120}
121
122// Close will emit any leftover data in the buffer to the callback. Subsequent calls to Write will fail. Subsequent calls to Close
123// will also fail.
124func (l *LineBuffer) Close() error {
125 if l.closed {
126 return fmt.Errorf("already closed")
127 }
128 l.mu.Lock()
129 defer l.mu.Unlock()
130 l.closed = true
131 if l.length > 0 {
132 l.commitLine()
133 }
134 return nil
135}