Skip to content

Lazy load logs#377

Merged
brucetony merged 9 commits into
developfrom
376-lazy-load-logs
May 26, 2026
Merged

Lazy load logs#377
brucetony merged 9 commits into
developfrom
376-lazy-load-logs

Conversation

@brucetony
Copy link
Copy Markdown
Collaborator

@brucetony brucetony commented May 26, 2026

Summary by CodeRabbit

  • New Features
    • Load older logs on demand and progressive chunked log streaming
    • Virtualized log rendering for large logs; improved log export/copy formatting
  • UI Improvements
    • Toggle refresh control simplified and timestamp display toggle for logs
    • Updated log list and header layout for better readability
  • Tests
    • Expanded unit tests for log handling, flattening, streaming, and UI behaviors
  • Chores
    • CI action switched to GitHub Container Registry auth; updated .gitignore

Review Change Stack

@brucetony brucetony linked an issue May 26, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@brucetony, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 23 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1099cda8-dc88-4e03-8503-5dd090236826

📥 Commits

Reviewing files that changed from the base of the PR and between b89fdd9 and f970adb.

📒 Files selected for processing (10)
  • .github/workflows/ci.yaml
  • app/components/analysis/AnalysesTable.vue
  • app/composables/useLogChunks.ts
  • test/composables/connectionErrorToast.test.ts
  • test/composables/useAuthRefresh.test.ts
  • test/composables/useLogChunks.test.ts
  • test/mockapi/setup.ts
  • test/utils/count-analyses.test.ts
  • test/utils/format-data-row.test.ts
  • test/utils/status-tag-severity.test.ts
📝 Walkthrough

Walkthrough

This PR refactors analysis log loading and rendering: adds a FlatLogLine model and flattenLogs, implements useLogChunks for chunked pagination, virtualizes AnalysisLogCardContent with VirtualScroller and scroll-based load/auto-scroll behavior, updates ContainerLogs to orchestrate polling/prev-runs, and adjusts related tests and infra.

Changes

Analysis Logs Chunked Loading and Virtualization

Layer / File(s) Summary
Flat log data model and conversion
app/types/logs.ts, test/utils/flatten-logs.test.ts
FlatLogLine type and flattenLogs convert PodLog[] into flat per-line arrays, splitting multi-line stacktraces into separate entries with isStacktrace markers and null levels.
Log chunk management composable
app/composables/useLogChunks.ts, test/composables/useLogChunks.test.ts
useLogChunks manages pagination by oldest-timestamp, exposes initialize, loadOlderChunk, appendPolled, and reset, and maintains reactive nginx/analysis flattened lines with hasOlder, initialized, httpError, and runNumber.
Log card component virtualization
app/components/analysis/logs/AnalysisLogCardContent.vue, test/components/analysis/logs/AnalysisLogCardContent.spec.ts
Component now renders FlatLogLine[] via VirtualScroller, tracks near-bottom state, auto-scrolls on append only when appropriate, triggers load-older near top while preserving viewport, and formats exports/copy via formatFlatLines.
Container logs orchestration and polling
app/components/analysis/logs/ContainerLogs.vue, test/components/analysis/logs/ContainerLogs.spec.ts
Initializes useLogChunks, derives flattened prevLogs from /history, filters current run, polls incremental /logs with start_date, and appends polled results via the composable instead of in-place merging.
Refresh switch UI simplification
app/components/analysis/logs/RefreshSwitch.vue, test/components/analysis/logs/RefreshSwitch.spec.ts
Replaces ToggleSwitch with ToggleButton, adds explicit defineEmits(["change"]), and updates tests to interact with the button element.
Infrastructure and test setup
.github/actions/build-docker-image/action.yaml, .gitignore, test/mockapi/setup.ts
GitHub Action uses GHCR authentication for pushes, .playwright-mcp/ added to .gitignore, and PrimeVue VirtualScroller is registered globally for component tests.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ContainerLogs
  participant LogChunks as useLogChunks
  participant Card as AnalysisLogCardContent
  participant API
  
  User->>ContainerLogs: mount page
  ContainerLogs->>LogChunks: initialize()
  LogChunks->>API: GET /logs/{id}?limit=300
  API-->>LogChunks: initial chunk
  LogChunks->>LogChunks: flatten nginx & analysis lines
  LogChunks-->>ContainerLogs: nginxLines, analysisLines, hasOlder
  ContainerLogs->>Card: pass lines + onLoadOlderNginx callback
  Card->>Card: render virtualized lines
  
  User->>Card: scroll to top
  Card->>Card: detect near-top, hasOlder=true
  Card->>ContainerLogs: onLoadOlderNginx()
  ContainerLogs->>LogChunks: loadOlderChunk()
  LogChunks->>API: GET /logs/{id}?start_date=oldest
  API-->>LogChunks: older chunk
  LogChunks->>LogChunks: prepend older, update hasOlder
  LogChunks-->>Card: updated nginxLines
  Card->>Card: adjust scrollTop, render new top items
  
  User->>Card: scroll near bottom
  ContainerLogs->>API: GET /logs/{id}?start_date=lastFetchedAt
  API-->>ContainerLogs: polled logs
  ContainerLogs->>LogChunks: appendPolled(newLogs)
  LogChunks-->>Card: updated lines (append)
  Card->>Card: auto-scroll to bottom (if near-bottom)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I flattened logs and split a trace,
