Skip to content

feat(observ): Show what a deployment is doing, and let rules be written#82

Merged
nfebe merged 7 commits into
mainfrom
feat/observability-views
Jul 15, 2026
Merged

feat(observ): Show what a deployment is doing, and let rules be written#82
nfebe merged 7 commits into
mainfrom
feat/observability-views

Conversation

@nfebe

@nfebe nfebe commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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.

nfebe added 6 commits July 15, 2026 20:20
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.
@sourceant

sourceant Bot commented Jul 15, 2026

Copy link
Copy Markdown

Code Review Summary

This 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

  • Introduced useLogStream composable for real-time log tailing.
  • Added AlertRulesPanel for UI-based management of agent alert rules.
  • Enhanced deployment overview with a 'verdict-first' health summary.
  • Improved performance by sub-setting Iconify JSON assets.

💡 Minor Suggestions

  • Consolidate byte-formatting logic into a shared utility.
  • Add a buffer threshold for log slicing to reduce GC frequency.

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

return `${metric} ${rule.comparison} ${value}${held}, on ${where}`;
};

function bytes(v: number): string {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
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(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 15, 2026

Copy link
Copy Markdown

Deploying flatrun-ui with  Cloudflare Pages  Cloudflare Pages

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

View logs

@sourceant sourceant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review complete. See the overview comment for a summary.

// number, so it is cast here rather than rejected there.
const rule: AlertRule = {
...draft.value,
threshold: Number(draft.value.threshold),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Suggested change
threshold: Number(draft.value.threshold),
threshold: draft.value.threshold === '' ? 0 : Number(draft.value.threshold),

@nfebe
nfebe merged commit c4006ec into main Jul 15, 2026
5 checks passed
@nfebe
nfebe deleted the feat/observability-views branch July 15, 2026 22:06
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.

1 participant