4 Commits
Author SHA1 Message Date
Joel Male 9365e64cdb chore: update collector v0.10.0 2026-02-25 21:53:04 +10:00
Joel Male ef6fa1a07f fix: measure process CPU concurrently with system CPU
Previously system CPU was measured first (1s window), then processes
were sampled 1+ second later. Short-lived spikes would appear in system
CPU but not in the process list because the offending process had already
finished. Now both run in parallel so their windows overlap — a process
that caused a spike during the CPU measurement window will be captured.
2026-02-25 21:52:00 +10:00
Joel Male b9b31414b0 fix: name and version on same line, blank line before success message 2026-02-25 16:13:05 +10:00
Joel Male ebdc5a2c48 chore: increase collection interval to 60s 2026-02-25 16:11:30 +10:00
2 changed files with 37 additions and 17 deletions
+1 -1
View File
@@ -1 +1 @@
0.9.0 0.10.0
+36 -16
View File
@@ -15,6 +15,7 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time" "time"
@@ -43,7 +44,7 @@ const (
const ( const (
defaultURL = "https://pulsewise.app/api/collect" defaultURL = "https://pulsewise.app/api/collect"
versionCheckURL = "https://pulsewise.app/collector/release/latest" versionCheckURL = "https://pulsewise.app/collector/release/latest"
collectionInterval = 30 * time.Second collectionInterval = 60 * time.Second
updateCheckInterval = 6 * time.Hour updateCheckInterval = 6 * time.Hour
configFilePath = "/etc/pulsewise-collector/config" configFilePath = "/etc/pulsewise-collector/config"
stateFilePath = "/etc/pulsewise-collector/state" stateFilePath = "/etc/pulsewise-collector/state"
@@ -246,8 +247,7 @@ func main() {
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
func runVersionCommand() { func runVersionCommand() {
fmt.Printf("\n %spulsewise-collector%s\n", ansiBold, ansiReset) fmt.Printf("\n %spulsewise-collector %s%sv%s%s\n\n", ansiBold, ansiReset, ansiCyan, version, ansiReset)
fmt.Printf(" %s%sv%s%s\n\n", ansiBold, ansiCyan, version, ansiReset)
fmt.Printf(" %sChecking for updates...%s ", ansiDim, ansiReset) fmt.Printf(" %sChecking for updates...%s ", ansiDim, ansiReset)
latest, err := fetchLatestVersion() latest, err := fetchLatestVersion()
@@ -270,8 +270,7 @@ func runUpdateCommand() {
return return
} }
fmt.Printf("\n %spulsewise-collector%s\n", ansiBold, ansiReset) fmt.Printf("\n %spulsewise-collector %s%sv%s%s\n\n", ansiBold, ansiReset, ansiCyan, version, ansiReset)
fmt.Printf(" %s%sv%s%s\n\n", ansiBold, ansiCyan, version, ansiReset)
fmt.Printf(" %sChecking for updates...%s ", ansiDim, ansiReset) fmt.Printf(" %sChecking for updates...%s ", ansiDim, ansiReset)
latest, err := fetchLatestVersion() latest, err := fetchLatestVersion()
@@ -293,7 +292,7 @@ func runUpdateCommand() {
os.Exit(1) os.Exit(1)
} }
fmt.Printf("%sDone.%s\n", ansiGreen, ansiReset) fmt.Printf("%sDone.%s\n\n", ansiGreen, ansiReset)
fmt.Printf(" Successfully updated to %sv%s%s.\n\n", ansiCyan, latest, ansiReset) fmt.Printf(" Successfully updated to %sv%s%s.\n\n", ansiCyan, latest, ansiReset)
restartService() restartService()
@@ -910,12 +909,34 @@ func collectMetrics(config *Config) (*Metrics, error) {
} }
} }
// CPU — single sample with percpu=true, then average for the total. // CPU and process collection run concurrently so their measurement
// Calling cpu.Percent twice with separate 1s windows can produce // windows overlap. Previously, processes were sampled 1+ second after
// wildly inconsistent totals vs per-core values. // the CPU window ended, causing spikes to appear in system CPU but not
cpuPerCore, err := cpu.Percent(time.Second, true) // in the process list (the offending process had already finished).
if err != nil { var (
return nil, fmt.Errorf("failed to get CPU percent: %w", err) cpuPerCore []float64
cpuErr error
processes []ProcessInfo
processErr error
)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
cpuPerCore, cpuErr = cpu.Percent(time.Second, true)
}()
go func() {
defer wg.Done()
processes, processErr = collectTopProcesses()
}()
wg.Wait()
if cpuErr != nil {
return nil, fmt.Errorf("failed to get CPU percent: %w", cpuErr)
} }
metrics.CPU.PerCore = cpuPerCore metrics.CPU.PerCore = cpuPerCore
if len(cpuPerCore) > 0 { if len(cpuPerCore) > 0 {
@@ -1054,10 +1075,9 @@ func collectMetrics(config *Config) (*Metrics, error) {
prevNet = current prevNet = current
} }
// Top processes // Top processes (collected concurrently with CPU above)
processes, err := collectTopProcesses() if processErr != nil {
if err != nil { log.Printf("Warning: Failed to collect process info: %v", processErr)
log.Printf("Warning: Failed to collect process info: %v", err)
metrics.Processes = []ProcessInfo{} metrics.Processes = []ProcessInfo{}
} else { } else {
metrics.Processes = processes metrics.Processes = processes