From 0b21b0711523448be1432af5a2e0cc148f01996e Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Mon, 15 Jun 2026 18:37:21 +0800 Subject: [PATCH] fix log cursor rotation gap --- internal/api/handlers/management/logs.go | 27 +++++++- internal/api/handlers/management/logs_test.go | 65 +++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/internal/api/handlers/management/logs.go b/internal/api/handlers/management/logs.go index ad15a741..b6de20e6 100644 --- a/internal/api/handlers/management/logs.go +++ b/internal/api/handlers/management/logs.go @@ -773,13 +773,34 @@ func locateLogCursorFile(files []string, cursor logCursor) (int, bool, error) { if cursor.File != defaultLogFileName || (cursor.Offset == 0 && cursor.Size == 0 && !deferEmptyMainMatch) { return 0, false, nil } + if cursor.Offset == 0 && cursor.Size == 0 { + for i := range files { + if filepath.Base(files[i]) == defaultLogFileName { + continue + } + if !logFileChangedAfterCursor(files[i], cursor) { + continue + } + matches, truncated, errMatch := logFileMatchesCursor(files[i], cursor) + if errMatch != nil { + if errors.Is(errMatch, os.ErrNotExist) { + continue + } + return 0, false, errMatch + } + if truncated { + continue + } + if matches { + return i, true, nil + } + } + return 0, false, nil + } for i := len(files) - 1; i >= 0; i-- { if filepath.Base(files[i]) == defaultLogFileName { continue } - if cursor.Offset == 0 && cursor.Size == 0 && !logFileChangedAfterCursor(files[i], cursor) { - continue - } matches, truncated, errMatch := logFileMatchesCursor(files[i], cursor) if errMatch != nil { if errors.Is(errMatch, os.ErrNotExist) { diff --git a/internal/api/handlers/management/logs_test.go b/internal/api/handlers/management/logs_test.go index f803b2cf..8c3e0ead 100644 --- a/internal/api/handlers/management/logs_test.go +++ b/internal/api/handlers/management/logs_test.go @@ -474,6 +474,71 @@ func TestGetLogsZeroOffsetCursorWithEmptyFileReadsAcrossRotation(t *testing.T) { } } +func TestGetLogsZeroOffsetCursorWithEmptyFileReadsAcrossTwoRotations(t *testing.T) { + dir := t.TempDir() + writeMainLog(t, dir, "") + initial := performGetLogs(t, newLogsTestHandler(dir, true), "/v0/management/logs?limit=1") + if initial.NextCursor == "" { + t.Fatal("initial next-cursor is empty") + } + cursor, errCursor := decodeLogCursor(initial.NextCursor) + if errCursor != nil { + t.Fatalf("decode initial cursor: %v", errCursor) + } + if cursor.Offset != 0 || cursor.Size != 0 { + t.Fatalf("cursor offset/size = %d/%d, want empty zero offset", cursor.Offset, cursor.Size) + } + + mainPath := filepath.Join(dir, defaultLogFileName) + firstRotatedPath := filepath.Join(dir, defaultLogFileName+".1") + secondRotatedPath := filepath.Join(dir, defaultLogFileName+".2") + firstModTime := time.Unix(0, cursorModTimeUnixNano(cursor)+int64(time.Second)) + secondModTime := time.Unix(0, cursorModTimeUnixNano(cursor)+2*int64(time.Second)) + + appendMainLog(t, dir, "first\n") + if err := os.Chtimes(mainPath, firstModTime, firstModTime); err != nil { + t.Fatalf("update first main log mtime: %v", err) + } + if err := os.Rename(mainPath, firstRotatedPath); err != nil { + t.Fatalf("rotate first main log: %v", err) + } + writeMainLog(t, dir, "second\n") + if err := os.Chtimes(mainPath, secondModTime, secondModTime); err != nil { + t.Fatalf("update second main log mtime: %v", err) + } + if err := os.Rename(firstRotatedPath, secondRotatedPath); err != nil { + t.Fatalf("advance first rotated log: %v", err) + } + if err := os.Rename(mainPath, firstRotatedPath); err != nil { + t.Fatalf("rotate second main log: %v", err) + } + writeMainLog(t, dir, "third\n") + + resp := performGetLogs(t, newLogsTestHandler(dir, true), "/v0/management/logs?cursor="+url.QueryEscape(initial.NextCursor)+"&limit=1") + if !reflect.DeepEqual(resp.Lines, []string{"first"}) { + t.Fatalf("lines = %#v, want oldest rotated line", resp.Lines) + } + if resp.CursorReset { + t.Fatal("cursor-reset = true, want false") + } + + next := performGetLogs(t, newLogsTestHandler(dir, true), "/v0/management/logs?cursor="+url.QueryEscape(resp.NextCursor)+"&limit=1") + if !reflect.DeepEqual(next.Lines, []string{"second"}) { + t.Fatalf("next lines = %#v, want newer rotated line", next.Lines) + } + if next.CursorReset { + t.Fatal("next cursor-reset = true, want false") + } + + latest := performGetLogs(t, newLogsTestHandler(dir, true), "/v0/management/logs?cursor="+url.QueryEscape(next.NextCursor)+"&limit=1") + if !reflect.DeepEqual(latest.Lines, []string{"third"}) { + t.Fatalf("latest lines = %#v, want main line", latest.Lines) + } + if latest.CursorReset { + t.Fatal("latest cursor-reset = true, want false") + } +} + func TestGetLogsZeroOffsetCursorWithEmptyFileResetsWhenRotationModTimeAmbiguous(t *testing.T) { dir := t.TempDir() mainPath := filepath.Join(dir, defaultLogFileName)