Files
3x-ui/internal/web/service/server_scope.go
T
VAIO73 746bdbe9f4 fix(monitoring): read LXC CPU, swap and uptime from the container (#6920)
In an LXC container with LXCFS the Overview showed the container's RAM
beside the host's swap, the host's CPU topology and a CPU percentage from
the host's view. Swap and uptime now come from LXCFS's virtualised files
and CPU from the container's cgroup v2 counter, normalised by the visible
vCPUs and any quota; container CPU and swap history is kept apart from
the earlier host samples.

A value the reader cannot measure (the first CPU sample, or a layout it
cannot verify such as cgroup v1) is reported as unavailable instead of 0:
the dashboard, the node list, node CPU history and the Telegram and
Discord uptime show a dash, and nodes keep the flags in a new
unavailable_metrics column.
2026-10-24 20:24:34 +02:00

59 lines
1.5 KiB
Go

package service
import (
"slices"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/util/sys"
)
const containerHistoryPrefix = "lxc-v1/"
func scopedMetric(metric string) bool {
return metric == "cpu" || metric == "swap"
}
func (s *ServerService) systemMetricKey(metric string) string {
if !scopedMetric(metric) {
return metric
}
if status := s.LastStatus(); status != nil {
if status.ResourceScope != "" {
return containerHistoryPrefix + metric
}
return metric
}
if s.lxcMetrics.UseScopedHistory() {
return containerHistoryPrefix + metric
}
return metric
}
// Scope metadata applies only to CPU, swap and uptime.
func applyLXCStats(status *Status, sample *sys.LXCStats) {
status.ResourceScope = sample.Scope
status.UnavailableMetrics = append([]string(nil), sample.Unavailable...)
status.Cpu = sample.CPU
status.CpuCores = sample.CPUs
status.LogicalPro = sample.CPUs
status.Swap.Current = sample.SwapUsed
status.Swap.Total = sample.SwapTotal
status.Uptime = sample.Uptime
}
// MetricAvailable distinguishes an unavailable metric from a measured zero.
// Legacy status responses omit metadata and retain their previous semantics.
func (s *Status) MetricAvailable(metric string) bool {
return s != nil && !slices.Contains(s.UnavailableMetrics, metric)
}
func appendScopedMetric(status *Status, metric string, t time.Time, value float64) {
if !status.MetricAvailable(metric) {
return
}
if status.ResourceScope != "" {
metric = containerHistoryPrefix + metric
}
systemMetrics.append(metric, t, value)
}