diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index d156e54775..a921052273 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -728,7 +728,7 @@ func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams * return &serviceOverviewItems, nil } -func buildFilterArrayQuery(ctx context.Context, excludeMap map[string]struct{}, params []string, filter string, query *string, args []interface{}) []interface{} { +func buildFilterArrayQuery(_ context.Context, excludeMap map[string]struct{}, params []string, filter string, query *string, args []interface{}) []interface{} { for i, e := range params { filterKey := filter + String(5) if i == 0 && i == len(params)-1 { @@ -1237,7 +1237,7 @@ func String(length int) string { return StringWithCharset(length, charset) } -func buildQueryWithTagParams(ctx context.Context, tags []model.TagQuery) (string, []interface{}, *model.ApiError) { +func buildQueryWithTagParams(_ context.Context, tags []model.TagQuery) (string, []interface{}, *model.ApiError) { query := "" var args []interface{} for _, item := range tags { @@ -1447,7 +1447,7 @@ func (r *ClickHouseReader) GetTagFilters(ctx context.Context, queryParams *model return &tagFiltersResult, nil } -func excludeTags(ctx context.Context, tags []string) []string { +func excludeTags(_ context.Context, tags []string) []string { excludedTagsMap := map[string]bool{ "http.code": true, "http.route": true, @@ -2201,7 +2201,7 @@ func (r *ClickHouseReader) SetTTL(ctx context.Context, return &model.SetTTLResponseItem{Message: "move ttl has been successfully set up"}, nil } -func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, numberOfTransactionsStore int) { +func (r *ClickHouseReader) deleteTtlTransactions(_ context.Context, numberOfTransactionsStore int) { _, err := r.localDB.Exec("DELETE FROM ttl_status WHERE transaction_id NOT IN (SELECT distinct transaction_id FROM ttl_status ORDER BY created_at DESC LIMIT ?)", numberOfTransactionsStore) if err != nil { zap.L().Error("Error in processing ttl_status delete sql query", zap.Error(err)) @@ -2209,7 +2209,7 @@ func (r *ClickHouseReader) deleteTtlTransactions(ctx context.Context, numberOfTr } // checkTTLStatusItem checks if ttl_status table has an entry for the given table name -func (r *ClickHouseReader) checkTTLStatusItem(ctx context.Context, tableName string) (model.TTLStatusItem, *model.ApiError) { +func (r *ClickHouseReader) checkTTLStatusItem(_ context.Context, tableName string) (model.TTLStatusItem, *model.ApiError) { statusItem := []model.TTLStatusItem{} query := `SELECT id, status, ttl, cold_storage_ttl FROM ttl_status WHERE table_name = ? ORDER BY created_at DESC` diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 9411b595a0..219181dc7f 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -41,6 +41,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/common" "go.signoz.io/signoz/pkg/query-service/constants" + "go.signoz.io/signoz/pkg/query-service/contextlinks" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.signoz.io/signoz/pkg/query-service/postprocess" @@ -767,6 +768,48 @@ func (aH *APIHandler) getOverallStateTransitions(w http.ResponseWriter, r *http. aH.Respond(w, stateItems) } +func (aH *APIHandler) metaForLinks(ctx context.Context, rule *rules.GettableRule) ([]v3.FilterItem, []v3.AttributeKey, map[string]v3.AttributeKey) { + filterItems := []v3.FilterItem{} + groupBy := []v3.AttributeKey{} + keys := make(map[string]v3.AttributeKey) + + if rule.AlertType == rules.AlertTypeLogs { + logFields, err := aH.reader.GetLogFields(ctx) + if err == nil { + params := &v3.QueryRangeParamsV3{ + CompositeQuery: rule.RuleCondition.CompositeQuery, + } + keys = model.GetLogFieldsV3(ctx, params, logFields) + } else { + zap.L().Error("failed to get log fields using empty keys; the link might not work as expected", zap.Error(err)) + } + } else if rule.AlertType == rules.AlertTypeTraces { + traceFields, err := aH.reader.GetSpanAttributeKeys(ctx) + if err == nil { + keys = traceFields + } else { + zap.L().Error("failed to get span attributes using empty keys; the link might not work as expected", zap.Error(err)) + } + } + + if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces { + if rule.RuleCondition.CompositeQuery != nil { + if rule.RuleCondition.QueryType() == v3.QueryTypeBuilder { + selectedQuery := rule.RuleCondition.GetSelectedQueryName() + if rule.RuleCondition.CompositeQuery.BuilderQueries[selectedQuery] != nil && + rule.RuleCondition.CompositeQuery.BuilderQueries[selectedQuery].Filters != nil { + filterItems = rule.RuleCondition.CompositeQuery.BuilderQueries[selectedQuery].Filters.Items + } + if rule.RuleCondition.CompositeQuery.BuilderQueries[selectedQuery] != nil && + rule.RuleCondition.CompositeQuery.BuilderQueries[selectedQuery].GroupBy != nil { + groupBy = rule.RuleCondition.CompositeQuery.BuilderQueries[selectedQuery].GroupBy + } + } + } + } + return filterItems, groupBy, keys +} + func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request) { ruleID := mux.Vars(r)["id"] params := model.QueryRuleStateHistory{} @@ -794,24 +837,18 @@ func (aH *APIHandler) getRuleStateHistory(w http.ResponseWriter, r *http.Request if err != nil { continue } - filterItems := []v3.FilterItem{} - if rule.AlertType == rules.AlertTypeLogs || rule.AlertType == rules.AlertTypeTraces { - 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) + filterItems, groupBy, keys := aH.metaForLinks(r.Context(), rule) + newFilters := contextlinks.PrepareFilters(lbls, filterItems, groupBy, keys) + end := time.Unix(res.Items[idx].UnixMilli/1000, 0) + // why are we subtracting 3 minutes? + // the query range is calculated based on the rule's evalWindow and evalDelay + // alerts have 2 minutes delay built in, so we need to subtract that from the start time + // to get the correct query range + start := end.Add(-time.Duration(rule.EvalWindow)).Add(-3 * time.Minute) if rule.AlertType == rules.AlertTypeLogs { - res.Items[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, newFilters) + res.Items[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters) } else if rule.AlertType == rules.AlertTypeTraces { - res.Items[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, newFilters) + res.Items[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters) } } } @@ -842,12 +879,14 @@ func (aH *APIHandler) getRuleStateHistoryTopContributors(w http.ResponseWriter, if err != nil { continue } - ts := time.Unix(params.End/1000, 0) - filters := common.PrepareFilters(lbls, nil) + filterItems, groupBy, keys := aH.metaForLinks(r.Context(), rule) + newFilters := contextlinks.PrepareFilters(lbls, filterItems, groupBy, keys) + end := time.Unix(params.End/1000, 0) + start := time.Unix(params.Start/1000, 0) if rule.AlertType == rules.AlertTypeLogs { - res[idx].RelatedLogsLink = common.PrepareLinksToLogs(ts, filters) + res[idx].RelatedLogsLink = contextlinks.PrepareLinksToLogs(start, end, newFilters) } else if rule.AlertType == rules.AlertTypeTraces { - res[idx].RelatedTracesLink = common.PrepareLinksToTraces(ts, filters) + res[idx].RelatedTracesLink = contextlinks.PrepareLinksToTraces(start, end, newFilters) } } } @@ -1051,23 +1090,6 @@ func (aH *APIHandler) getDashboard(w http.ResponseWriter, r *http.Request) { } -func (aH *APIHandler) saveAndReturn(w http.ResponseWriter, r *http.Request, signozDashboard model.DashboardData) { - toSave := make(map[string]interface{}) - toSave["title"] = signozDashboard.Title - toSave["description"] = signozDashboard.Description - toSave["tags"] = signozDashboard.Tags - toSave["layout"] = signozDashboard.Layout - toSave["widgets"] = signozDashboard.Widgets - toSave["variables"] = signozDashboard.Variables - - dashboard, apiError := dashboards.CreateDashboard(r.Context(), toSave, aH.featureFlags) - if apiError != nil { - RespondError(w, apiError, nil) - return - } - aH.Respond(w, dashboard) -} - func (aH *APIHandler) createDashboards(w http.ResponseWriter, r *http.Request) { var postData map[string]interface{} @@ -3527,55 +3549,6 @@ func (aH *APIHandler) autoCompleteAttributeValues(w http.ResponseWriter, r *http aH.Respond(w, response) } -func (aH *APIHandler) getLogFieldsV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3) (map[string]v3.AttributeKey, error) { - data := map[string]v3.AttributeKey{} - for _, query := range queryRangeParams.CompositeQuery.BuilderQueries { - if query.DataSource == v3.DataSourceLogs { - fields, apiError := aH.reader.GetLogFields(ctx) - if apiError != nil { - return nil, apiError.Err - } - - // top level fields meta will always be present in the frontend. (can be support for that as enchancement) - getType := func(t string) (v3.AttributeKeyType, bool) { - if t == "attributes" { - return v3.AttributeKeyTypeTag, false - } else if t == "resources" { - return v3.AttributeKeyTypeResource, false - } - return "", true - } - - for _, selectedField := range fields.Selected { - fieldType, pass := getType(selectedField.Type) - if pass { - continue - } - data[selectedField.Name] = v3.AttributeKey{ - Key: selectedField.Name, - Type: fieldType, - DataType: v3.AttributeKeyDataType(strings.ToLower(selectedField.DataType)), - IsColumn: true, - } - } - for _, interestingField := range fields.Interesting { - fieldType, pass := getType(interestingField.Type) - if pass { - continue - } - data[interestingField.Name] = v3.AttributeKey{ - Key: interestingField.Name, - Type: fieldType, - DataType: v3.AttributeKeyDataType(strings.ToLower(interestingField.DataType)), - IsColumn: false, - } - } - break - } - } - return data, nil -} - func (aH *APIHandler) getSpanKeysV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3) (map[string]v3.AttributeKey, error) { data := map[string]v3.AttributeKey{} for _, query := range queryRangeParams.CompositeQuery.BuilderQueries { @@ -3617,14 +3590,14 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder { // check if any enrichment is required for logs if yes then enrich them if logsv3.EnrichmentRequired(queryRangeParams) { - // get the fields if any logs query is present - var fields map[string]v3.AttributeKey - fields, err = aH.getLogFieldsV3(ctx, queryRangeParams) + logsFields, err := aH.reader.GetLogFields(ctx) if err != nil { apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} RespondError(w, apiErrObj, errQuriesByName) return } + // get the fields if any logs query is present + fields := model.GetLogFieldsV3(ctx, queryRangeParams, logsFields) logsv3.Enrich(queryRangeParams, fields) } @@ -3666,6 +3639,7 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que } else { // Adding queryId to the context signals clickhouse queries to report progress + //lint:ignore SA1029 ignore for now ctx = context.WithValue(ctx, "queryId", queryIdHeader) defer func() { @@ -3918,13 +3892,13 @@ func (aH *APIHandler) liveTailLogsV2(w http.ResponseWriter, r *http.Request) { // check if any enrichment is required for logs if yes then enrich them if logsv3.EnrichmentRequired(queryRangeParams) { // get the fields if any logs query is present - var fields map[string]v3.AttributeKey - fields, err = aH.getLogFieldsV3(r.Context(), queryRangeParams) + logsFields, err := aH.reader.GetLogFields(r.Context()) if err != nil { apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} RespondError(w, apiErrObj, nil) return } + fields := model.GetLogFieldsV3(r.Context(), queryRangeParams, logsFields) logsv3.Enrich(queryRangeParams, fields) } @@ -4003,14 +3977,14 @@ func (aH *APIHandler) liveTailLogs(w http.ResponseWriter, r *http.Request) { case v3.QueryTypeBuilder: // check if any enrichment is required for logs if yes then enrich them if logsv3.EnrichmentRequired(queryRangeParams) { - // get the fields if any logs query is present - var fields map[string]v3.AttributeKey - fields, err = aH.getLogFieldsV3(r.Context(), queryRangeParams) + logsFields, err := aH.reader.GetLogFields(r.Context()) if err != nil { apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} RespondError(w, apiErrObj, nil) return } + // get the fields if any logs query is present + fields := model.GetLogFieldsV3(r.Context(), queryRangeParams, logsFields) logsv3.Enrich(queryRangeParams, fields) } @@ -4087,13 +4061,13 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que // check if any enrichment is required for logs if yes then enrich them if logsv3.EnrichmentRequired(queryRangeParams) { // get the fields if any logs query is present - var fields map[string]v3.AttributeKey - fields, err = aH.getLogFieldsV3(ctx, queryRangeParams) + logsFields, err := aH.reader.GetLogFields(r.Context()) if err != nil { apiErrObj := &model.ApiError{Typ: model.ErrorInternal, Err: err} - RespondError(w, apiErrObj, errQuriesByName) + RespondError(w, apiErrObj, nil) return } + fields := model.GetLogFieldsV3(r.Context(), queryRangeParams, logsFields) logsv3.Enrich(queryRangeParams, fields) } diff --git a/pkg/query-service/app/logs/v3/enrich_query.go b/pkg/query-service/app/logs/v3/enrich_query.go index 8a7bc85970..b8ed0ff801 100644 --- a/pkg/query-service/app/logs/v3/enrich_query.go +++ b/pkg/query-service/app/logs/v3/enrich_query.go @@ -94,11 +94,11 @@ func Enrich(params *v3.QueryRangeParamsV3, fields map[string]v3.AttributeKey) { if query.Expression != queryName && query.DataSource != v3.DataSourceLogs { continue } - enrichLogsQuery(query, fields) + EnrichLogsQuery(query, fields) } } -func enrichLogsQuery(query *v3.BuilderQuery, fields map[string]v3.AttributeKey) error { +func EnrichLogsQuery(query *v3.BuilderQuery, fields map[string]v3.AttributeKey) error { // enrich aggregation attribute if query.AggregateAttribute.Key != "" { query.AggregateAttribute = enrichFieldWithMetadata(query.AggregateAttribute, fields) diff --git a/pkg/query-service/app/traces/v3/query_builder.go b/pkg/query-service/app/traces/v3/query_builder.go index 4f1ad86f23..c66b95ea56 100644 --- a/pkg/query-service/app/traces/v3/query_builder.go +++ b/pkg/query-service/app/traces/v3/query_builder.go @@ -545,25 +545,29 @@ func Enrich(params *v3.QueryRangeParamsV3, keys map[string]v3.AttributeKey) { if params.CompositeQuery.QueryType == v3.QueryTypeBuilder { for _, query := range params.CompositeQuery.BuilderQueries { if query.DataSource == v3.DataSourceTraces { - // enrich aggregate attribute - query.AggregateAttribute = enrichKeyWithMetadata(query.AggregateAttribute, keys) - // enrich filter items - if query.Filters != nil && len(query.Filters.Items) > 0 { - for idx, filter := range query.Filters.Items { - query.Filters.Items[idx].Key = enrichKeyWithMetadata(filter.Key, keys) - } - } - // enrich group by - for idx, groupBy := range query.GroupBy { - query.GroupBy[idx] = enrichKeyWithMetadata(groupBy, keys) - } - // enrich order by - query.OrderBy = enrichOrderBy(query.OrderBy, keys) - // enrich select columns - for idx, selectColumn := range query.SelectColumns { - query.SelectColumns[idx] = enrichKeyWithMetadata(selectColumn, keys) - } + EnrichTracesQuery(query, keys) } } } } + +func EnrichTracesQuery(query *v3.BuilderQuery, keys map[string]v3.AttributeKey) { + // enrich aggregate attribute + query.AggregateAttribute = enrichKeyWithMetadata(query.AggregateAttribute, keys) + // enrich filter items + if query.Filters != nil && len(query.Filters.Items) > 0 { + for idx, filter := range query.Filters.Items { + query.Filters.Items[idx].Key = enrichKeyWithMetadata(filter.Key, keys) + } + } + // enrich group by + for idx, groupBy := range query.GroupBy { + query.GroupBy[idx] = enrichKeyWithMetadata(groupBy, keys) + } + // enrich order by + query.OrderBy = enrichOrderBy(query.OrderBy, keys) + // enrich select columns + for idx, selectColumn := range query.SelectColumns { + query.SelectColumns[idx] = enrichKeyWithMetadata(selectColumn, keys) + } +} diff --git a/pkg/query-service/app/traces/v3/utils.go b/pkg/query-service/app/traces/v3/utils.go index cbd0940a16..7d4edd5223 100644 --- a/pkg/query-service/app/traces/v3/utils.go +++ b/pkg/query-service/app/traces/v3/utils.go @@ -8,6 +8,39 @@ import ( "go.uber.org/zap" ) +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, + }, +} + // check if traceId filter is used in traces query and return the list of traceIds func TraceIdFilterUsedWithEqual(params *v3.QueryRangeParamsV3) (bool, []string) { compositeQuery := params.CompositeQuery diff --git a/pkg/query-service/common/query_range.go b/pkg/query-service/common/query_range.go index e0c675c50a..c352c7d9f2 100644 --- a/pkg/query-service/common/query_range.go +++ b/pkg/query-service/common/query_range.go @@ -1,10 +1,7 @@ package common import ( - "encoding/json" - "fmt" "math" - "net/url" "time" "go.signoz.io/signoz/pkg/query-service/constants" @@ -73,183 +70,3 @@ 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(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(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/constants/constants.go b/pkg/query-service/constants/constants.go index 71a1e39032..78ee31e1a1 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -401,39 +401,6 @@ 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, - }, -} - const DefaultFilterSuggestionsAttributesLimit = 50 const MaxFilterSuggestionsAttributesLimit = 100 const DefaultFilterSuggestionsExamplesLimit = 2 diff --git a/pkg/query-service/contextlinks/links.go b/pkg/query-service/contextlinks/links.go new file mode 100644 index 0000000000..d0d8400e74 --- /dev/null +++ b/pkg/query-service/contextlinks/links.go @@ -0,0 +1,203 @@ +package contextlinks + +import ( + "encoding/json" + "fmt" + "net/url" + "time" + + tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + +func PrepareLinksToTraces(start, end time.Time, filterItems []v3.FilterItem) string { + + // 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: tracesV3.TracesListViewDefaultSelectedColumns, + } + + period, _ := json.Marshal(tr) + urlEncodedTimeRange := url.QueryEscape(string(period)) + + builderQuery := 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", + }, + }, + } + + urlData := v3.URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: v3.URLShareableBuilderQuery{ + QueryData: []v3.BuilderQuery{ + builderQuery, + }, + 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 PrepareLinksToLogs(start, end time.Time, filterItems []v3.FilterItem) string { + + // 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)) + + builderQuery := 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", + }, + }, + } + + urlData := v3.URLShareableCompositeQuery{ + QueryType: string(v3.QueryTypeBuilder), + Builder: v3.URLShareableBuilderQuery{ + QueryData: []v3.BuilderQuery{ + builderQuery, + }, + 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) +} + +// 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, whereClauseItems []v3.FilterItem, groupByItems []v3.AttributeKey, keys map[string]v3.AttributeKey) []v3.FilterItem { + var filterItems []v3.FilterItem + + added := make(map[string]struct{}) + + for _, item := range whereClauseItems { + 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 there is no label for the filter item, add it as it is + filterItems = append(filterItems, item) + } + } + + // if there are labels which are not part of the where clause, but + // exist in the result, then they could be part of the group by clause + for key, value := range labels { + if _, ok := added[key]; !ok { + // start by taking the attribute key from the keys map, if not present, create a new one + attributeKey, ok := keys[key] + if !ok { + attributeKey = v3.AttributeKey{Key: key} + } + + // if there is a group by item with the same key, use that instead + for _, groupByItem := range groupByItems { + if groupByItem.Key == key { + attributeKey = groupByItem + break + } + } + + filterItems = append(filterItems, v3.FilterItem{ + Key: attributeKey, + Operator: v3.FilterOperatorEqual, + Value: value, + }) + } + } + + return filterItems +} diff --git a/pkg/query-service/model/logs.go b/pkg/query-service/model/logs.go index ef1c7ff2e4..0319581516 100644 --- a/pkg/query-service/model/logs.go +++ b/pkg/query-service/model/logs.go @@ -1,5 +1,12 @@ package model +import ( + "context" + "strings" + + v3 "go.signoz.io/signoz/pkg/query-service/model/v3" +) + type LogsLiveTailClientV2 struct { Name string Logs chan *SignozLogV2 @@ -21,3 +28,48 @@ type QueryProgress struct { ElapsedMs uint64 `json:"elapsed_ms"` } + +func GetLogFieldsV3(ctx context.Context, queryRangeParams *v3.QueryRangeParamsV3, fields *GetFieldsResponse) map[string]v3.AttributeKey { + data := map[string]v3.AttributeKey{} + for _, query := range queryRangeParams.CompositeQuery.BuilderQueries { + if query.DataSource == v3.DataSourceLogs { + + // top level fields meta will always be present in the frontend. (can be support for that as enchancement) + getType := func(t string) (v3.AttributeKeyType, bool) { + if t == "attributes" { + return v3.AttributeKeyTypeTag, false + } else if t == "resources" { + return v3.AttributeKeyTypeResource, false + } + return "", true + } + + for _, selectedField := range fields.Selected { + fieldType, pass := getType(selectedField.Type) + if pass { + continue + } + data[selectedField.Name] = v3.AttributeKey{ + Key: selectedField.Name, + Type: fieldType, + DataType: v3.AttributeKeyDataType(strings.ToLower(selectedField.DataType)), + IsColumn: true, + } + } + for _, interestingField := range fields.Interesting { + fieldType, pass := getType(interestingField.Type) + if pass { + continue + } + data[interestingField.Name] = v3.AttributeKey{ + Key: interestingField.Name, + Type: fieldType, + DataType: v3.AttributeKeyDataType(strings.ToLower(interestingField.DataType)), + IsColumn: false, + } + } + break + } + } + return data +} diff --git a/pkg/query-service/rules/alerting.go b/pkg/query-service/rules/alerting.go index f6826ed3d8..cb5205f99e 100644 --- a/pkg/query-service/rules/alerting.go +++ b/pkg/query-service/rules/alerting.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/url" + "sort" "strings" "time" @@ -124,6 +125,47 @@ type RuleCondition struct { SelectedQuery string `json:"selectedQueryName,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 { diff --git a/pkg/query-service/rules/base_rule.go b/pkg/query-service/rules/base_rule.go index 6fbaa655c7..a108938b1d 100644 --- a/pkg/query-service/rules/base_rule.go +++ b/pkg/query-service/rules/base_rule.go @@ -202,6 +202,21 @@ func (r *BaseRule) Unit() string { return "" } +func (r *BaseRule) Timestamps(ts time.Time) (time.Time, time.Time) { + start := ts.Add(-time.Duration(r.evalWindow)).UnixMilli() + end := ts.UnixMilli() + + if r.evalDelay > 0 { + start = start - int64(r.evalDelay.Milliseconds()) + end = end - int64(r.evalDelay.Milliseconds()) + } + // round to minute otherwise we could potentially miss data + start = start - (start % (60 * 1000)) + end = end - (end % (60 * 1000)) + + return time.UnixMilli(start), time.UnixMilli(end) +} + func (r *BaseRule) SetLastError(err error) { r.mtx.Lock() defer r.mtx.Unlock() diff --git a/pkg/query-service/rules/threshold_rule.go b/pkg/query-service/rules/threshold_rule.go index f7cdfd6708..0f768314cf 100644 --- a/pkg/query-service/rules/threshold_rule.go +++ b/pkg/query-service/rules/threshold_rule.go @@ -6,9 +6,7 @@ import ( "encoding/json" "fmt" "math" - "net/url" "regexp" - "sort" "text/template" "time" "unicode" @@ -16,6 +14,7 @@ import ( "go.uber.org/zap" "go.signoz.io/signoz/pkg/query-service/common" + "go.signoz.io/signoz/pkg/query-service/contextlinks" "go.signoz.io/signoz/pkg/query-service/model" "go.signoz.io/signoz/pkg/query-service/postprocess" @@ -31,6 +30,7 @@ import ( "go.signoz.io/signoz/pkg/query-service/utils/timestamp" logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/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" @@ -53,6 +53,10 @@ type ThresholdRule struct { querier interfaces.Querier // querierV2 is used for alerts created after the introduction of new metrics query builder querierV2 interfaces.Querier + + // used for attribute metadata enrichment for logs and traces + logsKeys map[string]v3.AttributeKey + spansKeys map[string]v3.AttributeKey } func NewThresholdRule( @@ -164,16 +168,8 @@ func (r *ThresholdRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, zap.L().Info("prepareQueryRange", zap.Int64("ts", ts.UnixMilli()), zap.Int64("evalWindow", r.evalWindow.Milliseconds()), zap.Int64("evalDelay", r.evalDelay.Milliseconds())) - start := ts.Add(-time.Duration(r.evalWindow)).UnixMilli() - end := ts.UnixMilli() - - if r.evalDelay > 0 { - start = start - int64(r.evalDelay.Milliseconds()) - end = end - int64(r.evalDelay.Milliseconds()) - } - // round to minute otherwise we could potentially miss data - start = start - (start % (60 * 1000)) - end = end - (end % (60 * 1000)) + startTs, endTs := r.Timestamps(ts) + start, end := startTs.UnixMilli(), endTs.UnixMilli() if r.ruleCondition.QueryType() == v3.QueryTypeClickHouseSQL { params := &v3.QueryRangeParamsV3{ @@ -239,245 +235,76 @@ func (r *ThresholdRule) prepareQueryRange(ts time.Time) (*v3.QueryRangeParamsV3, }, nil } -// 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() + qr, err := r.prepareQueryRange(ts) + if err != nil { + return "" + } + start := time.UnixMilli(qr.Start) + end := time.UnixMilli(qr.End) + // TODO(srikanthccv): handle formula queries if selectedQuery < "A" || selectedQuery > "Z" { return "" } - q, err := r.prepareQueryRange(ts) - if err != nil { + q := r.ruleCondition.CompositeQuery.BuilderQueries[selectedQuery] + if q == nil { return "" } - // Logs list view expects time in milliseconds - tr := v3.URLShareableTimeRange{ - Start: q.Start, - End: q.End, - PageSize: 100, + + if q.DataSource != v3.DataSourceLogs { + return "" } - options := v3.URLShareableOptions{ - MaxLines: 2, - Format: "list", - SelectColumns: []v3.AttributeKey{}, + queryFilter := []v3.FilterItem{} + if q.Filters != nil { + queryFilter = q.Filters.Items } - period, _ := json.Marshal(tr) - urlEncodedTimeRange := url.QueryEscape(string(period)) + filterItems := contextlinks.PrepareFilters(lbls.Map(), queryFilter, q.GroupBy, r.logsKeys) - filterItems := r.fetchFilters(selectedQuery, lbls) - 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(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) + return contextlinks.PrepareLinksToLogs(start, end, filterItems) } func (r *ThresholdRule) prepareLinksToTraces(ts time.Time, lbls labels.Labels) string { selectedQuery := r.GetSelectedQuery() + qr, err := r.prepareQueryRange(ts) + if err != nil { + return "" + } + start := time.UnixMilli(qr.Start) + end := time.UnixMilli(qr.End) + // TODO(srikanthccv): handle formula queries if selectedQuery < "A" || selectedQuery > "Z" { return "" } - q, err := r.prepareQueryRange(ts) - if err != nil { + q := r.ruleCondition.CompositeQuery.BuilderQueries[selectedQuery] + if q == nil { return "" } - // Traces list view expects time in nanoseconds - tr := v3.URLShareableTimeRange{ - Start: q.Start * time.Second.Microseconds(), - End: q.End * time.Second.Microseconds(), - PageSize: 100, + + if q.DataSource != v3.DataSourceTraces { + return "" } - options := v3.URLShareableOptions{ - MaxLines: 2, - Format: "list", - SelectColumns: constants.TracesListViewDefaultSelectedColumns, + queryFilter := []v3.FilterItem{} + if q.Filters != nil { + queryFilter = q.Filters.Items } - period, _ := json.Marshal(tr) - urlEncodedTimeRange := url.QueryEscape(string(period)) + filterItems := contextlinks.PrepareFilters(lbls.Map(), queryFilter, q.GroupBy, r.spansKeys) - filterItems := r.fetchFilters(selectedQuery, lbls) - 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(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) + return contextlinks.PrepareLinksToTraces(start, end, filterItems) } func (r *ThresholdRule) GetSelectedQuery() string { - if r.ruleCondition != nil { - if r.ruleCondition.SelectedQuery != "" { - return r.ruleCondition.SelectedQuery - } - - queryNames := map[string]struct{}{} - - if r.ruleCondition.CompositeQuery != nil { - if r.ruleCondition.QueryType() == v3.QueryTypeBuilder { - for name := range r.ruleCondition.CompositeQuery.BuilderQueries { - queryNames[name] = struct{}{} - } - } else if r.ruleCondition.QueryType() == v3.QueryTypeClickHouseSQL { - for name := range r.ruleCondition.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 "" + return r.ruleCondition.GetSelectedQueryName() } func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time) (Vector, error) { @@ -492,11 +319,37 @@ func (r *ThresholdRule) buildAndRunQuery(ctx context.Context, ts time.Time) (Vec } if params.CompositeQuery.QueryType == v3.QueryTypeBuilder { - // check if any enrichment is required for logs if yes then enrich them - if logsv3.EnrichmentRequired(params) { - // Note: Sending empty fields key because enrichment is only needed for json - // TODO: Add support for attribute enrichment later - logsv3.Enrich(params, map[string]v3.AttributeKey{}) + hasLogsQuery := false + hasTracesQuery := false + for _, query := range params.CompositeQuery.BuilderQueries { + if query.DataSource == v3.DataSourceLogs { + hasLogsQuery = true + } + if query.DataSource == v3.DataSourceTraces { + hasTracesQuery = true + } + } + + if hasLogsQuery { + // check if any enrichment is required for logs if yes then enrich them + if logsv3.EnrichmentRequired(params) { + logsFields, err := r.reader.GetLogFields(ctx) + if err != nil { + return nil, err + } + logsKeys := model.GetLogFieldsV3(ctx, params, logsFields) + r.logsKeys = logsKeys + logsv3.Enrich(params, logsKeys) + } + } + + if hasTracesQuery { + spanKeys, err := r.reader.GetSpanAttributeKeys(ctx) + if err != nil { + return nil, err + } + r.spansKeys = spanKeys + tracesV3.Enrich(params, spanKeys) } } @@ -654,11 +507,13 @@ func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (interface{}, er if r.typ == AlertTypeTraces { link := r.prepareLinksToTraces(ts, smpl.MetricOrig) if link != "" && r.hostFromSource() != "" { + zap.L().Info("adding traces link to annotations", zap.String("link", fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link))) annotations = append(annotations, labels.Label{Name: "related_traces", Value: fmt.Sprintf("%s/traces-explorer?%s", r.hostFromSource(), link)}) } } else if r.typ == AlertTypeLogs { link := r.prepareLinksToLogs(ts, smpl.MetricOrig) if link != "" && r.hostFromSource() != "" { + zap.L().Info("adding logs link to annotations", zap.String("link", fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link))) annotations = append(annotations, labels.Label{Name: "related_logs", Value: fmt.Sprintf("%s/logs/logs-explorer?%s", r.hostFromSource(), link)}) } } diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index e000d71257..65d020d25f 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -1303,12 +1303,23 @@ func TestThresholdRuleTracesLink(t *testing.T) { t.Errorf("an error '%s' was not expected when opening a stub database connection", err) } + metaCols := make([]cmock.ColumnType, 0) + metaCols = append(metaCols, cmock.ColumnType{Name: "DISTINCT(tagKey)", Type: "String"}) + metaCols = append(metaCols, cmock.ColumnType{Name: "tagType", Type: "String"}) + metaCols = append(metaCols, cmock.ColumnType{Name: "dataType", Type: "String"}) + metaCols = append(metaCols, cmock.ColumnType{Name: "isColumn", Type: "Bool"}) + cols := make([]cmock.ColumnType, 0) cols = append(cols, cmock.ColumnType{Name: "value", Type: "Float64"}) cols = append(cols, cmock.ColumnType{Name: "attr", Type: "String"}) cols = append(cols, cmock.ColumnType{Name: "timestamp", Type: "String"}) for idx, c := range testCases { + metaRows := cmock.NewRows(metaCols, c.metaValues) + mock. + ExpectQuery("SELECT DISTINCT(tagKey), tagType, dataType, isColumn FROM archiveNamespace.span_attributes_keys"). + WillReturnRows(metaRows) + rows := cmock.NewRows(cols, c.values) // We are testing the eval logic after the query is run @@ -1402,12 +1413,38 @@ func TestThresholdRuleLogsLink(t *testing.T) { t.Errorf("an error '%s' was not expected when opening a stub database connection", err) } + attrMetaCols := make([]cmock.ColumnType, 0) + attrMetaCols = append(attrMetaCols, cmock.ColumnType{Name: "name", Type: "String"}) + attrMetaCols = append(attrMetaCols, cmock.ColumnType{Name: "dataType", Type: "String"}) + + resourceMetaCols := make([]cmock.ColumnType, 0) + resourceMetaCols = append(resourceMetaCols, cmock.ColumnType{Name: "name", Type: "String"}) + resourceMetaCols = append(resourceMetaCols, cmock.ColumnType{Name: "dataType", Type: "String"}) + + createTableCols := make([]cmock.ColumnType, 0) + createTableCols = append(createTableCols, cmock.ColumnType{Name: "statement", Type: "String"}) + cols := make([]cmock.ColumnType, 0) cols = append(cols, cmock.ColumnType{Name: "value", Type: "Float64"}) cols = append(cols, cmock.ColumnType{Name: "attr", Type: "String"}) cols = append(cols, cmock.ColumnType{Name: "timestamp", Type: "String"}) for idx, c := range testCases { + attrMetaRows := cmock.NewRows(attrMetaCols, c.attrMetaValues) + mock. + ExpectSelect("SELECT DISTINCT name, datatype from signoz_logs.distributed_logs_attribute_keys group by name, datatype"). + WillReturnRows(attrMetaRows) + + resourceMetaRows := cmock.NewRows(resourceMetaCols, c.resourceMetaValues) + mock. + ExpectSelect("SELECT DISTINCT name, datatype from signoz_logs.distributed_logs_resource_keys group by name, datatype"). + WillReturnRows(resourceMetaRows) + + createTableRows := cmock.NewRows(createTableCols, c.createTableValues) + mock. + ExpectSelect("SHOW CREATE TABLE signoz_logs.logs"). + WillReturnRows(createTableRows) + rows := cmock.NewRows(cols, c.values) // We are testing the eval logic after the query is run diff --git a/pkg/query-service/rules/threshold_rule_test_data.go b/pkg/query-service/rules/threshold_rule_test_data.go index 3a28bdf38b..cc301c5aa9 100644 --- a/pkg/query-service/rules/threshold_rule_test_data.go +++ b/pkg/query-service/rules/threshold_rule_test_data.go @@ -4,14 +4,18 @@ import "time" var ( testCases = []struct { - targetUnit string - yAxisUnit string - values [][]interface{} - expectAlerts int - compareOp string - matchType string - target float64 - summaryAny []string + targetUnit string + yAxisUnit string + values [][]interface{} + metaValues [][]interface{} + attrMetaValues [][]interface{} + resourceMetaValues [][]interface{} + createTableValues [][]interface{} + expectAlerts int + compareOp string + matchType string + target float64 + summaryAny []string }{ { targetUnit: "s", @@ -23,10 +27,16 @@ var ( {float64(299316000), "attr", time.Now().Add(3 * time.Second)}, // 0.3 seconds {float64(66640400.00000001), "attr", time.Now().Add(4 * time.Second)}, // 0.06 seconds }, - expectAlerts: 0, - compareOp: "1", // Above - matchType: "1", // Once - target: 1, // 1 second + metaValues: [][]interface{}{}, + createTableValues: [][]interface{}{ + {"statement"}, + }, + attrMetaValues: [][]interface{}{}, + resourceMetaValues: [][]interface{}{}, + expectAlerts: 0, + compareOp: "1", // Above + matchType: "1", // Once + target: 1, // 1 second }, { targetUnit: "ms", @@ -38,10 +48,16 @@ var ( {float64(299316000), "attr", time.Now().Add(3 * time.Second)}, // 299.31 ms {float64(66640400.00000001), "attr", time.Now().Add(4 * time.Second)}, // 66.64 ms }, - expectAlerts: 4, - compareOp: "1", // Above - matchType: "1", // Once - target: 200, // 200 ms + metaValues: [][]interface{}{}, + createTableValues: [][]interface{}{ + {"statement"}, + }, + attrMetaValues: [][]interface{}{}, + resourceMetaValues: [][]interface{}{}, + expectAlerts: 4, + compareOp: "1", // Above + matchType: "1", // Once + target: 200, // 200 ms summaryAny: []string{ "observed metric value is 299 ms", "the observed metric value is 573 ms", @@ -59,10 +75,16 @@ var ( {float64(299316000), "attr", time.Now().Add(3 * time.Second)}, // 0.3 GB {float64(66640400.00000001), "attr", time.Now().Add(4 * time.Second)}, // 66.64 MB }, - expectAlerts: 0, - compareOp: "1", // Above - matchType: "1", // Once - target: 200, // 200 GB + metaValues: [][]interface{}{}, + createTableValues: [][]interface{}{ + {"statement"}, + }, + attrMetaValues: [][]interface{}{}, + resourceMetaValues: [][]interface{}{}, + expectAlerts: 0, + compareOp: "1", // Above + matchType: "1", // Once + target: 200, // 200 GB }, } )