mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
MetricsHistory.Cleanup() was defined but never called, and even if called, it only removed old data points without deleting map entries for deleted containers/VMs. Each stale entry leaked ~224KB (7 pre-allocated slices). Changes: - Call metricsHistory.Cleanup() and rateTracker.Cleanup() in maintenance loop - Delete map entries entirely when all data points have expired - Return nil instead of empty slice in cleanupMetrics() to release backing arrays - Add Cleanup() method to RateTracker with 24-hour stale threshold - Add debug logging to track cleanup activity Related to #1153
1213 lines
33 KiB
Go
1213 lines
33 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewMetricsHistory(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
maxDataPoints int
|
|
retentionTime time.Duration
|
|
}{
|
|
{
|
|
name: "standard values",
|
|
maxDataPoints: 100,
|
|
retentionTime: time.Hour,
|
|
},
|
|
{
|
|
name: "zero max points",
|
|
maxDataPoints: 0,
|
|
retentionTime: time.Minute,
|
|
},
|
|
{
|
|
name: "zero retention",
|
|
maxDataPoints: 50,
|
|
retentionTime: 0,
|
|
},
|
|
{
|
|
name: "large values",
|
|
maxDataPoints: 10000,
|
|
retentionTime: 24 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(tt.maxDataPoints, tt.retentionTime)
|
|
if mh == nil {
|
|
t.Fatal("NewMetricsHistory returned nil")
|
|
}
|
|
if mh.maxDataPoints != tt.maxDataPoints {
|
|
t.Errorf("maxDataPoints = %d, want %d", mh.maxDataPoints, tt.maxDataPoints)
|
|
}
|
|
if mh.retentionTime != tt.retentionTime {
|
|
t.Errorf("retentionTime = %v, want %v", mh.retentionTime, tt.retentionTime)
|
|
}
|
|
if mh.guestMetrics == nil {
|
|
t.Error("guestMetrics map not initialized")
|
|
}
|
|
if mh.nodeMetrics == nil {
|
|
t.Error("nodeMetrics map not initialized")
|
|
}
|
|
if mh.storageMetrics == nil {
|
|
t.Error("storageMetrics map not initialized")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppendMetric(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
maxDataPoints int
|
|
retentionTime time.Duration
|
|
existing []MetricPoint
|
|
newPoint MetricPoint
|
|
wantLen int
|
|
wantFirst float64 // value of first point after append
|
|
wantLast float64 // value of last point after append
|
|
}{
|
|
{
|
|
name: "append to empty slice",
|
|
maxDataPoints: 10,
|
|
retentionTime: time.Hour,
|
|
existing: []MetricPoint{},
|
|
newPoint: MetricPoint{Value: 50.0, Timestamp: now},
|
|
wantLen: 1,
|
|
wantFirst: 50.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "append within limits",
|
|
maxDataPoints: 10,
|
|
retentionTime: time.Hour,
|
|
existing: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-30 * time.Minute)},
|
|
{Value: 20.0, Timestamp: now.Add(-20 * time.Minute)},
|
|
},
|
|
newPoint: MetricPoint{Value: 30.0, Timestamp: now},
|
|
wantLen: 3,
|
|
wantFirst: 10.0,
|
|
wantLast: 30.0,
|
|
},
|
|
{
|
|
name: "exceed max data points",
|
|
maxDataPoints: 3,
|
|
retentionTime: time.Hour,
|
|
existing: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-30 * time.Minute)},
|
|
{Value: 20.0, Timestamp: now.Add(-20 * time.Minute)},
|
|
{Value: 30.0, Timestamp: now.Add(-10 * time.Minute)},
|
|
},
|
|
newPoint: MetricPoint{Value: 40.0, Timestamp: now},
|
|
wantLen: 3,
|
|
wantFirst: 20.0, // oldest point dropped
|
|
wantLast: 40.0,
|
|
},
|
|
{
|
|
name: "old points beyond retention removed",
|
|
maxDataPoints: 100,
|
|
retentionTime: 30 * time.Minute,
|
|
existing: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)}, // beyond retention
|
|
{Value: 20.0, Timestamp: now.Add(-90 * time.Minute)}, // beyond retention
|
|
{Value: 30.0, Timestamp: now.Add(-20 * time.Minute)}, // within retention
|
|
},
|
|
newPoint: MetricPoint{Value: 40.0, Timestamp: now},
|
|
wantLen: 2,
|
|
wantFirst: 30.0, // old points removed
|
|
wantLast: 40.0,
|
|
},
|
|
{
|
|
name: "all old points removed except new",
|
|
maxDataPoints: 100,
|
|
retentionTime: 10 * time.Minute,
|
|
existing: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)},
|
|
{Value: 20.0, Timestamp: now.Add(-1 * time.Hour)},
|
|
},
|
|
newPoint: MetricPoint{Value: 30.0, Timestamp: now},
|
|
wantLen: 1,
|
|
wantFirst: 30.0,
|
|
wantLast: 30.0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(tt.maxDataPoints, tt.retentionTime)
|
|
result := mh.appendMetric(tt.existing, tt.newPoint)
|
|
|
|
if len(result) != tt.wantLen {
|
|
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
|
}
|
|
if len(result) > 0 {
|
|
if result[0].Value != tt.wantFirst {
|
|
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
|
|
}
|
|
if result[len(result)-1].Value != tt.wantLast {
|
|
t.Errorf("last value = %v, want %v", result[len(result)-1].Value, tt.wantLast)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanupMetrics(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
metrics []MetricPoint
|
|
cutoffTime time.Time
|
|
wantLen int
|
|
wantFirst float64
|
|
}{
|
|
{
|
|
name: "empty slice",
|
|
metrics: []MetricPoint{},
|
|
cutoffTime: now.Add(-time.Hour),
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "no points to remove",
|
|
metrics: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-30 * time.Minute)},
|
|
{Value: 20.0, Timestamp: now.Add(-20 * time.Minute)},
|
|
{Value: 30.0, Timestamp: now.Add(-10 * time.Minute)},
|
|
},
|
|
cutoffTime: now.Add(-time.Hour),
|
|
wantLen: 3,
|
|
wantFirst: 10.0,
|
|
},
|
|
{
|
|
name: "remove some old points",
|
|
metrics: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)},
|
|
{Value: 20.0, Timestamp: now.Add(-90 * time.Minute)},
|
|
{Value: 30.0, Timestamp: now.Add(-30 * time.Minute)},
|
|
{Value: 40.0, Timestamp: now.Add(-10 * time.Minute)},
|
|
},
|
|
cutoffTime: now.Add(-time.Hour),
|
|
wantLen: 2,
|
|
wantFirst: 30.0,
|
|
},
|
|
{
|
|
name: "remove all points",
|
|
metrics: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-3 * time.Hour)},
|
|
{Value: 20.0, Timestamp: now.Add(-2 * time.Hour)},
|
|
},
|
|
cutoffTime: now.Add(-time.Hour),
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "boundary - point exactly at cutoff",
|
|
metrics: []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-time.Hour)}, // exactly at cutoff (not after)
|
|
{Value: 20.0, Timestamp: now.Add(-30 * time.Minute)},
|
|
},
|
|
cutoffTime: now.Add(-time.Hour),
|
|
wantLen: 1,
|
|
wantFirst: 20.0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
result := mh.cleanupMetrics(tt.metrics, tt.cutoffTime)
|
|
|
|
if len(result) != tt.wantLen {
|
|
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
|
}
|
|
if len(result) > 0 && tt.wantFirst != 0 {
|
|
if result[0].Value != tt.wantFirst {
|
|
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddGuestMetric(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
guestID string
|
|
metricType string
|
|
value float64
|
|
}{
|
|
{name: "cpu metric", guestID: "vm-100", metricType: "cpu", value: 75.5},
|
|
{name: "memory metric", guestID: "vm-100", metricType: "memory", value: 80.0},
|
|
{name: "disk metric", guestID: "ct-101", metricType: "disk", value: 50.0},
|
|
{name: "diskread metric", guestID: "vm-102", metricType: "diskread", value: 100.5},
|
|
{name: "diskwrite metric", guestID: "vm-102", metricType: "diskwrite", value: 50.25},
|
|
{name: "netin metric", guestID: "vm-103", metricType: "netin", value: 1000.0},
|
|
{name: "netout metric", guestID: "vm-103", metricType: "netout", value: 500.0},
|
|
{name: "unknown metric type", guestID: "vm-104", metricType: "unknown", value: 99.9},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
mh.AddGuestMetric(tt.guestID, tt.metricType, tt.value, now)
|
|
|
|
// Verify guest was created
|
|
mh.mu.RLock()
|
|
defer mh.mu.RUnlock()
|
|
|
|
metrics, exists := mh.guestMetrics[tt.guestID]
|
|
if !exists {
|
|
t.Fatal("guest metrics not created")
|
|
}
|
|
|
|
// Verify correct metric type was populated
|
|
var slice []MetricPoint
|
|
switch tt.metricType {
|
|
case "cpu":
|
|
slice = metrics.CPU
|
|
case "memory":
|
|
slice = metrics.Memory
|
|
case "disk":
|
|
slice = metrics.Disk
|
|
case "diskread":
|
|
slice = metrics.DiskRead
|
|
case "diskwrite":
|
|
slice = metrics.DiskWrite
|
|
case "netin":
|
|
slice = metrics.NetworkIn
|
|
case "netout":
|
|
slice = metrics.NetworkOut
|
|
default:
|
|
// Unknown types should not populate any slice
|
|
if len(metrics.CPU)+len(metrics.Memory)+len(metrics.Disk)+
|
|
len(metrics.DiskRead)+len(metrics.DiskWrite)+
|
|
len(metrics.NetworkIn)+len(metrics.NetworkOut) != 0 {
|
|
t.Error("unknown metric type populated a slice")
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(slice) != 1 {
|
|
t.Errorf("slice length = %d, want 1", len(slice))
|
|
} else if slice[0].Value != tt.value {
|
|
t.Errorf("value = %v, want %v", slice[0].Value, tt.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddNodeMetric(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
nodeID string
|
|
metricType string
|
|
value float64
|
|
shouldAdd bool
|
|
}{
|
|
{name: "cpu metric", nodeID: "node1", metricType: "cpu", value: 45.5, shouldAdd: true},
|
|
{name: "memory metric", nodeID: "node1", metricType: "memory", value: 60.0, shouldAdd: true},
|
|
{name: "disk metric", nodeID: "node2", metricType: "disk", value: 70.0, shouldAdd: true},
|
|
{name: "unknown metric type", nodeID: "node3", metricType: "netin", value: 100.0, shouldAdd: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
mh.AddNodeMetric(tt.nodeID, tt.metricType, tt.value, now)
|
|
|
|
mh.mu.RLock()
|
|
defer mh.mu.RUnlock()
|
|
|
|
metrics, exists := mh.nodeMetrics[tt.nodeID]
|
|
if !exists {
|
|
t.Fatal("node metrics not created")
|
|
}
|
|
|
|
var slice []MetricPoint
|
|
switch tt.metricType {
|
|
case "cpu":
|
|
slice = metrics.CPU
|
|
case "memory":
|
|
slice = metrics.Memory
|
|
case "disk":
|
|
slice = metrics.Disk
|
|
default:
|
|
if tt.shouldAdd {
|
|
t.Error("expected metric to be added")
|
|
}
|
|
return
|
|
}
|
|
|
|
if tt.shouldAdd {
|
|
if len(slice) != 1 {
|
|
t.Errorf("slice length = %d, want 1", len(slice))
|
|
} else if slice[0].Value != tt.value {
|
|
t.Errorf("value = %v, want %v", slice[0].Value, tt.value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddStorageMetric(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
storageID string
|
|
metricType string
|
|
value float64
|
|
shouldAdd bool
|
|
}{
|
|
{name: "usage metric", storageID: "local-lvm", metricType: "usage", value: 45.5, shouldAdd: true},
|
|
{name: "used metric", storageID: "local-lvm", metricType: "used", value: 100e9, shouldAdd: true},
|
|
{name: "total metric", storageID: "ceph-pool", metricType: "total", value: 500e9, shouldAdd: true},
|
|
{name: "avail metric", storageID: "ceph-pool", metricType: "avail", value: 400e9, shouldAdd: true},
|
|
{name: "unknown metric type", storageID: "nfs-share", metricType: "iops", value: 1000.0, shouldAdd: false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
mh.AddStorageMetric(tt.storageID, tt.metricType, tt.value, now)
|
|
|
|
mh.mu.RLock()
|
|
defer mh.mu.RUnlock()
|
|
|
|
metrics, exists := mh.storageMetrics[tt.storageID]
|
|
if !exists {
|
|
t.Fatal("storage metrics not created")
|
|
}
|
|
|
|
var slice []MetricPoint
|
|
switch tt.metricType {
|
|
case "usage":
|
|
slice = metrics.Usage
|
|
case "used":
|
|
slice = metrics.Used
|
|
case "total":
|
|
slice = metrics.Total
|
|
case "avail":
|
|
slice = metrics.Avail
|
|
default:
|
|
if tt.shouldAdd {
|
|
t.Error("expected metric to be added")
|
|
}
|
|
return
|
|
}
|
|
|
|
if tt.shouldAdd {
|
|
if len(slice) != 1 {
|
|
t.Errorf("slice length = %d, want 1", len(slice))
|
|
} else if slice[0].Value != tt.value {
|
|
t.Errorf("value = %v, want %v", slice[0].Value, tt.value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGuestMetrics(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add test data
|
|
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-50*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-40*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-30*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-20*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-10*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "memory", 80.0, now.Add(-5*time.Minute))
|
|
|
|
tests := []struct {
|
|
name string
|
|
guestID string
|
|
metricType string
|
|
duration time.Duration
|
|
wantLen int
|
|
wantFirst float64
|
|
}{
|
|
{
|
|
name: "get all cpu within hour",
|
|
guestID: "vm-100",
|
|
metricType: "cpu",
|
|
duration: time.Hour,
|
|
wantLen: 5,
|
|
wantFirst: 10.0,
|
|
},
|
|
{
|
|
name: "get recent cpu only",
|
|
guestID: "vm-100",
|
|
metricType: "cpu",
|
|
duration: 25 * time.Minute,
|
|
wantLen: 2,
|
|
wantFirst: 40.0,
|
|
},
|
|
{
|
|
name: "get memory",
|
|
guestID: "vm-100",
|
|
metricType: "memory",
|
|
duration: time.Hour,
|
|
wantLen: 1,
|
|
wantFirst: 80.0,
|
|
},
|
|
{
|
|
name: "nonexistent guest",
|
|
guestID: "vm-999",
|
|
metricType: "cpu",
|
|
duration: time.Hour,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "nonexistent metric type",
|
|
guestID: "vm-100",
|
|
metricType: "invalid",
|
|
duration: time.Hour,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "zero duration returns nothing",
|
|
guestID: "vm-100",
|
|
metricType: "cpu",
|
|
duration: 0,
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mh.GetGuestMetrics(tt.guestID, tt.metricType, tt.duration)
|
|
|
|
if len(result) != tt.wantLen {
|
|
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
|
}
|
|
if len(result) > 0 && tt.wantFirst != 0 {
|
|
if result[0].Value != tt.wantFirst {
|
|
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGuestMetrics_AllMetricTypes(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
metricType string
|
|
value float64
|
|
}{
|
|
{name: "cpu metric", metricType: "cpu", value: 50.5},
|
|
{name: "memory metric", metricType: "memory", value: 70.0},
|
|
{name: "disk metric", metricType: "disk", value: 45.0},
|
|
{name: "diskread metric", metricType: "diskread", value: 1024.0},
|
|
{name: "diskwrite metric", metricType: "diskwrite", value: 512.0},
|
|
{name: "netin metric", metricType: "netin", value: 2048.0},
|
|
{name: "netout metric", metricType: "netout", value: 1536.0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
mh.AddGuestMetric("vm-test", tt.metricType, tt.value, now.Add(-5*time.Minute))
|
|
|
|
result := mh.GetGuestMetrics("vm-test", tt.metricType, time.Hour)
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("len(result) = %d, want 1", len(result))
|
|
}
|
|
if result[0].Value != tt.value {
|
|
t.Errorf("value = %v, want %v", result[0].Value, tt.value)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGuestMetrics_GuestNotFound(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add data for one guest
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, time.Now())
|
|
|
|
// Query non-existent guest
|
|
result := mh.GetGuestMetrics("vm-nonexistent", "cpu", time.Hour)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice for non-existent guest, got %d elements", len(result))
|
|
}
|
|
if result == nil {
|
|
t.Error("expected empty slice, got nil")
|
|
}
|
|
}
|
|
|
|
func TestGetGuestMetrics_UnknownMetricType(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add data for the guest
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-5*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "memory", 70.0, now.Add(-5*time.Minute))
|
|
|
|
unknownTypes := []string{"invalid", "unknown", "foo", "bar", "CPU", "Memory", ""}
|
|
|
|
for _, metricType := range unknownTypes {
|
|
t.Run("type_"+metricType, func(t *testing.T) {
|
|
result := mh.GetGuestMetrics("vm-100", metricType, time.Hour)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice for unknown metric type %q, got %d elements", metricType, len(result))
|
|
}
|
|
if result == nil {
|
|
t.Errorf("expected empty slice for unknown metric type %q, got nil", metricType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGuestMetrics_DurationFiltering(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, 2*time.Hour)
|
|
|
|
// Add points at different times
|
|
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-90*time.Minute)) // old
|
|
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-60*time.Minute)) // old
|
|
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-30*time.Minute)) // recent
|
|
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-15*time.Minute)) // recent
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-5*time.Minute)) // recent
|
|
|
|
tests := []struct {
|
|
name string
|
|
duration time.Duration
|
|
wantLen int
|
|
wantFirst float64
|
|
wantLast float64
|
|
}{
|
|
{
|
|
name: "all points within 2 hours",
|
|
duration: 2 * time.Hour,
|
|
wantLen: 5,
|
|
wantFirst: 10.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "points within 45 minutes",
|
|
duration: 45 * time.Minute,
|
|
wantLen: 3,
|
|
wantFirst: 30.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "points within 20 minutes",
|
|
duration: 20 * time.Minute,
|
|
wantLen: 2,
|
|
wantFirst: 40.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "points within 10 minutes",
|
|
duration: 10 * time.Minute,
|
|
wantLen: 1,
|
|
wantFirst: 50.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "zero duration excludes all",
|
|
duration: 0,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "very short duration excludes all",
|
|
duration: 1 * time.Minute,
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mh.GetGuestMetrics("vm-100", "cpu", tt.duration)
|
|
|
|
if len(result) != tt.wantLen {
|
|
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
|
}
|
|
if len(result) > 0 {
|
|
if result[0].Value != tt.wantFirst {
|
|
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
|
|
}
|
|
if result[len(result)-1].Value != tt.wantLast {
|
|
t.Errorf("last value = %v, want %v", result[len(result)-1].Value, tt.wantLast)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetGuestMetrics_EmptyMetricsData(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Directly populate an empty guest metrics entry
|
|
mh.mu.Lock()
|
|
mh.guestMetrics["vm-empty"] = &GuestMetrics{
|
|
CPU: []MetricPoint{},
|
|
Memory: []MetricPoint{},
|
|
Disk: []MetricPoint{},
|
|
DiskRead: []MetricPoint{},
|
|
DiskWrite: []MetricPoint{},
|
|
NetworkIn: []MetricPoint{},
|
|
NetworkOut: []MetricPoint{},
|
|
}
|
|
mh.mu.Unlock()
|
|
|
|
metricTypes := []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"}
|
|
|
|
for _, metricType := range metricTypes {
|
|
t.Run(metricType, func(t *testing.T) {
|
|
result := mh.GetGuestMetrics("vm-empty", metricType, time.Hour)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice for empty %s metrics, got %d elements", metricType, len(result))
|
|
}
|
|
if result == nil {
|
|
t.Errorf("expected empty slice for empty %s metrics, got nil", metricType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNodeMetrics(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add test data
|
|
mh.AddNodeMetric("node1", "cpu", 25.0, now.Add(-45*time.Minute))
|
|
mh.AddNodeMetric("node1", "cpu", 35.0, now.Add(-30*time.Minute))
|
|
mh.AddNodeMetric("node1", "cpu", 45.0, now.Add(-15*time.Minute))
|
|
mh.AddNodeMetric("node1", "memory", 60.0, now.Add(-10*time.Minute))
|
|
mh.AddNodeMetric("node1", "disk", 75.0, now.Add(-5*time.Minute))
|
|
|
|
tests := []struct {
|
|
name string
|
|
nodeID string
|
|
metricType string
|
|
duration time.Duration
|
|
wantLen int
|
|
}{
|
|
{
|
|
name: "get all cpu",
|
|
nodeID: "node1",
|
|
metricType: "cpu",
|
|
duration: time.Hour,
|
|
wantLen: 3,
|
|
},
|
|
{
|
|
name: "get recent cpu",
|
|
nodeID: "node1",
|
|
metricType: "cpu",
|
|
duration: 20 * time.Minute,
|
|
wantLen: 1,
|
|
},
|
|
{
|
|
name: "get memory",
|
|
nodeID: "node1",
|
|
metricType: "memory",
|
|
duration: time.Hour,
|
|
wantLen: 1,
|
|
},
|
|
{
|
|
name: "get disk",
|
|
nodeID: "node1",
|
|
metricType: "disk",
|
|
duration: time.Hour,
|
|
wantLen: 1,
|
|
},
|
|
{
|
|
name: "nonexistent node",
|
|
nodeID: "node999",
|
|
metricType: "cpu",
|
|
duration: time.Hour,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "invalid metric type",
|
|
nodeID: "node1",
|
|
metricType: "netin",
|
|
duration: time.Hour,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "zero duration returns nothing",
|
|
nodeID: "node1",
|
|
metricType: "cpu",
|
|
duration: 0,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "empty nodeID",
|
|
nodeID: "",
|
|
metricType: "cpu",
|
|
duration: time.Hour,
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mh.GetNodeMetrics(tt.nodeID, tt.metricType, tt.duration)
|
|
if len(result) != tt.wantLen {
|
|
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNodeMetrics_UnknownMetricTypes(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add data for the node
|
|
mh.AddNodeMetric("node1", "cpu", 50.0, now.Add(-5*time.Minute))
|
|
mh.AddNodeMetric("node1", "memory", 70.0, now.Add(-5*time.Minute))
|
|
mh.AddNodeMetric("node1", "disk", 40.0, now.Add(-5*time.Minute))
|
|
|
|
unknownTypes := []string{"invalid", "unknown", "netin", "netout", "diskread", "CPU", "Memory", "Disk", ""}
|
|
|
|
for _, metricType := range unknownTypes {
|
|
t.Run("type_"+metricType, func(t *testing.T) {
|
|
result := mh.GetNodeMetrics("node1", metricType, time.Hour)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice for unknown metric type %q, got %d elements", metricType, len(result))
|
|
}
|
|
if result == nil {
|
|
t.Errorf("expected empty slice for unknown metric type %q, got nil", metricType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNodeMetrics_EmptyMetricsData(t *testing.T) {
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Directly populate an empty node metrics entry (nodes use GuestMetrics type)
|
|
mh.mu.Lock()
|
|
mh.nodeMetrics["node-empty"] = &GuestMetrics{
|
|
CPU: []MetricPoint{},
|
|
Memory: []MetricPoint{},
|
|
Disk: []MetricPoint{},
|
|
}
|
|
mh.mu.Unlock()
|
|
|
|
metricTypes := []string{"cpu", "memory", "disk"}
|
|
|
|
for _, metricType := range metricTypes {
|
|
t.Run(metricType, func(t *testing.T) {
|
|
result := mh.GetNodeMetrics("node-empty", metricType, time.Hour)
|
|
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty slice for empty %s metrics, got %d elements", metricType, len(result))
|
|
}
|
|
if result == nil {
|
|
t.Errorf("expected empty slice for empty %s metrics, got nil", metricType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetNodeMetrics_DurationFiltering(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, 2*time.Hour)
|
|
|
|
// Add points at different times
|
|
mh.AddNodeMetric("node1", "cpu", 10.0, now.Add(-90*time.Minute)) // old
|
|
mh.AddNodeMetric("node1", "cpu", 20.0, now.Add(-60*time.Minute)) // old
|
|
mh.AddNodeMetric("node1", "cpu", 30.0, now.Add(-30*time.Minute)) // recent
|
|
mh.AddNodeMetric("node1", "cpu", 40.0, now.Add(-15*time.Minute)) // recent
|
|
mh.AddNodeMetric("node1", "cpu", 50.0, now.Add(-5*time.Minute)) // recent
|
|
|
|
tests := []struct {
|
|
name string
|
|
duration time.Duration
|
|
wantLen int
|
|
wantFirst float64
|
|
wantLast float64
|
|
}{
|
|
{
|
|
name: "all points within 2 hours",
|
|
duration: 2 * time.Hour,
|
|
wantLen: 5,
|
|
wantFirst: 10.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "points within 45 minutes",
|
|
duration: 45 * time.Minute,
|
|
wantLen: 3,
|
|
wantFirst: 30.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "points within 20 minutes",
|
|
duration: 20 * time.Minute,
|
|
wantLen: 2,
|
|
wantFirst: 40.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "points within 10 minutes",
|
|
duration: 10 * time.Minute,
|
|
wantLen: 1,
|
|
wantFirst: 50.0,
|
|
wantLast: 50.0,
|
|
},
|
|
{
|
|
name: "zero duration excludes all",
|
|
duration: 0,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "very short duration excludes all",
|
|
duration: 1 * time.Minute,
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mh.GetNodeMetrics("node1", "cpu", tt.duration)
|
|
|
|
if len(result) != tt.wantLen {
|
|
t.Errorf("len(result) = %d, want %d", len(result), tt.wantLen)
|
|
}
|
|
if len(result) > 0 {
|
|
if result[0].Value != tt.wantFirst {
|
|
t.Errorf("first value = %v, want %v", result[0].Value, tt.wantFirst)
|
|
}
|
|
if result[len(result)-1].Value != tt.wantLast {
|
|
t.Errorf("last value = %v, want %v", result[len(result)-1].Value, tt.wantLast)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetAllGuestMetrics(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add test data for multiple metric types
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-30*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "memory", 70.0, now.Add(-25*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "disk", 40.0, now.Add(-20*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "diskread", 100.0, now.Add(-15*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "diskwrite", 50.0, now.Add(-10*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "netin", 1000.0, now.Add(-5*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "netout", 500.0, now.Add(-1*time.Minute))
|
|
|
|
tests := []struct {
|
|
name string
|
|
guestID string
|
|
duration time.Duration
|
|
wantKeys []string
|
|
}{
|
|
{
|
|
name: "get all metrics within hour",
|
|
guestID: "vm-100",
|
|
duration: time.Hour,
|
|
wantKeys: []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"},
|
|
},
|
|
{
|
|
name: "nonexistent guest",
|
|
guestID: "vm-999",
|
|
duration: time.Hour,
|
|
wantKeys: []string{},
|
|
},
|
|
{
|
|
name: "short duration filters out old",
|
|
guestID: "vm-100",
|
|
duration: 10 * time.Minute,
|
|
wantKeys: []string{"cpu", "memory", "disk", "diskread", "diskwrite", "netin", "netout"}, // keys exist but may be empty
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mh.GetAllGuestMetrics(tt.guestID, tt.duration)
|
|
|
|
if tt.guestID == "vm-999" {
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty result for nonexistent guest")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Check that expected keys exist
|
|
for _, key := range tt.wantKeys {
|
|
if _, exists := result[key]; !exists {
|
|
t.Errorf("missing key: %s", key)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetAllStorageMetrics(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add test data
|
|
mh.AddStorageMetric("local-lvm", "usage", 45.0, now.Add(-30*time.Minute))
|
|
mh.AddStorageMetric("local-lvm", "used", 100e9, now.Add(-25*time.Minute))
|
|
mh.AddStorageMetric("local-lvm", "total", 200e9, now.Add(-20*time.Minute))
|
|
mh.AddStorageMetric("local-lvm", "avail", 100e9, now.Add(-15*time.Minute))
|
|
|
|
tests := []struct {
|
|
name string
|
|
storageID string
|
|
duration time.Duration
|
|
wantKeys []string
|
|
}{
|
|
{
|
|
name: "get all metrics",
|
|
storageID: "local-lvm",
|
|
duration: time.Hour,
|
|
wantKeys: []string{"usage", "used", "total", "avail"},
|
|
},
|
|
{
|
|
name: "nonexistent storage",
|
|
storageID: "nonexistent",
|
|
duration: time.Hour,
|
|
wantKeys: []string{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := mh.GetAllStorageMetrics(tt.storageID, tt.duration)
|
|
|
|
if len(tt.wantKeys) == 0 {
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty result")
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, key := range tt.wantKeys {
|
|
if _, exists := result[key]; !exists {
|
|
t.Errorf("missing key: %s", key)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanup(t *testing.T) {
|
|
now := time.Now()
|
|
retentionTime := 30 * time.Minute
|
|
mh := NewMetricsHistory(100, retentionTime)
|
|
|
|
// Add old data (beyond retention)
|
|
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-2*time.Hour))
|
|
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-90*time.Minute))
|
|
|
|
// Add recent data (within retention)
|
|
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-20*time.Minute))
|
|
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-10*time.Minute))
|
|
|
|
// Add node metrics
|
|
mh.AddNodeMetric("node1", "cpu", 50.0, now.Add(-2*time.Hour))
|
|
mh.AddNodeMetric("node1", "cpu", 60.0, now.Add(-10*time.Minute))
|
|
|
|
// Add storage metrics
|
|
mh.AddStorageMetric("local", "usage", 70.0, now.Add(-2*time.Hour))
|
|
mh.AddStorageMetric("local", "usage", 80.0, now.Add(-10*time.Minute))
|
|
|
|
// Run cleanup
|
|
mh.Cleanup()
|
|
|
|
// Verify old data was removed
|
|
mh.mu.RLock()
|
|
defer mh.mu.RUnlock()
|
|
|
|
// Check guest metrics
|
|
guestMetrics := mh.guestMetrics["vm-100"]
|
|
if len(guestMetrics.CPU) != 2 {
|
|
t.Errorf("guest CPU metrics count = %d, want 2", len(guestMetrics.CPU))
|
|
}
|
|
|
|
// Check node metrics
|
|
nodeMetrics := mh.nodeMetrics["node1"]
|
|
if len(nodeMetrics.CPU) != 1 {
|
|
t.Errorf("node CPU metrics count = %d, want 1", len(nodeMetrics.CPU))
|
|
}
|
|
|
|
// Check storage metrics
|
|
storageMetrics := mh.storageMetrics["local"]
|
|
if len(storageMetrics.Usage) != 1 {
|
|
t.Errorf("storage usage metrics count = %d, want 1", len(storageMetrics.Usage))
|
|
}
|
|
}
|
|
|
|
func TestMultipleGuestsIsolation(t *testing.T) {
|
|
now := time.Now()
|
|
mh := NewMetricsHistory(100, time.Hour)
|
|
|
|
// Add metrics for different guests
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, now)
|
|
mh.AddGuestMetric("vm-101", "cpu", 60.0, now)
|
|
mh.AddGuestMetric("vm-102", "cpu", 70.0, now)
|
|
|
|
// Verify isolation
|
|
result100 := mh.GetGuestMetrics("vm-100", "cpu", time.Hour)
|
|
result101 := mh.GetGuestMetrics("vm-101", "cpu", time.Hour)
|
|
result102 := mh.GetGuestMetrics("vm-102", "cpu", time.Hour)
|
|
|
|
if len(result100) != 1 || result100[0].Value != 50.0 {
|
|
t.Errorf("vm-100 metrics incorrect")
|
|
}
|
|
if len(result101) != 1 || result101[0].Value != 60.0 {
|
|
t.Errorf("vm-101 metrics incorrect")
|
|
}
|
|
if len(result102) != 1 || result102[0].Value != 70.0 {
|
|
t.Errorf("vm-102 metrics incorrect")
|
|
}
|
|
}
|
|
|
|
func TestMaxDataPointsEnforced(t *testing.T) {
|
|
now := time.Now()
|
|
maxPoints := 5
|
|
mh := NewMetricsHistory(maxPoints, time.Hour)
|
|
|
|
// Add more points than maxDataPoints
|
|
for i := 0; i < 10; i++ {
|
|
mh.AddGuestMetric("vm-100", "cpu", float64(i*10), now.Add(-time.Duration(10-i)*time.Minute))
|
|
}
|
|
|
|
result := mh.GetGuestMetrics("vm-100", "cpu", time.Hour)
|
|
if len(result) != maxPoints {
|
|
t.Errorf("got %d points, want %d", len(result), maxPoints)
|
|
}
|
|
|
|
// Verify we have the most recent points (values 50-90)
|
|
if result[0].Value != 50.0 {
|
|
t.Errorf("first value = %v, want 50.0 (most recent kept)", result[0].Value)
|
|
}
|
|
if result[len(result)-1].Value != 90.0 {
|
|
t.Errorf("last value = %v, want 90.0", result[len(result)-1].Value)
|
|
}
|
|
}
|
|
|
|
func TestRetentionTimeEnforced(t *testing.T) {
|
|
now := time.Now()
|
|
retentionTime := 30 * time.Minute
|
|
mh := NewMetricsHistory(100, retentionTime)
|
|
|
|
// Add points at various times
|
|
mh.AddGuestMetric("vm-100", "cpu", 10.0, now.Add(-2*time.Hour)) // beyond retention
|
|
mh.AddGuestMetric("vm-100", "cpu", 20.0, now.Add(-1*time.Hour)) // beyond retention
|
|
mh.AddGuestMetric("vm-100", "cpu", 30.0, now.Add(-45*time.Minute)) // beyond retention
|
|
mh.AddGuestMetric("vm-100", "cpu", 40.0, now.Add(-20*time.Minute)) // within retention
|
|
mh.AddGuestMetric("vm-100", "cpu", 50.0, now.Add(-10*time.Minute)) // within retention
|
|
mh.AddGuestMetric("vm-100", "cpu", 60.0, now) // within retention
|
|
|
|
// appendMetric removes old points on each add
|
|
result := mh.GetGuestMetrics("vm-100", "cpu", time.Hour)
|
|
|
|
// Due to appendMetric behavior, only recent points should remain
|
|
if len(result) > 3 {
|
|
t.Errorf("got %d points, expected <= 3 recent points after retention enforcement", len(result))
|
|
}
|
|
}
|
|
|
|
func TestCleanupRemovesStaleEntries(t *testing.T) {
|
|
now := time.Now()
|
|
retentionTime := 30 * time.Minute
|
|
mh := NewMetricsHistory(100, retentionTime)
|
|
|
|
// Add data for three guests:
|
|
// - "active-guest" has recent data
|
|
// - "stale-guest" has only old data (will be removed)
|
|
// - "mixed-guest" has both old and recent data
|
|
|
|
mh.AddGuestMetric("active-guest", "cpu", 50.0, now.Add(-10*time.Minute))
|
|
mh.AddGuestMetric("active-guest", "memory", 60.0, now.Add(-10*time.Minute))
|
|
|
|
mh.AddGuestMetric("stale-guest", "cpu", 30.0, now.Add(-2*time.Hour))
|
|
mh.AddGuestMetric("stale-guest", "memory", 40.0, now.Add(-2*time.Hour))
|
|
|
|
mh.AddGuestMetric("mixed-guest", "cpu", 10.0, now.Add(-2*time.Hour)) // old, will be removed
|
|
mh.AddGuestMetric("mixed-guest", "cpu", 70.0, now.Add(-5*time.Minute))
|
|
|
|
// Add similar for nodes and storage
|
|
mh.AddNodeMetric("active-node", "cpu", 50.0, now.Add(-10*time.Minute))
|
|
mh.AddNodeMetric("stale-node", "cpu", 30.0, now.Add(-2*time.Hour))
|
|
|
|
mh.AddStorageMetric("active-storage", "usage", 50.0, now.Add(-10*time.Minute))
|
|
mh.AddStorageMetric("stale-storage", "usage", 30.0, now.Add(-2*time.Hour))
|
|
|
|
// Run cleanup
|
|
mh.Cleanup()
|
|
|
|
// Verify stale entries were removed
|
|
mh.mu.RLock()
|
|
defer mh.mu.RUnlock()
|
|
|
|
// active-guest should still exist
|
|
if _, exists := mh.guestMetrics["active-guest"]; !exists {
|
|
t.Error("active-guest should still exist after cleanup")
|
|
}
|
|
|
|
// stale-guest should be removed (all data expired)
|
|
if _, exists := mh.guestMetrics["stale-guest"]; exists {
|
|
t.Error("stale-guest should be removed after cleanup")
|
|
}
|
|
|
|
// mixed-guest should still exist (has recent data)
|
|
if _, exists := mh.guestMetrics["mixed-guest"]; !exists {
|
|
t.Error("mixed-guest should still exist after cleanup")
|
|
}
|
|
|
|
// Verify node entries
|
|
if _, exists := mh.nodeMetrics["active-node"]; !exists {
|
|
t.Error("active-node should still exist after cleanup")
|
|
}
|
|
if _, exists := mh.nodeMetrics["stale-node"]; exists {
|
|
t.Error("stale-node should be removed after cleanup")
|
|
}
|
|
|
|
// Verify storage entries
|
|
if _, exists := mh.storageMetrics["active-storage"]; !exists {
|
|
t.Error("active-storage should still exist after cleanup")
|
|
}
|
|
if _, exists := mh.storageMetrics["stale-storage"]; exists {
|
|
t.Error("stale-storage should be removed after cleanup")
|
|
}
|
|
}
|
|
|
|
func TestCleanupMetricsReturnsNilForExpiredData(t *testing.T) {
|
|
retentionTime := 30 * time.Minute
|
|
mh := NewMetricsHistory(100, retentionTime)
|
|
now := time.Now()
|
|
cutoff := now.Add(-retentionTime)
|
|
|
|
// Create slice with only old data
|
|
oldData := []MetricPoint{
|
|
{Value: 10.0, Timestamp: now.Add(-2 * time.Hour)},
|
|
{Value: 20.0, Timestamp: now.Add(-1 * time.Hour)},
|
|
}
|
|
|
|
result := mh.cleanupMetrics(oldData, cutoff)
|
|
|
|
// Should return nil (not empty slice) to release backing array
|
|
if result != nil {
|
|
t.Errorf("cleanupMetrics should return nil for fully expired data, got slice with len=%d cap=%d", len(result), cap(result))
|
|
}
|
|
}
|