mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
Fix ambiguous empty log cursor handling
This commit is contained in:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user