mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-21 17:07:47 +02:00
Add a new "ContainerHealth" alert type that fires when a Docker container's health check reports unhealthy, and resolves when it recovers. This mirrors the existing Status (up/down) alert pattern: an alert can be armed per system and honors the "min minutes" delay before firing. When the alert fires, the notification (email and any configured webhook, including Discord via shoutrrr) includes a log excerpt fetched live from the agent for up to 2 of the unhealthy containers, prioritizing lines containing "error" or "fatal" (falling back to the log tail if none match), capped to keep the message well under Discord's size limit. --------- Co-authored-by: hank <hank@henrygd.me>
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
package system
|
|
|
|
import (
|
|
"encoding/json"
|
|
jsonv2 "encoding/json/v2"
|
|
"testing"
|
|
|
|
"github.com/fxamacker/cbor/v2"
|
|
"github.com/henrygd/beszel/internal/entities/container"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestStatsBatteryTransport(t *testing.T) {
|
|
stats := Stats{Battery: [2]uint8{0, 1}, Batteries: map[string]uint8{"Primary": 0, "Mouse": 75}}
|
|
|
|
for name, marshal := range map[string]func(any) ([]byte, error){
|
|
"json_v1": json.Marshal,
|
|
"json_v2": func(value any) ([]byte, error) { return jsonv2.Marshal(value) },
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
jsonData, err := marshal(stats)
|
|
require.NoError(t, err)
|
|
var jsonPayload map[string]any
|
|
require.NoError(t, json.Unmarshal(jsonData, &jsonPayload))
|
|
assert.Equal(t, []any{float64(0), float64(1)}, jsonPayload["bat"])
|
|
assert.Equal(t, map[string]any{"Primary": float64(0), "Mouse": float64(75)}, jsonPayload["bats"])
|
|
})
|
|
}
|
|
|
|
cborData, err := cbor.Marshal(stats)
|
|
require.NoError(t, err)
|
|
var decoded Stats
|
|
require.NoError(t, cbor.Unmarshal(cborData, &decoded))
|
|
assert.Equal(t, stats.Battery, decoded.Battery)
|
|
assert.Equal(t, stats.Batteries, decoded.Batteries)
|
|
}
|
|
|
|
func TestStatsDiskIOTotalAndFansTransport(t *testing.T) {
|
|
stats := Stats{
|
|
DiskIOTotal: [2]uint64{437348527104, 331522465792},
|
|
Fans: map[string]uint16{"cpu": 1200},
|
|
}
|
|
|
|
cborData, err := cbor.Marshal(stats)
|
|
require.NoError(t, err)
|
|
var decoded Stats
|
|
require.NoError(t, cbor.Unmarshal(cborData, &decoded))
|
|
assert.Equal(t, stats.DiskIOTotal, decoded.DiskIOTotal)
|
|
assert.Equal(t, stats.Fans, decoded.Fans)
|
|
}
|
|
|
|
func TestStatsBatteryNumericArrayUnmarshal(t *testing.T) {
|
|
var stats Stats
|
|
require.NoError(t, json.Unmarshal([]byte(`{"bat":[50,4]}`), &stats))
|
|
assert.Equal(t, Battery{50, 4}, stats.Battery)
|
|
}
|
|
|
|
func TestStatsLegacyBatteryPayload(t *testing.T) {
|
|
data, err := json.Marshal(Stats{Battery: [2]uint8{50, 4}})
|
|
require.NoError(t, err)
|
|
var payload map[string]any
|
|
require.NoError(t, json.Unmarshal(data, &payload))
|
|
assert.Contains(t, payload, "bat")
|
|
assert.NotContains(t, payload, "bats")
|
|
}
|
|
|
|
func TestCombinedDataSystemdUpdateMarkerTransport(t *testing.T) {
|
|
data := CombinedData{SystemdServicesUpdated: true}
|
|
|
|
jsonData, err := json.Marshal(data)
|
|
require.NoError(t, err)
|
|
var decodedJSON CombinedData
|
|
require.NoError(t, json.Unmarshal(jsonData, &decodedJSON))
|
|
assert.True(t, decodedJSON.SystemdServicesUpdated)
|
|
assert.Empty(t, decodedJSON.SystemdServices)
|
|
|
|
cborData, err := cbor.Marshal(data)
|
|
require.NoError(t, err)
|
|
var decodedCBOR CombinedData
|
|
require.NoError(t, cbor.Unmarshal(cborData, &decodedCBOR))
|
|
assert.True(t, decodedCBOR.SystemdServicesUpdated)
|
|
assert.Empty(t, decodedCBOR.SystemdServices)
|
|
|
|
var legacy CombinedData
|
|
require.NoError(t, json.Unmarshal([]byte(`{"stats":{},"info":{},"container":[]}`), &legacy))
|
|
assert.False(t, legacy.SystemdServicesUpdated)
|
|
}
|
|
|
|
func TestCombinedDataContainerValidityTransport(t *testing.T) {
|
|
validEmpty := CombinedData{Containers: []*container.Stats{}}
|
|
|
|
jsonData, err := json.Marshal(validEmpty)
|
|
require.NoError(t, err)
|
|
var decodedJSON CombinedData
|
|
require.NoError(t, json.Unmarshal(jsonData, &decodedJSON))
|
|
assert.NotNil(t, decodedJSON.Containers)
|
|
assert.Empty(t, decodedJSON.Containers)
|
|
|
|
jsonV2Data, err := jsonv2.Marshal(validEmpty)
|
|
require.NoError(t, err)
|
|
var decodedJSONV2 CombinedData
|
|
require.NoError(t, jsonv2.Unmarshal(jsonV2Data, &decodedJSONV2))
|
|
assert.NotNil(t, decodedJSONV2.Containers)
|
|
assert.Empty(t, decodedJSONV2.Containers)
|
|
|
|
cborData, err := cbor.Marshal(validEmpty)
|
|
require.NoError(t, err)
|
|
var decodedCBOR CombinedData
|
|
require.NoError(t, cbor.Unmarshal(cborData, &decodedCBOR))
|
|
assert.NotNil(t, decodedCBOR.Containers)
|
|
assert.Empty(t, decodedCBOR.Containers)
|
|
|
|
invalidData, err := cbor.Marshal(CombinedData{})
|
|
require.NoError(t, err)
|
|
var decodedInvalid CombinedData
|
|
require.NoError(t, cbor.Unmarshal(invalidData, &decodedInvalid))
|
|
assert.Nil(t, decodedInvalid.Containers)
|
|
}
|