feat(alerts/query-service): measurement unit support for alerts (#2673)

This commit is contained in:
Srikanth Chekuri 2023-08-16 14:22:40 +05:30 committed by GitHub
parent 8844144c01
commit 7a4156a3b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 242 additions and 14 deletions

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230523034029-2b7ff773052c github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230523034029-2b7ff773052c
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230517094211-cd3f3f0aea85 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230517094211-cd3f3f0aea85
github.com/coreos/go-oidc/v3 v3.4.0 github.com/coreos/go-oidc/v3 v3.4.0
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.1
github.com/go-co-op/gocron v1.30.1 github.com/go-co-op/gocron v1.30.1
github.com/go-kit/log v0.2.1 github.com/go-kit/log v0.2.1
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5

3
go.sum
View File

@ -171,8 +171,9 @@ github.com/docker/docker v20.10.22+incompatible h1:6jX4yB+NtcbldT90k7vBSaWJDB3i+
github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y= github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y=
github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q= github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE=

View File

@ -7,6 +7,10 @@ func NewBoolConverter() Converter {
return &boolConverter{} return &boolConverter{}
} }
func (*boolConverter) Name() string {
return "bool"
}
func (c *boolConverter) Convert(v Value, to Unit) Value { func (c *boolConverter) Convert(v Value, to Unit) Value {
// There is no conversion to be done for bool // There is no conversion to be done for bool
return Value{ return Value{

View File

@ -11,12 +11,20 @@ type Value struct {
// Converter converts values from one unit to another // Converter converts values from one unit to another
type Converter interface { type Converter interface {
// Convert converts the given value to the given unit
Convert(v Value, to Unit) Value Convert(v Value, to Unit) Value
// Name returns the name of the converter
Name() string
} }
// noneConverter is a converter that does not convert // noneConverter is a converter that does not convert
type noneConverter struct{} type noneConverter struct{}
func (*noneConverter) Name() string {
return "none"
}
func (c *noneConverter) Convert(v Value, to Unit) Value { func (c *noneConverter) Convert(v Value, to Unit) Value {
return v return v
} }
@ -51,3 +59,132 @@ func FromUnit(u Unit) Converter {
return NoneConverter return NoneConverter
} }
} }
func UnitToName(u string) string {
switch u {
case "ns":
return " ns"
case "us":
return " us"
case "ms":
return " ms"
case "s":
return " s"
case "m":
return " minutes"
case "h":
return " hours"
case "d":
return " days"
case "bytes":
return " bytes"
case "decbytes":
return " bytes"
case "bits":
return " bits"
case "decbits":
return " bits"
case "kbytes":
return " KiB"
case "decKbytes":
return " kB"
case "mbytes":
return " MiB"
case "decMbytes":
return " MB"
case "gbytes":
return " GiB"
case "decGbytes":
return " GB"
case "tbytes":
return " TiB"
case "decTbytes":
return " TB"
case "pbytes":
return " PiB"
case "decPbytes":
return " PB"
case "binBps":
return " bytes/sec(IEC)"
case "Bps":
return " bytes/sec(SI)"
case "binbps":
return " bits/sec(IEC)"
case "bps":
return " bits/sec(SI)"
case "KiBs":
return " KiB/sec"
case "Kibits":
return " Kibit/sec"
case "KBs":
return " kB/sec"
case "Kbits":
return " kbit/sec"
case "MiBs":
return " MiB/sec"
case "Mibits":
return " Mibit/sec"
case "MBs":
return " MB/sec"
case "Mbits":
return " Mbit/sec"
case "GiBs":
return " GiB/sec"
case "Gibits":
return " Gibit/sec"
case "GBs":
return " GB/sec"
case "Gbits":
return " Gbit/sec"
case "TiBs":
return " TiB/sec"
case "Tibits":
return " Tibit/sec"
case "TBs":
return " TB/sec"
case "Tbits":
return " Tbit/sec"
case "PiBs":
return " PiB/sec"
case "Pibits":
return " Pibit/sec"
case "PBs":
return " PB/sec"
case "Pbits":
return " Pbit/sec"
case "percent":
return " %"
case "percentunit":
return " %"
case "bool":
return ""
case "bool_yes_no":
return ""
case "bool_true_false":
return ""
case "bool_1_0":
return ""
case "cps":
return " counts/sec (cps)"
case "ops":
return " ops/sec (ops)"
case "reqps":
return " requests/sec (rps)"
case "rps":
return " reads/sec (rps)"
case "wps":
return " writes/sec (wps)"
case "iops":
return " I/O ops/sec (iops)"
case "cpm":
return " counts/min (cpm)"
case "opm":
return " ops/min (opm)"
case "rpm":
return " reads/min (rpm)"
case "wpm":
return " writes/min (wpm)"
default:
return u
}
}

View File

@ -54,6 +54,10 @@ func NewDataConverter() Converter {
return &dataConverter{} return &dataConverter{}
} }
func (*dataConverter) Name() string {
return "data"
}
func FromDataUnit(u Unit) float64 { func FromDataUnit(u Unit) float64 {
switch u { switch u {
case "bytes": // base 2 case "bytes": // base 2

View File

@ -50,6 +50,10 @@ func NewDataRateConverter() Converter {
return &dataRateConverter{} return &dataRateConverter{}
} }
func (*dataRateConverter) Name() string {
return "data_rate"
}
func FromDataRateUnit(u Unit) float64 { func FromDataRateUnit(u Unit) float64 {
// See https://github.com/SigNoz/signoz/blob/5a81f5f90b34845f5b4b3bdd46acf29d04bf3987/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts#L62-L85 // See https://github.com/SigNoz/signoz/blob/5a81f5f90b34845f5b4b3bdd46acf29d04bf3987/frontend/src/container/NewWidget/RightContainer/dataFormatCategories.ts#L62-L85
switch u { switch u {

View File

@ -7,6 +7,10 @@ func NewPercentConverter() Converter {
return &percentConverter{} return &percentConverter{}
} }
func (*percentConverter) Name() string {
return "percent"
}
func FromPercentUnit(u Unit) float64 { func FromPercentUnit(u Unit) float64 {
switch u { switch u {
case "percent": case "percent":

View File

@ -8,6 +8,10 @@ func NewThroughputConverter() Converter {
return &throughputConverter{} return &throughputConverter{}
} }
func (*throughputConverter) Name() string {
return "throughput"
}
func (c *throughputConverter) Convert(v Value, to Unit) Value { func (c *throughputConverter) Convert(v Value, to Unit) Value {
// There is no conversion to be done for throughput // There is no conversion to be done for throughput
return Value{ return Value{

View File

@ -23,6 +23,10 @@ func NewDurationConverter() Converter {
return &durationConverter{} return &durationConverter{}
} }
func (*durationConverter) Name() string {
return "duration"
}
func FromTimeUnit(u Unit) Duration { func FromTimeUnit(u Unit) Duration {
switch u { switch u {
case "ns": case "ns":

View File

@ -8,6 +8,10 @@ func NewBoolFormatter() Formatter {
return &boolFormatter{} return &boolFormatter{}
} }
func (*boolFormatter) Name() string {
return "bool"
}
func toBool(value float64) string { func toBool(value float64) string {
if value == 0 { if value == 0 {
return "false" return "false"

View File

@ -14,6 +14,10 @@ func NewDataFormatter() Formatter {
return &dataFormatter{} return &dataFormatter{}
} }
func (*dataFormatter) Name() string {
return "data"
}
func (f *dataFormatter) Format(value float64, unit string) string { func (f *dataFormatter) Format(value float64, unit string) string {
switch unit { switch unit {
case "bytes": case "bytes":

View File

@ -14,6 +14,10 @@ func NewDataRateFormatter() Formatter {
return &dataRateFormatter{} return &dataRateFormatter{}
} }
func (*dataRateFormatter) Name() string {
return "data_rate"
}
func (f *dataRateFormatter) Format(value float64, unit string) string { func (f *dataRateFormatter) Format(value float64, unit string) string {
switch unit { switch unit {
case "binBps": case "binBps":

View File

@ -2,6 +2,8 @@ package formatter
type Formatter interface { type Formatter interface {
Format(value float64, unit string) string Format(value float64, unit string) string
Name() string
} }
var ( var (

View File

@ -8,6 +8,10 @@ func NewNoneFormatter() Formatter {
return &noneFormatter{} return &noneFormatter{}
} }
func (*noneFormatter) Name() string {
return "none"
}
func (f *noneFormatter) Format(value float64, unit string) string { func (f *noneFormatter) Format(value float64, unit string) string {
return fmt.Sprintf("%v", value) return fmt.Sprintf("%v", value)
} }

View File

@ -8,6 +8,10 @@ func NewPercentFormatter() Formatter {
return &percentFormatter{} return &percentFormatter{}
} }
func (*percentFormatter) Name() string {
return "percent"
}
func toPercent(value float64, decimals DecimalCount) string { func toPercent(value float64, decimals DecimalCount) string {
return toFixed(value, decimals) + "%" return toFixed(value, decimals) + "%"
} }

View File

@ -9,6 +9,10 @@ func NewThroughputFormatter() Formatter {
return &throughputFormatter{} return &throughputFormatter{}
} }
func (*throughputFormatter) Name() string {
return "throughput"
}
func simpleCountUnit(value float64, decimals *int, symbol string) string { func simpleCountUnit(value float64, decimals *int, symbol string) string {
units := []string{"", "K", "M", "B", "T"} units := []string{"", "K", "M", "B", "T"}
scaler := scaledUnits(1000, units, 0) scaler := scaledUnits(1000, units, 0)

View File

@ -12,6 +12,10 @@ func NewDurationFormatter() Formatter {
return &durationFormatter{} return &durationFormatter{}
} }
func (*durationFormatter) Name() string {
return "duration"
}
func (f *durationFormatter) Format(value float64, unit string) string { func (f *durationFormatter) Format(value float64, unit string) string {
switch unit { switch unit {
case "ns": case "ns":

View File

@ -373,6 +373,7 @@ type CompositeQuery struct {
PromQueries map[string]*PromQuery `json:"promQueries,omitempty"` PromQueries map[string]*PromQuery `json:"promQueries,omitempty"`
PanelType PanelType `json:"panelType"` PanelType PanelType `json:"panelType"`
QueryType QueryType `json:"queryType"` QueryType QueryType `json:"queryType"`
Unit string `json:"unit,omitempty"`
} }
func (c *CompositeQuery) Validate() error { func (c *CompositeQuery) Validate() error {

View File

@ -143,6 +143,7 @@ type RuleCondition struct {
CompareOp CompareOp `yaml:"op,omitempty" json:"op,omitempty"` CompareOp CompareOp `yaml:"op,omitempty" json:"op,omitempty"`
Target *float64 `yaml:"target,omitempty" json:"target,omitempty"` Target *float64 `yaml:"target,omitempty" json:"target,omitempty"`
MatchType `json:"matchType,omitempty"` MatchType `json:"matchType,omitempty"`
TargetUnit string `json:"targetUnit,omitempty"`
} }
func (rc *RuleCondition) IsValid() bool { func (rc *RuleCondition) IsValid() bool {

View File

@ -198,7 +198,7 @@ func testTemplateParsing(rl *PostableRule) (errs []error) {
} }
// Trying to parse templates. // Trying to parse templates.
tmplData := AlertTemplateData(make(map[string]string), 0, 0) tmplData := AlertTemplateData(make(map[string]string), "0", "0")
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
parseTest := func(text string) error { parseTest := func(text string) error {
tmpl := NewTemplateExpander( tmpl := NewTemplateExpander(

View File

@ -3,6 +3,7 @@ package rules
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"sync" "sync"
"time" "time"
@ -12,6 +13,8 @@ import (
plabels "github.com/prometheus/prometheus/model/labels" plabels "github.com/prometheus/prometheus/model/labels"
pql "github.com/prometheus/prometheus/promql" pql "github.com/prometheus/prometheus/promql"
"go.signoz.io/signoz/pkg/query-service/converter"
"go.signoz.io/signoz/pkg/query-service/formatter"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
qslabels "go.signoz.io/signoz/pkg/query-service/utils/labels" qslabels "go.signoz.io/signoz/pkg/query-service/utils/labels"
"go.signoz.io/signoz/pkg/query-service/utils/times" "go.signoz.io/signoz/pkg/query-service/utils/times"
@ -255,6 +258,13 @@ func (r *PromRule) ActiveAlerts() []*Alert {
return res return res
} }
func (r *PromRule) Unit() string {
if r.ruleCondition != nil && r.ruleCondition.CompositeQuery != nil {
return r.ruleCondition.CompositeQuery.Unit
}
return ""
}
// ForEachActiveAlert runs the given function on each alert. // ForEachActiveAlert runs the given function on each alert.
// This should be used when you want to use the actual alerts from the ThresholdRule // This should be used when you want to use the actual alerts from the ThresholdRule
// and not on its copy. // and not on its copy.
@ -296,7 +306,9 @@ func (r *PromRule) getPqlQuery() (string, error) {
return query, fmt.Errorf("a promquery needs to be set for this rule to function") return query, fmt.Errorf("a promquery needs to be set for this rule to function")
} }
if r.ruleCondition.Target != nil && r.ruleCondition.CompareOp != CompareOpNone { if r.ruleCondition.Target != nil && r.ruleCondition.CompareOp != CompareOpNone {
query = fmt.Sprintf("%s %s %f", query, ResolveCompareOp(r.ruleCondition.CompareOp), *r.ruleCondition.Target) unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit))
value := unitConverter.Convert(converter.Value{F: *r.ruleCondition.Target, U: converter.Unit(r.ruleCondition.TargetUnit)}, converter.Unit(r.Unit()))
query = fmt.Sprintf("%s %s %f", query, ResolveCompareOp(r.ruleCondition.CompareOp), value.F)
return query, nil return query, nil
} else { } else {
return query, nil return query, nil
@ -310,6 +322,8 @@ func (r *PromRule) getPqlQuery() (string, error) {
func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (interface{}, error) { func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (interface{}, error) {
valueFormatter := formatter.FromUnit(r.Unit())
q, err := r.getPqlQuery() q, err := r.getPqlQuery()
if err != nil { if err != nil {
return nil, err return nil, err
@ -335,7 +349,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (
l[lbl.Name] = lbl.Value l[lbl.Name] = lbl.Value
} }
tmplData := AlertTemplateData(l, smpl.V, r.targetVal()) tmplData := AlertTemplateData(l, valueFormatter.Format(smpl.V, r.Unit()), strconv.FormatFloat(r.targetVal(), 'f', 2, 64)+converter.UnitToName(r.ruleCondition.TargetUnit))
// Inject some convenience variables that are easier to remember for users // Inject some convenience variables that are easier to remember for users
// who are not used to Go's templating system. // who are not used to Go's templating system.
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"

View File

@ -201,11 +201,11 @@ func NewTemplateExpander(
} }
// AlertTemplateData returns the interface to be used in expanding the template. // AlertTemplateData returns the interface to be used in expanding the template.
func AlertTemplateData(labels map[string]string, value float64, threshold float64) interface{} { func AlertTemplateData(labels map[string]string, value string, threshold string) interface{} {
return struct { return struct {
Labels map[string]string Labels map[string]string
Value float64 Value string
Threshold float64 Threshold string
}{ }{
Labels: labels, Labels: labels,
Value: value, Value: value,

View File

@ -7,6 +7,7 @@ import (
"math" "math"
"reflect" "reflect"
"sort" "sort"
"strconv"
"sync" "sync"
"text/template" "text/template"
"time" "time"
@ -14,6 +15,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"github.com/ClickHouse/clickhouse-go/v2" "github.com/ClickHouse/clickhouse-go/v2"
"go.signoz.io/signoz/pkg/query-service/converter"
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder" "go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
"go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/constants"
@ -26,6 +28,7 @@ import (
logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3" logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3" metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
"go.signoz.io/signoz/pkg/query-service/formatter"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -324,6 +327,14 @@ func (r *ThresholdRule) SendAlerts(ctx context.Context, ts time.Time, resendDela
}) })
notifyFunc(ctx, "", alerts...) notifyFunc(ctx, "", alerts...)
} }
func (r *ThresholdRule) Unit() string {
if r.ruleCondition != nil && r.ruleCondition.CompositeQuery != nil {
return r.ruleCondition.CompositeQuery.Unit
}
return ""
}
func (r *ThresholdRule) CheckCondition(v float64) bool { func (r *ThresholdRule) CheckCondition(v float64) bool {
if math.IsNaN(v) { if math.IsNaN(v) {
@ -336,16 +347,20 @@ func (r *ThresholdRule) CheckCondition(v float64) bool {
return false return false
} }
zap.S().Debugf("target:", v, *r.ruleCondition.Target) unitConverter := converter.FromUnit(converter.Unit(r.ruleCondition.TargetUnit))
value := unitConverter.Convert(converter.Value{F: *r.ruleCondition.Target, U: converter.Unit(r.ruleCondition.TargetUnit)}, converter.Unit(r.Unit()))
zap.S().Debugf("Checking condition for rule: %s, Converter=%s, Value=%f, Target=%f, CompareOp=%s", r.Name(), unitConverter.Name(), v, value.F, r.ruleCondition.CompareOp)
switch r.ruleCondition.CompareOp { switch r.ruleCondition.CompareOp {
case ValueIsEq: case ValueIsEq:
return v == *r.ruleCondition.Target return v == value.F
case ValueIsNotEq: case ValueIsNotEq:
return v != *r.ruleCondition.Target return v != value.F
case ValueIsBelow: case ValueIsBelow:
return v < *r.ruleCondition.Target return v < value.F
case ValueIsAbove: case ValueIsAbove:
return v > *r.ruleCondition.Target return v > value.F
default: default:
return false return false
} }
@ -716,6 +731,7 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time, ch c
func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (interface{}, error) { func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) (interface{}, error) {
valueFormatter := formatter.FromUnit(r.Unit())
res, err := r.buildAndRunQuery(ctx, ts, queriers.Ch) res, err := r.buildAndRunQuery(ctx, ts, queriers.Ch)
if err != nil { if err != nil {
@ -737,7 +753,11 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
l[lbl.Name] = lbl.Value l[lbl.Name] = lbl.Value
} }
tmplData := AlertTemplateData(l, smpl.V, r.targetVal()) value := valueFormatter.Format(smpl.V, r.Unit())
threshold := strconv.FormatFloat(r.targetVal(), 'f', 2, 64) + converter.UnitToName(r.ruleCondition.TargetUnit)
zap.S().Debugf("Alert template data for rule %s: Formatter=%s, Value=%s, Threshold=%s", r.Name(), valueFormatter.Name(), value, threshold)
tmplData := AlertTemplateData(l, value, threshold)
// Inject some convenience variables that are easier to remember for users // Inject some convenience variables that are easier to remember for users
// who are not used to Go's templating system. // who are not used to Go's templating system.
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}" defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"