diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 8dce05250d..65cbf59d7d 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -5222,7 +5222,7 @@ func (r *ClickHouseReader) AddRuleStateHistory(ctx context.Context, ruleStateHis } func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( - ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) ([]v3.RuleStateHistory, error) { + ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (*v3.RuleStateTimeline, error) { var conditions []string @@ -5230,6 +5230,10 @@ func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli < %d", params.Start, params.End)) + if params.State != "" { + conditions = append(conditions, fmt.Sprintf("state = '%s'", params.State)) + } + if params.Filters != nil && len(params.Filters.Items) != 0 { for _, item := range params.Filters.Items { toFormat := item.Value @@ -5288,7 +5292,19 @@ func (r *ClickHouseReader) ReadRuleStateHistoryByRuleID( return nil, err } - return history, nil + var total uint64 + err = r.db.QueryRow(ctx, fmt.Sprintf("SELECT count(*) FROM %s.%s WHERE %s", + signozHistoryDBName, ruleStateHistoryTableName, whereClause)).Scan(&total) + if err != nil { + return nil, err + } + + timeline := &v3.RuleStateTimeline{ + Items: history, + Total: total, + } + + return timeline, nil } func (r *ClickHouseReader) ReadRuleStateHistoryTopContributorsByRuleID( diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 5064cc359b..073be0c056 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -727,6 +727,13 @@ func (aH *APIHandler) getRuleStats(w http.ResponseWriter, r *http.Request) { RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } + if math.IsNaN(currentAvgResolutionTime) || math.IsInf(currentAvgResolutionTime, 0) { + currentAvgResolutionTime = 0 + } + if math.IsNaN(pastAvgResolutionTime) || math.IsInf(pastAvgResolutionTime, 0) { + pastAvgResolutionTime = 0 + } + stats := v3.Stats{ TotalCurrentTriggers: totalCurrentTriggers, TotalPastTriggers: totalPastTriggers, @@ -799,6 +806,37 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } + + rule, err := aH.ruleManager.GetRule(r.Context(), ruleID) + if err == nil { + for idx := range res.Items { + lbls := make(map[string]string) + err := json.Unmarshal([]byte(res.Items[idx].Labels), &lbls) + if err != nil { + continue + } + filterItems := []v3.FilterItem{} + if rule.AlertType == "LOGS_BASED_ALERT" || rule.AlertType == "TRACES_BASED_ALERT" { + if rule.RuleCondition.CompositeQuery != nil { + if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder { + for _, query := range rule.RuleCondition.CompositeQuery.BuilderQueries { + if query.Filters != nil && len(query.Filters.Items) > 0 { + filterItems = append(filterItems, query.Filters.Items...) + } + } + } + } + } + newFilters := common.PrepareFilters(lbls, filterItems) + ts := time.Unix(res.Items[idx].UnixMilli/1000, 0) + if rule.AlertType == "LOGS_BASED_ALERT" { + res.Items[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, newFilters) + } else if rule.AlertType == "TRACES_BASED_ALERT" { + res.Items[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, newFilters) + } + } + } + aH.Respond(w, res) } @@ -816,6 +854,25 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter, RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) return } + + rule, err := aH.ruleManager.GetRule(r.Context(), ruleID) + if err == nil { + for idx := range res { + lbls := make(map[string]string) + err := json.Unmarshal([]byte(res[idx].Labels), &lbls) + if err != nil { + continue + } + ts := time.Unix(params.End/1000, 0) + filters := common.PrepareFilters(lbls, nil) + if rule.AlertType == "LOGS_BASED_ALERT" { + res[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, filters) + } else if rule.AlertType == "TRACES_BASED_ALERT" { + res[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, filters) + } + } + } + aH.Respond(w, res) } diff --git a/pkg/query-service/common/query_range.go b/pkg/query-service/common/query_range.go index c352c7d9f2..6e96b5897a 100644 --- a/pkg/query-service/common/query_range.go +++ b/pkg/query-service/common/query_range.go @@ -1,7 +1,10 @@ package common import ( + "encoding/json" + "fmt" "math" + "net/url" "time" "go.signoz.io/signoz/pkg/query-service/constants" @@ -70,3 +73,183 @@ func LCMList(nums []int64) int64 { } return result } + +// TODO(srikanthccv): move the custom function in threshold_rule.go to here +func PrepareLinksToTraces(ts time.Time, filterItems []v3.FilterItem) string { + + start := ts.Add(-time.Minute * 15) + end := ts.Add(time.Minute * 15) + + // Traces list view expects time in nanoseconds + tr := v3.URLShareableTimeRange{ + Start: start.UnixNano(), + End: end.UnixNano(), + PageSize: 100, + } + + options := v3.URLShareableOptions{ + MaxLines: 2, + Format: "list", + SelectColumns: constants.TracesListViewDefaultSelectedColumns, + } + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + urlData := v3.URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: v3.URLShareableBuilderQuery{ + 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) + compositeQuery := 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 PrepareLinksToLogs(ts time.Time, filterItems []v3.FilterItem) string { + start := ts.Add(-time.Minute * 15) + end := ts.Add(time.Minute * 15) + + // Logs list view expects time in milliseconds + // Logs list view expects time in milliseconds + tr := v3.URLShareableTimeRange{ + Start: start.UnixMilli(), + End: end.UnixMilli(), + PageSize: 100, + } + + options := v3.URLShareableOptions{ + MaxLines: 2, + Format: "list", + SelectColumns: []v3.AttributeKey{}, + } + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + urlData := v3.URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: v3.URLShareableBuilderQuery{ + 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(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) +} + +// 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 PrepareFilters(labels map[string]string, filters []v3.FilterItem) []v3.FilterItem { + var filterItems []v3.FilterItem + + added := make(map[string]struct{}) + + for _, item := range filters { + exists := false + for key, value := range labels { + if item.Key.Key == key { + // 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: value, + }) + exists = true + added[key] = 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 key, value := range labels { + if _, ok := added[key]; !ok { + filterItems = append(filterItems, v3.FilterItem{ + Key: v3.AttributeKey{Key: key}, + Operator: v3.FilterOperatorEqual, + Value: value, + }) + } + } + + return filterItems +} diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index 4086947d4f..f275579104 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -110,7 +110,7 @@ type Reader interface { AddRuleStateHistory(ctx context.Context, ruleStateHistory []v3.RuleStateHistory) error GetOverallStateTransitions(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) ([]v3.RuleStateTransition, error) - ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) ([]v3.RuleStateHistory, error) + ReadRuleStateHistoryByRuleID(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (*v3.RuleStateTimeline, error) GetTotalTriggers(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (uint64, error) GetTriggersByInterval(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (*v3.Series, error) GetAvgResolutionTime(ctx context.Context, ruleID string, params *v3.QueryRuleStateHistory) (float64, error) diff --git a/pkg/query-service/model/v3/v3.go b/pkg/query-service/model/v3/v3.go index 6f7881a336..b0e786a6d6 100644 --- a/pkg/query-service/model/v3/v3.go +++ b/pkg/query-service/model/v3/v3.go @@ -1181,6 +1181,11 @@ func (l LabelsString) String() string { return string(l) } +type RuleStateTimeline struct { + Items []RuleStateHistory `json:"items"` + Total uint64 `json:"total"` +} + type RuleStateHistory struct { RuleID string `json:"ruleID" ch:"rule_id"` RuleName string `json:"ruleName" ch:"rule_name"` @@ -1194,11 +1199,15 @@ type RuleStateHistory struct { Labels LabelsString `json:"labels" ch:"labels"` Fingerprint uint64 `json:"fingerprint" ch:"fingerprint"` Value float64 `json:"value" ch:"value"` + + RelatedTracesLink string `json:"relatedTracesLink"` + RelatedLogsLink string `json:"relatedLogsLink"` } type QueryRuleStateHistory struct { Start int64 `json:"start"` End int64 `json:"end"` + State string `json:"state"` Filters *FilterSet `json:"filters"` Offset int64 `json:"offset"` Limit int64 `json:"limit"` @@ -1219,9 +1228,11 @@ func (r *QueryRuleStateHistory) Validate() error { } type RuleStateHistoryContributor struct { - Fingerprint uint64 `json:"fingerprint" ch:"fingerprint"` - Labels LabelsString `json:"labels" ch:"labels"` - Count uint64 `json:"count" ch:"count"` + Fingerprint uint64 `json:"fingerprint" ch:"fingerprint"` + Labels LabelsString `json:"labels" ch:"labels"` + Count uint64 `json:"count" ch:"count"` + RelatedTracesLink string `json:"relatedTracesLink"` + RelatedLogsLink string `json:"relatedLogsLink"` } type RuleStateTransition struct { @@ -1255,3 +1266,25 @@ type QueryProgress struct { ElapsedMs uint64 `json:"elapsed_ms"` } + +type URLShareableTimeRange struct { + Start int64 `json:"start"` + End int64 `json:"end"` + PageSize int64 `json:"pageSize"` +} + +type URLShareableBuilderQuery struct { + QueryData []BuilderQuery `json:"queryData"` + QueryFormulas []string `json:"queryFormulas"` +} + +type URLShareableCompositeQuery struct { + QueryType string `json:"queryType"` + Builder URLShareableBuilderQuery `json:"builder"` +} + +type URLShareableOptions struct { + MaxLines int `json:"maxLines"` + Format string `json:"format"` + SelectColumns []AttributeKey `json:"selectColumns"` +} diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index e5660e0dfe..36a0ea1b69 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -67,6 +67,8 @@ type Alert struct { Labels labels.BaseLabels Annotations labels.BaseLabels + QueryResultLables labels.BaseLabels + GeneratorURL string // list of preferred receivers, e.g. slack diff --git a/pkg/query-service/rules/api_params.go b/pkg/query-service/rules/api_params.go index af7e9378f6..74ca041aae 100644 --- a/pkg/query-service/rules/api_params.go +++ b/pkg/query-service/rules/api_params.go @@ -242,25 +242,3 @@ 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"` -} diff --git a/pkg/query-service/rules/prom_rule.go b/pkg/query-service/rules/prom_rule.go index 235a5cf825..06f9ae311d 100644 --- a/pkg/query-service/rules/prom_rule.go +++ b/pkg/query-service/rules/prom_rule.go @@ -411,6 +411,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( } lb := plabels.NewBuilder(alertSmpl.Metric).Del(plabels.MetricName) + resultLabels := plabels.NewBuilder(alertSmpl.Metric).Del(plabels.MetricName).Labels() for _, l := range r.labels { lb.Set(l.Name, expand(l.Value)) @@ -439,13 +440,14 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( } alerts[h] = &Alert{ - Labels: lbs, - Annotations: annotations, - ActiveAt: ts, - State: StatePending, - Value: alertSmpl.F, - GeneratorURL: r.GeneratorURL(), - Receivers: r.preferredChannels, + Labels: lbs, + QueryResultLables: resultLabels, + Annotations: annotations, + ActiveAt: ts, + State: StatePending, + Value: alertSmpl.F, + GeneratorURL: r.GeneratorURL(), + Receivers: r.preferredChannels, } } @@ -489,7 +491,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), }) } continue @@ -509,7 +511,7 @@ func (r *PromRule) Eval(ctx context.Context, ts time.Time, queriers *Queriers) ( StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), Value: a.Value, }) } diff --git a/pkg/query-service/rules/threshold_rule.go b/pkg/query-service/rules/threshold_rule.go index 81185f9dca..6ada0e7844 100644 --- a/pkg/query-service/rules/threshold_rule.go +++ b/pkg/query-service/rules/threshold_rule.go @@ -625,13 +625,13 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str q := r.prepareQueryRange(ts) // Logs list view expects time in milliseconds - tr := timeRange{ + tr := v3.URLShareableTimeRange{ Start: q.Start, End: q.End, PageSize: 100, } - options := Options{ + options := v3.URLShareableOptions{ MaxLines: 2, Format: "list", SelectColumns: []v3.AttributeKey{}, @@ -641,9 +641,9 @@ func (r *ThresholdRule) prepareLinksToLogs(ts time.Time, lbls labels.Labels) str urlEncodedTimeRange := url.QueryEscape(string(period)) filterItems := r.fetchFilters(selectedQuery, lbls) - urlData := urlShareableCompositeQuery{ + urlData := v3.URLShareableCompositeQuery{ QueryType: string(v3.QueryTypeBuilder), - Builder: builderQuery{ + Builder: v3.URLShareableBuilderQuery{ QueryData: []v3.BuilderQuery{ { DataSource: v3.DataSourceLogs, @@ -689,13 +689,13 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s q := r.prepareQueryRange(ts) // Traces list view expects time in nanoseconds - tr := timeRange{ + tr := v3.URLShareableTimeRange{ Start: q.Start * time.Second.Microseconds(), End: q.End * time.Second.Microseconds(), PageSize: 100, } - options := Options{ + options := v3.URLShareableOptions{ MaxLines: 2, Format: "list", SelectColumns: constants.TracesListViewDefaultSelectedColumns, @@ -705,9 +705,9 @@ func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) s urlEncodedTimeRange := url.QueryEscape(string(period)) filterItems := r.fetchFilters(selectedQuery, lbls) - urlData := urlShareableCompositeQuery{ + urlData := v3.URLShareableCompositeQuery{ QueryType: string(v3.QueryTypeBuilder), - Builder: builderQuery{ + Builder: v3.URLShareableBuilderQuery{ QueryData: []v3.BuilderQuery{ { DataSource: v3.DataSourceTraces, @@ -954,6 +954,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie } lb := labels.NewBuilder(smpl.Metric).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel) + resultLabels := labels.NewBuilder(smpl.MetricOrig).Del(labels.MetricNameLabel).Del(labels.TemporalityLabel).Labels() for _, l := range r.labels { lb.Set(l.Name, expand(l.Value)) @@ -1001,14 +1002,15 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie } alerts[h] = &Alert{ - Labels: lbs, - Annotations: annotations, - ActiveAt: ts, - State: StatePending, - Value: smpl.V, - GeneratorURL: r.GeneratorURL(), - Receivers: r.preferredChannels, - Missing: smpl.IsMissing, + Labels: lbs, + QueryResultLables: resultLabels, + Annotations: annotations, + ActiveAt: ts, + State: StatePending, + Value: smpl.V, + GeneratorURL: r.GeneratorURL(), + Receivers: r.preferredChannels, + Missing: smpl.IsMissing, } } @@ -1034,7 +1036,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie // Check if any pending alerts should be removed or fire now. Write out alert timeseries. for fp, a := range r.active { - labelsJSON, err := json.Marshal(a.Labels) + labelsJSON, err := json.Marshal(a.QueryResultLables) if err != nil { zap.L().Error("error marshaling labels", zap.Error(err), zap.Any("labels", a.Labels)) } @@ -1054,7 +1056,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), }) } continue @@ -1074,7 +1076,7 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time, queriers *Querie StateChanged: true, UnixMilli: ts.UnixMilli(), Labels: v3.LabelsString(labelsJSON), - Fingerprint: a.Labels.Hash(), + Fingerprint: a.QueryResultLables.Hash(), Value: a.Value, }) }