2025-04-11 15:13:04 +05:30
|
|
|
package telemetrytraces
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
2025-05-20 16:54:34 +05:30
|
|
|
"slices"
|
2025-07-18 18:37:57 +05:30
|
|
|
"strconv"
|
2025-06-23 09:39:19 +05:30
|
|
|
"strings"
|
2025-07-18 18:37:57 +05:30
|
|
|
"time"
|
2025-04-11 15:13:04 +05:30
|
|
|
|
|
|
|
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
2025-06-23 09:39:19 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
2025-08-13 09:59:28 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/querybuilder"
|
2025-04-11 15:13:04 +05:30
|
|
|
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
|
|
|
"github.com/huandu/go-sqlbuilder"
|
2025-07-02 10:39:16 +05:30
|
|
|
"golang.org/x/exp/maps"
|
2025-04-11 15:13:04 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type conditionBuilder struct {
|
2025-05-16 20:09:57 +05:30
|
|
|
fm qbtypes.FieldMapper
|
2025-04-11 15:13:04 +05:30
|
|
|
}
|
|
|
|
|
|
2025-05-16 20:09:57 +05:30
|
|
|
var _ qbtypes.ConditionBuilder = (*conditionBuilder)(nil)
|
2025-04-11 15:13:04 +05:30
|
|
|
|
2025-05-16 20:09:57 +05:30
|
|
|
func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
|
|
|
|
|
return &conditionBuilder{fm: fm}
|
2025-04-11 15:13:04 +05:30
|
|
|
}
|
|
|
|
|
|
2025-05-20 16:54:34 +05:30
|
|
|
func (c *conditionBuilder) conditionFor(
|
2025-04-11 15:13:04 +05:30
|
|
|
ctx context.Context,
|
|
|
|
|
key *telemetrytypes.TelemetryFieldKey,
|
|
|
|
|
operator qbtypes.FilterOperator,
|
|
|
|
|
value any,
|
|
|
|
|
sb *sqlbuilder.SelectBuilder,
|
|
|
|
|
) (string, error) {
|
2025-08-13 09:59:28 +05:30
|
|
|
|
|
|
|
|
switch operator {
|
|
|
|
|
case qbtypes.FilterOperatorContains,
|
|
|
|
|
qbtypes.FilterOperatorNotContains,
|
|
|
|
|
qbtypes.FilterOperatorILike,
|
|
|
|
|
qbtypes.FilterOperatorNotILike,
|
|
|
|
|
qbtypes.FilterOperatorLike,
|
|
|
|
|
qbtypes.FilterOperatorNotLike:
|
|
|
|
|
value = querybuilder.FormatValueForContains(value)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 20:09:57 +05:30
|
|
|
// first, locate the raw column type (so we can choose the right EXISTS logic)
|
|
|
|
|
column, err := c.fm.ColumnFor(ctx, key)
|
2025-04-11 15:13:04 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 20:09:57 +05:30
|
|
|
// then ask the mapper for the actual SQL reference
|
|
|
|
|
tblFieldName, err := c.fm.FieldFor(ctx, key)
|
2025-04-11 15:13:04 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-18 18:37:57 +05:30
|
|
|
// TODO(srikanthccv): maybe extend this to every possible attribute
|
|
|
|
|
if key.Name == "duration_nano" || key.Name == "durationNano" { // QoL improvement
|
|
|
|
|
if strDuration, ok := value.(string); ok {
|
|
|
|
|
duration, err := time.ParseDuration(strDuration)
|
|
|
|
|
if err == nil {
|
|
|
|
|
value = duration.Nanoseconds()
|
|
|
|
|
} else {
|
|
|
|
|
duration, err := strconv.ParseFloat(strDuration, 64)
|
|
|
|
|
if err == nil {
|
|
|
|
|
value = duration
|
|
|
|
|
} else {
|
|
|
|
|
return "", errors.WrapInvalidInputf(err, errors.CodeInvalidInput, "invalid duration value: %s", strDuration)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
tblFieldName, value = telemetrytypes.DataTypeCollisionHandledFieldName(key, value, tblFieldName)
|
|
|
|
|
}
|
2025-04-11 15:13:04 +05:30
|
|
|
|
|
|
|
|
// regular operators
|
|
|
|
|
switch operator {
|
|
|
|
|
// regular operators
|
|
|
|
|
case qbtypes.FilterOperatorEqual:
|
|
|
|
|
return sb.E(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorNotEqual:
|
|
|
|
|
return sb.NE(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorGreaterThan:
|
|
|
|
|
return sb.G(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorGreaterThanOrEq:
|
|
|
|
|
return sb.GE(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorLessThan:
|
|
|
|
|
return sb.LT(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorLessThanOrEq:
|
|
|
|
|
return sb.LE(tblFieldName, value), nil
|
|
|
|
|
|
|
|
|
|
// like and not like
|
|
|
|
|
case qbtypes.FilterOperatorLike:
|
|
|
|
|
return sb.Like(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorNotLike:
|
|
|
|
|
return sb.NotLike(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorILike:
|
|
|
|
|
return sb.ILike(tblFieldName, value), nil
|
|
|
|
|
case qbtypes.FilterOperatorNotILike:
|
|
|
|
|
return sb.NotILike(tblFieldName, value), nil
|
|
|
|
|
|
|
|
|
|
case qbtypes.FilterOperatorContains:
|
2025-04-11 16:20:27 +05:30
|
|
|
return sb.ILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
2025-04-11 15:13:04 +05:30
|
|
|
case qbtypes.FilterOperatorNotContains:
|
2025-04-11 16:20:27 +05:30
|
|
|
return sb.NotILike(tblFieldName, fmt.Sprintf("%%%s%%", value)), nil
|
2025-04-11 15:13:04 +05:30
|
|
|
|
|
|
|
|
case qbtypes.FilterOperatorRegexp:
|
2025-05-20 16:54:34 +05:30
|
|
|
return fmt.Sprintf(`match(%s, %s)`, tblFieldName, sb.Var(value)), nil
|
2025-04-11 15:13:04 +05:30
|
|
|
case qbtypes.FilterOperatorNotRegexp:
|
2025-05-20 16:54:34 +05:30
|
|
|
return fmt.Sprintf(`NOT match(%s, %s)`, tblFieldName, sb.Var(value)), nil
|
2025-04-11 15:13:04 +05:30
|
|
|
|
|
|
|
|
// between and not between
|
|
|
|
|
case qbtypes.FilterOperatorBetween:
|
|
|
|
|
values, ok := value.([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", qbtypes.ErrBetweenValues
|
|
|
|
|
}
|
|
|
|
|
if len(values) != 2 {
|
|
|
|
|
return "", qbtypes.ErrBetweenValues
|
|
|
|
|
}
|
|
|
|
|
return sb.Between(tblFieldName, values[0], values[1]), nil
|
|
|
|
|
case qbtypes.FilterOperatorNotBetween:
|
|
|
|
|
values, ok := value.([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", qbtypes.ErrBetweenValues
|
|
|
|
|
}
|
|
|
|
|
if len(values) != 2 {
|
|
|
|
|
return "", qbtypes.ErrBetweenValues
|
|
|
|
|
}
|
|
|
|
|
return sb.NotBetween(tblFieldName, values[0], values[1]), nil
|
|
|
|
|
|
|
|
|
|
// in and not in
|
|
|
|
|
case qbtypes.FilterOperatorIn:
|
|
|
|
|
values, ok := value.([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", qbtypes.ErrInValues
|
|
|
|
|
}
|
2025-05-20 16:54:34 +05:30
|
|
|
// instead of using IN, we use `=` + `OR` to make use of index
|
|
|
|
|
conditions := []string{}
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
conditions = append(conditions, sb.E(tblFieldName, value))
|
|
|
|
|
}
|
|
|
|
|
return sb.Or(conditions...), nil
|
2025-04-11 15:13:04 +05:30
|
|
|
case qbtypes.FilterOperatorNotIn:
|
|
|
|
|
values, ok := value.([]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", qbtypes.ErrInValues
|
|
|
|
|
}
|
2025-05-20 16:54:34 +05:30
|
|
|
// instead of using NOT IN, we use `!=` + `AND` to make use of index
|
|
|
|
|
conditions := []string{}
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
conditions = append(conditions, sb.NE(tblFieldName, value))
|
|
|
|
|
}
|
|
|
|
|
return sb.And(conditions...), nil
|
2025-04-11 15:13:04 +05:30
|
|
|
|
|
|
|
|
// exists and not exists
|
|
|
|
|
// in the query builder, `exists` and `not exists` are used for
|
|
|
|
|
// key membership checks, so depending on the column type, the condition changes
|
|
|
|
|
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
2025-05-27 20:54:48 +05:30
|
|
|
|
2025-04-11 15:13:04 +05:30
|
|
|
var value any
|
|
|
|
|
switch column.Type {
|
2025-09-14 18:18:39 +05:30
|
|
|
case schema.JSONColumnType{}:
|
|
|
|
|
if operator == qbtypes.FilterOperatorExists {
|
2025-10-03 11:12:42 +05:30
|
|
|
return sb.IsNotNull(tblFieldName), nil
|
2025-09-14 18:18:39 +05:30
|
|
|
} else {
|
2025-10-03 11:12:42 +05:30
|
|
|
return sb.IsNull(tblFieldName), nil
|
2025-09-14 18:18:39 +05:30
|
|
|
}
|
2025-04-11 15:13:04 +05:30
|
|
|
case schema.ColumnTypeString,
|
|
|
|
|
schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
|
|
|
|
schema.FixedStringColumnType{Length: 32},
|
|
|
|
|
schema.DateTime64ColumnType{Precision: 9, Timezone: "UTC"}:
|
|
|
|
|
value = ""
|
|
|
|
|
if operator == qbtypes.FilterOperatorExists {
|
|
|
|
|
return sb.NE(tblFieldName, value), nil
|
|
|
|
|
} else {
|
|
|
|
|
return sb.E(tblFieldName, value), nil
|
|
|
|
|
}
|
|
|
|
|
case schema.ColumnTypeUInt64,
|
|
|
|
|
schema.ColumnTypeUInt32,
|
|
|
|
|
schema.ColumnTypeUInt8,
|
|
|
|
|
schema.ColumnTypeInt8,
|
|
|
|
|
schema.ColumnTypeInt16,
|
|
|
|
|
schema.ColumnTypeBool:
|
|
|
|
|
value = 0
|
|
|
|
|
if operator == qbtypes.FilterOperatorExists {
|
|
|
|
|
return sb.NE(tblFieldName, value), nil
|
|
|
|
|
} else {
|
|
|
|
|
return sb.E(tblFieldName, value), nil
|
|
|
|
|
}
|
|
|
|
|
case schema.MapColumnType{
|
|
|
|
|
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
|
|
|
|
ValueType: schema.ColumnTypeString,
|
|
|
|
|
}, schema.MapColumnType{
|
|
|
|
|
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
|
|
|
|
ValueType: schema.ColumnTypeBool,
|
|
|
|
|
}, schema.MapColumnType{
|
|
|
|
|
KeyType: schema.LowCardinalityColumnType{ElementType: schema.ColumnTypeString},
|
|
|
|
|
ValueType: schema.ColumnTypeFloat64,
|
|
|
|
|
}:
|
|
|
|
|
leftOperand := fmt.Sprintf("mapContains(%s, '%s')", column.Name, key.Name)
|
|
|
|
|
if key.Materialized {
|
|
|
|
|
leftOperand = telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
|
|
|
|
|
}
|
|
|
|
|
if operator == qbtypes.FilterOperatorExists {
|
|
|
|
|
return sb.E(leftOperand, true), nil
|
|
|
|
|
} else {
|
|
|
|
|
return sb.NE(leftOperand, true), nil
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
return "", fmt.Errorf("exists operator is not supported for column type %s", column.Type)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
2025-05-20 16:54:34 +05:30
|
|
|
|
|
|
|
|
func (c *conditionBuilder) ConditionFor(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
key *telemetrytypes.TelemetryFieldKey,
|
|
|
|
|
operator qbtypes.FilterOperator,
|
|
|
|
|
value any,
|
|
|
|
|
sb *sqlbuilder.SelectBuilder,
|
|
|
|
|
) (string, error) {
|
2025-06-23 09:39:19 +05:30
|
|
|
if c.isSpanScopeField(key.Name) {
|
|
|
|
|
return c.buildSpanScopeCondition(key, operator, value)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-20 16:54:34 +05:30
|
|
|
condition, err := c.conditionFor(ctx, key, operator, value, sb)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if operator.AddDefaultExistsFilter() {
|
|
|
|
|
// skip adding exists filter for intrinsic fields
|
|
|
|
|
field, _ := c.fm.FieldFor(ctx, key)
|
2025-07-02 10:39:16 +05:30
|
|
|
if slices.Contains(maps.Keys(IntrinsicFields), field) ||
|
|
|
|
|
slices.Contains(maps.Keys(IntrinsicFieldsDeprecated), field) ||
|
|
|
|
|
slices.Contains(maps.Keys(CalculatedFields), field) ||
|
|
|
|
|
slices.Contains(maps.Keys(CalculatedFieldsDeprecated), field) {
|
2025-05-20 16:54:34 +05:30
|
|
|
return condition, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
existsCondition, err := c.conditionFor(ctx, key, qbtypes.FilterOperatorExists, nil, sb)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return sb.And(condition, existsCondition), nil
|
|
|
|
|
}
|
|
|
|
|
return condition, nil
|
|
|
|
|
}
|
2025-06-23 09:39:19 +05:30
|
|
|
|
|
|
|
|
func (c *conditionBuilder) isSpanScopeField(name string) bool {
|
|
|
|
|
keyName := strings.ToLower(name)
|
|
|
|
|
return keyName == SpanSearchScopeRoot || keyName == SpanSearchScopeEntryPoint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *conditionBuilder) buildSpanScopeCondition(key *telemetrytypes.TelemetryFieldKey, operator qbtypes.FilterOperator, value any) (string, error) {
|
|
|
|
|
if operator != qbtypes.FilterOperatorEqual {
|
|
|
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s only supports '=' operator", key.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isTrue := false
|
|
|
|
|
switch v := value.(type) {
|
|
|
|
|
case bool:
|
|
|
|
|
isTrue = v
|
|
|
|
|
case string:
|
|
|
|
|
isTrue = strings.ToLower(v) == "true"
|
|
|
|
|
default:
|
|
|
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s expects boolean value, got %T", key.Name, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !isTrue {
|
|
|
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "span scope field %s can only be filtered with value 'true'", key.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keyName := strings.ToLower(key.Name)
|
|
|
|
|
switch keyName {
|
|
|
|
|
case SpanSearchScopeRoot:
|
|
|
|
|
return "parent_span_id = ''", nil
|
|
|
|
|
case SpanSearchScopeEntryPoint:
|
2025-07-12 16:47:59 +05:30
|
|
|
return fmt.Sprintf("((name, resource_string_service$$$name) GLOBAL IN (SELECT DISTINCT name, serviceName from %s.%s)) AND parent_span_id != ''",
|
2025-06-23 09:39:19 +05:30
|
|
|
DBName, TopLevelOperationsTableName), nil
|
|
|
|
|
default:
|
|
|
|
|
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid span search scope: %s", key.Name)
|
|
|
|
|
}
|
|
|
|
|
}
|