package historian import ( "context" "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 <- Join(errs...) }() return errCh } func (h *MultipleBackend) Query(ctx context.Context, query ngmodels.HistoryQuery) (*data.Frame, error) { return h.primary.Query(ctx, query) } // TODO: This is vendored verbatim from the Go standard library. // TODO: The grafana project doesn't support go 1.20 yet, so we can't use errors.Join() directly. // TODO: Remove this and replace calls with "errors.Join(...)" when go 1.20 becomes the minimum supported version. // // Join returns an error that wraps the given errors. // Any nil error values are discarded. // Join returns nil if errs contains no non-nil values. // The error formats as the concatenation of the strings obtained // by calling the Error method of each element of errs, with a newline // between each string. func Join(errs ...error) error { n := 0 for _, err := range errs { if err != nil { n++ } } if n == 0 { return nil } e := &joinError{ errs: make([]error, 0, n), } for _, err := range errs { if err != nil { e.errs = append(e.errs, err) } } return e } type joinError struct { errs []error } func (e *joinError) Error() string { var b []byte for i, err := range e.errs { if i > 0 { b = append(b, '\n') } b = append(b, err.Error()...) } return string(b) } func (e *joinError) Unwrap() []error { return e.errs }