mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 00:09:02 +08:00
feat: add alerts to explorer link in notification (#4446)
This commit is contained in:
parent
f5b5a9a657
commit
00b111fbe3
@ -352,3 +352,36 @@ const TIMESTAMP = "timestamp"
|
|||||||
|
|
||||||
const FirstQueryGraphLimit = "first_query_graph_limit"
|
const FirstQueryGraphLimit = "first_query_graph_limit"
|
||||||
const SecondQueryGraphLimit = "second_query_graph_limit"
|
const SecondQueryGraphLimit = "second_query_graph_limit"
|
||||||
|
|
||||||
|
var TracesListViewDefaultSelectedColumns = []v3.AttributeKey{
|
||||||
|
{
|
||||||
|
Key: "serviceName",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "name",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "durationNano",
|
||||||
|
DataType: v3.AttributeKeyDataTypeArrayFloat64,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "httpMethod",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "responseStatusCode",
|
||||||
|
DataType: v3.AttributeKeyDataTypeString,
|
||||||
|
Type: v3.AttributeKeyTypeTag,
|
||||||
|
IsColumn: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -246,3 +246,25 @@ type GettableRule struct {
|
|||||||
UpdatedAt *time.Time `json:"updateAt"`
|
UpdatedAt *time.Time `json:"updateAt"`
|
||||||
UpdatedBy *string `json:"updateBy"`
|
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"`
|
||||||
|
}
|
||||||
|
@ -3,8 +3,10 @@ package rules
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@ -60,6 +62,7 @@ type ThresholdRule struct {
|
|||||||
queryBuilder *queryBuilder.QueryBuilder
|
queryBuilder *queryBuilder.QueryBuilder
|
||||||
|
|
||||||
opts ThresholdRuleOpts
|
opts ThresholdRuleOpts
|
||||||
|
typ string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThresholdRuleOpts struct {
|
type ThresholdRuleOpts struct {
|
||||||
@ -98,6 +101,7 @@ func NewThresholdRule(
|
|||||||
health: HealthUnknown,
|
health: HealthUnknown,
|
||||||
active: map[uint64]*Alert{},
|
active: map[uint64]*Alert{},
|
||||||
opts: opts,
|
opts: opts,
|
||||||
|
typ: p.AlertType,
|
||||||
}
|
}
|
||||||
|
|
||||||
if int64(t.evalWindow) == 0 {
|
if int64(t.evalWindow) == 0 {
|
||||||
@ -625,6 +629,210 @@ func (r *ThresholdRule) prepareBuilderQueries(ts time.Time) (map[string]string,
|
|||||||
return runQueries, err
|
return runQueries, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The following function is used to prepare the where clause for the query
|
||||||
|
// `lbls` contains the key value pairs of the labels from the result of the query
|
||||||
|
// We iterate over the where clause and replace the labels with the actual values
|
||||||
|
// There are two cases:
|
||||||
|
// 1. The label is present in the where clause
|
||||||
|
// 2. The label is not present in the where clause
|
||||||
|
//
|
||||||
|
// Example for case 2:
|
||||||
|
// Latency by serviceName without any filter
|
||||||
|
// In this case, for each service with latency > threshold we send a notification
|
||||||
|
// The expectation will be that clicking on the related traces for service A, will
|
||||||
|
// take us to the traces page with the filter serviceName=A
|
||||||
|
// So for all the missing labels in the where clause, we add them as key = value
|
||||||
|
//
|
||||||
|
// Example for case 1:
|
||||||
|
// Severity text IN (WARN, ERROR)
|
||||||
|
// In this case, the Severity text will appear in the `lbls` if it were part of the group
|
||||||
|
// by clause, in which case we replace it with the actual value for the notification
|
||||||
|
// i.e Severity text = WARN
|
||||||
|
// If the Severity text is not part of the group by clause, then we add it as it is
|
||||||
|
func (r *ThresholdRule) fetchFilters(selectedQuery string, lbls labels.Labels) []v3.FilterItem {
|
||||||
|
var filterItems []v3.FilterItem
|
||||||
|
|
||||||
|
added := make(map[string]struct{})
|
||||||
|
|
||||||
|
if r.ruleCondition.CompositeQuery.QueryType == v3.QueryTypeBuilder &&
|
||||||
|
r.ruleCondition.CompositeQuery.BuilderQueries[selectedQuery] != nil &&
|
||||||
|
r.ruleCondition.CompositeQuery.BuilderQueries[selectedQuery].Filters != nil {
|
||||||
|
|
||||||
|
for _, item := range r.ruleCondition.CompositeQuery.BuilderQueries[selectedQuery].Filters.Items {
|
||||||
|
exists := false
|
||||||
|
for _, label := range lbls {
|
||||||
|
if item.Key.Key == label.Name {
|
||||||
|
// if the label is present in the where clause, replace it with key = value
|
||||||
|
filterItems = append(filterItems, v3.FilterItem{
|
||||||
|
Key: item.Key,
|
||||||
|
Operator: v3.FilterOperatorEqual,
|
||||||
|
Value: label.Value,
|
||||||
|
})
|
||||||
|
exists = true
|
||||||
|
added[label.Name] = struct{}{}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
// if the label is not present in the where clause, add it as it is
|
||||||
|
filterItems = append(filterItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the labels which are not present in the where clause
|
||||||
|
for _, label := range lbls {
|
||||||
|
if _, ok := added[label.Name]; !ok {
|
||||||
|
filterItems = append(filterItems, v3.FilterItem{
|
||||||
|
Key: v3.AttributeKey{Key: label.Name},
|
||||||
|
Operator: v3.FilterOperatorEqual,
|
||||||
|
Value: label.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterItems
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) string {
|
||||||
|
selectedQuery := r.GetSelectedQuery()
|
||||||
|
|
||||||
|
// TODO(srikanthccv): handle formula queries
|
||||||
|
if selectedQuery < "A" || selectedQuery > "Z" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs list view expects time in milliseconds
|
||||||
|
tr := timeRange{
|
||||||
|
Start: ts.Add(-time.Duration(r.evalWindow)).UnixMilli(),
|
||||||
|
End: ts.UnixMilli(),
|
||||||
|
PageSize: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
options := Options{
|
||||||
|
MaxLines: 2,
|
||||||
|
Format: "list",
|
||||||
|
SelectColumns: []v3.AttributeKey{},
|
||||||
|
}
|
||||||
|
|
||||||
|
period, _ := json.Marshal(tr)
|
||||||
|
urlEncodedTimeRange := url.QueryEscape(string(period))
|
||||||
|
|
||||||
|
filterItems := r.fetchFilters(selectedQuery, lbls)
|
||||||
|
urlData := urlShareableCompositeQuery{
|
||||||
|
QueryType: string(v3.QueryTypeBuilder),
|
||||||
|
Builder: builderQuery{
|
||||||
|
QueryData: []v3.BuilderQuery{
|
||||||
|
{
|
||||||
|
DataSource: v3.DataSourceLogs,
|
||||||
|
QueryName: "A",
|
||||||
|
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||||
|
AggregateAttribute: v3.AttributeKey{},
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Items: filterItems,
|
||||||
|
Operator: "AND",
|
||||||
|
},
|
||||||
|
Expression: "A",
|
||||||
|
Disabled: false,
|
||||||
|
Having: []v3.Having{},
|
||||||
|
StepInterval: 60,
|
||||||
|
OrderBy: []v3.OrderBy{
|
||||||
|
{
|
||||||
|
ColumnName: "timestamp",
|
||||||
|
Order: "desc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QueryFormulas: make([]string, 0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(urlData)
|
||||||
|
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
|
||||||
|
|
||||||
|
optionsData, _ := json.Marshal(options)
|
||||||
|
urlEncodedOptions := url.QueryEscape(string(optionsData))
|
||||||
|
|
||||||
|
return fmt.Sprintf("compositeQuery=%s&timeRange=%s&startTime=%d&endTime=%d&options=%s", compositeQuery, urlEncodedTimeRange, tr.Start, tr.End, urlEncodedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) string {
|
||||||
|
selectedQuery := r.GetSelectedQuery()
|
||||||
|
|
||||||
|
// TODO(srikanthccv): handle formula queries
|
||||||
|
if selectedQuery < "A" || selectedQuery > "Z" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traces list view expects time in nanoseconds
|
||||||
|
tr := timeRange{
|
||||||
|
Start: ts.Add(-time.Duration(r.evalWindow)).UnixNano(),
|
||||||
|
End: ts.UnixNano(),
|
||||||
|
PageSize: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
options := Options{
|
||||||
|
MaxLines: 2,
|
||||||
|
Format: "list",
|
||||||
|
SelectColumns: constants.TracesListViewDefaultSelectedColumns,
|
||||||
|
}
|
||||||
|
|
||||||
|
period, _ := json.Marshal(tr)
|
||||||
|
urlEncodedTimeRange := url.QueryEscape(string(period))
|
||||||
|
|
||||||
|
filterItems := r.fetchFilters(selectedQuery, lbls)
|
||||||
|
urlData := urlShareableCompositeQuery{
|
||||||
|
QueryType: string(v3.QueryTypeBuilder),
|
||||||
|
Builder: builderQuery{
|
||||||
|
QueryData: []v3.BuilderQuery{
|
||||||
|
{
|
||||||
|
DataSource: v3.DataSourceTraces,
|
||||||
|
QueryName: "A",
|
||||||
|
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||||
|
AggregateAttribute: v3.AttributeKey{},
|
||||||
|
Filters: &v3.FilterSet{
|
||||||
|
Items: filterItems,
|
||||||
|
Operator: "AND",
|
||||||
|
},
|
||||||
|
Expression: "A",
|
||||||
|
Disabled: false,
|
||||||
|
Having: []v3.Having{},
|
||||||
|
StepInterval: 60,
|
||||||
|
OrderBy: []v3.OrderBy{
|
||||||
|
{
|
||||||
|
ColumnName: "timestamp",
|
||||||
|
Order: "desc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QueryFormulas: make([]string, 0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := json.Marshal(urlData)
|
||||||
|
// We need to double encode the composite query to remain compatible with the UI
|
||||||
|
compositeQuery := url.QueryEscape(url.QueryEscape(string(data)))
|
||||||
|
|
||||||
|
optionsData, _ := json.Marshal(options)
|
||||||
|
urlEncodedOptions := url.QueryEscape(string(optionsData))
|
||||||
|
|
||||||
|
return fmt.Sprintf("compositeQuery=%s&timeRange=%s&startTime=%d&endTime=%d&options=%s", compositeQuery, urlEncodedTimeRange, tr.Start, tr.End, urlEncodedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) hostFromSource() string {
|
||||||
|
parsedUrl, err := url.Parse(r.source)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if parsedUrl.Port() != "" {
|
||||||
|
return fmt.Sprintf("%s:%s", parsedUrl.Hostname(), parsedUrl.Port())
|
||||||
|
}
|
||||||
|
return parsedUrl.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]string, error) {
|
func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]string, error) {
|
||||||
queries := make(map[string]string)
|
queries := make(map[string]string)
|
||||||
|
|
||||||
@ -668,7 +876,7 @@ func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]strin
|
|||||||
|
|
||||||
func (r *ThresholdRule) GetSelectedQuery() string {
|
func (r *ThresholdRule) GetSelectedQuery() string {
|
||||||
|
|
||||||
// The acutal query string is not relevant here
|
// The actual query string is not relevant here
|
||||||
// we just need to know the selected query
|
// we just need to know the selected query
|
||||||
|
|
||||||
var queries map[string]string
|
var queries map[string]string
|
||||||
@ -846,6 +1054,18 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie
|
|||||||
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
lb.Set(labels.AlertRuleIdLabel, r.ID())
|
||||||
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
lb.Set(labels.RuleSourceLabel, r.GeneratorURL())
|
||||||
|
|
||||||
|
if r.typ == "TRACES_BASED_ALERT" {
|
||||||
|
link := r.prepareLinksToTraces(ts, smpl.Metric)
|
||||||
|
if link != "" && r.hostFromSource() != "" {
|
||||||
|
lb.Set("See related traces: ", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link))
|
||||||
|
}
|
||||||
|
} else if r.typ == "LOGS_BASED_ALERT" {
|
||||||
|
link := r.prepareLinksToLogs(ts, smpl.Metric)
|
||||||
|
if link != "" && r.hostFromSource() != "" {
|
||||||
|
lb.Set("See related logs: ", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
annotations := make(labels.Labels, 0, len(r.annotations))
|
annotations := make(labels.Labels, 0, len(r.annotations))
|
||||||
for _, a := range r.annotations {
|
for _, a := range r.annotations {
|
||||||
annotations = append(annotations, labels.Label{Name: normalizeLabelName(a.Name), Value: expand(a.Value)})
|
annotations = append(annotations, labels.Label{Name: normalizeLabelName(a.Name), Value: expand(a.Value)})
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"go.signoz.io/signoz/pkg/query-service/featureManager"
|
"go.signoz.io/signoz/pkg/query-service/featureManager"
|
||||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||||
|
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestThresholdRuleCombinations(t *testing.T) {
|
func TestThresholdRuleCombinations(t *testing.T) {
|
||||||
@ -335,3 +336,87 @@ func TestNormalizeLabelName(t *testing.T) {
|
|||||||
assert.Equal(t, c.expected, normalizeLabelName(c.labelName))
|
assert.Equal(t, c.expected, normalizeLabelName(c.labelName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrepareLinksToLogs(t *testing.T) {
|
||||||
|
postableRule := PostableRule{
|
||||||
|
Alert: "Tricky Condition Tests",
|
||||||
|
AlertType: "LOGS_BASED_ALERT",
|
||||||
|
RuleType: RuleTypeThreshold,
|
||||||
|
EvalWindow: Duration(5 * time.Minute),
|
||||||
|
Frequency: Duration(1 * time.Minute),
|
||||||
|
RuleCondition: &RuleCondition{
|
||||||
|
CompositeQuery: &v3.CompositeQuery{
|
||||||
|
QueryType: v3.QueryTypeBuilder,
|
||||||
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||||
|
"A": {
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
AggregateAttribute: v3.AttributeKey{
|
||||||
|
Key: "",
|
||||||
|
},
|
||||||
|
AggregateOperator: v3.AggregateOperatorNoOp,
|
||||||
|
DataSource: v3.DataSourceLogs,
|
||||||
|
Expression: "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareOp: "4", // Not Equals
|
||||||
|
MatchType: "1", // Once
|
||||||
|
Target: &[]float64{0.0}[0],
|
||||||
|
SelectedQuery: "A",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fm := featureManager.StartManager()
|
||||||
|
|
||||||
|
rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm)
|
||||||
|
if err != nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.UnixMilli(1705469040000)
|
||||||
|
|
||||||
|
link := rule.prepareLinksToLogs(ts, labels.Labels{})
|
||||||
|
assert.Contains(t, link, "&timeRange=%7B%22start%22%3A1705468740000%2C%22end%22%3A1705469040000%2C%22pageSize%22%3A100%7D&startTime=1705468740000&endTime=1705469040000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareLinksToTraces(t *testing.T) {
|
||||||
|
postableRule := PostableRule{
|
||||||
|
Alert: "Links to traces test",
|
||||||
|
AlertType: "TRACES_BASED_ALERT",
|
||||||
|
RuleType: RuleTypeThreshold,
|
||||||
|
EvalWindow: Duration(5 * time.Minute),
|
||||||
|
Frequency: Duration(1 * time.Minute),
|
||||||
|
RuleCondition: &RuleCondition{
|
||||||
|
CompositeQuery: &v3.CompositeQuery{
|
||||||
|
QueryType: v3.QueryTypeBuilder,
|
||||||
|
BuilderQueries: map[string]*v3.BuilderQuery{
|
||||||
|
"A": {
|
||||||
|
QueryName: "A",
|
||||||
|
StepInterval: 60,
|
||||||
|
AggregateAttribute: v3.AttributeKey{
|
||||||
|
Key: "durationNano",
|
||||||
|
},
|
||||||
|
AggregateOperator: v3.AggregateOperatorAvg,
|
||||||
|
DataSource: v3.DataSourceTraces,
|
||||||
|
Expression: "A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CompareOp: "4", // Not Equals
|
||||||
|
MatchType: "1", // Once
|
||||||
|
Target: &[]float64{0.0}[0],
|
||||||
|
SelectedQuery: "A",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fm := featureManager.StartManager()
|
||||||
|
|
||||||
|
rule, err := NewThresholdRule("69", &postableRule, ThresholdRuleOpts{}, fm)
|
||||||
|
if err != nil {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := time.UnixMilli(1705469040000)
|
||||||
|
|
||||||
|
link := rule.prepareLinksToTraces(ts, labels.Labels{})
|
||||||
|
assert.Contains(t, link, "&timeRange=%7B%22start%22%3A1705468740000000000%2C%22end%22%3A1705469040000000000%2C%22pageSize%22%3A100%7D&startTime=1705468740000000000&endTime=1705469040000000000")
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user