diff --git a/pkg/modules/tracefunnel/clickhouse_queries.go b/pkg/modules/tracefunnel/clickhouse_queries.go index 659c47769896..0dadc390aa11 100644 --- a/pkg/modules/tracefunnel/clickhouse_queries.go +++ b/pkg/modules/tracefunnel/clickhouse_queries.go @@ -4,6 +4,14 @@ import ( "fmt" ) +// Helper function to format clause with AND if not empty +func formatClause(clause string) string { + if clause == "" { + return "" + } + return fmt.Sprintf("AND %s", clause) +} + func BuildTwoStepFunnelValidationQuery( containsErrorT1 int, containsErrorT2 int, @@ -87,8 +95,8 @@ LIMIT 5;` spanNameT1, serviceNameT2, spanNameT2, - clauseStep1, - clauseStep2, + formatClause(clauseStep1), + formatClause(clauseStep2), ) return query @@ -205,9 +213,9 @@ LIMIT 5;` spanNameT2, serviceNameT3, spanNameT3, - clauseStep1, - clauseStep2, - clauseStep3, + formatClause(clauseStep1), + formatClause(clauseStep2), + formatClause(clauseStep3), ) return query @@ -307,8 +315,8 @@ FROM joined;` spanNameT1, serviceNameT2, spanNameT2, - clauseStep1, - clauseStep2, + formatClause(clauseStep1), + formatClause(clauseStep2), ) return query @@ -456,9 +464,9 @@ FROM joined_t3;` spanNameT2, serviceNameT3, spanNameT3, - clauseStep1, - clauseStep2, - clauseStep3, + formatClause(clauseStep1), + formatClause(clauseStep2), + formatClause(clauseStep3), ) return query @@ -603,9 +611,9 @@ FROM joined_t3;` spanNameT2, serviceNameT3, spanNameT3, - clauseStep1, - clauseStep2, - clauseStep3, + formatClause(clauseStep1), + formatClause(clauseStep2), + formatClause(clauseStep3), ) return query @@ -700,8 +708,8 @@ FROM joined;` spanNameT1, serviceNameT2, spanNameT2, - clauseStep1, - clauseStep2, + formatClause(clauseStep1), + formatClause(clauseStep2), ) return query @@ -840,9 +848,9 @@ FROM joined_t3;` spanNameT2, serviceNameT3, spanNameT3, - clauseStep1, - clauseStep2, - clauseStep3, + formatClause(clauseStep1), + formatClause(clauseStep2), + formatClause(clauseStep3), ) return query @@ -955,8 +963,8 @@ LIMIT 5;` spanNameT1, serviceNameT2, spanNameT2, - clauseStep1, - clauseStep2, + formatClause(clauseStep1), + formatClause(clauseStep2), ) return query @@ -1074,8 +1082,8 @@ LIMIT 5;` spanNameT1, serviceNameT2, spanNameT2, - clauseStep1, - clauseStep2, + formatClause(clauseStep1), + formatClause(clauseStep2), ) return query diff --git a/pkg/modules/tracefunnel/query.go b/pkg/modules/tracefunnel/query.go index 703e50bceab9..2939514b5f2e 100644 --- a/pkg/modules/tracefunnel/query.go +++ b/pkg/modules/tracefunnel/query.go @@ -1,22 +1,106 @@ package tracefunnel import ( - tracesv3 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v3" + "fmt" + "strings" + v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" + "github.com/SigNoz/signoz/pkg/query-service/utils" tracefunnel "github.com/SigNoz/signoz/pkg/types/tracefunnel" ) +func getColumnName(key v3.AttributeKey) string { + if key.IsColumn { + return key.Key + } + filterType, filterDataType := getClickhouseTracesColumnDataTypeAndType(key) + return fmt.Sprintf("%s%s['%s']", filterDataType, filterType, key.Key) +} + +func getClickhouseTracesColumnDataTypeAndType(key v3.AttributeKey) (v3.AttributeKeyType, string) { + filterType := key.Type + filterDataType := "string" + if key.DataType == v3.AttributeKeyDataTypeFloat64 || key.DataType == v3.AttributeKeyDataTypeInt64 { + filterDataType = "number" + } else if key.DataType == v3.AttributeKeyDataTypeBool { + filterDataType = "bool" + } + if filterType == v3.AttributeKeyTypeTag { + filterType = "TagMap" + } else { + filterType = "resourceTagsMap" + filterDataType = "" + } + return filterType, filterDataType +} + // buildFilterClause converts a FilterSet into a SQL WHERE clause string func buildFilterClause(filters *v3.FilterSet) string { - if filters == nil { + if filters == nil || len(filters.Items) == 0 { return "" } - clause, err := tracesv3.BuildTracesFilterQuery(filters) - if err != nil { - return "" + var conditions []string + for _, item := range filters.Items { + // Get the column name based on the key type + columnName := getColumnName(item.Key) + + // Convert operator to lowercase for consistency + op := strings.ToLower(string(item.Operator)) + + // Format the value based on its type + var valueStr string + var err error + if op != "exists" && op != "nexists" { + item.Value, err = utils.ValidateAndCastValue(item.Value, item.Key.DataType) + if err != nil { + continue // Skip invalid values + } + valueStr = utils.ClickHouseFormattedValue(item.Value) + } + + // Build the condition based on the operator + var condition string + switch op { + case "exists": + if item.Key.IsColumn { + condition = fmt.Sprintf("%s != ''", columnName) + } else { + columnType, columnDataType := getClickhouseTracesColumnDataTypeAndType(item.Key) + condition = fmt.Sprintf("has(%s%s, '%s')", columnDataType, columnType, item.Key.Key) + } + case "nexists": + if item.Key.IsColumn { + condition = fmt.Sprintf("%s = ''", columnName) + } else { + columnType, columnDataType := getClickhouseTracesColumnDataTypeAndType(item.Key) + condition = fmt.Sprintf("NOT has(%s%s, '%s')", columnDataType, columnType, item.Key.Key) + } + case "contains", "ncontains": + val := utils.QuoteEscapedString(fmt.Sprintf("%v", item.Value)) + operator := "ILIKE" + if op == "ncontains" { + operator = "NOT ILIKE" + } + condition = fmt.Sprintf("%s %s '%%%s%%'", columnName, operator, val) + case "regex", "nregex": + operator := "match" + if op == "nregex" { + operator = "NOT match" + } + condition = fmt.Sprintf("%s(%s, %s)", operator, columnName, valueStr) + default: + condition = fmt.Sprintf("%s %s %s", columnName, strings.ToUpper(op), valueStr) + } + conditions = append(conditions, condition) } - return clause + + // Join conditions with the operator (AND/OR) + operator := strings.ToUpper(filters.Operator) + if operator == "" { + operator = "AND" // Default to AND if not specified + } + return strings.Join(conditions, fmt.Sprintf(" %s ", operator)) } func ValidateTraces(funnel *tracefunnel.Funnel, timeRange tracefunnel.TimeRange) (*v3.ClickHouseQuery, error) {