feat(observability): Console, structured logs, and screen rework - #100
Conversation
The log viewer's auto-scroll checkbox and the live-streaming button were both labelled "Follow", so it was unclear which one tailed the container. The scroll checkbox now reads "Auto-scroll", leaving "Follow" to mean the live stream.
Deployment logs now render as one row per line, each showing the source service, time, and severity, with the message on a single line and a click to expand the full text, any structured JSON fields, and the raw line. Rows are colour-coded by level and can be narrowed to errors, warnings, info, or debug. The old terminal remains one toggle away as the raw view, and both the live stream and the snapshot feed the same rows, so following a container and reading a tail look identical.
The attached-databases panel moves out of the bottom of Overview into a dedicated Databases tab, with a clearer layout, an empty state for deployments with none, and a link through to the database-server manager.
Deployment metrics, container health, and recovery history now have a first-class Monitoring tab next to Logs, so the two observability views sit together instead of the metrics living in a plugin tab tacked on after Configuration. The tab is always present and degrades to a clear "observability app not running" state when the plugin is absent; the old metrics deep-link still resolves here.
The logs view now has a source picker: container output by default, the log files a deployment's kind conventionally writes, and any file the user points it at from the "Point at a file" action. Switching source re-reads it, following included, so an app's own log files show up in the same structured view without leaving the page.
A single unbreakable line, such as a long file-log entry, forced the main content area past the viewport and added a page-level horizontal scrollbar, because the flex column was allowed to keep its min-content width. It now shrinks to the available width, so long lines clip within the log view instead of stretching the layout.
The sidebar now has a Logs entry alongside Observability: pick any deployment and read its logs in the same structured view, from container output or one of its own log files, with live follow. The chosen deployment is kept in the URL so the view is shareable.
A log entry that spans several lines, such as an exception with its stack trace, now reads as a single expandable row instead of dozens of level-less lines. Continuation lines (indented frames, numbered frames, trailing brackets) fold into the entry above them; lines that are not continuations always start their own entry, so independent streams like access logs are never merged. Level detection matches the agent, so a class name mentioned in a frame no longer reads as an error.
Selects projected into the log toolbar showed a light background against the dark toolbar. They now match the toolbar regardless of the app theme, so the source and deployment pickers are readable on the Logs page.
The deployment metrics view goes back to the observability plugin's own "Metrics & Health" tab, now rendered right after Quick Actions instead of trailing the tab bar. The Databases tab gets a proper card layout: a type-coloured icon, clearer heading, and a readable key/value grid, with real padding around the tab.
Drop the explanatory comments added across the log views, keeping only a short note on the flex min-width guard. No behaviour change.
…ne console Observability was scattered across several sidebar entries. It is now a single console at /observability with a left rail (Overview, Logs, Alerts, Dashboards), so the pillars sit together instead of as flat siblings. The fleet Logs page moves in as the Logs section, and the sidebar carries one Observability entry. Per-deployment detail keeps its own Logs and Metrics & Health tabs.
The metrics screen replaces the four oversized count cards with a compact stat strip, adds a per-deployment filter that scopes the charts, shrinks the charts so several fit without scrolling, and gains real padding. The alerts screen moves to a stable two-column layout: the rules stay put as the main column while firing and recent alerts sit in bounded, scrollable side panels, so a burst of alerts no longer shoves the rules around.
The console now fills the viewport below the header: the left rail and the page header stay put while each section scrolls its own content. This is opt-in per route, so other pages keep their normal page scroll.
The rules table was stretched across most of the width while showing little, so rules and the firing/recent panels now share the space evenly.
…panel Network alert metrics and charts now read as a rate (per second), so the threshold you set and the value you see match what the agent measures. The firing and recent lists move into one card with a divider instead of two separate boxes, so the recent list no longer floats oddly beside the rules.
Collapsing the sidebar hid every group's items with no way to reach them. Each group now opens a hover flyout showing its links, so navigation still works with only icons on screen. The sidebar starts collapsed and remembers the choice, and the direct items carry tooltips.
The overview stacked the host charts above the fleet charts, so both competed for the same scroll. They are now two tabs with fleet first; the deployment filter shows only for fleet, and the time range applies to both.
The connection dot hung to the left of the collapsed footer. It now centers under the collapse control and carries a tooltip, so its state reads clearly with the label hidden.
Code Review SummaryThe PR significantly improves the observability experience, introducing a structured log viewer, a unified observability console, and improved layout density. The code is well-structured and follows Vue 3 best practices, with good use of composables and reactive state management. 🚀 Key Improvements
💡 Minor Suggestions
|
|
|
||
| const uiVersion = __APP_VERSION__; | ||
| const sidebarCollapsed = ref(false); | ||
| const sidebarCollapsed = ref(localStorage.getItem("sidebar_collapsed") !== "false"); |
There was a problem hiding this comment.
The current check !== "false" results in the sidebar defaulting to a collapsed state for first-time visitors (where localStorage.getItem returns null). For better navigation discoverability, it is generally preferred to default to an expanded state.
| const sidebarCollapsed = ref(localStorage.getItem("sidebar_collapsed") !== "false"); | |
| const sidebarCollapsed = ref(localStorage.getItem("sidebar_collapsed") === "true"); |
| if (isContinuation) { | ||
| entries[entries.length - 1].lines.push(rec.raw); | ||
| } else { | ||
| entries.push({ record: rec, lines: [rec.raw], key: i }); |
There was a problem hiding this comment.
Using the array index i as a key is problematic when logs are being pruned from the start of the array in useLogStream. This shifts indices and causes UI state (like row expansion) to apply to the wrong rows. Using a stable ID assigned when the record is created is safer.
| entries.push({ record: rec, lines: [rec.raw], key: i }); | |
| entries.push({ record: rec, lines: [rec.raw], key: rec.id ?? i }); |
Deploying flatrun-ui with
|
| Latest commit: |
cf30a0d
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://85df0e02.flatrun-ui.pages.dev |
| Branch Preview URL: | https://feat-observability-logs.flatrun-ui.pages.dev |
| const effectiveRecords = computed<LogRecord[]>(() => { | ||
| if (props.records && props.records.length) return props.records; | ||
| if (!props.logs) return []; | ||
| return props.logs |
There was a problem hiding this comment.
The current split logic only handles \n and ignores \r. In environments where logs might contain carriage returns (e.g., Windows-based containers or specific network protocols), this leaves trailing \r characters. Additionally, the fallback mapping does not assign stable IDs, which will cause UI flickering or loss of expansion state in the structured view when the log buffer slides, as groupLogRecords will fall back to using unstable array indices as keys.
| return props.logs | |
| return props.logs | |
| .replace(/\r?\n$/, "") | |
| .split(/\r?\n/) | |
| .map((line, i) => ({ ...parseLogLine(line), id: i })); |
| try { | ||
| const response = await deploymentsApi.logs(route.params.name as string); | ||
| const response = await deploymentsApi.logs(route.params.name as string, { | ||
| tail: logsTail.value || 100, |
There was a problem hiding this comment.
Same issue as in the logStream.start call: selecting 'All logs' (0) results in only fetching 100 lines due to the || operator.
| tail: logsTail.value || 100, | |
| tail: logsTail.value ?? 100, |
86c8ffc to
cf30a0d
Compare
Reworks the observability and logs experience.
/observabilitywith a left rail, replacing the scattered sidebar entries.Fixes: a long unbreakable log line no longer widens the whole page; the console body scrolls on its own; the collapsed sidebar opens hover flyouts, so grouped nav stays reachable (it was unreachable before).
Depends on the companion agent change for the structured records, file log sources, and network-rate metrics.