2025-09-26 18:54:58 +05:30
|
|
|
package rulebasednotification
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2025-10-03 19:47:15 +05:30
|
|
|
"strings"
|
2025-09-26 18:54:58 +05:30
|
|
|
"sync"
|
|
|
|
|
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
2025-10-03 19:47:15 +05:30
|
|
|
"github.com/expr-lang/expr"
|
|
|
|
|
"github.com/prometheus/common/model"
|
2025-09-26 18:54:58 +05:30
|
|
|
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/factory"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type provider struct {
|
|
|
|
|
settings factory.ScopedProviderSettings
|
|
|
|
|
orgToFingerprintToNotificationConfig map[string]map[string]alertmanagertypes.NotificationConfig
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore alertmanagertypes.RouteStore
|
2025-09-26 18:54:58 +05:30
|
|
|
mutex sync.RWMutex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewFactory creates a new factory for the rule-based grouping strategy.
|
2025-10-03 19:47:15 +05:30
|
|
|
func NewFactory(routeStore alertmanagertypes.RouteStore) factory.ProviderFactory[nfmanager.NotificationManager, nfmanager.Config] {
|
2025-09-26 18:54:58 +05:30
|
|
|
return factory.NewProviderFactory(
|
|
|
|
|
factory.MustNewName("rulebased"),
|
|
|
|
|
func(ctx context.Context, settings factory.ProviderSettings, config nfmanager.Config) (nfmanager.NotificationManager, error) {
|
2025-10-03 19:47:15 +05:30
|
|
|
return New(ctx, settings, config, routeStore)
|
2025-09-26 18:54:58 +05:30
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates a new rule-based grouping strategy provider.
|
2025-10-03 19:47:15 +05:30
|
|
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config nfmanager.Config, routeStore alertmanagertypes.RouteStore) (nfmanager.NotificationManager, error) {
|
2025-09-26 18:54:58 +05:30
|
|
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification")
|
|
|
|
|
|
|
|
|
|
return &provider{
|
|
|
|
|
settings: settings,
|
|
|
|
|
orgToFingerprintToNotificationConfig: make(map[string]map[string]alertmanagertypes.NotificationConfig),
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore: routeStore,
|
2025-09-26 18:54:58 +05:30
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetNotificationConfig retrieves the notification configuration for the specified alert and organization.
|
|
|
|
|
func (r *provider) GetNotificationConfig(orgID string, ruleID string) (*alertmanagertypes.NotificationConfig, error) {
|
|
|
|
|
notificationConfig := alertmanagertypes.GetDefaultNotificationConfig()
|
|
|
|
|
if orgID == "" || ruleID == "" {
|
|
|
|
|
return ¬ificationConfig, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.mutex.RLock()
|
|
|
|
|
defer r.mutex.RUnlock()
|
|
|
|
|
|
|
|
|
|
if orgConfigs, exists := r.orgToFingerprintToNotificationConfig[orgID]; exists {
|
|
|
|
|
if config, configExists := orgConfigs[ruleID]; configExists {
|
|
|
|
|
if config.Renotify.RenotifyInterval != 0 {
|
|
|
|
|
notificationConfig.Renotify.RenotifyInterval = config.Renotify.RenotifyInterval
|
|
|
|
|
}
|
|
|
|
|
if config.Renotify.NoDataInterval != 0 {
|
|
|
|
|
notificationConfig.Renotify.NoDataInterval = config.Renotify.NoDataInterval
|
|
|
|
|
}
|
|
|
|
|
for k, v := range config.NotificationGroup {
|
|
|
|
|
notificationConfig.NotificationGroup[k] = v
|
|
|
|
|
}
|
2025-10-03 19:47:15 +05:30
|
|
|
notificationConfig.UsePolicy = config.UsePolicy
|
|
|
|
|
notificationConfig.GroupByAll = config.GroupByAll
|
2025-09-26 18:54:58 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ¬ificationConfig, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetNotificationConfig updates the notification configuration for the specified alert and organization.
|
|
|
|
|
func (r *provider) SetNotificationConfig(orgID string, ruleID string, config *alertmanagertypes.NotificationConfig) error {
|
|
|
|
|
if orgID == "" || ruleID == "" {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "no org or rule id provided")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if config == nil {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "notification config cannot be nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.mutex.Lock()
|
|
|
|
|
defer r.mutex.Unlock()
|
|
|
|
|
|
|
|
|
|
// Initialize org map if it doesn't exist
|
|
|
|
|
if r.orgToFingerprintToNotificationConfig[orgID] == nil {
|
|
|
|
|
r.orgToFingerprintToNotificationConfig[orgID] = make(map[string]alertmanagertypes.NotificationConfig)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.orgToFingerprintToNotificationConfig[orgID][ruleID] = config.DeepCopy()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) DeleteNotificationConfig(orgID string, ruleID string) error {
|
|
|
|
|
if orgID == "" || ruleID == "" {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "no org or rule id provided")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.mutex.Lock()
|
|
|
|
|
defer r.mutex.Unlock()
|
|
|
|
|
|
|
|
|
|
if _, exists := r.orgToFingerprintToNotificationConfig[orgID]; exists {
|
|
|
|
|
delete(r.orgToFingerprintToNotificationConfig[orgID], ruleID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-10-03 19:47:15 +05:30
|
|
|
|
|
|
|
|
func (r *provider) CreateRoutePolicy(ctx context.Context, orgID string, route *alertmanagertypes.RoutePolicy) error {
|
|
|
|
|
if route == nil {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policy cannot be nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := route.Validate()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid route policy: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return r.routeStore.Create(ctx, route)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) CreateRoutePolicies(ctx context.Context, orgID string, routes []*alertmanagertypes.RoutePolicy) error {
|
|
|
|
|
if len(routes) == 0 {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policies cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, route := range routes {
|
|
|
|
|
if route == nil {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policy cannot be nil")
|
|
|
|
|
}
|
|
|
|
|
if err := route.Validate(); err != nil {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "route policy with name %s: %s", route.Name, err.Error())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return r.routeStore.CreateBatch(ctx, routes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) GetRoutePolicyByID(ctx context.Context, orgID string, routeID string) (*alertmanagertypes.RoutePolicy, error) {
|
|
|
|
|
if routeID == "" {
|
|
|
|
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "routeID cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return r.routeStore.GetByID(ctx, orgID, routeID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) GetAllRoutePolicies(ctx context.Context, orgID string) ([]*alertmanagertypes.RoutePolicy, error) {
|
|
|
|
|
if orgID == "" {
|
|
|
|
|
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "orgID cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return r.routeStore.GetAllByKind(ctx, orgID, alertmanagertypes.PolicyBasedExpression)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) DeleteRoutePolicy(ctx context.Context, orgID string, routeID string) error {
|
|
|
|
|
if routeID == "" {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "routeID cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return r.routeStore.Delete(ctx, orgID, routeID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) DeleteAllRoutePoliciesByName(ctx context.Context, orgID string, name string) error {
|
|
|
|
|
if orgID == "" {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "orgID cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
if name == "" {
|
|
|
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "name cannot be empty")
|
|
|
|
|
}
|
|
|
|
|
return r.routeStore.DeleteRouteByName(ctx, orgID, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) Match(ctx context.Context, orgID string, ruleID string, set model.LabelSet) ([]string, error) {
|
|
|
|
|
config, err := r.GetNotificationConfig(orgID, ruleID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.NewInternalf(errors.CodeInternal, "error getting notification configuration: %v", err)
|
|
|
|
|
}
|
|
|
|
|
var expressionRoutes []*alertmanagertypes.RoutePolicy
|
|
|
|
|
if config.UsePolicy {
|
|
|
|
|
expressionRoutes, err = r.routeStore.GetAllByKind(ctx, orgID, alertmanagertypes.PolicyBasedExpression)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return []string{}, errors.NewInternalf(errors.CodeInternal, "error getting route policies: %v", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
expressionRoutes, err = r.routeStore.GetAllByName(ctx, orgID, ruleID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return []string{}, errors.NewInternalf(errors.CodeInternal, "error getting route policies: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var matchedChannels []string
|
|
|
|
|
if _, ok := set[alertmanagertypes.NoDataLabel]; ok && !config.UsePolicy {
|
|
|
|
|
for _, expressionRoute := range expressionRoutes {
|
|
|
|
|
matchedChannels = append(matchedChannels, expressionRoute.Channels...)
|
|
|
|
|
}
|
|
|
|
|
return matchedChannels, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, route := range expressionRoutes {
|
|
|
|
|
evaluateExpr, err := r.evaluateExpr(route.Expression, set)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if evaluateExpr {
|
|
|
|
|
matchedChannels = append(matchedChannels, route.Channels...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return matchedChannels, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *provider) evaluateExpr(expression string, labelSet model.LabelSet) (bool, error) {
|
|
|
|
|
env := make(map[string]interface{})
|
|
|
|
|
|
|
|
|
|
for k, v := range labelSet {
|
|
|
|
|
key := string(k)
|
|
|
|
|
value := string(v)
|
|
|
|
|
|
|
|
|
|
if strings.Contains(key, ".") {
|
|
|
|
|
parts := strings.Split(key, ".")
|
|
|
|
|
current := env
|
|
|
|
|
|
|
|
|
|
for i, part := range parts {
|
|
|
|
|
if i == len(parts)-1 {
|
|
|
|
|
current[part] = value
|
|
|
|
|
} else {
|
|
|
|
|
if current[part] == nil {
|
|
|
|
|
current[part] = make(map[string]interface{})
|
|
|
|
|
}
|
|
|
|
|
current = current[part].(map[string]interface{})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
env[key] = value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
program, err := expr.Compile(expression, expr.Env(env))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, errors.NewInternalf(errors.CodeInternal, "error compiling route policy %s: %v", expression, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output, err := expr.Run(program, env)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, errors.NewInternalf(errors.CodeInternal, "error running route policy %s: %v", expression, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if boolVal, ok := output.(bool); ok {
|
|
|
|
|
return boolVal, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false, errors.NewInternalf(errors.CodeInternal, "error in evaluating route policy %s: %v", expression, err)
|
|
|
|
|
}
|