mirror of https://github.com/grafana/grafana
Alerting: Create new state history "fanout" backend that dispatches to multiple other backends at once (#64774)
* Rename RecordStatesAsync to Record * Rename QueryStates to Query * Implement fanout writes * Implement primary queries * Simplify error joining * Add test for query path * Add tests for writes and error propagation * Allow fanout backend to be configured * Touch up log messages and config validation * Consistent documentation for all backend structs * Parse and normalize backend names more consistently against an enum * Touch-ups to documentation * Improve clarity around multi-record blocking * Keep primary and secondaries more distinct * Rename fanout backend to multiple backend * Simplify config keys for multi backend modepull/64985/head
parent
e01a3e0ea5
commit
a31672fa40
@ -0,0 +1,39 @@ |
||||
package historian |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strings" |
||||
) |
||||
|
||||
// BackendType identifies different kinds of state history backends.
|
||||
type BackendType string |
||||
|
||||
// String implements Stringer for BackendType.
|
||||
func (bt BackendType) String() string { |
||||
return string(bt) |
||||
} |
||||
|
||||
const ( |
||||
BackendTypeAnnotations BackendType = "annotations" |
||||
BackendTypeLoki BackendType = "loki" |
||||
BackendTypeMultiple BackendType = "multiple" |
||||
BackendTypeNoop BackendType = "noop" |
||||
BackendTypeSQL BackendType = "sql" |
||||
) |
||||
|
||||
func ParseBackendType(s string) (BackendType, error) { |
||||
norm := strings.ToLower(strings.TrimSpace(s)) |
||||
|
||||
types := map[BackendType]struct{}{ |
||||
BackendTypeAnnotations: {}, |
||||
BackendTypeLoki: {}, |
||||
BackendTypeMultiple: {}, |
||||
BackendTypeNoop: {}, |
||||
BackendTypeSQL: {}, |
||||
} |
||||
p := BackendType(norm) |
||||
if _, ok := types[p]; !ok { |
||||
return "", fmt.Errorf("unrecognized state history backend: %s", p) |
||||
} |
||||
return p, nil |
||||
} |
||||
@ -0,0 +1,55 @@ |
||||
package historian |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/state" |
||||
history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" |
||||
) |
||||
|
||||
type Backend interface { |
||||
Record(ctx context.Context, rule history_model.RuleMeta, states []state.StateTransition) <-chan error |
||||
Query(ctx context.Context, query ngmodels.HistoryQuery) (*data.Frame, error) |
||||
} |
||||
|
||||
// MultipleBackend is a state.Historian that records history to multiple backends at once.
|
||||
// Only one backend is used for reads. The backend selected for read traffic is called the primary and all others are called secondaries.
|
||||
type MultipleBackend struct { |
||||
primary Backend |
||||
secondaries []Backend |
||||
} |
||||
|
||||
func NewMultipleBackend(primary Backend, secondaries ...Backend) *MultipleBackend { |
||||
return &MultipleBackend{ |
||||
primary: primary, |
||||
secondaries: secondaries, |
||||
} |
||||
} |
||||
|
||||
func (h *MultipleBackend) Record(ctx context.Context, rule history_model.RuleMeta, states []state.StateTransition) <-chan error { |
||||
jobs := make([]<-chan error, 0, len(h.secondaries)+1) // One extra for the primary.
|
||||
for _, b := range append([]Backend{h.primary}, h.secondaries...) { |
||||
jobs = append(jobs, b.Record(ctx, rule, states)) |
||||
} |
||||
errCh := make(chan error, 1) |
||||
go func() { |
||||
defer close(errCh) |
||||
errs := make([]error, 0) |
||||
// Wait for all jobs to complete. Order doesn't matter here, as we always need to wait on the slowest job regardless.
|
||||
for _, ch := range jobs { |
||||
err := <-ch |
||||
if err != nil { |
||||
errs = append(errs, err) |
||||
} |
||||
} |
||||
errCh <- errors.Join(errs...) |
||||
}() |
||||
return errCh |
||||
} |
||||
|
||||
func (h *MultipleBackend) Query(ctx context.Context, query ngmodels.HistoryQuery) (*data.Frame, error) { |
||||
return h.primary.Query(ctx, query) |
||||
} |
||||
@ -0,0 +1,78 @@ |
||||
package historian |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/state" |
||||
history_model "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestMultipleBackend(t *testing.T) { |
||||
t.Run("querying dispatches to primary", func(t *testing.T) { |
||||
one := &fakeBackend{resp: data.NewFrame("one")} |
||||
two := &fakeBackend{resp: data.NewFrame("two")} |
||||
three := &fakeBackend{resp: data.NewFrame("three")} |
||||
fan := NewMultipleBackend(one, two, three) |
||||
|
||||
resp, err := fan.Query(context.Background(), ngmodels.HistoryQuery{}) |
||||
|
||||
require.NoError(t, err) |
||||
require.Equal(t, "one", resp.Name) |
||||
}) |
||||
|
||||
t.Run("writes dispatch to all", func(t *testing.T) { |
||||
one := &fakeBackend{} |
||||
two := &fakeBackend{} |
||||
three := &fakeBackend{} |
||||
fan := NewMultipleBackend(one, two, three) |
||||
rule := history_model.RuleMeta{} |
||||
vs := []state.StateTransition{{}} |
||||
|
||||
err := <-fan.Record(context.Background(), rule, vs) |
||||
|
||||
require.NoError(t, err) |
||||
require.NotEmpty(t, one.last) |
||||
require.NotEmpty(t, two.last) |
||||
require.NotEmpty(t, three.last) |
||||
}) |
||||
|
||||
t.Run("writes combine errors", func(t *testing.T) { |
||||
one := &fakeBackend{err: fmt.Errorf("error one")} |
||||
two := &fakeBackend{err: fmt.Errorf("error two")} |
||||
three := &fakeBackend{} |
||||
fan := NewMultipleBackend(one, two, three) |
||||
rule := history_model.RuleMeta{} |
||||
vs := []state.StateTransition{{}} |
||||
|
||||
err := <-fan.Record(context.Background(), rule, vs) |
||||
|
||||
require.Error(t, err) |
||||
require.ErrorContains(t, err, "error one") |
||||
require.ErrorContains(t, err, "error two") |
||||
}) |
||||
} |
||||
|
||||
type fakeBackend struct { |
||||
resp *data.Frame |
||||
err error |
||||
last []state.StateTransition |
||||
} |
||||
|
||||
func (f *fakeBackend) Record(ctx context.Context, rule history_model.RuleMeta, states []state.StateTransition) <-chan error { |
||||
ch := make(chan error, 1) |
||||
if f.err != nil { |
||||
ch <- f.err |
||||
} |
||||
f.last = states |
||||
defer close(ch) |
||||
return ch |
||||
} |
||||
|
||||
func (f *fakeBackend) Query(ctx context.Context, query ngmodels.HistoryQuery) (*data.Frame, error) { |
||||
return f.resp, f.err |
||||
} |
||||
Loading…
Reference in new issue