mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 20:59:00 +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 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"`
|
||||
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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
@ -60,6 +62,7 @@ type ThresholdRule struct {
|
||||
queryBuilder *queryBuilder.QueryBuilder
|
||||
|
||||
opts ThresholdRuleOpts
|
||||
typ string
|
||||
}
|
||||
|
||||
type ThresholdRuleOpts struct {
|
||||
@ -98,6 +101,7 @@ func NewThresholdRule(
|
||||
health: HealthUnknown,
|
||||
active: map[uint64]*Alert{},
|
||||
opts: opts,
|
||||
typ: p.AlertType,
|
||||
}
|
||||
|
||||
if int64(t.evalWindow) == 0 {
|
||||
@ -625,6 +629,210 @@ func (r *ThresholdRule) prepareBuilderQueries(ts time.Time) (map[string]string,
|
||||
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) {
|
||||
queries := make(map[string]string)
|
||||
|
||||
@ -668,7 +876,7 @@ func (r *ThresholdRule) prepareClickhouseQueries(ts time.Time) (map[string]strin
|
||||
|
||||
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
|
||||
|
||||
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.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))
|
||||
for _, a := range r.annotations {
|
||||
annotations = append(annotations, labels.Label{Name: normalizeLabelName(a.Name), Value: expand(a.Value)})
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.signoz.io/signoz/pkg/query-service/featureManager"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
"go.signoz.io/signoz/pkg/query-service/utils/labels"
|
||||
)
|
||||
|
||||
func TestThresholdRuleCombinations(t *testing.T) {
|
||||
@ -335,3 +336,87 @@ func TestNormalizeLabelName(t *testing.T) {
|
||||
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