package rules import ( "context" "encoding/json" "fmt" "time" "unicode/utf8" "github.com/pkg/errors" "go.signoz.io/signoz/pkg/query-service/model" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/utils/times" "go.signoz.io/signoz/pkg/query-service/utils/timestamp" yaml "gopkg.in/yaml.v2" ) // this file contains api request and responses to be // served over http // newApiErrorInternal returns a new api error object of type internal func newApiErrorInternal(err error) *model.ApiError { return &model.ApiError{Typ: model.ErrorInternal, Err: err} } // newApiErrorBadData returns a new api error object of bad request type func newApiErrorBadData(err error) *model.ApiError { return &model.ApiError{Typ: model.ErrorBadData, Err: err} } // PostableRule is used to create alerting rule from HTTP api type PostableRule struct { AlertName string `yaml:"alert,omitempty" json:"alert,omitempty"` AlertType string `yaml:"alertType,omitempty" json:"alertType,omitempty"` Description string `yaml:"description,omitempty" json:"description,omitempty"` RuleType RuleType `yaml:"ruleType,omitempty" json:"ruleType,omitempty"` EvalWindow Duration `yaml:"evalWindow,omitempty" json:"evalWindow,omitempty"` Frequency Duration `yaml:"frequency,omitempty" json:"frequency,omitempty"` RuleCondition *RuleCondition `yaml:"condition,omitempty" json:"condition,omitempty"` Labels map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"` Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"` Disabled bool `json:"disabled"` // Source captures the source url where rule has been created Source string `json:"source,omitempty"` PreferredChannels []string `json:"preferredChannels,omitempty"` Version string `json:"version,omitempty"` // legacy Expr string `yaml:"expr,omitempty" json:"expr,omitempty"` OldYaml string `json:"yaml,omitempty"` } func ParsePostableRule(content []byte) (*PostableRule, []error) { return parsePostableRule(content, "json") } func parsePostableRule(content []byte, kind string) (*PostableRule, []error) { return parseIntoRule(PostableRule{}, content, kind) } // parseIntoRule loads the content (data) into PostableRule and also // validates the end result func parseIntoRule(initRule PostableRule, content []byte, kind string) (*PostableRule, []error) { rule := &initRule var err error if kind == "json" { if err = json.Unmarshal(content, rule); err != nil { return nil, []error{fmt.Errorf("failed to load json")} } } else if kind == "yaml" { if err = yaml.Unmarshal(content, rule); err != nil { return nil, []error{fmt.Errorf("failed to load yaml")} } } else { return nil, []error{fmt.Errorf("invalid data type")} } if rule.RuleCondition == nil && rule.Expr != "" { // account for legacy rules rule.RuleType = RuleTypeProm rule.EvalWindow = Duration(5 * time.Minute) rule.Frequency = Duration(1 * time.Minute) rule.RuleCondition = &RuleCondition{ CompositeQuery: &v3.CompositeQuery{ QueryType: v3.QueryTypePromQL, PromQueries: map[string]*v3.PromQuery{ "A": { Query: rule.Expr, }, }, }, } } if rule.EvalWindow == 0 { rule.EvalWindow = Duration(5 * time.Minute) } if rule.Frequency == 0 { rule.Frequency = Duration(1 * time.Minute) } if rule.RuleCondition != nil { if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder { rule.RuleType = RuleTypeThreshold } else if rule.RuleCondition.CompositeQuery.QueryType == v3.QueryTypePromQL { rule.RuleType = RuleTypeProm } for qLabel, q := range rule.RuleCondition.CompositeQuery.BuilderQueries { if q.AggregateAttribute.Key != "" && q.Expression == "" { q.Expression = qLabel } } } if errs := rule.Validate(); len(errs) > 0 { return nil, errs } return rule, []error{} } func isValidLabelName(ln string) bool { if len(ln) == 0 { return false } for i, b := range ln { if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { return false } } return true } func isValidLabelValue(v string) bool { return utf8.ValidString(v) } func (r *PostableRule) Validate() (errs []error) { if r.RuleCondition == nil { errs = append(errs, errors.Errorf("rule condition is required")) } else { if r.RuleCondition.CompositeQuery == nil { errs = append(errs, errors.Errorf("composite metric query is required")) } } if r.RuleType == RuleTypeThreshold { if r.RuleCondition.Target == nil { errs = append(errs, errors.Errorf("rule condition missing the threshold")) } if r.RuleCondition.CompareOp == "" { errs = append(errs, errors.Errorf("rule condition missing the compare op")) } if r.RuleCondition.MatchType == "" { errs = append(errs, errors.Errorf("rule condition missing the match option")) } } for k, v := range r.Labels { if !isValidLabelName(k) { errs = append(errs, errors.Errorf("invalid label name: %s", k)) } if !isValidLabelValue(v) { errs = append(errs, errors.Errorf("invalid label value: %s", v)) } } for k := range r.Annotations { if !isValidLabelName(k) { errs = append(errs, errors.Errorf("invalid annotation name: %s", k)) } } errs = append(errs, testTemplateParsing(r)...) return errs } func testTemplateParsing(rl *PostableRule) (errs []error) { if rl.AlertName == "" { // Not an alerting rule. return errs } // Trying to parse templates. tmplData := AlertTemplateData(make(map[string]string), "0", "0") defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" parseTest := func(text string) error { tmpl := NewTemplateExpander( context.TODO(), defs+text, "__alert_"+rl.AlertName, tmplData, times.Time(timestamp.FromTime(time.Now())), nil, ) return tmpl.ParseTest() } // Parsing Labels. for _, val := range rl.Labels { err := parseTest(val) if err != nil { errs = append(errs, fmt.Errorf("msg=%s", err.Error())) } } // Parsing Annotations. for _, val := range rl.Annotations { err := parseTest(val) if err != nil { errs = append(errs, fmt.Errorf("msg=%s", err.Error())) } } return errs } // GettableRules has info for all stored rules. type GettableRules struct { Rules []*GettableRule `json:"rules"` } // GettableRule has info for an alerting rules. type GettableRule struct { Id string `json:"id"` State string `json:"state"` PostableRule CreatedAt *time.Time `json:"createAt"` CreatedBy *string `json:"createBy"` UpdatedAt *time.Time `json:"updateAt"` UpdatedBy *string `json:"updateBy"` } type timeRange struct { Start int64 `json:"start"` End int64 `json:"end"` PageSize int64 `json:"pageSize"` } type builderQuery struct { QueryData []v3.BuilderQuery `json:"queryData"` QueryFormulas []string `json:"queryFormulas"` } type urlShareableCompositeQuery struct { QueryType string `json:"queryType"` Builder builderQuery `json:"builder"` } type Options struct { MaxLines int `json:"maxLines"` Format string `json:"format"` SelectColumns []v3.AttributeKey `json:"selectColumns"` }