Skip to content

Commit 6c43f98

Browse files
committed
feat: measure mounted disk emas as well
1 parent a71eaf4 commit 6c43f98

2 files changed

Lines changed: 143 additions & 11 deletions

File tree

main.go

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type SystemMonitor struct {
3838
// EMA tracking
3939
cpuEMA float64
4040
memoryEMA float64
41-
diskEMA float64
41+
diskEMAs map[string]float64 // Map to track EMAs for all disks (root and mounted)
4242
alpha float64 // EMA smoothing factor
4343
}
4444

@@ -65,6 +65,7 @@ func NewSystemMonitor(betterStackURL string, interval int, cpuLimit, memoryLimit
6565
diskLimit: diskLimit,
6666
interval: interval,
6767
log: New(),
68+
diskEMAs: make(map[string]float64), // Initialize the map for all disk EMAs
6869
alpha: alpha,
6970
}, nil
7071
}
@@ -151,20 +152,31 @@ func (s *SystemMonitor) checkMemory() error {
151152

152153
func (s *SystemMonitor) checkDisk() error {
153154
// Check root partition
154-
usage, err := disk.Usage("/")
155+
rootPath := "/"
156+
usage, err := disk.Usage(rootPath)
155157
if err != nil {
156158
return fmt.Errorf("failed to get disk usage: %v", err)
157159
}
158160

159161
instantValue := usage.UsedPercent
160-
s.diskEMA = s.calculateEMA(instantValue, s.diskEMA)
161162

162-
status := s.getStatus(s.diskEMA, s.diskLimit)
163+
// Calculate or update EMA for root disk
164+
if _, exists := s.diskEMAs[rootPath]; !exists {
165+
// Initialize EMA with current value if this is first check
166+
s.diskEMAs[rootPath] = instantValue
167+
}
168+
169+
// Update EMA for root disk
170+
s.diskEMAs[rootPath] = s.calculateEMA(instantValue, s.diskEMAs[rootPath])
171+
172+
rootEMA := s.diskEMAs[rootPath]
173+
status := s.getStatus(rootEMA, s.diskLimit)
174+
163175
if status == "fail" {
164-
s.log.Warn("Root disk usage EMA %.2f%% exceeds limit of %.2f%% (instant: %.2f%%)", s.diskEMA, s.diskLimit, instantValue)
176+
s.log.Warn("Root disk usage EMA %.2f%% exceeds limit of %.2f%% (instant: %.2f%%)", rootEMA, s.diskLimit, instantValue)
165177
} else {
166178
s.log.Log("Root disk usage EMA: %.2f%% (limit: %.2f%%, instant: %.2f%%), Free: %d MB, Total: %d MB",
167-
s.diskEMA,
179+
rootEMA,
168180
s.diskLimit,
169181
instantValue,
170182
usage.Free/(1024*1024),
@@ -177,7 +189,7 @@ func (s *SystemMonitor) checkDisk() error {
177189
AlertID: fmt.Sprintf("disk-root-%s", s.hostname),
178190
Timestamp: time.Now().Unix(),
179191
Status: status,
180-
Value: s.diskEMA,
192+
Value: rootEMA,
181193
Limit: s.diskLimit,
182194
}); err != nil {
183195
return err
@@ -197,13 +209,25 @@ func (s *SystemMonitor) checkDisk() error {
197209
}
198210

199211
instantValue := usage.UsedPercent
200-
status := s.getStatus(s.diskEMA, s.diskLimit)
212+
213+
// Calculate or update EMA for this mount
214+
if _, exists := s.diskEMAs[mount]; !exists {
215+
// Initialize EMA with current value if this is first check
216+
s.diskEMAs[mount] = instantValue
217+
}
218+
219+
// Update EMA for this mount
220+
s.diskEMAs[mount] = s.calculateEMA(instantValue, s.diskEMAs[mount])
221+
222+
mountEMA := s.diskEMAs[mount]
223+
status := s.getStatus(mountEMA, s.diskLimit)
224+
201225
if status == "fail" {
202-
s.log.Warn("Disk usage for %s EMA %.2f%% exceeds limit of %.2f%% (instant: %.2f%%)", mount, s.diskEMA, s.diskLimit, instantValue)
226+
s.log.Warn("Disk usage for %s EMA %.2f%% exceeds limit of %.2f%% (instant: %.2f%%)", mount, mountEMA, s.diskLimit, instantValue)
203227
} else {
204228
s.log.Log("Disk usage for %s EMA: %.2f%% (limit: %.2f%%, instant: %.2f%%), Free: %d MB, Total: %d MB",
205229
mount,
206-
s.diskEMA,
230+
mountEMA,
207231
s.diskLimit,
208232
instantValue,
209233
usage.Free/(1024*1024),
@@ -216,7 +240,7 @@ func (s *SystemMonitor) checkDisk() error {
216240
AlertID: fmt.Sprintf("disk-%s-%s", filepath.Base(mount), s.hostname),
217241
Timestamp: time.Now().Unix(),
218242
Status: status,
219-
Value: s.diskEMA,
243+
Value: mountEMA,
220244
Limit: s.diskLimit,
221245
}); err != nil {
222246
return err

main_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,112 @@ func TestEMADifferentIntervals(t *testing.T) {
141141
}
142142
})
143143
}
144+
}
145+
146+
func TestDiskEMAs(t *testing.T) {
147+
// Create a monitor with a 60-second interval
148+
monitor, err := NewSystemMonitor("http://test.com", 60, 90.0, 90.0, 90.0)
149+
if err != nil {
150+
t.Fatalf("Failed to create monitor: %v", err)
151+
}
152+
153+
// Test case 1: Verify root disk EMA updates correctly
154+
t.Run("RootDiskEMA", func(t *testing.T) {
155+
rootPath := "/"
156+
157+
// Simulate initial usage
158+
monitor.diskEMAs[rootPath] = 50.0
159+
160+
// Simulate a series of measurements
161+
values := []float64{60.0, 70.0, 80.0, 90.0}
162+
for _, v := range values {
163+
monitor.diskEMAs[rootPath] = monitor.calculateEMA(v, monitor.diskEMAs[rootPath])
164+
}
165+
166+
// EMA should be between 70% and 90%
167+
if monitor.diskEMAs[rootPath] < 70.0 || monitor.diskEMAs[rootPath] > 90.0 {
168+
t.Errorf("Expected root disk EMA to be between 70%% and 90%%, got %.2f%%", monitor.diskEMAs[rootPath])
169+
}
170+
})
171+
172+
// Test case 2: Verify multiple mount points are tracked independently
173+
t.Run("MultipleMountEMAs", func(t *testing.T) {
174+
// Setup test mounts with different starting values
175+
mounts := map[string]float64{
176+
"/mnt/data1": 30.0,
177+
"/mnt/data2": 50.0,
178+
"/mnt/logs": 70.0,
179+
}
180+
181+
// Initialize starting values
182+
for path, value := range mounts {
183+
monitor.diskEMAs[path] = value
184+
}
185+
186+
// Apply the same change to all mounts (+20%)
187+
for path := range mounts {
188+
currentValue := monitor.diskEMAs[path]
189+
newValue := currentValue + 20.0
190+
if newValue > 100.0 {
191+
newValue = 100.0
192+
}
193+
monitor.diskEMAs[path] = monitor.calculateEMA(newValue, currentValue)
194+
}
195+
196+
// Verify each mount's EMA updated independently
197+
for path, initialValue := range mounts {
198+
expectedMinimum := initialValue
199+
expectedMaximum := initialValue + 20.0 // Full change would be +20%
200+
if expectedMaximum > 100.0 {
201+
expectedMaximum = 100.0
202+
}
203+
204+
// With our alpha, the EMA should be approximately between the initial value and the initial + 7% (1/3 of 20%)
205+
expectedMinimum = initialValue
206+
expectedMaximum = initialValue + 7.0
207+
208+
actualValue := monitor.diskEMAs[path]
209+
if actualValue < expectedMinimum || actualValue > expectedMaximum {
210+
t.Errorf("Mount %s: Expected EMA between %.2f%% and %.2f%%, got %.2f%%",
211+
path, expectedMinimum, expectedMaximum, actualValue)
212+
}
213+
}
214+
215+
// Extract just the mount values we're testing
216+
mountValues := make([]float64, 0, len(mounts))
217+
for path := range mounts {
218+
mountValues = append(mountValues, monitor.diskEMAs[path])
219+
}
220+
221+
// Verify mount points have different values (they're independent)
222+
if len(unique(mountValues)) != len(mounts) {
223+
t.Errorf("Expected all mount EMAs to be different, got: %v", mountValues)
224+
}
225+
})
226+
}
227+
228+
// Helper functions for the disk EMA tests
229+
230+
// Return values from a map as a slice
231+
func getValues(m map[string]float64) []float64 {
232+
values := make([]float64, 0, len(m))
233+
for _, v := range m {
234+
values = append(values, v)
235+
}
236+
return values
237+
}
238+
239+
// Return unique values from a slice
240+
func unique(values []float64) []float64 {
241+
seen := make(map[float64]bool)
242+
unique := make([]float64, 0)
243+
244+
for _, v := range values {
245+
if !seen[v] {
246+
seen[v] = true
247+
unique = append(unique, v)
248+
}
249+
}
250+
251+
return unique
144252
}

0 commit comments

Comments
 (0)