From 6cdf81e4bbde2d701f544edfb5342b0cac64551a Mon Sep 17 00:00:00 2001 From: Amir Raminfar Date: Thu, 28 May 2026 17:55:29 -0700 Subject: [PATCH] fix: normalize CPU by core count in metric alerts (#4754) Co-authored-by: Claude Opus 4.7 (1M context) --- docs/guide/alerts-and-webhooks.md | 10 +++++----- internal/notification/processing.go | 16 +++++++++++++--- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/guide/alerts-and-webhooks.md b/docs/guide/alerts-and-webhooks.md index 1d7636f9..378c947c 100644 --- a/docs/guide/alerts-and-webhooks.md +++ b/docs/guide/alerts-and-webhooks.md @@ -176,11 +176,11 @@ Metric alerts fire when a container's CPU or memory usage crosses a threshold. T Available properties: -| Property | Type | Description | -| ------------- | ------ | ----------------------------------------------- | -| `cpu` | number | CPU usage percentage (0–100, per core-adjusted) | -| `memory` | number | Memory usage percentage (0–100) | -| `memoryUsage` | number | Memory usage in bytes | +| Property | Type | Description | +| ------------- | ------ | --------------------------------------------- | +| `cpu` | number | CPU usage percentage (0–100), matching the UI | +| `memory` | number | Memory usage percentage (0–100) | +| `memoryUsage` | number | Memory usage in bytes | ### Cooldown & Sample Window diff --git a/internal/notification/processing.go b/internal/notification/processing.go index d849a683..83f61d3d 100644 --- a/internal/notification/processing.go +++ b/internal/notification/processing.go @@ -114,8 +114,18 @@ func (m *Manager) processStatEvents() { // processStatEvent processes a single stat event and sends notifications for matching metric subscriptions func (m *Manager) processStatEvent(event *ContainerStatEvent) { + // Normalize CPU by core count so alerts report overall load (0-100%), + // matching the UI. Stat.CPUPercent is per-core (100% = one full core). + cores := event.Container.CPULimit + if cores <= 0 { + cores = float64(event.Host.NCPU) + } + if cores <= 0 { + cores = 1 + } + notificationStat := types.NotificationStat{ - CPUPercent: event.Stat.CPUPercent, + CPUPercent: event.Stat.CPUPercent / cores, MemoryPercent: event.Stat.MemoryPercent, MemoryUsage: event.Stat.MemoryUsage, Mounts: FromContainerMounts(event.Container), @@ -154,8 +164,8 @@ func (m *Manager) processStatEvent(event *ContainerStatEvent) { log.Debug(). Str("containerID", event.Stat.ID). - Float64("cpu", event.Stat.CPUPercent). - Float64("memory", event.Stat.MemoryPercent). + Float64("cpu", notificationStat.CPUPercent). + Float64("memory", notificationStat.MemoryPercent). Str("subscription", sub.Name). Msg("Metric alert triggered")