package queryBuilderToExpr import ( "fmt" "reflect" "strings" expr "github.com/antonmedv/expr" v3 "go.signoz.io/signoz/pkg/query-service/model/v3" "go.uber.org/zap" ) var logOperatorsToExpr = map[v3.FilterOperator]string{ v3.FilterOperatorEqual: "==", v3.FilterOperatorNotEqual: "!=", v3.FilterOperatorLessThan: "<", v3.FilterOperatorLessThanOrEq: "<=", v3.FilterOperatorGreaterThan: ">", v3.FilterOperatorGreaterThanOrEq: ">=", v3.FilterOperatorContains: "contains", v3.FilterOperatorNotContains: "not contains", v3.FilterOperatorRegex: "matches", v3.FilterOperatorNotRegex: "not matches", v3.FilterOperatorIn: "in", v3.FilterOperatorNotIn: "not in", v3.FilterOperatorExists: "in", v3.FilterOperatorNotExists: "not in", // we dont support like and nlike as of now. } func getName(v v3.AttributeKey) string { if v.Type == v3.AttributeKeyTypeTag { return fmt.Sprintf(`attributes["%s"]`, v.Key) } else if v.Type == v3.AttributeKeyTypeResource { return fmt.Sprintf(`resource["%s"]`, v.Key) } return v.Key } func getTypeName(v v3.AttributeKeyType) string { if v == v3.AttributeKeyTypeTag { return "attributes" } else if v == v3.AttributeKeyTypeResource { return "resource" } return "" } func Parse(filters *v3.FilterSet) (string, error) { var res []string for _, v := range filters.Items { if _, ok := logOperatorsToExpr[v.Operator]; !ok { return "", fmt.Errorf("operator not supported") } name := getName(v.Key) var filter string switch v.Operator { // uncomment following lines when new version of expr is used // case v3.FilterOperatorIn, v3.FilterOperatorNotIn: // filter = fmt.Sprintf("%s %s list%s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value)) case v3.FilterOperatorExists, v3.FilterOperatorNotExists: filter = fmt.Sprintf("%s %s %s", exprFormattedValue(v.Key.Key), logOperatorsToExpr[v.Operator], getTypeName(v.Key.Type)) default: filter = fmt.Sprintf("%s %s %s", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value)) if v.Operator == v3.FilterOperatorContains || v.Operator == v3.FilterOperatorNotContains { // `contains` and `ncontains` should be case insensitive to match how they work when querying logs. filter = fmt.Sprintf( "lower(%s) %s lower(%s)", name, logOperatorsToExpr[v.Operator], exprFormattedValue(v.Value), ) } // Avoid running operators on nil values if v.Operator != v3.FilterOperatorEqual && v.Operator != v3.FilterOperatorNotEqual { filter = fmt.Sprintf("%s != nil && %s", name, filter) } } // check if the filter is a correct expression language _, err := expr.Compile(filter) if err != nil { return "", err } res = append(res, filter) } // check the final filter q := strings.Join(res, " "+strings.ToLower(filters.Operator)+" ") _, err := expr.Compile(q) if err != nil { return "", err } return q, nil } func exprFormattedValue(v interface{}) string { switch x := v.(type) { case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64: return fmt.Sprintf("%d", x) case float32, float64: return fmt.Sprintf("%f", x) case string: return fmt.Sprintf("\"%s\"", quoteEscapedString(x)) case bool: return fmt.Sprintf("%v", x) case []interface{}: if len(x) == 0 { return "" } switch x[0].(type) { case string: str := "[" for idx, sVal := range x { str += fmt.Sprintf("'%s'", quoteEscapedString(sVal.(string))) if idx != len(x)-1 { str += "," } } str += "]" return str case uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64, bool: return strings.Join(strings.Fields(fmt.Sprint(x)), ",") default: zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x[0]))) return "" } default: zap.L().Error("invalid type for formatted value", zap.Any("type", reflect.TypeOf(x))) return "" } } func quoteEscapedString(str string) string { str = strings.ReplaceAll(str, `\`, `\\`) str = strings.ReplaceAll(str, `"`, `\"`) return str }