feat(alerts): add container health alerts with log excerpt on notifications (#2225)

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>
This commit is contained in:
Michał Mleczko
2026-09-02 18:46:23 +02:00
committed by GitHub
parent b1895247ba
commit 5969d36856
44 changed files with 1245 additions and 16 deletions

View File

@@ -6,6 +6,7 @@ import (
"testing"
"github.com/fxamacker/cbor/v2"
"github.com/henrygd/beszel/internal/entities/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -85,3 +86,34 @@ func TestCombinedDataSystemdUpdateMarkerTransport(t *testing.T) {
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)
}