blob: 92b70e9f46488f9f1854130b65aa606c07ea87a7 [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"
Serge Bazanskib0272182020-11-02 18:39:44 +010024
Serge Bazanskida114862023-03-29 17:46:42 +020025 cpb "source.monogon.dev/metropolis/proto/common"
Serge Bazanski248b2ec2020-10-26 15:55:51 +010026)
27
Serge Bazanski216fe7b2021-05-21 18:36:16 +020028// Line is a line stored in the log buffer - a string, that has been perhaps
29// truncated (due to exceeded limits).
Serge Bazanski248b2ec2020-10-26 15:55:51 +010030type Line struct {
31 Data string
32 OriginalLength int
33}
34
35// Truncated returns whether this line has been truncated to fit limits.
36func (l *Line) Truncated() bool {
37 return l.OriginalLength > len(l.Data)
38}
39
Serge Bazanski216fe7b2021-05-21 18:36:16 +020040// String returns the line with an ellipsis at the end (...) if the line has been
41// truncated, or the original line otherwise.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010042func (l *Line) String() string {
43 if l.Truncated() {
44 return l.Data + "..."
45 }
46 return l.Data
47}
48
Serge Bazanskib0272182020-11-02 18:39:44 +010049// ProtoLog returns a Logging-specific protobuf structure.
Serge Bazanskida114862023-03-29 17:46:42 +020050func (l *Line) ProtoLog() *cpb.LogEntry_Raw {
51 return &cpb.LogEntry_Raw{
Serge Bazanskib0272182020-11-02 18:39:44 +010052 Data: l.Data,
53 OriginalLength: int64(l.OriginalLength),
54 }
55}
56
57// LineFromLogProto converts a Logging-specific protobuf message back into a Line.
Serge Bazanskida114862023-03-29 17:46:42 +020058func LineFromLogProto(raw *cpb.LogEntry_Raw) (*Line, error) {
Serge Bazanskib0272182020-11-02 18:39:44 +010059 if raw.OriginalLength < int64(len(raw.Data)) {
60 return nil, fmt.Errorf("original_length smaller than length of data")
61 }
62 originalLength := int(raw.OriginalLength)
63 if int64(originalLength) < raw.OriginalLength {
64 return nil, fmt.Errorf("original_length larger than native int size")
65 }
66 return &Line{
67 Data: raw.Data,
68 OriginalLength: originalLength,
69 }, nil
70}
71
Serge Bazanski216fe7b2021-05-21 18:36:16 +020072// LineBuffer is a io.WriteCloser that will call a given callback every time a line
73// is completed.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010074type LineBuffer struct {
75 maxLineLength int
76 cb LineBufferCallback
77
78 mu sync.Mutex
79 cur strings.Builder
Serge Bazanski216fe7b2021-05-21 18:36:16 +020080 // length is the length of the line currently being written - this will continue to
81 // increase, even if the string exceeds maxLineLength.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010082 length int
83 closed bool
84}
85
Serge Bazanski216fe7b2021-05-21 18:36:16 +020086// LineBufferCallback is a callback that will get called any time the line is
87// completed. The function must not cause another write to the LineBuffer, or the
88// program will deadlock.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010089type LineBufferCallback func(*Line)
90
Serge Bazanski216fe7b2021-05-21 18:36:16 +020091// NewLineBuffer creates a new LineBuffer with a given line length limit and
92// callback.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010093func NewLineBuffer(maxLineLength int, cb LineBufferCallback) *LineBuffer {
94 return &LineBuffer{
95 maxLineLength: maxLineLength,
96 cb: cb,
97 }
98}
99
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100// writeLimited writes to the internal buffer, making sure that its size does not
101// exceed the maxLineLength.
Serge Bazanski248b2ec2020-10-26 15:55:51 +0100102func (l *LineBuffer) writeLimited(data []byte) {
103 l.length += len(data)
104 if l.cur.Len()+len(data) > l.maxLineLength {
105 data = data[:l.maxLineLength-l.cur.Len()]
106 }
107 l.cur.Write(data)
108}
109
110// comitLine calls the callback and resets the builder.
111func (l *LineBuffer) commitLine() {
112 l.cb(&Line{
113 Data: l.cur.String(),
114 OriginalLength: l.length,
115 })
116 l.cur.Reset()
117 l.length = 0
118}
119
120func (l *LineBuffer) Write(data []byte) (int, error) {
121 var pos = 0
122
123 l.mu.Lock()
124 defer l.mu.Unlock()
125
126 if l.closed {
127 return 0, fmt.Errorf("closed")
128 }
129
130 for {
131 nextNewline := bytes.IndexRune(data[pos:], '\n')
132
133 // No newline in the data, write everything to the current line
134 if nextNewline == -1 {
135 l.writeLimited(data[pos:])
136 break
137 }
138
139 // Write this line and update position
140 l.writeLimited(data[pos : pos+nextNewline])
141 l.commitLine()
142 pos += nextNewline + 1
143
144 // Data ends with a newline, stop now without writing an empty line
145 if nextNewline == len(data)-1 {
146 break
147 }
148 }
149 return len(data), nil
150}
151
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200152// Close will emit any leftover data in the buffer to the callback. Subsequent
153// calls to Write will fail. Subsequent calls to Close will also fail.
Serge Bazanski248b2ec2020-10-26 15:55:51 +0100154func (l *LineBuffer) Close() error {
155 if l.closed {
156 return fmt.Errorf("already closed")
157 }
158 l.mu.Lock()
159 defer l.mu.Unlock()
160 l.closed = true
161 if l.length > 0 {
162 l.commitLine()
163 }
164 return nil
165}
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200166
167func (l *LineBuffer) Sync() error {
168 return nil
169}