From b7e96facbe57e180a9f7b5a8a75a537ea3475dfa Mon Sep 17 00:00:00 2001 From: zarzet Date: Thu, 16 Jul 2026 10:56:26 +0700 Subject: [PATCH] fix(logging): keep cursors valid after rollover --- .../log_progress_timeout_supplement_test.go | 35 +++++++++++ go_backend/logbuffer.go | 59 +++++++++++++++---- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/go_backend/log_progress_timeout_supplement_test.go b/go_backend/log_progress_timeout_supplement_test.go index e2529e4b..a37e5d5b 100644 --- a/go_backend/log_progress_timeout_supplement_test.go +++ b/go_backend/log_progress_timeout_supplement_test.go @@ -57,6 +57,41 @@ func TestLogBufferExportedHelpersAndRedaction(t *testing.T) { } } +func TestLogBufferCursorSurvivesRollover(t *testing.T) { + lb := &LogBuffer{ + entries: make([]LogEntry, 3), + maxSize: 3, + loggingEnabled: true, + } + for _, message := range []string{"one", "two", "three"} { + lb.Add("INFO", "Test", message) + } + initial, cursor := lb.getSince(0) + if cursor != 3 || len(initial) != 3 || initial[0].Message != "one" { + t.Fatalf("initial logs/cursor = %#v/%d", initial, cursor) + } + + lb.Add("INFO", "Test", "four") + newLogs, cursor := lb.getSince(cursor) + if cursor != 4 || len(newLogs) != 1 || newLogs[0].Message != "four" { + t.Fatalf("rollover logs/cursor = %#v/%d", newLogs, cursor) + } + + lb.Add("INFO", "Test", "five") + lb.Add("INFO", "Test", "six") + retained, cursor := lb.getSince(1) + if cursor != 6 || len(retained) != 3 || retained[0].Message != "four" || retained[2].Message != "six" { + t.Fatalf("retained logs/cursor = %#v/%d", retained, cursor) + } + + lb.Clear() + lb.Add("INFO", "Test", "seven") + afterClear, cursor := lb.getSince(6) + if cursor != 7 || len(afterClear) != 1 || afterClear[0].Message != "seven" { + t.Fatalf("after clear logs/cursor = %#v/%d", afterClear, cursor) + } +} + func TestProgressItemHelpersAndWriter(t *testing.T) { ClearAllItemProgress() itemID := "progress-writer" diff --git a/go_backend/logbuffer.go b/go_backend/logbuffer.go index 8279317e..a09332a4 100644 --- a/go_backend/logbuffer.go +++ b/go_backend/logbuffer.go @@ -19,6 +19,9 @@ type LogEntry struct { type LogBuffer struct { entries []LogEntry maxSize int + head int + count int + nextIndex int mu sync.RWMutex loggingEnabled bool } @@ -49,7 +52,7 @@ func sanitizeSensitiveLogText(message string) string { func GetLogBuffer() *LogBuffer { logBufferOnce.Do(func() { globalLogBuffer = &LogBuffer{ - entries: make([]LogEntry, 0, defaultLogBufferSize), + entries: make([]LogEntry, defaultLogBufferSize), maxSize: defaultLogBufferSize, loggingEnabled: false, } @@ -86,10 +89,23 @@ func (lb *LogBuffer) Add(level, tag, message string) { Message: message, } - if len(lb.entries) >= lb.maxSize { - lb.entries = lb.entries[1:] + if lb.maxSize <= 0 { + return } - lb.entries = append(lb.entries, entry) + if len(lb.entries) != lb.maxSize { + lb.entries = make([]LogEntry, lb.maxSize) + lb.head = 0 + lb.count = 0 + } + if lb.count < lb.maxSize { + index := (lb.head + lb.count) % lb.maxSize + lb.entries[index] = entry + lb.count++ + } else { + lb.entries[lb.head] = entry + lb.head = (lb.head + 1) % lb.maxSize + } + lb.nextIndex++ fmt.Printf("[%s] %s\n", tag, message) } @@ -98,7 +114,7 @@ func (lb *LogBuffer) GetAll() string { lb.mu.RLock() defer lb.mu.RUnlock() - jsonBytes, _ := json.Marshal(lb.entries) + jsonBytes, _ := json.Marshal(lb.snapshotLocked(0)) return string(jsonBytes) } @@ -106,27 +122,44 @@ func (lb *LogBuffer) getSince(index int) ([]LogEntry, int) { lb.mu.RLock() defer lb.mu.RUnlock() - if index < 0 { - index = 0 + earliest := lb.nextIndex - lb.count + if index < earliest { + index = earliest } - if index >= len(lb.entries) { - return []LogEntry{}, len(lb.entries) + if index >= lb.nextIndex { + return []LogEntry{}, lb.nextIndex } + return lb.snapshotLocked(index - earliest), lb.nextIndex +} - entries := lb.entries[index:] - return entries, len(lb.entries) +func (lb *LogBuffer) snapshotLocked(offset int) []LogEntry { + if offset < 0 { + offset = 0 + } + if offset >= lb.count { + return []LogEntry{} + } + result := make([]LogEntry, lb.count-offset) + for i := offset; i < lb.count; i++ { + result[i-offset] = lb.entries[(lb.head+i)%lb.maxSize] + } + return result } func (lb *LogBuffer) Clear() { lb.mu.Lock() defer lb.mu.Unlock() - lb.entries = lb.entries[:0] + for i := range lb.entries { + lb.entries[i] = LogEntry{} + } + lb.head = 0 + lb.count = 0 } func (lb *LogBuffer) Count() int { lb.mu.RLock() defer lb.mu.RUnlock() - return len(lb.entries) + return lb.count } func LogDebug(tag, format string, args ...any) {