Virtual scroller saved the race,
Chunks arrive and older hop,
Scroll preserves your viewing stop,
Hooray — fewer leaps, more grace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Lazy load logs' directly reflects the main change: refactoring log loading to use lazy/progressive chunk-based fetching via the new useLogChunks composable with scroll-triggered older log loading.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 376-lazy-load-logs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
test/composables/useLogChunks.test.ts (1)

109-151: ⚡ Quick win

Add a regression test for repeated boundary timestamps in loadOlderChunk().

Current tests don’t cover the case where two consecutive chunks share the same oldest timestamp; that’s the failure mode that can duplicate prepended rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/composables/useLogChunks.test.ts` around lines 109 - 151, Add a
regression test to the describe("loadOlderChunk") suite that simulates two
consecutive chunks sharing the same boundary/oldest timestamp and assert no
duplicate lines are prepended: use testServer to return an initial chunk (via
useLogChunks(...).initialize()) and then a second chunk whose newest/oldest
timestamp equals the first chunk's oldest timestamp, call loadOlderChunk(), and
assert nginxLines and hasOlder reflect deduplication (no duplicated content at
the boundary and total length equals unique lines). Reference loadOlderChunk,
useLogChunks, nginxLines, and hasOlder when locating where to add this test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/analysis/logs/AnalysisLogCardContent.vue`:
- Around line 226-233: The VirtualScroller instances (the components using
:itemSize="20" for nginxLines and the analysis list) assume fixed row heights
but the rendered rows (elements with classes .log-message and
.log-stacktrace-line) use white-space: pre-wrap and can vary in height; either
make each rendered log row a fixed, stable height that matches the itemSize
(e.g., add CSS to .log-message and .log-stacktrace-line to clamp lines, set a
fixed height and overflow:hidden so every item is exactly 20px) or remove the
fixed itemSize and switch to a virtualization approach that measures per-item
heights; apply the change to both VirtualScroller usages (the one with
ref="nginxScrollerRef" and the other analysis VirtualScroller) so virtualization
offsets remain correct.

In `@app/components/analysis/logs/ContainerLogs.vue`:
- Around line 38-40: The polling cursor currently advances lastFetchedAt using
client time (new Date().toISOString()) in the log initialization and polling
code (when checking logChunks.initialized.value and in the fetch/update block
around lines ~88-100); change both places to set lastFetchedAt from the
server-provided timestamp in the returned logs (e.g., the latest log entry's
timestamp or a server cursor field in the fetch response) and only fall back to
client time if no server timestamp is available; update the assignments to
lastFetchedAt.value accordingly and ensure any parsing/formatting matches the
server's ISO timestamp format.

