feat(observ): Show what a deployment is doing, and let rules be written#82
Conversation
CPU and memory say a container is alive. They do not say whether anyone is being served, and a deployment can sit at 3% CPU while every request returns 502. Metrics & Health now switches between the containers and the serving side, measured at the proxy every request already crosses, so nothing is installed in the container to produce it. The worst 95th percentile in the window is shown rather than an average, which hides the slow requests users notice. Overview's resource card is gone rather than moved. Its CPU and memory repeated what Metrics & Health already charts, and its disk and network bars were never fed by anything, so they sat at zero however busy the deployment was. Overview describes the deployment; its numbers live under metrics.
Overview said what a deployment is and nothing about how it is doing, so the question everyone opens it with went unanswered until they found the right tab. It now opens with the answer in a sentence: healthy, needs attention, serving errors, starting, or not running, with the container or the number behind that verdict named. The figures follow underneath rather than leading, since a grid of numbers leaves the reader to work out the verdict themselves. It links to Metrics & Health for the charts. Container health and serving are both read, because they disagree: a container can pass its health check while every request it answers fails. Each source is optional, so a summary is drawn from whatever is available rather than failing whole because one number is missing.
The summary took a quarter of the landing tab to say one sentence. It is one row now: the verdict where the eye lands, the figures beside it, and the detail dropping out first when the row runs short. Narrow screens get two rows rather than a sideways scroll. Icons come from Solar, whose filled and duotone glyphs hold up at 15px where a hairline outline reads as a smudge. Only the eleven icons the app renders are compiled in: importing the published set puts its whole 6MB of JSON in the bundle however few are registered, which cost a megabyte gzipped before this was caught. A script regenerates the subset. The status mark sits inside the card as its own rounded stripe rather than running the full height as a border. Card headers are all one height again. An action now sits over its header rather than in it, since a button is taller than a line of text and the one card that has one stood taller than the rest.
Thresholds have alerted through the configured notification targets since the engine landed, but a rule could only be created by hand-editing a YAML file on the host, which nobody was going to find. Rules can now be written, edited and removed from Observability, and each one says in words what it watches rather than restating its fields. A rule currently breached is marked, so the list answers what is wrong now as well as what is watched. The wait before a rule fires is offered with the reason for it: a container is briefly at full CPU every time it starts, and an alert for that teaches an operator to ignore the next one. Where the agent refuses a rule it could never evaluate, it says why here rather than failing quietly.
The log viewer asked for a tail and drew it, so it only ever showed what was true at the moment it asked. Watching a container start meant hitting refresh and hoping to catch the line that explains the failure. Follow hands each line over as the container writes it, and stops when the viewer stops, which closes the process on the agent rather than leaving it attached. Turning it off falls back to the tail it drew before. The buffer is bounded. A container logging a line a millisecond would otherwise grow it until the tab dies, and the oldest lines go first, which is what someone watching a live log is not looking at.
Rules were a panel at the foot of the fleet view, which reads fine with two and becomes a scroll with twenty. They have their own page now, reached from the sidebar and from Observability, with what is firing above the rules and what happened recently below. A rule's own page is also where its history belongs: the fleet view answers what is happening across the host, not what a rule has been doing.
Code Review SummaryThis PR significantly improves the observability capabilities of the platform by adding alert rule management, serving metrics (RED indicators), and live log streaming via WebSockets. It also optimizes the bundle size by sub-setting the Solar icon set. 🚀 Key Improvements
💡 Minor Suggestions
|
| return `${metric} ${rule.comparison} ${value}${held}, on ${where}`; | ||
| }; | ||
|
|
||
| function bytes(v: number): string { |
There was a problem hiding this comment.
This byte formatting logic can be simplified and hardened against edge cases (like zero or very small values). The current implementation might return '0 B' but for larger values it uses a fixed decimal. Using a standard utility or refining the ternary makes it more consistent.
| function bytes(v: number): string { | |
| function bytes(v: number): string { | |
| const units = ["B", "KB", "MB", "GB", "TB"]; | |
| let i = 0; | |
| while (v >= 1024 && i < units.length - 1) { | |
| v /= 1024; | |
| i++; | |
| } | |
| return `${v.toFixed(i > 0 ? 1 : 0)} ${units[i]}`; | |
| } |
|
|
||
| watch(range, fetchSeries); | ||
|
|
||
| onMounted(() => { |
There was a problem hiding this comment.
The component sets up an interval to refresh metrics but doesn't clear it if the component is unmounted or if multiple intervals are accidentally started. While onUnmounted is called at the bottom, it's safer to ensure any previous timer is cleared if onMounted were to be re-run or logic changed.
| onMounted(() => { | |
| onMounted(() => { | |
| fetchSeries(); | |
| if (timer) clearInterval(timer); | |
| timer = setInterval(fetchSeries, 15000); | |
| }); |
A firing alert is also the newest thing that happened to its rule, so it was listed under what is firing and again under what happened recently. Recent is what has happened since, and the current breach is named once, above it. The deployment detail tests also reached the network. The view now carries a summary that reads health and serving on mount, and unmocked those calls left the run failing after every test in it had passed.
Deploying flatrun-ui with
|
| Latest commit: |
caa8b5c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2d00c9d9.flatrun-ui.pages.dev |
| Branch Preview URL: | https://feat-observability-views.flatrun-ui.pages.dev |
| // number, so it is cast here rather than rejected there. | ||
| const rule: AlertRule = { | ||
| ...draft.value, | ||
| threshold: Number(draft.value.threshold), |
There was a problem hiding this comment.
When creating a new rule, draft.value.threshold is an empty string in the DOM until the user types. Number('') results in 0, which might be a valid threshold for some metrics but usually indicates an uninitialized value. Consider adding basic validation.
| threshold: Number(draft.value.threshold), | |
| threshold: draft.value.threshold === '' ? 0 : Number(draft.value.threshold), |
The UI side of flatrun/agent#174. Three of its backends were unreachable: rules could only be written as YAML on the host, serving metrics were kept for the security views alone, and logs could only be tailed.
Overview opens with a verdict rather than a grid of numbers, and reads health and serving together because they disagree: a container can pass its health check while every request it answers fails.
Overview's resource card is gone rather than moved. Its CPU and memory repeated charts that already existed, and its disk and network bars were never fed by anything, so they sat at zero however busy the deployment was.
Depends on flatrun/agent#174.