Skip to content

Commit 50ef902

Browse files
authored
Merge pull request #68 from it-novum/linux-cpu
openITCOCKPIT Monitoring Agent 3.0.9
2 parents 1daaa14 + d7360fc commit 50ef902

33 files changed

+938
-353
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.0.8
1+
3.0.9

basiclog/basiclog.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package basiclog
2+
3+
import (
4+
"fmt"
5+
"runtime"
6+
"strings"
7+
"time"
8+
)
9+
10+
// This very basic log gets used to log any config parser errors
11+
// to the syslog or Windows Event Log on systems that do not have a syslog.
12+
// This is because if the agent can not parse it's config.ini it has no logfile path.
13+
14+
func (l *BasicLogger) Errorln(args ...interface{}) error {
15+
msg := fmt.Sprint(args...)
16+
17+
t := time.Now()
18+
source := l.source()
19+
template := "time=\"%v\" level=error msg=\"%v\" source=\"%v\"\n"
20+
21+
msg = fmt.Sprintf(template, t.Format(time.RFC3339), msg, source)
22+
23+
// Print msg
24+
fmt.Print(msg)
25+
26+
if l.handler == nil {
27+
return fmt.Errorf("No log handler initialized! Message will only be printed to stdout.")
28+
}
29+
30+
// Save msg to logfile via given handler
31+
return l.LogError(msg)
32+
}
33+
34+
func (l *BasicLogger) source() string {
35+
_, file, line, ok := runtime.Caller(2)
36+
if !ok {
37+
file = "<???>"
38+
line = 1
39+
} else {
40+
slash := strings.LastIndex(file, "/")
41+
file = file[slash+1:]
42+
}
43+
44+
return fmt.Sprintf("%s:%d", file, line)
45+
}

basiclog/basiclog_posix.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build linux || darwin
2+
// +build linux darwin
3+
4+
package basiclog
5+
6+
import (
7+
"log/syslog"
8+
)
9+
10+
// This very basic log gets used to log any config parser errors
11+
// to the syslog or Windows Event Log on systems that do not have a syslog.
12+
// This is because if the agent can not parse it's config.ini it has no logfile path.
13+
14+
type BasicLogger struct {
15+
handler *syslog.Writer
16+
}
17+
18+
func New() (*BasicLogger, error) {
19+
20+
logHandle, err := syslog.New(syslog.LOG_ERR, "openITCOCKPITAgent")
21+
if err != nil {
22+
return &BasicLogger{}, err
23+
}
24+
25+
return &BasicLogger{
26+
handler: logHandle,
27+
}, nil
28+
}
29+
30+
func (l *BasicLogger) LogError(msg string) error {
31+
return l.handler.Err(msg)
32+
}

basiclog/basiclog_windows.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package basiclog
2+
3+
import (
4+
"golang.org/x/sys/windows/svc/eventlog"
5+
)
6+
7+
// This very basic log gets used to log any config parser errors
8+
// to the syslog or Windows Event Log on systems that do not have a syslog.
9+
// This is because if the agent can not parse it's config.ini it has no logfile path.
10+
11+
type BasicLogger struct {
12+
handler *eventlog.Log
13+
}
14+
15+
func New() (*BasicLogger, error) {
16+
17+
logHandle, err := eventlog.Open("openITCOCKPITAgent")
18+
if err != nil {
19+
return &BasicLogger{}, err
20+
}
21+
22+
return &BasicLogger{
23+
handler: logHandle,
24+
}, nil
25+
}
26+
27+
func (l *BasicLogger) LogError(msg string) error {
28+
return l.handler.Error(1, msg)
29+
}

checks/agent_posix.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build linux || darwin
12
// +build linux darwin
23

34
package checks

checks/agent_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"runtime"
66
"time"
77

8-
"github.com/StackExchange/wmi"
98
"github.com/it-novum/openitcockpit-agent-go/config"
9+
"github.com/yusufpapurcu/wmi"
1010
"golang.org/x/sys/windows/registry"
1111
)
1212

checks/checks_libvirt_linux.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build libvirt
12
// +build libvirt
23

34
package checks

checks/checks_linux.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build !libvirt
12
// +build !libvirt
23

34
package checks

checks/cpu_posix.go

Lines changed: 114 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,147 @@
1+
//go:build linux || darwin
12
// +build linux darwin
23

34
package checks
45

56
import (
67
"context"
7-
"runtime"
8+
"fmt"
9+
"math"
810
"time"
911

1012
"github.com/shirou/gopsutil/v3/cpu"
1113
)
1214

