Skip to content

Commit 32c03ed

Browse files
committed
Add TriggerLevelWriter.
See: rs#583
1 parent bb14b8b commit 32c03ed

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

globals.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ var (
132132
FatalLevel: "FTL",
133133
PanicLevel: "PNC",
134134
}
135+
136+
// TriggerLevelWriterBufferReuseLimit is a limit in bytes that a buffer is dropped
137+
// from the TriggerLevelWriter buffer pool if the buffer grows above the limit.
138+
TriggerLevelWriterBufferReuseLimit = 64 * 1024
135139
)
136140

137141
var (

writer.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,135 @@ func (w *FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error) {
180180
}
181181
return len(p), nil
182182
}
183+
184+
var triggerWriterPool = &sync.Pool{
185+
New: func() interface{} {
186+
return bytes.NewBuffer(make([]byte, 0, 1024))
187+
},
188+
}
189+
190+
// TriggerLevelWriter buffers log lines at the ConditionalLevel or below
191+
// until a trigger level (or higher) line is emitted. Log lines with level
192+
// higher than ConditionalLevel are always written out to the destination
193+
// writer. If trigger never happens, buffered log lines are never written out.
194+
//
195+
// It can be used to configure "log level per request".
196+
type TriggerLevelWriter struct {
197+
// Destination writer. If LevelWriter is provided (usually), its WriteLevel is used
198+
// instead of Write.
199+
io.Writer
200+
201+
// ConditionalLevel is the level (and below) at which lines are buffered until
202+
// a trigger level (or higher) line is emitted. Usually this is set to DebugLevel.
203+
ConditionalLevel Level
204+
205+
// TriggerLevel is the lowest level that triggers the sending of the conditional
206+
// level lines. Usually this is set to ErrorLevel.
207+
TriggerLevel Level
208+
209+
buf *bytes.Buffer
210+
triggered bool
211+
mu sync.Mutex
212+
}
213+
214+
func (w *TriggerLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) {
215+
w.mu.Lock()
216+
defer w.mu.Unlock()
217+
218+
// At first trigger level or above log line, we flush the buffer and change the
219+
// trigger state to triggered.
220+
if !w.triggered && l >= w.TriggerLevel {
221+
err := w.trigger()
222+
if err != nil {
223+
return 0, err
224+
}
225+
}
226+
227+
// Unless triggered, we buffer everything at and below ConditionalLevel.
228+
if !w.triggered && l <= w.ConditionalLevel {
229+
if w.buf == nil {
230+
w.buf = triggerWriterPool.Get().(*bytes.Buffer)
231+
}
232+
233+
// We prefix each log line with a byte with the level.
234+
// Hopefully we will never have a level value which equals a newline
235+
// (which could interfere with reconstruction of log lines in the trigger method).
236+
w.buf.WriteByte(byte(l))
237+
w.buf.Write(p)
238+
return len(p), nil
239+
}
240+
241+
// Anything above ConditionalLevel is always passed through.
242+
// Once triggered, everything is passed through.
243+
if lw, ok := w.Writer.(LevelWriter); ok {
244+
return lw.WriteLevel(l, p)
245+
}
246+
return w.Write(p)
247+
}
248+
249+
// trigger expects lock to be held.
250+
func (w *TriggerLevelWriter) trigger() error {
251+
if w.triggered {
252+
return nil
253+
}
254+
w.triggered = true
255+
256+
if w.buf == nil {
257+
return nil
258+
}
259+
260+
p := w.buf.Bytes()
261+
for len(p) > 0 {
262+
// We do not use bufio.Scanner here because we already have full buffer
263+
// in the memory and we do not want extra copying from the buffer to
264+
// scanner's token slice, nor we want to hit scanner's token size limit,
265+
// and we also want to preserve newlines.
266+
i := bytes.IndexByte(p, '\n')
267+
line := p[0 : i+1]
268+
p = p[i+1:]
269+
// We prefixed each log line with a byte with the level.
270+
level := Level(line[0])
271+
line = line[1:]
272+
var err error
273+
if lw, ok := w.Writer.(LevelWriter); ok {
274+
_, err = lw.WriteLevel(level, line)
275+
} else {
276+
_, err = w.Write(line)
277+
}
278+
if err != nil {
279+
return err
280+
}
281+
}
282+
283+
return nil
284+
}
285+
286+
// Trigger forces flushing the buffer and change the trigger state to
287+
// triggered, if the writer has not already been triggered before.
288+
func (w *TriggerLevelWriter) Trigger() error {
289+
w.mu.Lock()
290+
defer w.mu.Unlock()
291+
292+
return w.trigger()
293+
}
294+
295+
// Close closes the writer and returns the buffer to the pool.
296+
func (w *TriggerLevelWriter) Close() error {
297+
w.mu.Lock()
298+
defer w.mu.Unlock()
299+
300+
if w.buf == nil {
301+
return nil
302+
}
303+
304+
// We return the buffer only if it has not grown above the limit.
305+
// This prevents accumulation of large buffers in the pool just
306+
// because occasionally a large buffer might be needed.
307+
if w.buf.Cap() <= TriggerLevelWriterBufferReuseLimit {
308+
w.buf.Reset()
309+
triggerWriterPool.Put(w.buf)
310+
}
311+
w.buf = nil
312+
313+
return nil
314+
}

writer_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,58 @@ func TestFilteredLevelWriter(t *testing.T) {
195195
t.Errorf("Expected %q, got %q.", want, p)
196196
}
197197
}
198+
199+
type testWrite struct {
200+
Level
201+
Line []byte
202+
}
203+
204+
func TestTriggerLevelWriter(t *testing.T) {
205+
tests := []struct {
206+
write []testWrite
207+
want []byte
208+
all []byte
209+
}{{
210+
[]testWrite{
211+
{DebugLevel, []byte("no\n")},
212+
{InfoLevel, []byte("yes\n")},
213+
},
214+
[]byte("yes\n"),
215+
[]byte("yes\nno\n"),
216+
}, {
217+
[]testWrite{
218+
{DebugLevel, []byte("yes1\n")},
219+
{InfoLevel, []byte("yes2\n")},
220+
{ErrorLevel, []byte("yes3\n")},
221+
{DebugLevel, []byte("yes4\n")},
222+
},
223+
[]byte("yes2\nyes1\nyes3\nyes4\n"),
224+
[]byte("yes2\nyes1\nyes3\nyes4\n"),
225+
}}
226+
227+
for k, tt := range tests {
228+
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
229+
buf := bytes.Buffer{}
230+
writer := TriggerLevelWriter{Writer: LevelWriterAdapter{&buf}, ConditionalLevel: DebugLevel, TriggerLevel: ErrorLevel}
231+
t.Cleanup(func() { writer.Close() })
232+
for _, w := range tt.write {
233+
_, err := writer.WriteLevel(w.Level, w.Line)
234+
if err != nil {
235+
t.Error(err)
236+
}
237+
}
238+
p := buf.Bytes()
239+
if want := tt.want; !bytes.Equal([]byte(want), p) {
240+
t.Errorf("Expected %q, got %q.", want, p)
241+
}
242+
err := writer.Trigger()
243+
if err != nil {
244+
t.Error(err)
245+
}
246+
p = buf.Bytes()
247+
if want := tt.all; !bytes.Equal([]byte(want), p) {
248+
t.Errorf("Expected %q, got %q.", want, p)
249+
}
250+
})
251+
}
252+
}

0 commit comments

Comments
 (0)