In `@app/composables/useLogChunks.ts`:
- Around line 64-80: loadOlderChunk can prepend duplicate rows when the same
timestamp boundary is reused; to fix, after calling fetchChunk({ end_date:
oldestTimestamp.value }) filter out any returned nginx_logs and analysis_logs
whose timestamp equals the current oldestTimestamp.value (or otherwise treat
end_date as exclusive) before flattening and prepending to nginxLines.value and
analysisLines.value, then compute oldestTimestampOf over the deduplicated
results and set hasOlder based on the deduplicated counts vs CHUNK_SIZE;
reference symbols: loadOlderChunk, fetchChunk, oldestTimestamp.value,
nginx_logs, analysis_logs, nginxLines.value, analysisLines.value,
oldestTimestampOf, CHUNK_SIZE.

---

Nitpick comments:
In `@test/composables/useLogChunks.test.ts`:
- Around line 109-151: Add a regression test to the describe("loadOlderChunk")
suite that simulates two consecutive chunks sharing the same boundary/oldest
timestamp and assert no duplicate lines are prepended: use testServer to return
an initial chunk (via useLogChunks(...).initialize()) and then a second chunk
whose newest/oldest timestamp equals the first chunk's oldest timestamp, call
loadOlderChunk(), and assert nginxLines and hasOlder reflect deduplication (no
duplicated content at the boundary and total length equals unique lines).
Reference loadOlderChunk, useLogChunks, nginxLines, and hasOlder when locating
where to add this test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a851187b-c7dd-4cce-b0e4-ab81f12925f8

📥 Commits

Reviewing files that changed from the base of the PR and between 9140a9c and f8cad02.

📒 Files selected for processing (13)
  • .github/actions/build-docker-image/action.yaml
  • .gitignore
  • app/components/analysis/logs/AnalysisLogCardContent.vue
  • app/components/analysis/logs/ContainerLogs.vue
  • app/components/analysis/logs/RefreshSwitch.vue
  • app/composables/useLogChunks.ts
  • app/types/logs.ts
  • test/components/analysis/logs/AnalysisLogCardContent.spec.ts
  • test/components/analysis/logs/ContainerLogs.spec.ts
  • test/components/analysis/logs/RefreshSwitch.spec.ts
  • test/composables/useLogChunks.test.ts
  • test/mockapi/setup.ts
  • test/utils/flatten-logs.test.ts
💤 Files with no reviewable changes (1)
  • .github/actions/build-docker-image/action.yaml

Comment thread app/components/analysis/logs/AnalysisLogCardContent.vue
Comment thread app/components/analysis/logs/ContainerLogs.vue
Comment thread app/composables/useLogChunks.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/analysis/logs/ContainerLogs.vue (1)

179-184: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Using one prepend callback for both panes will make the other pane jump.