15+
func saturatingSub(a, b float64) float64 {
16+
if a > b {
17+
return a - b
18+
}
19+
return 0
20+
}
21+
22+
func calcuateBusy(t cpu.TimesStat) (float64, float64, float64) {
23+
busy := t.User + t.Nice + t.System + t.Irq + t.Softirq + t.Steal
24+
idle := t.Idle + t.Iowait
25+
total := busy + idle
26+
return busy, idle, total
27+
}
28+
29+
func calculateUsagePercentage(prev, current cpu.TimesStat) float64 {
30+
// This is highly inspired by
31+
// https://github.com/shirou/gopsutil/blob/master/cpu/cpu.go#L107
32+
// and htop
33+
// https://github.com/htop-dev/htop/blob/main/linux/LinuxProcessList.c#L1948-L2006
34+
// and yes - htop is reading /proc/stat as we do
35+
prevBusy, _, prevTotal := calcuateBusy(prev)
36+
currentBusy, _, currentTotal := calcuateBusy(current)
37+
38+
if currentBusy <= prevBusy {
39+
return 0
40+
}
41+
42+
if currentTotal <= prevTotal {
43+
return 100
44+
}
45+
return math.Min(100, math.Max(0, (currentBusy-prevBusy)/(currentTotal-prevTotal)*100))
46+
}
47+
1348
// Run the actual check
1449
// if error != nil the check result will be nil
1550
// ctx can be canceled and runs the timeout
1651
// CheckResult will be serialized after the return and should not change until the next call to Run
1752
func (c *CheckCpu) Run(ctx context.Context) (interface{}, error) {
1853
result := &resultCpu{}
1954

20-
cpuPercentages, err := cpu.PercentWithContext(ctx, 1*time.Second, true)
21-
22-
if err == nil {
23-
result.PercentagePerCore = cpuPercentages
55+
prevTimeStats, err := cpu.TimesWithContext(ctx, true)
56+
if err != nil {
57+
return nil, err
2458
}
2559

26-
//get total CPU percentage
27-
cpuPercentageTotal, err := cpu.PercentWithContext(ctx, 1*time.Second, false)
60+
if err := c.SleepWithContext(ctx, 1*time.Second); err != nil {
61+
return nil, err
62+
}
2863

29-
if err == nil {
30-
result.PercentageTotal = cpuPercentageTotal[0]
64+
timeStats, err := cpu.TimesWithContext(ctx, true)
65+
if err != nil {
66+
return nil, err
3167
}
3268

33-
if runtime.GOOS == "linux" {
34-
timeStats, err := cpu.TimesWithContext(ctx, false)
35-
36-
if err == nil {
37-
total := timeStats[0].User + timeStats[0].Nice + timeStats[0].System + timeStats[0].Idle + timeStats[0].Iowait + timeStats[0].Irq + timeStats[0].Softirq
38-
result.DetailsTotal = &cpuDetails{
39-
User: timeStats[0].User / total * 100,
40-
Nice: timeStats[0].Nice / total * 100,
41-
System: timeStats[0].System / total * 100,
42-
Iowait: timeStats[0].Iowait / total * 100,
43-
}
44-
}
69+
if len(prevTimeStats) != len(timeStats) {
70+
return nil, fmt.Errorf("number of CPU cores has changed %v != %v", len(prevTimeStats), len(timeStats))
71+
}
4572

46-
//get timings per CPU
47-
var timings []cpuDetails
48-
timeStats, err = cpu.TimesWithContext(ctx, true)
49-
if err == nil {
50-
for _, timeStat := range timeStats {
51-
total := timeStat.User + timeStat.Nice + timeStat.System + timeStat.Idle + timeStat.Iowait + timeStat.Irq + timeStat.Softirq
52-
timings = append(timings, cpuDetails{
53-
User: timeStat.User / total * 100,
54-
Nice: timeStat.Nice / total * 100,
55-
System: timeStat.System / total * 100,
56-
Idle: timeStat.Idle / total * 100,
57-
Iowait: timeStat.Iowait / total * 100,
58-
})
59-
}
60-
result.DetailsPerCore = timings
61-
}
73+
// Get CPU usage per core
74+
var cpuPercentages []float64
75+
for i := range prevTimeStats {
76+
cpuPercentage := calculateUsagePercentage(prevTimeStats[i], timeStats[i])
77+
cpuPercentages = append(cpuPercentages, cpuPercentage)
6278
}
79+
result.PercentagePerCore = cpuPercentages
6380

64-
if runtime.GOOS == "darwin" {
65-
timeStats, err := cpu.TimesWithContext(ctx, false)
66-
67-
if err == nil {
68-
total := timeStats[0].User + timeStats[0].Nice + timeStats[0].System + timeStats[0].Idle
69-
result.DetailsTotal = &cpuDetails{
70-
User: timeStats[0].User / total * 100,
71-
Nice: timeStats[0].Nice / total * 100,
72-
System: timeStats[0].System / total * 100,
73-
Idle: timeStats[0].Idle / total * 100,
74-
}
81+
// Get total CPU usage as percentage
82+
var totalCpuPercentage float64
83+
for i := range cpuPercentages {
84+
totalCpuPercentage = totalCpuPercentage + cpuPercentages[i]
85+
}
86+
result.PercentageTotal = totalCpuPercentage / float64(len(cpuPercentages))
87+
88+
// Get CPU timing details per core
89+
var timings []cpuDetails
90+
var totalUser, totalNice, totalSystem, totalIoWait, totalIdle, totalTotal float64
91+
for i := range prevTimeStats {
92+
// Ignore t.Irq + t.Softirq + t.Steal to get 100% over all values we list in the json
93+
prevTotal := prevTimeStats[i].User + prevTimeStats[i].Nice + prevTimeStats[i].System + prevTimeStats[i].Idle + prevTimeStats[i].Iowait
94+
currentTotal := timeStats[i].User + timeStats[i].Nice + timeStats[i].System + timeStats[i].Idle + timeStats[i].Iowait
95+
96+
userDelta := saturatingSub(timeStats[i].User, prevTimeStats[i].User)
97+
niceDelta := saturatingSub(timeStats[i].Nice, prevTimeStats[i].Nice)
98+
systemDelta := saturatingSub(timeStats[i].System, prevTimeStats[i].System)
99+
ioWaitDelta := saturatingSub(timeStats[i].Iowait, prevTimeStats[i].Iowait)
100+
idleDelta := saturatingSub(timeStats[i].Idle, prevTimeStats[i].Idle)
101+
totalDelta := saturatingSub(currentTotal, prevTotal)
102+
103+
if totalDelta == 0 {
104+
timings = append(timings, cpuDetails{})
105+
} else {
106+
// Do not Division by zero (not sure if this could ever happen to be honest but better safe than sorry)
107+
timings = append(timings, cpuDetails{
108+
User: userDelta / totalDelta * 100,
109+
Nice: niceDelta / totalDelta * 100,
110+
System: systemDelta / totalDelta * 100,
111+
Idle: idleDelta / totalDelta * 100,
112+
Iowait: ioWaitDelta / totalDelta * 100,
113+
})
75114
}
76115

77-
//get timings per CPU
78-
var timings []cpuDetails
79-
timeStats, err = cpu.TimesWithContext(ctx, true)
80-
if err == nil {
81-
for _, timeStat := range timeStats {
82-
total := timeStat.User + timeStat.Nice + timeStat.System + timeStat.Idle
83-
timings = append(timings, cpuDetails{
84-
User: timeStat.User / total * 100,
85-
Nice: timeStat.Nice / total * 100,
86-
System: timeStat.System / total * 100,
87-
Idle: timeStat.Idle / total * 100,
88-
})
89-
}
90-
result.DetailsPerCore = timings
91-
}
116+
totalUser = totalUser + userDelta
117+
totalNice = totalNice + niceDelta
118+
totalSystem = totalSystem + systemDelta
119+
totalIoWait = totalIoWait + ioWaitDelta
120+
totalIdle = totalIdle + idleDelta
121+
totalTotal = totalTotal + totalDelta
122+
}
123+
result.DetailsPerCore = timings
124+
125+
// Get total CPU timing details
126+
result.DetailsTotal = &cpuDetails{
127+
User: totalUser / totalTotal * 100,
128+
Nice: totalNice / totalTotal * 100,
129+
System: totalSystem / totalTotal * 100,
130+
Idle: totalIdle / totalTotal * 100,
131+
Iowait: totalIoWait / totalTotal * 100,
92132
}
93133

94134
return result, nil
95135

96136
}
137+
138+
// Like sleep but can be canceled by context
139+
func (c *CheckCpu) SleepWithContext(ctx context.Context, interval time.Duration) error {
140+
var timer = time.NewTimer(interval)
141+
select {
142+
case <-ctx.Done():
143+
return ctx.Err()
144+
case <-timer.C:
145+
return nil
146+
}
147+
}

checks/cpu_windows.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package checks
33
import (
44
"context"
55

6-
"github.com/StackExchange/wmi"
6+
"github.com/yusufpapurcu/wmi"
77
)
88

99
// https://wutils.com/wmi/root/cimv2/win32_perfformatteddata_perfos_processor/

0 commit comments

Comments
 (0)