This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user