AnalysisLogCardContent only preserves the scroll position of the pane that triggered onLoadOlder*, but both props here point at the same logChunks.loadOlderChunk, which prepends both arrays at once. Scrolling to the top in one pane will therefore shift the other pane’s viewport without restoring it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/analysis/logs/ContainerLogs.vue` around lines 179 - 184, The
two panes share the same prepend callback which causes the non-triggering pane
to jump; replace the single logChunks.loadOlderChunk usage with two distinct
callbacks (e.g., loadOlderNginxChunk and loadOlderAnalysisChunk or small wrapper
functions) and wire those to AnalysisLogCardContent's onLoadOlderNginx and
onLoadOlderAnalysis so each callback only prepends to its own array; ensure you
still use logChunks.hasOlder.value and logChunks.isLoading.value for each pane
but update the underlying loader functions to target nginx vs analysis chunks
separately.
♻️ Duplicate comments (1)
app/components/analysis/logs/ContainerLogs.vue (1)

100-116: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the polling cursor unchanged when a poll returns no rows.

The fetchTime fallback still advances lastFetchedAt with client time on empty responses, so delayed logs with older server timestamps can be skipped permanently. Only move the cursor when the response actually contains a newer log timestamp.

💡 Minimal fix
 async function refreshLogs() {
   if (!lastFetchedAt.value) return;
-  const fetchTime = new Date().toISOString();
   const result = await useNuxtApp()
     .$hubApi<AnalysisLogsResponse>(`/logs/${analysisId}`, {
       method: "GET",
       query: { start_date: lastFetchedAt.value },
     })
     .catch(() => undefined);
   if (result) {
     logChunks.appendPolled(result);
     const allLogs = [...result.nginx_logs, ...result.analysis_logs];
-    lastFetchedAt.value =
-      allLogs.length > 0
-        ? allLogs.reduce(
-            (max, l) => (l.timestamp > max ? l.timestamp : max),
-            allLogs[0]!.timestamp,
-          )
-        : fetchTime;
+    if (allLogs.length > 0) {
+      lastFetchedAt.value = allLogs.reduce(
+        (max, l) => (l.timestamp > max ? l.timestamp : max),
+        allLogs[0]!.timestamp,
+      );
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/analysis/logs/ContainerLogs.vue` around lines 100 - 116, The
current polling logic advances the cursor to fetchTime even when the API returns
no logs, which can skip delayed server-side logs; change the update so
lastFetchedAt is only modified when the response contains logs: after calling
useNuxtApp().$hubApi and logChunks.appendPolled(result), compute allLogs from
result.nginx_logs and result.analysis_logs and only set lastFetchedAt to the max
timestamp when allLogs.length > 0, otherwise leave lastFetchedAt unchanged; keep
fetchTime and logChunks.appendPolled(result) as-is and ensure no assignment to
lastFetchedAt occurs on empty result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/composables/useLogChunks.ts`:
- Around line 67-85: The code currently deduplicates both result.nginx_logs and
result.analysis_logs against a shared boundary (oldestTimestamp.value), which
can drop valid rows; change to deduplicate each stream against its own current
first-item timestamp (e.g., compute nginxBoundary =
nginxLines.value[0]?.timestamp and analysisBoundary =
analysisLines.value[0]?.timestamp) and use those when filtering
result.nginx_logs and result.analysis_logs; also determine hasOlder using the
raw page sizes (result.nginx_logs.length === CHUNK_SIZE ||
result.analysis_logs.length === CHUNK_SIZE) instead of the lengths after
deduplication, and keep updating oldestTimestamp.value via
oldestTimestampOf(...) from the deduped arrays as before.

---

Outside diff comments:
In `@app/components/analysis/logs/ContainerLogs.vue`:
- Around line 179-184: The two panes share the same prepend callback which
causes the non-triggering pane to jump; replace the single
logChunks.loadOlderChunk usage with two distinct callbacks (e.g.,
loadOlderNginxChunk and loadOlderAnalysisChunk or small wrapper functions) and
wire those to AnalysisLogCardContent's onLoadOlderNginx and onLoadOlderAnalysis
so each callback only prepends to its own array; ensure you still use
logChunks.hasOlder.value and logChunks.isLoading.value for each pane but update
the underlying loader functions to target nginx vs analysis chunks separately.

---

Duplicate comments:
In `@app/components/analysis/logs/ContainerLogs.vue`:
- Around line 100-116: The current polling logic advances the cursor to
fetchTime even when the API returns no logs, which can skip delayed server-side
logs; change the update so lastFetchedAt is only modified when the response
contains logs: after calling useNuxtApp().$hubApi and
logChunks.appendPolled(result), compute allLogs from result.nginx_logs and
result.analysis_logs and only set lastFetchedAt to the max timestamp when
allLogs.length > 0, otherwise leave lastFetchedAt unchanged; keep fetchTime and
logChunks.appendPolled(result) as-is and ensure no assignment to lastFetchedAt
occurs on empty result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 57a6c1aa-0f87-49a9-9ed3-1491fb0f5a34

📥 Commits

Reviewing files that changed from the base of the PR and between f8cad02 and b89fdd9.

📒 Files selected for processing (3)
  • app/components/analysis/logs/AnalysisLogCardContent.vue
  • app/components/analysis/logs/ContainerLogs.vue
  • app/composables/useLogChunks.ts

Comment thread app/composables/useLogChunks.ts Outdated
@brucetony brucetony merged commit 3f27708 into develop May 26, 2026
5 checks passed
@brucetony brucetony deleted the 376-lazy-load-logs branch May 26, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lazy load logs

1 participant