Fix ambiguous empty log cursor handling

This commit is contained in:
LTbinglingfeng
2026-06-15 16:53:50 +08:00
parent a47c386319
commit 5036513bf9
2 changed files with 81 additions and 0 deletions
+34
View File
@@ -31,6 +31,12 @@ const (
)
// GetLogs returns log lines with optional incremental loading.
//
// The legacy timestamp path keeps line-count as the total scanned line count for
// compatibility. Cursor and tail reads avoid scanning older files, so line-count
// is the number of returned lines there. A cursor emitted by the legacy path
// points at the latest complete log boundary; combining after with limit is
// therefore tail semantics and does not replay lines trimmed by limit.
func (h *Handler) GetLogs(c *gin.Context) {
if h == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "handler unavailable"})
@@ -756,6 +762,8 @@ func locateLogCursorFile(files []string, cursor logCursor) (int, bool, error) {
if matches && !truncated {
if shouldDeferEmptyMainCursorToRotated(files, cursor) {
deferEmptyMainMatch = true
} else if shouldResetAmbiguousEmptyMainCursor(files, index, cursor) {
return 0, false, nil
} else {
return index, true, nil
}
@@ -804,6 +812,32 @@ func shouldDeferEmptyMainCursorToRotated(files []string, cursor logCursor) bool
return false
}
func shouldResetAmbiguousEmptyMainCursor(files []string, mainIndex int, cursor logCursor) bool {
if cursor.File != defaultLogFileName || cursor.Offset != 0 || cursor.Size != 0 {
return false
}
info, errStat := os.Stat(files[mainIndex])
if errStat != nil || info.IsDir() {
return false
}
if info.Size() == cursor.Size && info.ModTime().UnixNano() == cursorModTimeUnixNano(cursor) {
return false
}
for i := range files {
if i == mainIndex || filepath.Base(files[i]) == defaultLogFileName {
continue
}
rotatedInfo, errRotated := os.Stat(files[i])
if errRotated != nil || rotatedInfo.IsDir() || rotatedInfo.Size() == 0 {
continue
}
if !logFileChangedAfterCursor(files[i], cursor) {
return true
}
}
return false
}
func logFileChangedAfterCursor(path string, cursor logCursor) bool {
info, errStat := os.Stat(path)
if errStat != nil || info.IsDir() || info.Size() == 0 {
@@ -474,6 +474,53 @@ func TestGetLogsZeroOffsetCursorWithEmptyFileReadsAcrossRotation(t *testing.T) {
}
}
func TestGetLogsZeroOffsetCursorWithEmptyFileResetsWhenRotationModTimeAmbiguous(t *testing.T) {
dir := t.TempDir()
mainPath := filepath.Join(dir, defaultLogFileName)
fixedModTime := time.Date(2026, 6, 15, 10, 0, 0, 0, time.Local)
writeMainLog(t, dir, "")
if err := os.Chtimes(mainPath, fixedModTime, fixedModTime); err != nil {
t.Fatalf("set initial main mtime: %v", err)
}
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)
}
first := "[2026-06-15 10:00:01] first"
second := "[2026-06-15 10:00:02] second"
appendMainLog(t, dir, first+"\n")
if err := os.Chtimes(mainPath, fixedModTime, fixedModTime); err != nil {
t.Fatalf("set rotated mtime: %v", err)
}
if err := os.Rename(mainPath, filepath.Join(dir, defaultLogFileName+".1")); err != nil {
t.Fatalf("rotate main log: %v", err)
}
writeMainLog(t, dir, second+"\n")
if err := os.Chtimes(mainPath, fixedModTime, fixedModTime); err != nil {
t.Fatalf("set new main mtime: %v", err)
}
resp := performGetLogs(t, newLogsTestHandler(dir, true), "/v0/management/logs?cursor="+url.QueryEscape(initial.NextCursor)+"&limit=2")
wantLines := []string{first, second}
if !reflect.DeepEqual(resp.Lines, wantLines) {
t.Fatalf("lines = %#v, want %#v", resp.Lines, wantLines)
}
if !resp.CursorReset {
t.Fatal("cursor-reset = false, want true for ambiguous empty cursor rotation")
}
if resp.LineCount != len(wantLines) {
t.Fatalf("line-count = %d, want returned line count %d", resp.LineCount, len(wantLines))
}
}
func TestGetLogsInvalidCursorResetsToTail(t *testing.T) {
dir := t.TempDir()
lines := []string{