From f929979b6ed0040924981f01d8571d90048d79f0 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 25 Aug 2026 16:44:43 +0100 Subject: [PATCH 1/2] feat(alerts): Add memory utilization rules --- CHANGELOG.md | 3 +++ src/components/AlertRulesPanel.test.ts | 11 +++++++++++ src/components/AlertRulesPanel.vue | 18 ++++++++++++++++-- src/components/dashboards/PanelCard.vue | 1 + src/components/dashboards/PanelEditorModal.vue | 1 + src/services/observability.ts | 1 + 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdec7d2..b70988e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [0.4.0-beta.7] - 2026-08-23 +### Added +- Container memory utilization for percentage-based alert rules and dashboard panels + ### Fixed - Deployment domain summaries show the target service and container port diff --git a/src/components/AlertRulesPanel.test.ts b/src/components/AlertRulesPanel.test.ts index 5fc3258..8efa6a3 100644 --- a/src/components/AlertRulesPanel.test.ts +++ b/src/components/AlertRulesPanel.test.ts @@ -60,6 +60,17 @@ describe("AlertRulesPanel", () => { expect(wrapper.text()).toContain("CPU usage above 80% for 60s, on shop"); }); + it("presents container memory utilization as a percentage", async () => { + vi.mocked(observabilityApi.alertRules).mockResolvedValue({ + data: [{ ...rule, name: "Memory high", metric: "container.memory.utilization", threshold: 90 }], + } as any); + + const wrapper = mountPanel(); + await flushPromises(); + + expect(wrapper.text()).toContain("Container memory utilization (%) above 90% for 60s, on shop"); + }); + it("marks a rule that is currently firing", async () => { vi.mocked(observabilityApi.firingAlerts).mockResolvedValue({ data: [{ rule_id: "r1", rule_name: "CPU high", state: "firing" }], diff --git a/src/components/AlertRulesPanel.vue b/src/components/AlertRulesPanel.vue index cfa9052..435bf4f 100644 --- a/src/components/AlertRulesPanel.vue +++ b/src/components/AlertRulesPanel.vue @@ -164,9 +164,22 @@ const firingSnapshot = (id?: string): string => { return ev.snapshot.map((c) => `${c.container} (${asBytes ? bytes(c.value) : `${c.value.toFixed(1)}%`})`).join(", "); }; -const allMetricOptions: { value: string; label: string; unit: string; host?: boolean; rate?: boolean }[] = [ +const allMetricOptions: { + value: string; + label: string; + unit: string; + hint?: string; + host?: boolean; + rate?: boolean; +}[] = [ { value: METRIC.cpu, label: "Container CPU usage", unit: "percent" }, - { value: METRIC.memUsage, label: "Container memory usage", unit: "bytes" }, + { + value: METRIC.memUtilization, + label: "Container memory utilization (%)", + unit: "percent", + hint: "A percentage of the container's effective memory limit.", + }, + { value: METRIC.memUsage, label: "Container memory usage (bytes)", unit: "bytes" }, { value: METRIC.netRx, label: "Container network in (per second)", unit: "bytes", rate: true }, { value: METRIC.netTx, label: "Container network out (per second)", unit: "bytes", rate: true }, { value: METRIC.hostCpu, label: "Host CPU", unit: "percent", host: true }, @@ -212,6 +225,7 @@ watch(isHostMetric, (host) => { const unitHint = computed(() => { const opt = metricOptions.value.find((m) => m.value === draft.value.metric); + if (opt?.hint) return opt.hint; if (opt?.unit !== "bytes") return "A percentage."; return opt.rate ? "In bytes per second." : "In bytes."; }); diff --git a/src/components/dashboards/PanelCard.vue b/src/components/dashboards/PanelCard.vue index 2107665..f054d6b 100644 --- a/src/components/dashboards/PanelCard.vue +++ b/src/components/dashboards/PanelCard.vue @@ -53,6 +53,7 @@ const series = ref(null); const unit = computed<"percent" | "bytes" | "ms" | "count">(() => { if (props.panel.source === "serving") return props.panel.series === "latency" ? "ms" : "count"; if (props.panel.series.startsWith("container.cpu")) return "percent"; + if (props.panel.series === "container.memory.utilization") return "percent"; if (props.panel.series.startsWith("container.memory")) return "bytes"; if (props.panel.series.startsWith("container.network")) return "bytes"; return "count"; diff --git a/src/components/dashboards/PanelEditorModal.vue b/src/components/dashboards/PanelEditorModal.vue index ad6c79d..0d753c5 100644 --- a/src/components/dashboards/PanelEditorModal.vue +++ b/src/components/dashboards/PanelEditorModal.vue @@ -79,6 +79,7 @@ const containerSeries = [ { value: "container.cpu.usage", label: "CPU usage" }, { value: "container.memory.usage", label: "Memory usage" }, { value: "container.memory.limit", label: "Memory limit" }, + { value: "container.memory.utilization", label: "Memory utilization" }, { value: "container.network.io.rx", label: "Network in" }, { value: "container.network.io.tx", label: "Network out" }, ]; diff --git a/src/services/observability.ts b/src/services/observability.ts index fba8665..880d9a2 100644 --- a/src/services/observability.ts +++ b/src/services/observability.ts @@ -5,6 +5,7 @@ export const METRIC = { cpu: "container.cpu.usage", memUsage: "container.memory.usage", memLimit: "container.memory.limit", + memUtilization: "container.memory.utilization", netRx: "container.network.io.rx", netTx: "container.network.io.tx", hostCpu: "system.cpu.utilization", From 9e08883e45c0dfe27a26682323bbf2736e9ac252 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 25 Aug 2026 17:49:13 +0100 Subject: [PATCH 2/2] docs: Move new changes to Unreleased --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70988e..19818d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # Changelog -## [0.4.0-beta.7] - 2026-08-23 +## [Unreleased] ### Added - Container memory utilization for percentage-based alert rules and dashboard panels +## [0.4.0-beta.7] - 2026-08-23 + ### Fixed - Deployment domain summaries show the target service and container port