mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-23 14:06:18 +01:00
Compare commits
3 Commits
v0.15.3
...
top-proces
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbbdd49fc2 | ||
|
|
fc0947aa04 | ||
|
|
1d546a4091 |
119
agent/cpu.go
119
agent/cpu.go
@@ -2,14 +2,19 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/entities/system"
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v4/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
var lastCpuTimes = make(map[uint16]cpu.TimesStat)
|
var lastCpuTimes = make(map[uint16]cpu.TimesStat)
|
||||||
var lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)
|
var lastPerCoreCpuTimes = make(map[uint16][]cpu.TimesStat)
|
||||||
|
var lastProcessCpuTimes = make(map[uint16]map[int32]float64)
|
||||||
|
var lastProcessCpuSampleTime = make(map[uint16]time.Time)
|
||||||
|
|
||||||
// init initializes the CPU monitoring by storing the initial CPU times
|
// init initializes the CPU monitoring by storing the initial CPU times
|
||||||
// for the default 60-second cache interval.
|
// for the default 60-second cache interval.
|
||||||
@@ -20,6 +25,16 @@ func init() {
|
|||||||
if perCoreTimes, err := cpu.Times(true); err == nil {
|
if perCoreTimes, err := cpu.Times(true); err == nil {
|
||||||
lastPerCoreCpuTimes[60000] = perCoreTimes
|
lastPerCoreCpuTimes[60000] = perCoreTimes
|
||||||
}
|
}
|
||||||
|
if processes, err := process.Processes(); err == nil {
|
||||||
|
snapshot := make(map[int32]float64, len(processes))
|
||||||
|
for _, proc := range processes {
|
||||||
|
if times, err := proc.Times(); err == nil {
|
||||||
|
snapshot[proc.Pid] = times.Total()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastProcessCpuTimes[60000] = snapshot
|
||||||
|
lastProcessCpuSampleTime[60000] = time.Now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CpuMetrics contains detailed CPU usage breakdown
|
// CpuMetrics contains detailed CPU usage breakdown
|
||||||
@@ -105,6 +120,110 @@ func getPerCoreCpuUsage(cacheTimeMs uint16) (system.Uint8Slice, error) {
|
|||||||
return usage, nil
|
return usage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTopCpuProcess returns the process with the highest CPU usage since the last run
|
||||||
|
// for the given cache interval. It returns nil if insufficient data is available.
|
||||||
|
func getTopCpuProcess(cacheTimeMs uint16) (*system.TopCpuProcess, error) {
|
||||||
|
processes, err := process.Processes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
lastTimes, ok := lastProcessCpuTimes[cacheTimeMs]
|
||||||
|
if !ok {
|
||||||
|
if fallback := lastProcessCpuTimes[60000]; fallback != nil {
|
||||||
|
copied := make(map[int32]float64, len(fallback))
|
||||||
|
for pid, total := range fallback {
|
||||||
|
copied[pid] = total
|
||||||
|
}
|
||||||
|
lastTimes = copied
|
||||||
|
lastProcessCpuTimes[cacheTimeMs] = copied
|
||||||
|
} else {
|
||||||
|
lastTimes = make(map[int32]float64)
|
||||||
|
lastProcessCpuTimes[cacheTimeMs] = lastTimes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSample := lastProcessCpuSampleTime[cacheTimeMs]
|
||||||
|
if lastSample.IsZero() {
|
||||||
|
if fallback := lastProcessCpuSampleTime[60000]; !fallback.IsZero() {
|
||||||
|
lastSample = fallback
|
||||||
|
lastProcessCpuSampleTime[cacheTimeMs] = fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := now.Sub(lastSample).Seconds()
|
||||||
|
if lastSample.IsZero() || elapsed <= 0 {
|
||||||
|
snapshot := make(map[int32]float64, len(processes))
|
||||||
|
for _, proc := range processes {
|
||||||
|
if times, err := proc.Times(); err == nil {
|
||||||
|
snapshot[proc.Pid] = times.Total()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastProcessCpuTimes[cacheTimeMs] = snapshot
|
||||||
|
lastProcessCpuSampleTime[cacheTimeMs] = now
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuCount := float64(runtime.NumCPU())
|
||||||
|
if cpuCount <= 0 {
|
||||||
|
cpuCount = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot := make(map[int32]float64, len(processes))
|
||||||
|
var topName string
|
||||||
|
var topPercent float64
|
||||||
|
|
||||||
|
for _, proc := range processes {
|
||||||
|
times, err := proc.Times()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
total := times.Total()
|
||||||
|
pid := proc.Pid
|
||||||
|
snapshot[pid] = total
|
||||||
|
|
||||||
|
lastTotal, ok := lastTimes[pid]
|
||||||
|
if !ok || total <= lastTotal {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
percent := clampPercent((total - lastTotal) / (elapsed * cpuCount) * 100)
|
||||||
|
if percent <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name, err := proc.Name()
|
||||||
|
if err != nil || name == "" {
|
||||||
|
if exe, exeErr := proc.Exe(); exeErr == nil && exe != "" {
|
||||||
|
name = filepath.Base(exe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if percent > topPercent {
|
||||||
|
topPercent = percent
|
||||||
|
topName = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastProcessCpuTimes[cacheTimeMs] = snapshot
|
||||||
|
lastProcessCpuSampleTime[cacheTimeMs] = now
|
||||||
|
|
||||||
|
if topName == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &system.TopCpuProcess{
|
||||||
|
Name: topName,
|
||||||
|
Percent: topPercent,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// calculateBusy calculates the CPU busy percentage between two time points.
|
// calculateBusy calculates the CPU busy percentage between two time points.
|
||||||
// It computes the ratio of busy time to total time elapsed between t1 and t2,
|
// It computes the ratio of busy time to total time elapsed between t1 and t2,
|
||||||
// returning a percentage clamped between 0 and 100.
|
// returning a percentage clamped between 0 and 100.
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func (a *Agent) initializeDiskInfo() {
|
|||||||
filesystem, _ := GetEnv("FILESYSTEM")
|
filesystem, _ := GetEnv("FILESYSTEM")
|
||||||
efPath := "/extra-filesystems"
|
efPath := "/extra-filesystems"
|
||||||
hasRoot := false
|
hasRoot := false
|
||||||
|
isWindows := runtime.GOOS == "windows"
|
||||||
|
|
||||||
partitions, err := disk.Partitions(false)
|
partitions, err := disk.Partitions(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,6 +39,13 @@ func (a *Agent) initializeDiskInfo() {
|
|||||||
}
|
}
|
||||||
slog.Debug("Disk", "partitions", partitions)
|
slog.Debug("Disk", "partitions", partitions)
|
||||||
|
|
||||||
|
// trim trailing backslash for Windows devices (#1361)
|
||||||
|
if isWindows {
|
||||||
|
for i, p := range partitions {
|
||||||
|
partitions[i].Device = strings.TrimSuffix(p.Device, "\\")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ioContext := context.WithValue(a.sensorsContext,
|
// ioContext := context.WithValue(a.sensorsContext,
|
||||||
// common.EnvKey, common.EnvMap{common.HostProcEnvKey: "/tmp/testproc"},
|
// common.EnvKey, common.EnvMap{common.HostProcEnvKey: "/tmp/testproc"},
|
||||||
// )
|
// )
|
||||||
@@ -52,7 +60,7 @@ func (a *Agent) initializeDiskInfo() {
|
|||||||
// Helper function to add a filesystem to fsStats if it doesn't exist
|
// Helper function to add a filesystem to fsStats if it doesn't exist
|
||||||
addFsStat := func(device, mountpoint string, root bool, customName ...string) {
|
addFsStat := func(device, mountpoint string, root bool, customName ...string) {
|
||||||
var key string
|
var key string
|
||||||
if runtime.GOOS == "windows" {
|
if isWindows {
|
||||||
key = device
|
key = device
|
||||||
} else {
|
} else {
|
||||||
key = filepath.Base(device)
|
key = filepath.Base(device)
|
||||||
|
|||||||
@@ -98,6 +98,15 @@ func (a *Agent) getSystemStats(cacheTimeMs uint16) system.Stats {
|
|||||||
slog.Error("Error getting cpu metrics", "err", err)
|
slog.Error("Error getting cpu metrics", "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if topProcess, err := getTopCpuProcess(cacheTimeMs); err == nil {
|
||||||
|
if topProcess != nil {
|
||||||
|
topProcess.Percent = twoDecimals(topProcess.Percent)
|
||||||
|
systemStats.TopCpuProcess = topProcess
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Error("Error getting top cpu process", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
// per-core cpu usage
|
// per-core cpu usage
|
||||||
if perCoreUsage, err := getPerCoreCpuUsage(cacheTimeMs); err == nil {
|
if perCoreUsage, err := getPerCoreCpuUsage(cacheTimeMs); err == nil {
|
||||||
systemStats.CpuCoresUsage = perCoreUsage
|
systemStats.CpuCoresUsage = perCoreUsage
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS builder
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
COPY ../go.mod ../go.sum ./
|
COPY ../go.mod ../go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
@@ -13,7 +12,24 @@ COPY . ./
|
|||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent
|
||||||
|
|
||||||
RUN rm -rf /tmp/*
|
# --------------------------
|
||||||
|
# Smartmontools builder stage
|
||||||
|
# --------------------------
|
||||||
|
FROM nvidia/cuda:12.2.2-base-ubuntu22.04 AS smartmontools-builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
build-essential \
|
||||||
|
&& wget https://downloads.sourceforge.net/project/smartmontools/smartmontools/7.5/smartmontools-7.5.tar.gz \
|
||||||
|
&& tar zxvf smartmontools-7.5.tar.gz \
|
||||||
|
&& cd smartmontools-7.5 \
|
||||||
|
&& ./configure --prefix=/usr --sysconfdir=/etc \
|
||||||
|
&& make \
|
||||||
|
&& make install \
|
||||||
|
&& rm -rf /smartmontools-7.5* \
|
||||||
|
&& apt-get remove -y wget build-essential \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# --------------------------
|
# --------------------------
|
||||||
# Final image: GPU-enabled agent with nvidia-smi
|
# Final image: GPU-enabled agent with nvidia-smi
|
||||||
@@ -21,10 +37,8 @@ RUN rm -rf /tmp/*
|
|||||||
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
|
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
|
||||||
COPY --from=builder /agent /agent
|
COPY --from=builder /agent /agent
|
||||||
|
|
||||||
# this is so we don't need to create the /tmp directory in the scratch container
|
# Copy smartmontools binaries and config files
|
||||||
COPY --from=builder /tmp /tmp
|
COPY --from=smartmontools-builder /usr/sbin/smartctl /usr/sbin/smartctl
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y smartmontools && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Ensure data persistence across container recreations
|
# Ensure data persistence across container recreations
|
||||||
VOLUME ["/var/lib/beszel-agent"]
|
VOLUME ["/var/lib/beszel-agent"]
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type Stats struct {
|
|||||||
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
|
||||||
CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle]
|
CpuBreakdown []float64 `json:"cpub,omitempty" cbor:"33,keyasint,omitempty"` // [user, system, iowait, steal, idle]
|
||||||
CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..]
|
CpuCoresUsage Uint8Slice `json:"cpus,omitempty" cbor:"34,keyasint,omitempty"` // per-core busy usage [CPU0..]
|
||||||
|
TopCpuProcess *TopCpuProcess `json:"tcp,omitempty" cbor:"35,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
|
// Uint8Slice wraps []uint8 to customize JSON encoding while keeping CBOR efficient.
|
||||||
@@ -153,3 +154,8 @@ type CombinedData struct {
|
|||||||
Info Info `json:"info" cbor:"1,keyasint"`
|
Info Info `json:"info" cbor:"1,keyasint"`
|
||||||
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
Containers []*container.Stats `json:"container" cbor:"2,keyasint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TopCpuProcess struct {
|
||||||
|
Name string `json:"n" cbor:"0,keyasint"`
|
||||||
|
Percent float64 `json:"p" cbor:"1,keyasint"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user