diff --git a/internal/api/handlers/management/logs.go b/internal/api/handlers/management/logs.go index c41a0ed1..f42433e2 100644 --- a/internal/api/handlers/management/logs.go +++ b/internal/api/handlers/management/logs.go @@ -101,7 +101,12 @@ func (h *Handler) GetLogs(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to read log files: %v", errTail)}) return } - writeLogsResponse(c, result.lines, len(result.lines), result.latest, result.nextCursor, false) + total, errCount := countLogFileLines(files) + if errCount != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to read log files: %v", errCount)}) + return + } + writeLogsResponse(c, result.lines, total, result.latest, result.nextCursor, false) return } @@ -606,6 +611,66 @@ func readTailLogLines(path string, limit int) (completeLogRead, error) { return readCompleteLogLines(path, start, boundary, limit) } +func countLogFileLines(files []string) (int, error) { + total := 0 + for i := range files { + count, errCount := countLogLines(files[i]) + if errCount != nil { + if errors.Is(errCount, os.ErrNotExist) { + continue + } + return 0, errCount + } + total += count + } + return total, nil +} + +func countLogLines(path string) (int, error) { + file, errOpen := os.Open(path) + if errOpen != nil { + return 0, errOpen + } + defer func() { + _ = file.Close() + }() + info, errStat := file.Stat() + if errStat != nil { + return 0, errStat + } + if info.IsDir() { + return 0, fmt.Errorf("invalid log file") + } + + buf := make([]byte, 32*1024) + count := 0 + lineLen := 0 + for { + n, errRead := file.Read(buf) + for _, b := range buf[:n] { + if b == '\n' { + count++ + lineLen = 0 + continue + } + lineLen++ + if lineLen > logScannerMaxBuffer { + return 0, fmt.Errorf("log line exceeds %d bytes", logScannerMaxBuffer) + } + } + if errRead == io.EOF { + break + } + if errRead != nil { + return 0, errRead + } + } + if lineLen > 0 { + count++ + } + return count, nil +} + func tailStartOffset(path string, boundary int64, limit int) (int64, error) { if limit <= 0 { return 0, nil diff --git a/internal/api/handlers/management/logs_test.go b/internal/api/handlers/management/logs_test.go index b7b73b4d..55b5ba3f 100644 --- a/internal/api/handlers/management/logs_test.go +++ b/internal/api/handlers/management/logs_test.go @@ -149,8 +149,8 @@ func TestGetLogsTailLimitReturnsRecentLinesWithCursor(t *testing.T) { if !reflect.DeepEqual(resp.Lines, wantLines) { t.Fatalf("lines = %#v, want %#v", resp.Lines, wantLines) } - if resp.LineCount != 2 { - t.Fatalf("line-count = %d, want 2", resp.LineCount) + if resp.LineCount != 4 { + t.Fatalf("line-count = %d, want full scan count 4", resp.LineCount) } if resp.NextCursor == "" { t.Fatal("next-cursor is empty")