blob: 5d1590d0bea99af44a60fb0c4f8e705a13196a85 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanski248b2ec2020-10-26 15:55:51 +01002// SPDX-License-Identifier: Apache-2.0
Serge Bazanski248b2ec2020-10-26 15:55:51 +01003
4package logbuffer
5
6import (
7 "bytes"
8 "fmt"
9 "strings"
10 "sync"
Serge Bazanskib0272182020-11-02 18:39:44 +010011
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020012 lpb "source.monogon.dev/osbase/logtree/proto"
Serge Bazanski248b2ec2020-10-26 15:55:51 +010013)
14
Serge Bazanski216fe7b2021-05-21 18:36:16 +020015// Line is a line stored in the log buffer - a string, that has been perhaps
16// truncated (due to exceeded limits).
Serge Bazanski248b2ec2020-10-26 15:55:51 +010017type Line struct {
18 Data string
19 OriginalLength int
20}
21
22// Truncated returns whether this line has been truncated to fit limits.
23func (l *Line) Truncated() bool {
24 return l.OriginalLength > len(l.Data)
25}
26
Serge Bazanski216fe7b2021-05-21 18:36:16 +020027// String returns the line with an ellipsis at the end (...) if the line has been
28// truncated, or the original line otherwise.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010029func (l *Line) String() string {
30 if l.Truncated() {
31 return l.Data + "..."
32 }
33 return l.Data
34}
35
Serge Bazanskib0272182020-11-02 18:39:44 +010036// ProtoLog returns a Logging-specific protobuf structure.
Tim Windelschmidt8814f522024-05-08 00:41:13 +020037func (l *Line) ProtoLog() *lpb.LogEntry_Raw {
38 return &lpb.LogEntry_Raw{
Serge Bazanskib0272182020-11-02 18:39:44 +010039 Data: l.Data,
40 OriginalLength: int64(l.OriginalLength),
41 }
42}
43
44// LineFromLogProto converts a Logging-specific protobuf message back into a Line.
Tim Windelschmidt8814f522024-05-08 00:41:13 +020045func LineFromLogProto(raw *lpb.LogEntry_Raw) (*Line, error) {
Serge Bazanskib0272182020-11-02 18:39:44 +010046 if raw.OriginalLength < int64(len(raw.Data)) {
47 return nil, fmt.Errorf("original_length smaller than length of data")
48 }
49 originalLength := int(raw.OriginalLength)
50 if int64(originalLength) < raw.OriginalLength {
51 return nil, fmt.Errorf("original_length larger than native int size")
52 }
53 return &Line{
54 Data: raw.Data,
55 OriginalLength: originalLength,
56 }, nil
57}
58
Serge Bazanski216fe7b2021-05-21 18:36:16 +020059// LineBuffer is a io.WriteCloser that will call a given callback every time a line
60// is completed.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010061type LineBuffer struct {
62 maxLineLength int
63 cb LineBufferCallback
64
65 mu sync.Mutex
66 cur strings.Builder
Serge Bazanski216fe7b2021-05-21 18:36:16 +020067 // length is the length of the line currently being written - this will continue to
68 // increase, even if the string exceeds maxLineLength.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010069 length int
70 closed bool
71}
72
Serge Bazanski216fe7b2021-05-21 18:36:16 +020073// LineBufferCallback is a callback that will get called any time the line is
74// completed. The function must not cause another write to the LineBuffer, or the
75// program will deadlock.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010076type LineBufferCallback func(*Line)
77
Serge Bazanski216fe7b2021-05-21 18:36:16 +020078// NewLineBuffer creates a new LineBuffer with a given line length limit and
79// callback.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010080func NewLineBuffer(maxLineLength int, cb LineBufferCallback) *LineBuffer {
81 return &LineBuffer{
82 maxLineLength: maxLineLength,
83 cb: cb,
84 }
85}
86
Serge Bazanski216fe7b2021-05-21 18:36:16 +020087// writeLimited writes to the internal buffer, making sure that its size does not
88// exceed the maxLineLength.
Serge Bazanski248b2ec2020-10-26 15:55:51 +010089func (l *LineBuffer) writeLimited(data []byte) {
90 l.length += len(data)
91 if l.cur.Len()+len(data) > l.maxLineLength {
92 data = data[:l.maxLineLength-l.cur.Len()]
93 }
94 l.cur.Write(data)
95}
96
97// comitLine calls the callback and resets the builder.
98func (l *LineBuffer) commitLine() {
99 l.cb(&Line{
100 Data: l.cur.String(),
101 OriginalLength: l.length,
102 })
103 l.cur.Reset()
104 l.length = 0
105}
106
107func (l *LineBuffer) Write(data []byte) (int, error) {
108 var pos = 0
109
110 l.mu.Lock()
111 defer l.mu.Unlock()
112
113 if l.closed {
114 return 0, fmt.Errorf("closed")
115 }
116
117 for {
118 nextNewline := bytes.IndexRune(data[pos:], '\n')
119
120 // No newline in the data, write everything to the current line
121 if nextNewline == -1 {
122 l.writeLimited(data[pos:])
123 break
124 }
125
126 // Write this line and update position
127 l.writeLimited(data[pos : pos+nextNewline])
128 l.commitLine()
129 pos += nextNewline + 1
130
131 // Data ends with a newline, stop now without writing an empty line
132 if nextNewline == len(data)-1 {
133 break
134 }
135 }
136 return len(data), nil
137}
138
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200139// Close will emit any leftover data in the buffer to the callback. Subsequent
140// calls to Write will fail. Subsequent calls to Close will also fail.
Serge Bazanski248b2ec2020-10-26 15:55:51 +0100141func (l *LineBuffer) Close() error {
142 if l.closed {
143 return fmt.Errorf("already closed")
144 }
145 l.mu.Lock()
146 defer l.mu.Unlock()
147 l.closed = true
148 if l.length > 0 {
149 l.commitLine()
150 }
151 return nil
152}
Serge Bazanski6c8ee0b2023-04-05 12:29:57 +0200153
154func (l *LineBuffer) Sync() error {
155 return nil
156}