Files
ITWorx-Pulse-Public/internal/alertdefaults/seed.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

112 lines
3.7 KiB
Go

package alertdefaults
import (
"bytes"
"context"
"embed"
"encoding/json"
"errors"
"fmt"
"github.com/itworx/pulse/internal/alert"
"github.com/itworx/pulse/internal/metriccatalog"
)
//go:embed seed.json
var seedFS embed.FS
type seedDocument struct {
SchemaVersion int `json:"schemaVersion"`
Rules []alert.Document `json:"rules"`
}
type RuleStore interface {
Create(context.Context, string, alert.Document, string) (alert.Rule, alert.Version, error)
Get(context.Context, string) (alert.Rule, error)
// Update is optional for the seed: stores that also implement it allow the
// seed to refresh an implementation-owned default that has never been edited
// (revision 1) when a newer seed corrects it. See Seed.
}
// RuleUpdater is implemented by stores that support versioned updates. The seed
// uses it only for rules that are still at revision 1 (created by the seed and
// never changed by an operator), so operator edits are never overwritten.
type RuleUpdater interface {
Update(ctx context.Context, id, actor string, expected int64, document alert.Document, changeSummary string) (alert.Rule, error)
}
type Report struct {
Added int
Existing int
Refreshed int
}
func Load(registry metriccatalog.Registry) ([]alert.Document, error) {
raw, err := seedFS.ReadFile("seed.json")
if err != nil {
return nil, fmt.Errorf("read embedded alert defaults: %w", err)
}
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
var bundle seedDocument
if err := decoder.Decode(&bundle); err != nil {
return nil, fmt.Errorf("decode embedded alert defaults: %w", err)
}
if bundle.SchemaVersion != 1 || len(bundle.Rules) == 0 {
return nil, errors.New("embedded alert defaults have an invalid bundle")
}
seen := make(map[string]struct{}, len(bundle.Rules))
for _, rule := range bundle.Rules {
if _, ok := seen[rule.ID]; ok {
return nil, fmt.Errorf("embedded alert defaults contain duplicate rule %q", rule.ID)
}
seen[rule.ID] = struct{}{}
if err := rule.Validate(registry); err != nil {
return nil, fmt.Errorf("validate embedded alert default %q: %w", rule.ID, err)
}
}
return bundle.Rules, nil
}
func Seed(ctx context.Context, store RuleStore, registry metriccatalog.Registry, actor string) (Report, error) {
if store == nil {
return Report{}, alert.ErrUnavailable
}
rules, err := Load(registry)
if err != nil {
return Report{}, err
}
report := Report{}
for _, document := range rules {
if _, _, err := store.Create(ctx, actor, document, "system default seed v1"); err == nil {
report.Added++
} else if errors.Is(err, alert.ErrConflict) {
existing, getErr := store.Get(ctx, document.ID)
if getErr != nil {
return Report{}, fmt.Errorf("verify existing alert default %q: %w", document.ID, getErr)
}
if updater, ok := store.(RuleUpdater); ok && existing.Revision == 1 && !sameDocument(existing.Document, document) {
// The stored default is untouched since the seed created it, but the
// seed itself changed (for example a corrected metric binding). Refresh
// it through the normal versioned update path so history is kept.
if _, updErr := updater.Update(ctx, document.ID, actor, existing.Revision, document, "system default seed v1 refresh"); updErr != nil {
return Report{}, fmt.Errorf("refresh alert default %q: %w", document.ID, updErr)
}
report.Refreshed++
continue
}
report.Existing++
} else {
return Report{}, fmt.Errorf("seed alert default %q: %w", document.ID, err)
}
}
return report, nil
}
// sameDocument compares two rule documents by their canonical JSON encoding.
func sameDocument(a, b alert.Document) bool {
left, errA := json.Marshal(a)
right, errB := json.Marshal(b)
return errA == nil && errB == nil && bytes.Equal(left, right)
}