Files
zeekay b9f9cdf918 feat: bump o11y fork to latest SigNoz — full re-sync, builds green (§4c retained)
Re-sync the entire SigNoz-derived Go tree to a clean fork of latest SigNoz
main (upstream 3e6339019), eliminating the version-skew that broke
`go build ./...` (mixed-era packages, ~14 imported-but-absent packages,
partial signoz->o11y / clickhouse->datastore rebrands, self-import cycle,
duplicate package decls).

- pkg/ + cmd/ wholesale replaced with latest SigNoz; imports rewritten
  github.com/SigNoz/signoz/ -> github.com/hanzoai/o11y/.
- Adopt SigNoz's CURRENT structure (resourcefilter->telemetryresourcefilter,
  integrationtypes->cloudintegrationtypes, roletypes/grammar/times folded in
  upstream); old paths dropped.
- Fork deps repathed to hanzoai + bumped: signoz-otel-collector v0.144.6,
  clickhouse-go-mock v0.14.1, govaluate v0.1.0; expr-lang/expr => hanzoai/expr
  v1.17.8 (replace). go.mod reconciled from latest SigNoz; go.sum re-tidied.
- ee/ + cmd/enterprise stripped — o11y stays MIT-only; community edition
  imports no ee packages.
- Preserved o11y additions: module path github.com/hanzoai/o11y, mount.go
  (cloud.Register/zip mount adapter, HIP-0106), NOTICE + LICENSE (SigNoz §4c
  attribution retained; upstream pkg files are headerless per SigNoz convention).
- Reverted o11y cosmetic renames (o11y*/datastore*) and experimental originals
  (authz/iamauthz, billing, zapreceiver, zapmetricreceiver) to SigNoz canonical
  to kill the skew; snapshots retained in git history for re-layering.

go build ./... = exit 0. go vet clean except benign "unreachable code" in
generated ANTLR parser files (inherited from upstream).
2026-07-02 14:20:32 -07:00

1063 lines
29 KiB
Go

