test: Add Docker container health and OOM kill alert tests
Coverage improvements: - checkDockerContainerHealth: 21.1% -> 94.7% - checkDockerContainerOOMKill: 19.2% -> 96.2% Tests cover: - Health states (healthy, empty, none, starting, unhealthy, degraded) - Health alert recovery when container becomes healthy - OOM kill detection (exit code 137) - OOM alert deduplication (repeated 137 doesn't re-alert) - OOM alert clearing when container recovers or exits with different code
This commit is contained in:
parent
1ed8b18c12
commit
b6624e32cf
1 changed files with 501 additions and 0 deletions
|
|
@ -9448,3 +9448,504 @@ func TestCheckPMGNodeQueues(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerContainerHealthAlert(t *testing.T) {
|
||||
t.Run("healthy container - no alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-health-1",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-1",
|
||||
Name: "healthy-app",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes",
|
||||
Health: "healthy",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected no health alert for healthy container")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("container with empty health - no alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-health-2",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-2",
|
||||
Name: "no-health-check",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes",
|
||||
Health: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected no health alert for container with empty health")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("container with none health - no alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-health-3",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-3",
|
||||
Name: "no-health-check",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes",
|
||||
Health: "none",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected no health alert for container with none health")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("container starting - no alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-health-4",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-4",
|
||||
Name: "starting-app",
|
||||
State: "running",
|
||||
Status: "Up 5 seconds",
|
||||
Health: "starting",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected no health alert for starting container")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unhealthy container - critical alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-health-5",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-5",
|
||||
Name: "unhealthy-app",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes (unhealthy)",
|
||||
Health: "unhealthy",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
alert, exists := m.activeAlerts[alertID]
|
||||
if !exists {
|
||||
t.Fatal("expected health alert for unhealthy container")
|
||||
}
|
||||
if alert.Level != AlertLevelCritical {
|
||||
t.Fatalf("expected critical alert for unhealthy container, got %s", alert.Level)
|
||||
}
|
||||
if alert.Type != "docker-container-health" {
|
||||
t.Fatalf("expected alert type docker-container-health, got %s", alert.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("container with other health status - warning alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-health-6",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-6",
|
||||
Name: "degraded-app",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes",
|
||||
Health: "degraded",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
alert, exists := m.activeAlerts[alertID]
|
||||
if !exists {
|
||||
t.Fatal("expected health alert for degraded container")
|
||||
}
|
||||
if alert.Level != AlertLevelWarning {
|
||||
t.Fatalf("expected warning alert for non-unhealthy bad status, got %s", alert.Level)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("alert cleared when container becomes healthy", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
hostID := "host-health-7"
|
||||
containerID := "container-7"
|
||||
|
||||
// First check with unhealthy container
|
||||
hostUnhealthy := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "recovering-app",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes (unhealthy)",
|
||||
Health: "unhealthy",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(hostUnhealthy)
|
||||
|
||||
resourceID := dockerResourceID(hostID, containerID)
|
||||
alertID := fmt.Sprintf("docker-container-health-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; !exists {
|
||||
t.Fatal("expected health alert to be raised")
|
||||
}
|
||||
|
||||
// Now container becomes healthy
|
||||
hostHealthy := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "recovering-app",
|
||||
State: "running",
|
||||
Status: "Up 15 minutes",
|
||||
Health: "healthy",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(hostHealthy)
|
||||
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected health alert to be cleared when container became healthy")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerContainerOOMKillAlert(t *testing.T) {
|
||||
t.Run("running container - no alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-oom-1",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-1",
|
||||
Name: "running-app",
|
||||
State: "running",
|
||||
Status: "Up 10 minutes",
|
||||
ExitCode: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected no OOM alert for running container")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exited container with non-137 exit code - no alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-oom-2",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-2",
|
||||
Name: "normal-exit-app",
|
||||
State: "exited",
|
||||
Status: "Exited (1) 5 minutes ago",
|
||||
ExitCode: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected no OOM alert for container with exit code 1")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exited container with exit code 137 - critical OOM alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-oom-3",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-3",
|
||||
Name: "oom-killed-app",
|
||||
State: "exited",
|
||||
Status: "Exited (137) 1 minute ago",
|
||||
ExitCode: 137,
|
||||
MemoryUsage: 512 * 1024 * 1024,
|
||||
MemoryLimit: 512 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
alert, exists := m.activeAlerts[alertID]
|
||||
if !exists {
|
||||
t.Fatal("expected OOM alert for container with exit code 137")
|
||||
}
|
||||
if alert.Level != AlertLevelCritical {
|
||||
t.Fatalf("expected critical OOM alert, got %s", alert.Level)
|
||||
}
|
||||
if alert.Type != "docker-container-oom-kill" {
|
||||
t.Fatalf("expected alert type docker-container-oom-kill, got %s", alert.Type)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dead container with exit code 137 - critical OOM alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: "host-oom-dead",
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: "container-dead",
|
||||
Name: "dead-oom-app",
|
||||
State: "dead",
|
||||
Status: "Dead",
|
||||
ExitCode: 137,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(host.ID, host.Containers[0].ID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
alert, exists := m.activeAlerts[alertID]
|
||||
if !exists {
|
||||
t.Fatal("expected OOM alert for dead container with exit code 137")
|
||||
}
|
||||
if alert.Level != AlertLevelCritical {
|
||||
t.Fatalf("expected critical OOM alert, got %s", alert.Level)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("repeated 137 exit code - no new alert", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
hostID := "host-oom-4"
|
||||
containerID := "container-4"
|
||||
|
||||
host := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "oom-killed-app",
|
||||
State: "exited",
|
||||
Status: "Exited (137) 1 minute ago",
|
||||
ExitCode: 137,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// First check - should create alert
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
resourceID := dockerResourceID(hostID, containerID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
alert1, exists := m.activeAlerts[alertID]
|
||||
if !exists {
|
||||
t.Fatal("expected OOM alert on first check")
|
||||
}
|
||||
startTime := alert1.StartTime
|
||||
|
||||
// Second check with same exit code - should not create new alert
|
||||
m.CheckDockerHost(host)
|
||||
|
||||
alert2, exists := m.activeAlerts[alertID]
|
||||
if !exists {
|
||||
t.Fatal("expected OOM alert to still exist on second check")
|
||||
}
|
||||
if alert2.StartTime != startTime {
|
||||
t.Fatal("expected alert start time to be preserved (not a new alert)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("container recovers - alert cleared", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
hostID := "host-oom-5"
|
||||
containerID := "container-5"
|
||||
|
||||
// First check with OOM killed container
|
||||
hostOOM := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "recovering-app",
|
||||
State: "exited",
|
||||
Status: "Exited (137) 1 minute ago",
|
||||
ExitCode: 137,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(hostOOM)
|
||||
|
||||
resourceID := dockerResourceID(hostID, containerID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; !exists {
|
||||
t.Fatal("expected OOM alert to be raised")
|
||||
}
|
||||
|
||||
// Container is restarted and running again
|
||||
hostRunning := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "recovering-app",
|
||||
State: "running",
|
||||
Status: "Up 30 seconds",
|
||||
ExitCode: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(hostRunning)
|
||||
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected OOM alert to be cleared when container started running")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("container exits with different code - alert cleared", func(t *testing.T) {
|
||||
m := newTestManager(t)
|
||||
|
||||
hostID := "host-oom-6"
|
||||
containerID := "container-6"
|
||||
|
||||
// First check with OOM killed container
|
||||
hostOOM := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "multi-exit-app",
|
||||
State: "exited",
|
||||
Status: "Exited (137) 1 minute ago",
|
||||
ExitCode: 137,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(hostOOM)
|
||||
|
||||
resourceID := dockerResourceID(hostID, containerID)
|
||||
alertID := fmt.Sprintf("docker-container-oom-%s", resourceID)
|
||||
if _, exists := m.activeAlerts[alertID]; !exists {
|
||||
t.Fatal("expected OOM alert to be raised")
|
||||
}
|
||||
|
||||
// Container exits with different exit code (normal error)
|
||||
hostNormalExit := models.DockerHost{
|
||||
ID: hostID,
|
||||
DisplayName: "Docker Host",
|
||||
Hostname: "docker.local",
|
||||
Containers: []models.DockerContainer{
|
||||
{
|
||||
ID: containerID,
|
||||
Name: "multi-exit-app",
|
||||
State: "exited",
|
||||
Status: "Exited (1) 30 seconds ago",
|
||||
ExitCode: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.CheckDockerHost(hostNormalExit)
|
||||
|
||||
if _, exists := m.activeAlerts[alertID]; exists {
|
||||
t.Fatal("expected OOM alert to be cleared when container exited with different code")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue