265 lines
6.7 KiB
Go

package rules
import (
"encoding/json"
"fmt"
"net/url"
"sort"
"strings"
"time"
"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/labels"
)
// this file contains common structs and methods used by
// rule engine
const (
// how long before re-sending the alert
ResolvedRetention = 15 * time.Minute
TestAlertPostFix = "_TEST_ALERT"
)
type RuleType string
const (
RuleTypeThreshold = "threshold_rule"
RuleTypeProm = "promql_rule"
RuleTypeAnomaly = "anomaly_rule"
)
type RuleHealth string
const (
HealthUnknown RuleHealth = "unknown"
HealthGood RuleHealth = "ok"
HealthBad RuleHealth = "err"
)
type Alert struct {
State model.AlertState
Labels labels.BaseLabels
Annotations labels.BaseLabels
QueryResultLables labels.BaseLabels
GeneratorURL string
// list of preferred receivers, e.g. slack
Receivers []string
Value float64
ActiveAt time.Time
FiredAt time.Time
ResolvedAt time.Time
LastSentAt time.Time
ValidUntil time.Time
Missing bool
}
func (a *Alert) needsSending(ts time.Time, resendDelay time.Duration) bool {
if a.State == model.StatePending {
return false
}
// if an alert has been resolved since the last send, resend it
if a.ResolvedAt.After(a.LastSentAt) {
return true
}
return a.LastSentAt.Add(resendDelay).Before(ts)
}
type NamedAlert struct {
Name string
*Alert
}
type CompareOp string
const (
CompareOpNone CompareOp = "0"
ValueIsAbove CompareOp = "1"
ValueIsBelow CompareOp = "2"
ValueIsEq CompareOp = "3"
ValueIsNotEq CompareOp = "4"
ValueAboveOrEq CompareOp = "5"
ValueBelowOrEq CompareOp = "6"
ValueOutsideBounds CompareOp = "7"
)
type MatchType string
const (
MatchTypeNone MatchType = "0"
AtleastOnce MatchType = "1"
AllTheTimes MatchType = "2"
OnAverage MatchType = "3"
InTotal MatchType = "4"
Last MatchType = "5"
)
type RuleCondition struct {
CompositeQuery *v3.CompositeQuery `json:"compositeQuery,omitempty" yaml:"compositeQuery,omitempty"`
CompareOp CompareOp `yaml:"op,omitempty" json:"op,omitempty"`
Target *float64 `yaml:"target,omitempty" json:"target,omitempty"`
AlertOnAbsent bool `yaml:"alertOnAbsent,omitempty" json:"alertOnAbsent,omitempty"`
AbsentFor uint64 `yaml:"absentFor,omitempty" json:"absentFor,omitempty"`
MatchType MatchType `json:"matchType,omitempty"`
TargetUnit string `json:"targetUnit,omitempty"`
Algorithm string `json:"algorithm,omitempty"`
Seasonality string `json:"seasonality,omitempty"`
SelectedQuery string `json:"selectedQueryName,omitempty"`
RequireMinPoints bool `yaml:"requireMinPoints,omitempty" json:"requireMinPoints,omitempty"`
RequiredNumPoints int `yaml:"requiredNumPoints,omitempty" json:"requiredNumPoints,omitempty"`
}
func (rc *RuleCondition) GetSelectedQueryName() string {
if rc != nil {
if rc.SelectedQuery != "" {
return rc.SelectedQuery
}
queryNames := map[string]struct{}{}
if rc.CompositeQuery != nil {
if rc.QueryType() == v3.QueryTypeBuilder {
for name := range rc.CompositeQuery.BuilderQueries {
queryNames[name] = struct{}{}
}
} else if rc.QueryType() == v3.QueryTypeClickHouseSQL {
for name := range rc.CompositeQuery.ClickHouseQueries {
queryNames[name] = struct{}{}
}
}
}
// The following logic exists for backward compatibility
// If there is no selected query, then
// - check if F1 is present, if yes, return F1
// - else return the query with max ascii value
// this logic is not really correct. we should be considering
// whether the query is enabled or not. but this is a temporary
// fix to support backward compatibility
if _, ok := queryNames["F1"]; ok {
return "F1"
}
keys := make([]string, 0, len(queryNames))
for k := range queryNames {
keys = append(keys, k)
}
sort.Strings(keys)
return keys[len(keys)-1]
}
// This should never happen
return ""
}
func (rc *RuleCondition) IsValid() bool {
if rc.CompositeQuery == nil {
return false
}
if rc.QueryType() == v3.QueryTypeBuilder {
if rc.Target == nil {
return false
}
if rc.CompareOp == "" {
return false
}
}
if rc.QueryType() == v3.QueryTypePromQL {
if len(rc.CompositeQuery.PromQueries) == 0 {
return false
}
}
return true
}
// QueryType is a short hand method to get query type
func (rc *RuleCondition) QueryType() v3.QueryType {
if rc.CompositeQuery != nil {
return rc.CompositeQuery.QueryType
}
return v3.QueryTypeUnknown
}
// String is useful in printing rule condition in logs
func (rc *RuleCondition) String() string {
if rc == nil {
return ""
}
data, _ := json.Marshal(*rc)
return string(data)
}
type Duration time.Duration
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
*d = Duration(time.Duration(value))
return nil
case string:
tmp, err := time.ParseDuration(value)
if err != nil {
return err
}
*d = Duration(tmp)
return nil
default:
return errors.New("invalid duration")
}
}
// prepareRuleGeneratorURL creates an appropriate url
// for the rule. the URL is sent in slack messages as well as
// to other systems and allows backtracking to the rule definition
// from the third party systems.
func prepareRuleGeneratorURL(ruleId string, source string) string {
if source == "" {
return source
}
// check if source is a valid url
parsedSource, err := url.Parse(source)
if err != nil {
return ""
}
// since we capture window.location when a new rule is created
// we end up with rulesource host:port/alerts/new. in this case
// we want to replace new with rule id parameter
hasNew := strings.LastIndex(source, "new")
if hasNew > -1 {
ruleURL := fmt.Sprintf("%sedit?ruleId=%s", source[0:hasNew], ruleId)
return ruleURL
}
// The source contains the encoded query, start and end time
// and other parameters. We don't want to include them in the generator URL
// mainly to keep the URL short and lower the alert body contents
// The generator URL with /alerts/edit?ruleId= is enough
if parsedSource.Port() != "" {
return fmt.Sprintf("%s://%s:%s/alerts/edit?ruleId=%s", parsedSource.Scheme, parsedSource.Hostname(), parsedSource.Port(), ruleId)
}
return fmt.Sprintf("%s://%s/alerts/edit?ruleId=%s", parsedSource.Scheme, parsedSource.Hostname(), ruleId)
}