package rules
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"sort"
"strings"
"sync"
"time"
"github.com/hanzoai/o11y/pkg/querier"
"github.com/hanzoai/o11y/pkg/queryparser"
"github.com/go-openapi/strfmt"
"github.com/hanzoai/o11y/pkg/alertmanager"
"github.com/hanzoai/o11y/pkg/cache"
"github.com/hanzoai/o11y/pkg/errors"
"github.com/hanzoai/o11y/pkg/modules/organization"
"github.com/hanzoai/o11y/pkg/modules/rulestatehistory"
"github.com/hanzoai/o11y/pkg/prometheus"
"github.com/hanzoai/o11y/pkg/sqlstore"
"github.com/hanzoai/o11y/pkg/telemetrystore"
"github.com/hanzoai/o11y/pkg/types"
"github.com/hanzoai/o11y/pkg/types/alertmanagertypes"
"github.com/hanzoai/o11y/pkg/types/authtypes"
"github.com/hanzoai/o11y/pkg/types/ruletypes"
"github.com/hanzoai/o11y/pkg/types/telemetrytypes"
"github.com/hanzoai/o11y/pkg/valuer"
)
type PrepareTaskOptions struct {
Rule *ruletypes.PostableRule
TaskName string
RuleStore ruletypes.RuleStore
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
ManagerOpts *ManagerOptions
NotifyFunc NotifyFunc
SQLStore sqlstore.SQLStore
OrgID valuer.UUID
}
type PrepareTestRuleOptions struct {
Rule *ruletypes.PostableRule
RuleStore ruletypes.RuleStore
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
ManagerOpts *ManagerOptions
NotifyFunc NotifyFunc
SQLStore sqlstore.SQLStore
OrgID valuer.UUID
}
const taskNameSuffix = "webAppEditor"
func RuleIDFromTaskName(n string) string {
return strings.Split(n, "-groupname")[0]
}
func prepareTaskName(ruleID string) string {
return fmt.Sprintf("%s-groupname", ruleID)
}
// ManagerOptions bundles options for the Manager.
type ManagerOptions struct {
TelemetryStore telemetrystore.TelemetryStore
MetadataStore telemetrytypes.MetadataStore
Prometheus prometheus.Prometheus
Context context.Context
ResendDelay time.Duration
Querier querier.Querier
Logger *slog.Logger
Cache cache.Cache
EvalDelay valuer.TextDuration
RuleStateHistoryModule rulestatehistory.Module
PrepareTaskFunc func(opts PrepareTaskOptions) (Task, error)
PrepareTestRuleFunc func(opts PrepareTestRuleOptions) (int, error)
Alertmanager alertmanager.Alertmanager
OrgGetter organization.Getter
RuleStore ruletypes.RuleStore
MaintenanceStore alertmanagertypes.MaintenanceStore
SQLStore sqlstore.SQLStore
QueryParser queryparser.QueryParser
}
// The Manager manages recording and alerting rules.
type Manager struct {
opts *ManagerOptions
tasks map[string]Task
rules map[string]Rule
mtx sync.RWMutex
block chan struct{}
// datastore to store alert definitions
ruleStore ruletypes.RuleStore
maintenanceStore alertmanagertypes.MaintenanceStore
logger *slog.Logger
cache cache.Cache
prepareTaskFunc func(opts PrepareTaskOptions) (Task, error)
prepareTestRuleFunc func(opts PrepareTestRuleOptions) (int, error)
alertmanager alertmanager.Alertmanager
sqlstore sqlstore.SQLStore
orgGetter organization.Getter
// queryParser is used for parsing queries for rules
queryParser queryparser.QueryParser
}
func defaultOptions(o *ManagerOptions) *ManagerOptions {
if o.ResendDelay == time.Duration(0) {
o.ResendDelay = 1 * time.Minute
}
if o.Logger == nil {
o.Logger = slog.Default()
}
if o.PrepareTaskFunc == nil {
o.PrepareTaskFunc = defaultPrepareTaskFunc
}
if o.PrepareTestRuleFunc == nil {
o.PrepareTestRuleFunc = defaultTestNotification
}
return o
}
func defaultPrepareTaskFunc(opts PrepareTaskOptions) (Task, error) {
rules := make([]Rule, 0)
var task Task
ruleID := RuleIDFromTaskName(opts.TaskName)
evaluation, err := opts.Rule.Evaluation.GetEvaluation()
if err != nil {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
}
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
// create a threshold rule
tr, err := NewThresholdRule(
ruleID,
opts.OrgID,
opts.Rule,
opts.Querier,
opts.Logger,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
WithEvalDelay(opts.ManagerOpts.EvalDelay),
WithSQLStore(opts.SQLStore),
WithQueryParser(opts.ManagerOpts.QueryParser),
WithMetadataStore(opts.ManagerOpts.MetadataStore),
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err
}
rules = append(rules, tr)
// create ch rule task for evaluation
task = newTask(TaskTypeCh, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
// create promql rule
pr, err := NewPromRule(
ruleID,
opts.OrgID,
opts.Rule,
opts.Logger,
opts.ManagerOpts.Prometheus,
opts.ManagerOpts.Alertmanager.Config().ExternalURL,
WithSQLStore(opts.SQLStore),
WithQueryParser(opts.ManagerOpts.QueryParser),
WithMetadataStore(opts.ManagerOpts.MetadataStore),
WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
)
if err != nil {
return task, err
}
rules = append(rules, pr)
// create promql rule task for evaluation
task = newTask(TaskTypeProm, opts.TaskName, taskNameSuffix, evaluation.GetFrequency().Duration(), rules, opts.ManagerOpts, opts.NotifyFunc)
} else {
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
}
return task, nil
}
// NewManager returns an implementation of Manager, ready to be started
// by calling the Run method.
func NewManager(o *ManagerOptions) (*Manager, error) {
o = defaultOptions(o)
m := &Manager{
tasks: map[string]Task{},
rules: map[string]Rule{},
ruleStore: o.RuleStore,
maintenanceStore: o.MaintenanceStore,
opts: o,
block: make(chan struct{}),
logger: o.Logger,
cache: o.Cache,
prepareTaskFunc: o.PrepareTaskFunc,
prepareTestRuleFunc: o.PrepareTestRuleFunc,
alertmanager: o.Alertmanager,
orgGetter: o.OrgGetter,
sqlstore: o.SQLStore,
queryParser: o.QueryParser,
}
m.logger.Debug("manager created successfully with notification group")
return m, nil
}
func (m *Manager) Start(ctx context.Context) {
if err := m.initiate(ctx); err != nil {
m.logger.ErrorContext(ctx, "failed to initialize alerting rules manager", errors.Attr(err))
}
m.run(ctx)
}
func (m *Manager) RuleStore() ruletypes.RuleStore {
return m.ruleStore
}
func (m *Manager) MaintenanceStore() alertmanagertypes.MaintenanceStore {
return m.maintenanceStore
}
// TODO(jatinderjit): remove (unused)?
func (m *Manager) Pause(b bool) {
m.mtx.Lock()
defer m.mtx.Unlock()
for _, t := range m.tasks {
t.Pause(b)
}
}
func (m *Manager) initiate(ctx context.Context) error {
orgs, err := m.orgGetter.ListByOwnedKeyRange(ctx)
if err != nil {
return err
}
var loadErrors []error
for _, org := range orgs {
storedRules, err := m.ruleStore.GetStoredRules(ctx, org.ID.StringValue())
if err != nil {
return err
}
if len(storedRules) == 0 {
return nil
}
for _, rec := range storedRules {
taskName := fmt.Sprintf("%s-groupname", rec.ID.StringValue())
parsedRule := ruletypes.PostableRule{}
err := json.Unmarshal([]byte(rec.Data), &parsedRule)
if err != nil {
m.logger.InfoContext(ctx, "failed to load rule in json format", "name", taskName)
loadErrors = append(loadErrors, err)
continue
}
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, org.ID, rec.ID.StringValue(), &config)
if err != nil {
loadErrors = append(loadErrors, err)
m.logger.WarnContext(ctx, "failed to set rule notification config", slog.String("rule.id", rec.ID.StringValue()), errors.Attr(err))
}
}
if !parsedRule.Disabled {
err := m.addTask(ctx, org.ID, &parsedRule, taskName)
if err != nil {
m.logger.ErrorContext(ctx, "failed to load the rule definition", "name", taskName, errors.Attr(err))
}
}
}
}
if len(loadErrors) > 0 {
return errors.Join(loadErrors...)
}
return nil
}
// Run starts processing of the rule manager.
func (m *Manager) run(_ context.Context) {
// initiate blocked tasks
close(m.block)
}
// Stop the rule manager's rule evaluation cycles.
func (m *Manager) Stop(_ context.Context) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.logger.Info("stopping rule manager")
for _, t := range m.tasks {
t.Stop()
}
m.logger.Info("rule manager stopped")
}
// validateChannels checks that every channel referenced by the rule
// exists as a notification channel for the given org.
func (m *Manager) validateChannels(ctx context.Context, orgID string, rule *ruletypes.PostableRule) error {
channels := rule.Channels()
if len(channels) == 0 {
return nil
}
orgChannels, err := m.alertmanager.ListChannels(ctx, orgID)
if err != nil {
return err
}
known := make(map[string]struct{}, len(orgChannels))
for _, ch := range orgChannels {
known[ch.Name] = struct{}{}
}
var unknown []string
for _, name := range channels {
if _, ok := known[name]; !ok {
unknown = append(unknown, name)
}
}
if len(unknown) > 0 {
return errors.NewInvalidInputf(errors.CodeInvalidInput,
"channels: the following channels do not exist: %v", unknown)
}
return nil
}
// EditRule writes the rule definition to the
// datastore and also updates the rule executor
func (m *Manager) EditRule(ctx context.Context, ruleStr string, id valuer.UUID) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return err
}
parsedRule := ruletypes.PostableRule{}
err = json.Unmarshal([]byte(ruleStr), &parsedRule)
if err != nil {
return err
}
if err := parsedRule.Validate(); err != nil {
return err
}
if err := m.validateChannels(ctx, claims.OrgID, &parsedRule); err != nil {
return err
}
existingRule, err := m.ruleStore.GetStoredRule(ctx, id)
if err != nil {
return err
}
existingRule.UpdatedAt = time.Now()
existingRule.UpdatedBy = claims.Email
existingRule.Data = ruleStr
return m.ruleStore.EditRule(ctx, existingRule, func(ctx context.Context) error {
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, id.StringValue(), &config)
if err != nil {
return err
}
if !parsedRule.NotificationSettings.UsePolicy {
request, err := parsedRule.GetRuleRouteRequest(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.UpdateAllRoutePoliciesByRuleId(ctx, id.StringValue(), request)
if err != nil {
return err
}
err = m.alertmanager.DeleteAllInhibitRulesByRuleId(ctx, orgID, id.StringValue())
if err != nil {
return err
}
inhibitRules, err := parsedRule.GetInhibitRules(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.CreateInhibitRules(ctx, orgID, inhibitRules)
if err != nil {
return err
}
}
}
err = m.syncRuleStateWithTask(ctx, orgID, prepareTaskName(existingRule.ID.StringValue()), &parsedRule)
if err != nil {
return err
}
return nil
})
}
func (m *Manager) editTask(_ context.Context, orgID valuer.UUID, rule *ruletypes.PostableRule, taskName string) error {
m.mtx.Lock()
defer m.mtx.Unlock()
m.logger.Debug("editing a rule task", "name", taskName)
newTask, err := m.prepareTaskFunc(PrepareTaskOptions{
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.notifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
})
if err != nil {
m.logger.Error("loading tasks failed", errors.Attr(err))
return errors.NewInvalidInputf(errors.CodeInvalidInput, "error preparing rule with given parameters, previous rule set restored")
}
for _, r := range newTask.Rules() {
m.rules[r.ID()] = r
}
// If there is an old task with the same identifier, stop it and wait for
// it to finish the current iteration. Then copy it into the new group.
oldTask, ok := m.tasks[taskName]
if !ok {
m.logger.Warn("rule task not found, a new task will be created", "name", taskName)
}
delete(m.tasks, taskName)
if ok {
oldTask.Stop()
newTask.CopyState(oldTask)
}
go func() {
// Wait with starting evaluation until the rule manager
// is told to run. This is necessary to avoid running
// queries against a bootstrapping storage.
<-m.block
newTask.Run(m.opts.Context)
}()
m.tasks[taskName] = newTask
return nil
}
func (m *Manager) DeleteRule(ctx context.Context, idStr string) error {
id, err := valuer.NewUUID(idStr)
if err != nil {
m.logger.ErrorContext(ctx, "delete rule received a rule id in invalid format, must be a valid uuid-v7", slog.String("rule.id", idStr), errors.Attr(err))
return errors.NewInvalidInputf(errors.CodeInvalidInput, "delete rule received an rule id in invalid format, must be a valid uuid-v7")
}
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return err
}
_, err = m.ruleStore.GetStoredRule(ctx, id)
if err != nil {
return err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return err
}
return m.ruleStore.DeleteRule(ctx, id, func(ctx context.Context) error {
cfg, err := m.alertmanager.GetConfig(ctx, claims.OrgID)
if err != nil {
return err
}
err = cfg.DeleteRuleIDMatcher(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.SetConfig(ctx, cfg)
if err != nil {
return err
}
err = m.alertmanager.DeleteNotificationConfig(ctx, orgID, id.String())
if err != nil {
return err
}
err = m.alertmanager.DeleteAllRoutePoliciesByRuleId(ctx, id.String())
if err != nil {
return err
}
err = m.alertmanager.DeleteAllInhibitRulesByRuleId(ctx, orgID, id.String())
if err != nil {
return err
}
taskName := prepareTaskName(id.StringValue())
m.deleteTask(taskName)
return nil
})
}
func (m *Manager) deleteTask(taskName string) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.logger.Debug("deleting a rule task", "name", taskName)
oldg, ok := m.tasks[taskName]
if ok {
oldg.Stop()
delete(m.tasks, taskName)
delete(m.rules, RuleIDFromTaskName(taskName))
m.logger.Debug("rule task deleted", "name", taskName)
} else {
m.logger.Info("rule not found for deletion", "name", taskName)
}
}
// CreateRule stores rule def into db and also
// starts an executor for the rule
func (m *Manager) CreateRule(ctx context.Context, ruleStr string) (*ruletypes.GettableRule, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
parsedRule := ruletypes.PostableRule{}
err = json.Unmarshal([]byte(ruleStr), &parsedRule)
if err != nil {
return nil, err
}
if err := parsedRule.Validate(); err != nil {
return nil, err
}
if err := m.validateChannels(ctx, claims.OrgID, &parsedRule); err != nil {
return nil, err
}
now := time.Now()
storedRule := &ruletypes.StorableRule{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: now,
UpdatedAt: now,
},
UserAuditable: types.UserAuditable{
CreatedBy: claims.Email,
UpdatedBy: claims.Email,
},
Data: ruleStr,
OrgID: claims.OrgID,
}
id, err := m.ruleStore.CreateRule(ctx, storedRule, func(ctx context.Context, id valuer.UUID) error {
if parsedRule.NotificationSettings != nil {
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, id.StringValue(), &config)
if err != nil {
return err
}
if !parsedRule.NotificationSettings.UsePolicy {
request, err := parsedRule.GetRuleRouteRequest(id.StringValue())
if err != nil {
return err
}
_, err = m.alertmanager.CreateRoutePolicies(ctx, request)
if err != nil {
return err
}
inhibitRules, err := parsedRule.GetInhibitRules(id.StringValue())
if err != nil {
return err
}
err = m.alertmanager.CreateInhibitRules(ctx, orgID, inhibitRules)
if err != nil {
return err
}
}
}
taskName := prepareTaskName(id.StringValue())
if err = m.addTask(ctx, orgID, &parsedRule, taskName); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return &ruletypes.GettableRule{
Id: id.StringValue(),
PostableRule: parsedRule,
}, nil
}
func (m *Manager) addTask(_ context.Context, orgID valuer.UUID, rule *ruletypes.PostableRule, taskName string) error {
m.mtx.Lock()
defer m.mtx.Unlock()
m.logger.Debug("adding a new rule task", "name", taskName)
newTask, err := m.prepareTaskFunc(PrepareTaskOptions{
Rule: rule,
TaskName: taskName,
RuleStore: m.ruleStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.notifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
})
if err != nil {
m.logger.Error("creating rule task failed", "name", taskName, errors.Attr(err))
return errors.NewInvalidInputf(errors.CodeInvalidInput, "error loading rules, previous rule set restored")
}
for _, r := range newTask.Rules() {
m.rules[r.ID()] = r
}
// If there is another task with the same identifier, raise an error
_, ok := m.tasks[taskName]
if ok {
return errors.NewInvalidInputf(errors.CodeInvalidInput, "a rule with the same name already exists")
}
go func() {
// Wait with starting evaluation until the rule manager
// is told to run. This is necessary to avoid running
// queries against a bootstrapping storage.
<-m.block
newTask.Run(m.opts.Context)
}()
m.tasks[taskName] = newTask
return nil
}
// RuleTasks returns the list of manager's rule tasks.
func (m *Manager) RuleTasks() []Task {
m.mtx.RLock()
defer m.mtx.RUnlock()
rgs := make([]Task, 0, len(m.tasks))
for _, g := range m.tasks {
rgs = append(rgs, g)
}
sort.Slice(rgs, func(i, j int) bool {
return rgs[i].Name() < rgs[j].Name()
})
return rgs
}
// RuleTasksWithoutLock returns the list of manager's rule tasks without
// acquiring a lock on the manager.
func (m *Manager) RuleTasksWithoutLock() []Task {
rgs := make([]Task, 0, len(m.tasks))
for _, g := range m.tasks {
rgs = append(rgs, g)
}
sort.Slice(rgs, func(i, j int) bool {
return rgs[i].Name() < rgs[j].Name()
})
return rgs
}
// Rules returns the list of the manager's rules.
func (m *Manager) Rules() []Rule {
m.mtx.RLock()
defer m.mtx.RUnlock()
rules := []Rule{}
for _, r := range m.rules {
rules = append(rules, r)
}
return rules
}
// TriggeredAlerts returns the list of the manager's rules.
func (m *Manager) TriggeredAlerts() []*ruletypes.NamedAlert {
// m.mtx.RLock()
// defer m.mtx.RUnlock()
namedAlerts := []*ruletypes.NamedAlert{}
for _, r := range m.rules {
active := r.ActiveAlerts()
for _, a := range active {
awn := &ruletypes.NamedAlert{
Alert: a,
Name: r.Name(),
}
namedAlerts = append(namedAlerts, awn)
}
}
return namedAlerts
}
type AlertStats struct {
FiringRules int64
LastFiredAt time.Time
}
func (m *Manager) AlertStats(ctx context.Context) AlertStats {
m.mtx.RLock()
defer m.mtx.RUnlock()
// TODO(therealpandey): scope these stats per org; rules for all orgs are aggregated here.
stats := AlertStats{}
for _, r := range m.rules {
if r.State() == ruletypes.StateFiring {
stats.FiringRules++
}
for _, alert := range r.ActiveAlerts() {
if alert.FiredAt.After(stats.LastFiredAt) {
stats.LastFiredAt = alert.FiredAt
}
}
}
return stats
}
// NotifyFunc sends notifications about a set of alerts generated by the given expression.
type NotifyFunc func(ctx context.Context, orgID string, alerts ...*ruletypes.Alert)
// notifyFunc implements the NotifyFunc for a Notifier.
func (m *Manager) notifyFunc(ctx context.Context, orgID string, alerts ...*ruletypes.Alert) {
var res []*alertmanagertypes.PostableAlert
for _, alert := range alerts {
generatorURL := alert.GeneratorURL
a := &alertmanagertypes.PostableAlert{
Annotations: alert.Annotations.Map(),
StartsAt: strfmt.DateTime(alert.FiredAt),
Alert: alertmanagertypes.AlertModel{
Labels: alert.Labels.Map(),
GeneratorURL: strfmt.URI(generatorURL),
},
}
if !alert.ResolvedAt.IsZero() {
a.EndsAt = strfmt.DateTime(alert.ResolvedAt)
} else {
a.EndsAt = strfmt.DateTime(alert.ValidUntil)
}
res = append(res, a)
}
if len(alerts) > 0 {
m.alertmanager.PutAlerts(ctx, orgID, res)
}
}
func (m *Manager) testNotifyFunc(ctx context.Context, orgID string, alerts ...*ruletypes.Alert) {
if len(alerts) == 0 {
return
}
ruleID := alerts[0].Labels.Map()[ruletypes.AlertRuleIDLabel]
receiverMap := make(map[*alertmanagertypes.PostableAlert][]string)
for _, alert := range alerts {
generatorURL := alert.GeneratorURL
a := &alertmanagertypes.PostableAlert{}
a.Annotations = alert.Annotations.Map()
a.StartsAt = strfmt.DateTime(alert.FiredAt)
labelsMap := alert.Labels.Map()
labelsMap[ruletypes.TestAlertLabel] = "true"
a.Alert = alertmanagertypes.AlertModel{
Labels: labelsMap,
GeneratorURL: strfmt.URI(generatorURL),
}
if !alert.ResolvedAt.IsZero() {
a.EndsAt = strfmt.DateTime(alert.ResolvedAt)
} else {
a.EndsAt = strfmt.DateTime(alert.ValidUntil)
}
receiverMap[a] = alert.Receivers
}
err := m.alertmanager.TestAlert(ctx, orgID, ruleID, receiverMap)
if err != nil {
m.logger.ErrorContext(ctx, "failed to send test notification", errors.Attr(err))
}
}
func (m *Manager) ListActiveRules() ([]Rule, error) {
ruleList := []Rule{}
for _, r := range m.rules {
ruleList = append(ruleList, r)
}
return ruleList, nil
}
func (m *Manager) ListRuleStates(ctx context.Context) (*ruletypes.GettableRules, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
// fetch rules from DB
storedRules, err := m.ruleStore.GetStoredRules(ctx, claims.OrgID)
if err != nil {
return nil, err
}
// initiate response object
resp := make([]*ruletypes.GettableRule, 0)
for _, s := range storedRules {
ruleResponse := ruletypes.GettableRule{}
err = json.Unmarshal([]byte(s.Data), &ruleResponse)
if err != nil {
m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", slog.String("rule.id", s.ID.StringValue()), errors.Attr(err))
continue
}
ruleResponse.Id = s.ID.StringValue()
// fetch state of rule from memory
if rm, ok := m.rules[ruleResponse.Id]; !ok {
ruleResponse.State = ruletypes.StateDisabled
ruleResponse.Disabled = true
} else {
ruleResponse.State = rm.State()
}
ruleResponse.CreatedAt = s.CreatedAt
ruleResponse.CreatedBy = &s.CreatedBy
ruleResponse.UpdatedAt = s.UpdatedAt
ruleResponse.UpdatedBy = &s.UpdatedBy
resp = append(resp, &ruleResponse)
}
return &ruletypes.GettableRules{Rules: resp}, nil
}
func (m *Manager) GetRule(ctx context.Context, id valuer.UUID) (*ruletypes.GettableRule, error) {
s, err := m.ruleStore.GetStoredRule(ctx, id)
if err != nil {
return nil, err
}
r := ruletypes.GettableRule{}
err = json.Unmarshal([]byte(s.Data), &r)
if err != nil {
m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", slog.String("rule.id", s.ID.StringValue()), errors.Attr(err))
return nil, err
}
r.Id = id.StringValue()
// fetch state of rule from memory
if rm, ok := m.rules[r.Id]; !ok {
r.State = ruletypes.StateDisabled
r.Disabled = true
} else {
r.State = rm.State()
}
r.CreatedAt = s.CreatedAt
r.CreatedBy = &s.CreatedBy
r.UpdatedAt = s.UpdatedAt
r.UpdatedBy = &s.UpdatedBy
return &r, nil
}
// syncRuleStateWithTask ensures that the state of a stored rule matches
// the task state. For example - if a stored rule is disabled, then
// there is no task running against it.
func (m *Manager) syncRuleStateWithTask(ctx context.Context, orgID valuer.UUID, taskName string, rule *ruletypes.PostableRule) error {
if rule.Disabled {
// check if rule has any task running
if _, ok := m.tasks[taskName]; ok {
// delete task from memory
m.deleteTask(taskName)
}
} else {
// check if rule has a task running
if _, ok := m.tasks[taskName]; !ok {
// rule has no task, start one
if err := m.addTask(ctx, orgID, rule, taskName); err != nil {
return err
}
} else {
if err := m.editTask(ctx, orgID, rule, taskName); err != nil {
return err
}
}
}
return nil
}
// PatchRule supports attribute level changes to the rule definition unlike
// EditRule, which updates entire rule definition in the DB.
// the process:
// - get the latest rule from db
// - over write the patch attributes received in input (ruleStr)
// - re-deploy or undeploy task as necessary
// - update the patched rule in the DB
func (m *Manager) PatchRule(ctx context.Context, ruleStr string, id valuer.UUID) (*ruletypes.GettableRule, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return nil, err
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
return nil, err
}
taskName := prepareTaskName(id.StringValue())
// retrieve rule from DB
storedJSON, err := m.ruleStore.GetStoredRule(ctx, id)
if err != nil {
m.logger.ErrorContext(ctx, "failed to get stored rule with given id", slog.String("rule.id", id.StringValue()), errors.Attr(err))
return nil, err
}
storedRule := ruletypes.PostableRule{}
if err := json.Unmarshal([]byte(storedJSON.Data), &storedRule); err != nil {
m.logger.ErrorContext(ctx, "failed to unmarshal rule from db", slog.String("rule.id", id.StringValue()), errors.Attr(err))
return nil, err
}
if err := json.Unmarshal([]byte(ruleStr), &storedRule); err != nil {
m.logger.ErrorContext(ctx, "failed to unmarshal patched rule with given id", slog.String("rule.id", id.StringValue()), errors.Attr(err))
return nil, err
}
if err := storedRule.Validate(); err != nil {
return nil, err
}
if err := m.validateChannels(ctx, claims.OrgID, &storedRule); err != nil {
return nil, err
}
// deploy or un-deploy task according to patched (new) rule state
if err := m.syncRuleStateWithTask(ctx, orgID, taskName, &storedRule); err != nil {
m.logger.ErrorContext(ctx, "failed to sync stored rule state with the task", slog.String("task.name", taskName), errors.Attr(err))
return nil, err
}
newStoredJson, err := json.Marshal(&storedRule)
if err != nil {
m.logger.ErrorContext(ctx, "failed to marshal new stored rule with given id", slog.String("rule.id", id.StringValue()), errors.Attr(err))
return nil, err
}
now := time.Now()
storedJSON.Data = string(newStoredJson)
storedJSON.UpdatedBy = claims.Email
storedJSON.UpdatedAt = now
err = m.ruleStore.EditRule(ctx, storedJSON, func(ctx context.Context) error { return nil })
if err != nil {
if err := m.syncRuleStateWithTask(ctx, orgID, taskName, &storedRule); err != nil {
m.logger.ErrorContext(ctx, "failed to restore rule after patch failure", "task_name", taskName, errors.Attr(err))
}
return nil, err
}
// prepare http response
response := ruletypes.GettableRule{
Id: id.StringValue(),
PostableRule: storedRule,
}
// fetch state of rule from memory
if rm, ok := m.rules[id.StringValue()]; !ok {
response.State = ruletypes.StateDisabled
response.Disabled = true
} else {
response.State = rm.State()
}
return &response, nil
}
// TestNotification prepares a dummy rule for given rule parameters and
// sends a test notification. returns alert count and error (if any)
func (m *Manager) TestNotification(ctx context.Context, orgID valuer.UUID, ruleStr string) (int, error) {
parsedRule := ruletypes.PostableRule{}
err := json.Unmarshal([]byte(ruleStr), &parsedRule)
if err != nil {
return 0, errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "failed to unmarshal rule")
}
if err := parsedRule.Validate(); err != nil {
return 0, err
}
if err := m.validateChannels(ctx, orgID.StringValue(), &parsedRule); err != nil {
return 0, err
}
if !parsedRule.NotificationSettings.UsePolicy {
parsedRule.NotificationSettings.GroupBy = append(parsedRule.NotificationSettings.GroupBy, ruletypes.LabelThresholdName)
}
config := parsedRule.NotificationSettings.GetAlertManagerNotificationConfig()
err = m.alertmanager.SetNotificationConfig(ctx, orgID, parsedRule.AlertName, &config)
if err != nil {
return 0, err
}
alertCount, err := m.prepareTestRuleFunc(PrepareTestRuleOptions{
Rule: &parsedRule,
RuleStore: m.ruleStore,
Querier: m.opts.Querier,
Logger: m.opts.Logger,
Cache: m.cache,
ManagerOpts: m.opts,
NotifyFunc: m.testNotifyFunc,
SQLStore: m.sqlstore,
OrgID: orgID,
})
return alertCount, err
}