mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-06 22:50:38 +01:00
Compare commits
1 Commits
fix/query-
...
tvats-conv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e08f757c8 |
@@ -21,6 +21,42 @@ func newConditionBuilder(fm qbtypes.FieldMapper) qbtypes.ConditionBuilder {
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for rule state history.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
|
||||
@@ -220,9 +220,7 @@ func (v *exprVisitor) VisitFunctionExpr(fn *chparser.FunctionExpr) error {
|
||||
FieldKeys: v.fieldKeys,
|
||||
FieldMapper: v.fieldMapper,
|
||||
ConditionBuilder: v.conditionBuilder,
|
||||
BodyJSONEnabled: bodyJSONEnabled,
|
||||
FullTextColumn: v.fullTextColumn,
|
||||
JsonKeyToKey: v.jsonKeyToKey,
|
||||
StartNs: v.startNs,
|
||||
EndNs: v.endNs,
|
||||
},
|
||||
|
||||
@@ -47,11 +47,11 @@ func CollisionHandledFinalExpr(
|
||||
|
||||
addCondition := func(key *telemetrytypes.TelemetryFieldKey) error {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
condition, err := cb.ConditionFor(ctx, startNs, endNs, key, qbtypes.FilterOperatorExists, nil, sb)
|
||||
conds, _, err := cb.ConditionFor(ctx, startNs, endNs, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorExists, nil, sb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sb.Where(condition)
|
||||
sb.Where(conds[0])
|
||||
|
||||
expr, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
expr = strings.TrimPrefix(expr, "WHERE ")
|
||||
|
||||
82
pkg/querybuilder/key_resolution.go
Normal file
82
pkg/querybuilder/key_resolution.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package querybuilder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
const (
|
||||
// FieldContextDataTypesDocURL documents how to disambiguate a key that resolves
|
||||
// to multiple field context / data type combinations.
|
||||
FieldContextDataTypesDocURL = "https://signoz.io/docs/userguide/field-context-data-types/"
|
||||
// KeyNotFoundDocURL documents the "key not found" error.
|
||||
KeyNotFoundDocURL = "https://signoz.io/docs/userguide/search-troubleshooting/#key-fieldname-not-found"
|
||||
|
||||
// Doc URLs for the has/hasAny/hasAll and hasToken "unsupported" errors.
|
||||
functionBodyJSONSearchDocURL = "https://signoz.io/docs/userguide/search-troubleshooting/#function-supports-only-body-json-search"
|
||||
hasTokenFunctionDocURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
|
||||
)
|
||||
|
||||
// ResolveKeys picks which matching field keys a filter term builds conditions for.
|
||||
// With 0 or 1 match it returns the input unchanged and no warning. When a name is
|
||||
// ambiguous it returns a warning; a resource+attribute mix defaults to the resource
|
||||
// keys (the common intent), noted in the warning.
|
||||
func ResolveKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeysForName []*telemetrytypes.TelemetryFieldKey) ([]*telemetrytypes.TelemetryFieldKey, string) {
|
||||
if len(fieldKeysForName) <= 1 {
|
||||
return fieldKeysForName, ""
|
||||
}
|
||||
|
||||
warning := fmt.Sprintf(
|
||||
"Key `%s` is ambiguous, found %d different combinations of field context / data type: %v.",
|
||||
field.Name,
|
||||
len(fieldKeysForName),
|
||||
fieldKeysForName,
|
||||
)
|
||||
|
||||
hasResource, hasAttribute := false, false
|
||||
for _, item := range fieldKeysForName {
|
||||
switch item.FieldContext {
|
||||
case telemetrytypes.FieldContextResource:
|
||||
hasResource = true
|
||||
case telemetrytypes.FieldContextAttribute:
|
||||
hasAttribute = true
|
||||
}
|
||||
}
|
||||
|
||||
// when there is both resource and attribute context, default to resource only
|
||||
if hasResource && hasAttribute {
|
||||
filteredKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(fieldKeysForName))
|
||||
for _, item := range fieldKeysForName {
|
||||
if item.FieldContext == telemetrytypes.FieldContextResource {
|
||||
filteredKeys = append(filteredKeys, item)
|
||||
}
|
||||
}
|
||||
fieldKeysForName = filteredKeys
|
||||
warning += " " + "Using `resource` context by default. To query attributes explicitly, " +
|
||||
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", field.Name)
|
||||
}
|
||||
|
||||
return fieldKeysForName, warning
|
||||
}
|
||||
|
||||
// NewKeyNotFoundError builds the error a condition builder returns when a filter term
|
||||
// references a key it has no matching field key for.
|
||||
func NewKeyNotFoundError(name string) error {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "key `%s` not found", name).WithUrl(KeyNotFoundDocURL)
|
||||
}
|
||||
|
||||
// NewFunctionUnsupportedError returns the error for a has/hasAny/hasAll/hasToken operator
|
||||
// on a builder that doesn't support it (logs body only), or nil for other operators.
|
||||
func NewFunctionUnsupportedError(operator qbtypes.FilterOperator) error {
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHasToken:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `hasToken` only supports body field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
case qbtypes.FilterOperatorHas, qbtypes.FilterOperatorHasAny, qbtypes.FilterOperatorHasAll:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `%s` supports only body JSON search", operator.FunctionName()).WithUrl(functionBodyJSONSearchDocURL)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ const stringMatchingOperatorDocURL = "https://signoz.io/docs/userguide/operators
|
||||
// to convert the parsed filter expressions into ClickHouse WHERE clause.
|
||||
type filterExpressionVisitor struct {
|
||||
context context.Context
|
||||
logger *slog.Logger
|
||||
fieldMapper qbtypes.FieldMapper
|
||||
conditionBuilder qbtypes.ConditionBuilder
|
||||
warnings []string
|
||||
@@ -35,12 +34,8 @@ type filterExpressionVisitor struct {
|
||||
mainErrorURL string
|
||||
builder *sqlbuilder.SelectBuilder
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
bodyJSONEnabled bool
|
||||
skipResourceFilter bool
|
||||
skipFullTextFilter bool
|
||||
skipFunctionCalls bool
|
||||
ignoreNotFoundKeys bool
|
||||
variables map[string]qbtypes.VariableItem
|
||||
|
||||
keysWithWarnings map[string]bool
|
||||
@@ -56,12 +51,8 @@ type FilterExprVisitorOpts struct {
|
||||
FieldKeys map[string][]*telemetrytypes.TelemetryFieldKey
|
||||
Builder *sqlbuilder.SelectBuilder
|
||||
FullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
JsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
BodyJSONEnabled bool
|
||||
SkipResourceFilter bool
|
||||
SkipFullTextFilter bool
|
||||
SkipFunctionCalls bool
|
||||
IgnoreNotFoundKeys bool
|
||||
Variables map[string]qbtypes.VariableItem
|
||||
StartNs uint64
|
||||
EndNs uint64
|
||||
@@ -71,18 +62,13 @@ type FilterExprVisitorOpts struct {
|
||||
func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVisitor {
|
||||
return &filterExpressionVisitor{
|
||||
context: opts.Context,
|
||||
logger: opts.Logger,
|
||||
fieldMapper: opts.FieldMapper,
|
||||
conditionBuilder: opts.ConditionBuilder,
|
||||
fieldKeys: opts.FieldKeys,
|
||||
builder: opts.Builder,
|
||||
fullTextColumn: opts.FullTextColumn,
|
||||
jsonKeyToKey: opts.JsonKeyToKey,
|
||||
bodyJSONEnabled: opts.BodyJSONEnabled,
|
||||
skipResourceFilter: opts.SkipResourceFilter,
|
||||
skipFullTextFilter: opts.SkipFullTextFilter,
|
||||
skipFunctionCalls: opts.SkipFunctionCalls,
|
||||
ignoreNotFoundKeys: opts.IgnoreNotFoundKeys,
|
||||
variables: opts.Variables,
|
||||
keysWithWarnings: make(map[string]bool),
|
||||
startNs: opts.StartNs,
|
||||
@@ -360,16 +346,17 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
}
|
||||
cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
||||
conds, ok := v.buildConditions(v.fullTextColumn, []*telemetrytypes.TelemetryFieldKey{v.fullTextColumn}, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText))
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
|
||||
v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
return cond
|
||||
if len(conds) == 1 {
|
||||
return conds[0]
|
||||
}
|
||||
return v.builder.Or(conds...)
|
||||
}
|
||||
|
||||
return ErrorConditionLiteral // Should not happen with valid input
|
||||
@@ -377,26 +364,26 @@ func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any
|
||||
|
||||
// VisitComparison handles all comparison operators.
|
||||
func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
|
||||
keys := v.Visit(ctx.Key()).([]*telemetrytypes.TelemetryFieldKey)
|
||||
key := v.Visit(ctx.Key()).(*telemetrytypes.TelemetryFieldKey)
|
||||
matching := matchingFieldKeys(key, v.fieldKeys)
|
||||
|
||||
// no keys resolved; VisitKey already recorded the error, skip this condition
|
||||
if len(keys) == 0 {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
// this is used to skip the resource filtering on main table if
|
||||
// the query may use the resources table sub-query filter
|
||||
if v.skipResourceFilter {
|
||||
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
|
||||
for _, key := range keys {
|
||||
if key.FieldContext != telemetrytypes.FieldContextResource {
|
||||
filteredKeys = append(filteredKeys, key)
|
||||
// Skip resource filtering on the main table when a sub-query covers it. Resolve
|
||||
// ambiguity first (resource+attribute defaults to resource), then drop resource
|
||||
// matches; skip the term if nothing remains. Empty matches flow to the builder.
|
||||
if v.skipResourceFilter && len(matching) > 0 {
|
||||
resolved, warning := ResolveKeys(key, matching)
|
||||
// emit the ambiguity warning even when the term is skipped below
|
||||
v.addWarnings([]string{warning}, len(matching) > 1)
|
||||
filtered := []*telemetrytypes.TelemetryFieldKey{}
|
||||
for _, k := range resolved {
|
||||
if k.FieldContext != telemetrytypes.FieldContextResource {
|
||||
filtered = append(filtered, k)
|
||||
}
|
||||
}
|
||||
keys = filteredKeys
|
||||
if len(keys) == 0 {
|
||||
if len(filtered) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
matching = filtered
|
||||
}
|
||||
|
||||
// Handle EXISTS specially
|
||||
@@ -405,18 +392,12 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
if ctx.NOT() != nil {
|
||||
op = qbtypes.FilterOperatorNotExists
|
||||
}
|
||||
var conds []string
|
||||
for _, key := range keys {
|
||||
condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, nil, v.builder)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if slices.Contains(SkippableConditionLiterals, condition) {
|
||||
continue
|
||||
}
|
||||
conds = append(conds, condition)
|
||||
|
||||
conds, ok := v.buildConditions(key, matching, op, nil)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
@@ -484,17 +465,12 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
if ctx.NotInClause() != nil {
|
||||
op = qbtypes.FilterOperatorNotIn
|
||||
}
|
||||
var conds []string
|
||||
for _, key := range keys {
|
||||
condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, values, v.builder)
|
||||
if err != nil {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if slices.Contains(SkippableConditionLiterals, condition) {
|
||||
continue
|
||||
}
|
||||
conds = append(conds, condition)
|
||||
|
||||
conds, ok := v.buildConditions(key, matching, op, values)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
@@ -525,29 +501,22 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
switch value1.(type) {
|
||||
case float64:
|
||||
if _, ok := value2.(float64); !ok {
|
||||
v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected number for both operands", keys[0].Name))
|
||||
v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected number for both operands", key.Name))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
case string:
|
||||
if _, ok := value2.(string); !ok {
|
||||
v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected string for both operands", keys[0].Name))
|
||||
v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected string for both operands", key.Name))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
default:
|
||||
v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: operands must be number or string", keys[0].Name))
|
||||
v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: operands must be number or string", key.Name))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
var conds []string
|
||||
for _, key := range keys {
|
||||
condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, []any{value1, value2}, v.builder)
|
||||
if err != nil {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if slices.Contains(SkippableConditionLiterals, condition) {
|
||||
continue
|
||||
}
|
||||
conds = append(conds, condition)
|
||||
conds, ok := v.buildConditions(key, matching, op, []any{value1, value2})
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
@@ -629,18 +598,11 @@ func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext
|
||||
}
|
||||
}
|
||||
|
||||
var conds []string
|
||||
for _, key := range keys {
|
||||
condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, value, v.builder)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
if slices.Contains(SkippableConditionLiterals, condition) {
|
||||
continue
|
||||
}
|
||||
conds = append(conds, condition)
|
||||
conds, ok := v.buildConditions(key, matching, op, value)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
@@ -718,35 +680,37 @@ func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) an
|
||||
v.errors = append(v.errors, "full text search is not supported")
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
|
||||
conds, ok := v.buildConditions(v.fullTextColumn, []*telemetrytypes.TelemetryFieldKey{v.fullTextColumn}, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text))
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
|
||||
v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
return cond
|
||||
if len(conds) == 1 {
|
||||
return conds[0]
|
||||
}
|
||||
return v.builder.Or(conds...)
|
||||
}
|
||||
|
||||
// VisitFunctionCall handles function calls like has(), hasAny(), etc.
|
||||
func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any {
|
||||
if v.skipFunctionCalls {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
|
||||
// Get function name based on which token is present
|
||||
var functionName string
|
||||
var operator qbtypes.FilterOperator
|
||||
if ctx.HAS() != nil {
|
||||
functionName = "has"
|
||||
operator = qbtypes.FilterOperatorHas
|
||||
} else if ctx.HASANY() != nil {
|
||||
functionName = "hasAny"
|
||||
operator = qbtypes.FilterOperatorHasAny
|
||||
} else if ctx.HASALL() != nil {
|
||||
functionName = "hasAll"
|
||||
operator = qbtypes.FilterOperatorHasAll
|
||||
} else if ctx.HASTOKEN() != nil {
|
||||
functionName = "hasToken"
|
||||
operator = qbtypes.FilterOperatorHasToken
|
||||
} else {
|
||||
// Default fallback
|
||||
v.errors = append(v.errors, fmt.Sprintf("unknown function `%s`", ctx.GetText()))
|
||||
@@ -759,86 +723,22 @@ func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallCon
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
keys, ok := params[0].([]*telemetrytypes.TelemetryFieldKey)
|
||||
key, ok := params[0].(*telemetrytypes.TelemetryFieldKey)
|
||||
if !ok {
|
||||
v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key parameter to be a field key", functionName))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
if v.bodyJSONEnabled && functionName != "hasToken" {
|
||||
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
|
||||
for _, key := range keys {
|
||||
if key.FieldDataType.IsArray() {
|
||||
filteredKeys = append(filteredKeys, key)
|
||||
}
|
||||
}
|
||||
if len(filteredKeys) == 0 {
|
||||
v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key parameter to be an array field; no array fields found", functionName))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
keys = filteredKeys
|
||||
}
|
||||
|
||||
value := params[1:]
|
||||
var conds []string
|
||||
for _, key := range keys {
|
||||
var fieldName string
|
||||
|
||||
if functionName == "hasToken" {
|
||||
|
||||
if key.Name != "body" {
|
||||
if v.mainErrorURL == "" {
|
||||
v.mainErrorURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
|
||||
}
|
||||
v.errors = append(v.errors, fmt.Sprintf("function `%s` only supports body field as first parameter", functionName))
|
||||
}
|
||||
|
||||
// this will only work with string.
|
||||
if _, ok := value[0].(string); !ok {
|
||||
if v.mainErrorURL == "" {
|
||||
v.mainErrorURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
|
||||
}
|
||||
v.errors = append(v.errors, fmt.Sprintf("function `%s` expects value parameter to be a string", functionName))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
conds = append(conds, fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", key.Name, v.builder.Var(value[0])))
|
||||
} else {
|
||||
// this is that all other functions only support array fields
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody {
|
||||
var err error
|
||||
if v.bodyJSONEnabled {
|
||||
fieldName, err = v.fieldMapper.FieldFor(v.context, v.startNs, v.endNs, key)
|
||||
if err != nil {
|
||||
v.errors = append(v.errors, fmt.Sprintf("failed to get field name for key %s: %s", key.Name, err.Error()))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
} else {
|
||||
fieldName, _ = v.jsonKeyToKey(v.context, key, qbtypes.FilterOperatorUnknown, value)
|
||||
}
|
||||
} else {
|
||||
// TODO(add docs for json body search)
|
||||
if v.mainErrorURL == "" {
|
||||
v.mainErrorURL = "https://signoz.io/docs/userguide/search-troubleshooting/#function-supports-only-body-json-search"
|
||||
}
|
||||
v.errors = append(v.errors, fmt.Sprintf("function `%s` supports only body JSON search", functionName))
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
var cond string
|
||||
// Map our functions to ClickHouse equivalents
|
||||
switch functionName {
|
||||
case "has":
|
||||
cond = fmt.Sprintf("has(%s, %s)", fieldName, v.builder.Var(value[0]))
|
||||
case "hasAny":
|
||||
cond = fmt.Sprintf("hasAny(%s, %s)", fieldName, v.builder.Var(value[0]))
|
||||
case "hasAll":
|
||||
cond = fmt.Sprintf("hasAll(%s, %s)", fieldName, v.builder.Var(value[0]))
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
conds, ok := v.buildConditions(key, matchingFieldKeys(key, v.fieldKeys), operator, value)
|
||||
if !ok {
|
||||
return ErrorConditionLiteral
|
||||
}
|
||||
|
||||
if len(conds) == 0 {
|
||||
return SkipConditionLiteral
|
||||
}
|
||||
if len(conds) == 1 {
|
||||
return conds[0]
|
||||
}
|
||||
@@ -903,106 +803,41 @@ func (v *filterExpressionVisitor) VisitValue(ctx *grammar.ValueContext) any {
|
||||
return ErrorConditionLiteral // Should not happen with valid input
|
||||
}
|
||||
|
||||
// VisitKey handles field/column references.
|
||||
// VisitKey resolves a field/column reference to its parsed key. It makes no
|
||||
// decisions — the condition builder owns those.
|
||||
func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
|
||||
fieldKey := telemetrytypes.GetFieldKeyFromKeyText(ctx.GetText())
|
||||
keyName := fieldKey.Name
|
||||
return &fieldKey
|
||||
}
|
||||
|
||||
// GetFieldKeyFromKeyText function extracts the context prefix (attribute., resource., body.) if present
|
||||
// extracts data type if present (e.g., :string, :int)
|
||||
// so we need to filter the available field keys based on the provided context and data type
|
||||
fieldKeysForName := []*telemetrytypes.TelemetryFieldKey{}
|
||||
// buildConditions invokes the condition builder for a filter term, folding its
|
||||
// warnings/errors into visitor state. It returns the conditions and false if an error
|
||||
// was recorded.
|
||||
func (v *filterExpressionVisitor) buildConditions(key *telemetrytypes.TelemetryFieldKey, matching []*telemetrytypes.TelemetryFieldKey, op qbtypes.FilterOperator, value any) ([]string, bool) {
|
||||
conds, warns, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, matching, op, value, v.builder)
|
||||
if err != nil {
|
||||
_, _, _, _, errURL, _ := errors.Unwrapb(err)
|
||||
assignIfEmpty(&v.mainErrorURL, errURL)
|
||||
v.errors = append(v.errors, err.Error())
|
||||
return nil, false
|
||||
}
|
||||
v.addWarnings(warns, len(matching) > 1)
|
||||
return conds, true
|
||||
}
|
||||
|
||||
// If user said key = 'xyz', we look up in the map for all possible field keys with name 'xyz'
|
||||
for _, item := range v.fieldKeys[keyName] {
|
||||
// Match items where:
|
||||
// 1) user didn't specify context OR context matches, AND
|
||||
// 2) user didn't specify data type OR data type matches.
|
||||
if (fieldKey.FieldContext == telemetrytypes.FieldContextUnspecified || fieldKey.FieldContext == item.FieldContext) &&
|
||||
(fieldKey.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || fieldKey.FieldDataType == item.FieldDataType) {
|
||||
fieldKeysForName = append(fieldKeysForName, item)
|
||||
// addWarnings appends de-duplicated warnings to the visitor. ambiguous marks warnings
|
||||
// from a multi-match key so the field-context doc URL is attached.
|
||||
func (v *filterExpressionVisitor) addWarnings(warns []string, ambiguous bool) {
|
||||
for _, w := range warns {
|
||||
if w == "" || v.keysWithWarnings[w] {
|
||||
continue
|
||||
}
|
||||
v.keysWithWarnings[w] = true
|
||||
v.warnings = append(v.warnings, w)
|
||||
if ambiguous {
|
||||
assignIfEmpty(&v.mainWarnURL, FieldContextDataTypesDocURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Now consider that GetFieldKeyFromKeyText may have extracted the context which was actually part of the name
|
||||
// If user said attribute.key = 'xyz',
|
||||
// 1. either user meant key ( this is already handled above in fieldKeysForName )
|
||||
// 2. or user meant `attribute.key` we look up in the map for all possible field keys with name 'attribute.key'
|
||||
|
||||
// Note:
|
||||
// If user only wants to search `attribute.key`, then they have to use `attribute.attribute.key`
|
||||
// If user only wants to search `key`, then they have to use `key`
|
||||
// If user wants to search both, they can use `attribute.key` and we will resolve the ambiguity
|
||||
if fieldKey.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
contextPrefixedFieldName := fmt.Sprintf("%s.%s", fieldKey.FieldContext.StringValue(), fieldKey.Name)
|
||||
for _, item := range v.fieldKeys[contextPrefixedFieldName] {
|
||||
// Match items where: user didn't specify data type OR data type matches. Context filtering not needed here since context was used to construct the lookup key.
|
||||
if fieldKey.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || item.FieldDataType == fieldKey.FieldDataType {
|
||||
fieldKeysForName = append(fieldKeysForName, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for the body json search, we need to add search on the body field even
|
||||
// if there is a field with the same name as attribute/resource attribute
|
||||
// Since it will ORed with the fieldKeysForName, it will not result empty
|
||||
// when either of them have values
|
||||
// Note: Skip this logic if body json query is enabled so we can look up the key inside fields
|
||||
//
|
||||
// TODO(Piyush): After entire migration this is supposed to be removed.
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
if fieldKey.FieldContext == telemetrytypes.FieldContextBody && !v.bodyJSONEnabled {
|
||||
fieldKeysForName = append(fieldKeysForName, &fieldKey)
|
||||
}
|
||||
|
||||
if len(fieldKeysForName) == 0 {
|
||||
if fieldKey.FieldContext == telemetrytypes.FieldContextBody && keyName == "" {
|
||||
v.errors = append(v.errors, "missing key for body json search - expected key of the form `body.key` (ex: `body.status`)")
|
||||
} else if !v.ignoreNotFoundKeys {
|
||||
// TODO(srikanthccv): do we want to return an error here?
|
||||
// should we infer the type and auto-magically build a key for expression?
|
||||
v.errors = append(v.errors, fmt.Sprintf("key `%s` not found", fieldKey.Name))
|
||||
v.mainErrorURL = "https://signoz.io/docs/userguide/search-troubleshooting/#key-fieldname-not-found"
|
||||
}
|
||||
}
|
||||
|
||||
if len(fieldKeysForName) > 1 {
|
||||
warnMsg := fmt.Sprintf(
|
||||
"Key `%s` is ambiguous, found %d different combinations of field context / data type: %v.",
|
||||
fieldKey.Name,
|
||||
len(fieldKeysForName),
|
||||
fieldKeysForName,
|
||||
)
|
||||
mixedFieldContext := map[string]bool{}
|
||||
for _, item := range fieldKeysForName {
|
||||
mixedFieldContext[item.FieldContext.StringValue()] = true
|
||||
}
|
||||
|
||||
// when there is both resource and attribute context, default to resource only
|
||||
if mixedFieldContext[telemetrytypes.FieldContextResource.StringValue()] &&
|
||||
mixedFieldContext[telemetrytypes.FieldContextAttribute.StringValue()] {
|
||||
filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
|
||||
for _, item := range fieldKeysForName {
|
||||
if item.FieldContext != telemetrytypes.FieldContextResource {
|
||||
continue
|
||||
}
|
||||
filteredKeys = append(filteredKeys, item)
|
||||
}
|
||||
fieldKeysForName = filteredKeys
|
||||
warnMsg += " " + "Using `resource` context by default. To query attributes explicitly, " +
|
||||
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", fieldKey.Name)
|
||||
}
|
||||
|
||||
if !v.keysWithWarnings[keyName] {
|
||||
v.mainWarnURL = "https://signoz.io/docs/userguide/field-context-data-types/"
|
||||
// this is warning state, we must have a unambiguous key
|
||||
v.warnings = append(v.warnings, warnMsg)
|
||||
}
|
||||
v.keysWithWarnings[keyName] = true
|
||||
v.logger.Warn("ambiguous key", slog.String("field_key_name", fieldKey.Name)) //nolint:sloglint
|
||||
}
|
||||
|
||||
return fieldKeysForName
|
||||
}
|
||||
|
||||
// hasLikeWildcards checks if a value contains LIKE wildcards (% or _).
|
||||
@@ -1028,3 +863,38 @@ func trimQuotes(txt string) string {
|
||||
txt = strings.ReplaceAll(txt, `\'`, `'`)
|
||||
return txt
|
||||
}
|
||||
|
||||
func assignIfEmpty(s *string, value string) {
|
||||
if *s == "" {
|
||||
*s = value
|
||||
}
|
||||
}
|
||||
|
||||
// matchingFieldKeys returns the field keys from the provided map that match the given
|
||||
// key, honoring any context/data type the user specified. It also resolves the case
|
||||
// where GetFieldKeyFromKeyText split a context off a name that legitimately contained it.
|
||||
func matchingFieldKeys(field *telemetrytypes.TelemetryFieldKey, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.TelemetryFieldKey {
|
||||
fieldKeysForName := []*telemetrytypes.TelemetryFieldKey{}
|
||||
|
||||
// match by name; keep items whose context and data type match (unspecified matches any)
|
||||
for _, item := range fieldKeys[field.Name] {
|
||||
if (field.FieldContext == telemetrytypes.FieldContextUnspecified || field.FieldContext == item.FieldContext) &&
|
||||
(field.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || field.FieldDataType == item.FieldDataType) {
|
||||
fieldKeysForName = append(fieldKeysForName, item)
|
||||
}
|
||||
}
|
||||
|
||||
// A context may have been split off a name that legitimately contained it (e.g.
|
||||
// `attribute.key`); also look up the context-prefixed name so both readings resolve.
|
||||
if field.FieldContext != telemetrytypes.FieldContextUnspecified {
|
||||
contextPrefixedFieldName := fmt.Sprintf("%s.%s", field.FieldContext.StringValue(), field.Name)
|
||||
for _, item := range fieldKeys[contextPrefixedFieldName] {
|
||||
// Context already matched via the lookup key; only data type needs checking.
|
||||
if field.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || item.FieldDataType == field.FieldDataType {
|
||||
fieldKeysForName = append(fieldKeysForName, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fieldKeysForName
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package querybuilder
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
@@ -79,15 +79,13 @@ func TestPrepareWhereClause_EmptyVariableList(t *testing.T) {
|
||||
}
|
||||
|
||||
// createTestVisitor creates a filterExpressionVisitor for testing VisitKey.
|
||||
func createTestVisitor(t *testing.T, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey, ignoreNotFoundKeys bool) *filterExpressionVisitor {
|
||||
func createTestVisitor(t *testing.T, fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey) *filterExpressionVisitor {
|
||||
t.Helper()
|
||||
return &filterExpressionVisitor{
|
||||
context: t.Context(),
|
||||
logger: slog.Default(),
|
||||
fieldKeys: fieldKeys,
|
||||
ignoreNotFoundKeys: ignoreNotFoundKeys,
|
||||
keysWithWarnings: make(map[string]bool),
|
||||
builder: sqlbuilder.NewSelectBuilder(),
|
||||
context: t.Context(),
|
||||
fieldKeys: fieldKeys,
|
||||
keysWithWarnings: make(map[string]bool),
|
||||
builder: sqlbuilder.NewSelectBuilder(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +158,7 @@ func TestVisitKey(t *testing.T) {
|
||||
name: "Key not found",
|
||||
keyText: "unknown_key",
|
||||
fieldKeys: map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"service": []*telemetrytypes.TelemetryFieldKey{
|
||||
"service": {
|
||||
{
|
||||
Name: "service",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
@@ -361,7 +359,7 @@ func TestVisitKey(t *testing.T) {
|
||||
name: "Unknown key with ignoreNotFoundKeys=true",
|
||||
keyText: "unknown_key",
|
||||
fieldKeys: map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"service": []*telemetrytypes.TelemetryFieldKey{
|
||||
"service": {
|
||||
{
|
||||
Name: "service",
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
@@ -574,7 +572,7 @@ func TestVisitKey(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
visitor := createTestVisitor(t, tt.fieldKeys, tt.ignoreNotFoundKeys)
|
||||
visitor := createTestVisitor(t, tt.fieldKeys)
|
||||
keyCtx := parseKeyContext(tt.keyText)
|
||||
|
||||
if keyCtx == nil {
|
||||
@@ -582,10 +580,30 @@ func TestVisitKey(t *testing.T) {
|
||||
}
|
||||
|
||||
result := visitor.VisitKey(keyCtx)
|
||||
keys, ok := result.([]*telemetrytypes.TelemetryFieldKey)
|
||||
|
||||
key, ok := result.(*telemetrytypes.TelemetryFieldKey)
|
||||
if !ok {
|
||||
t.Fatalf("expected []*TelemetryFieldKey, got %T", result)
|
||||
t.Fatalf("expected *TelemetryFieldKey, got %T", result)
|
||||
}
|
||||
|
||||
// VisitKey only parses; the condition builder matches, resolves ambiguity
|
||||
// and decides not-found handling. Replay that here against the generic
|
||||
// builder behavior (error unless the key is ignored).
|
||||
matching := matchingFieldKeys(key, tt.fieldKeys)
|
||||
keys, warning := ResolveKeys(key, matching)
|
||||
|
||||
var gotErrors []string
|
||||
var gotMainErrURL, gotMainWrnURL string
|
||||
var gotWarnings []string
|
||||
if len(keys) == 0 && !tt.ignoreNotFoundKeys {
|
||||
err := NewKeyNotFoundError(key.Name)
|
||||
gotErrors = append(gotErrors, err.Error())
|
||||
_, _, _, _, gotMainErrURL, _ = errors.Unwrapb(err)
|
||||
}
|
||||
if warning != "" {
|
||||
gotWarnings = append(gotWarnings, warning)
|
||||
if len(matching) > 1 {
|
||||
gotMainWrnURL = FieldContextDataTypesDocURL
|
||||
}
|
||||
}
|
||||
|
||||
// Check expected keys count
|
||||
@@ -612,55 +630,55 @@ func TestVisitKey(t *testing.T) {
|
||||
|
||||
// Check errors
|
||||
if tt.expectedErrors != nil {
|
||||
if len(visitor.errors) != len(tt.expectedErrors) {
|
||||
t.Errorf("expected %d errors, got %d: %v", len(tt.expectedErrors), len(visitor.errors), visitor.errors)
|
||||
if len(gotErrors) != len(tt.expectedErrors) {
|
||||
t.Errorf("expected %d errors, got %d: %v", len(tt.expectedErrors), len(gotErrors), gotErrors)
|
||||
}
|
||||
for _, expectedError := range tt.expectedErrors {
|
||||
found := false
|
||||
for _, err := range visitor.errors {
|
||||
for _, err := range gotErrors {
|
||||
if strings.Contains(err, expectedError) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected error containing %q, got errors: %v", expectedError, visitor.errors)
|
||||
t.Errorf("expected error containing %q, got errors: %v", expectedError, gotErrors)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(visitor.errors) != 0 {
|
||||
t.Errorf("expected no errors, got %d: %v", len(visitor.errors), visitor.errors)
|
||||
if len(gotErrors) != 0 {
|
||||
t.Errorf("expected no errors, got %d: %v", len(gotErrors), gotErrors)
|
||||
}
|
||||
}
|
||||
|
||||
// Check mainErrorURL
|
||||
if visitor.mainErrorURL != tt.expectedMainErrURL {
|
||||
t.Errorf("expected mainErrorURL %q, got %q", tt.expectedMainErrURL, visitor.mainErrorURL)
|
||||
if gotMainErrURL != tt.expectedMainErrURL {
|
||||
t.Errorf("expected mainErrorURL %q, got %q", tt.expectedMainErrURL, gotMainErrURL)
|
||||
}
|
||||
|
||||
// Check warnings
|
||||
if tt.expectedWarnings != nil {
|
||||
for _, expectedWarn := range tt.expectedWarnings {
|
||||
found := false
|
||||
for _, warn := range visitor.warnings {
|
||||
for _, warn := range gotWarnings {
|
||||
if strings.Contains(strings.ToLower(warn), strings.ToLower(expectedWarn)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected warning containing %q, got warnings: %v", expectedWarn, visitor.warnings)
|
||||
t.Errorf("expected warning containing %q, got warnings: %v", expectedWarn, gotWarnings)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(visitor.warnings) != 0 {
|
||||
t.Errorf("expected no warnings, got %d: %v", len(visitor.warnings), visitor.warnings)
|
||||
if len(gotWarnings) != 0 {
|
||||
t.Errorf("expected no warnings, got %d: %v", len(gotWarnings), gotWarnings)
|
||||
}
|
||||
}
|
||||
|
||||
// Check mainWarnURL
|
||||
if visitor.mainWarnURL != tt.expectedMainWrnURL {
|
||||
t.Errorf("expected mainWarnURL %q, got %q", tt.expectedMainWrnURL, visitor.mainWarnURL)
|
||||
if gotMainWrnURL != tt.expectedMainWrnURL {
|
||||
t.Errorf("expected mainWarnURL %q, got %q", tt.expectedMainWrnURL, gotMainWrnURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -673,13 +691,12 @@ func TestVisitKey(t *testing.T) {
|
||||
// This suite exercises the visitor with two different configurations
|
||||
// side-by-side for each expression, asserting both expected outputs:
|
||||
//
|
||||
// resourceConditionBuilder (wantRSB) — returns TrueConditionLiteral for
|
||||
// non-resource keys and "{name}_cond" for resource keys (x, y, z).
|
||||
// Opts: SkipFullTextFilter:true, SkipFunctionCalls:true, IgnoreNotFoundKeys:true.
|
||||
// resourceConditionBuilder (wantRSB) — produces "{name}_cond" only for resource
|
||||
// keys (x, y, z); non-resource keys, unknown keys, and function calls yield no
|
||||
// condition. Opts: SkipFullTextFilter:true.
|
||||
//
|
||||
// conditionBuilder (wantSB) — returns "{name}_cond" for every key regardless
|
||||
// of FieldContext. Opts: SkipFullTextFilter:false, SkipFunctionCalls:false,
|
||||
// IgnoreNotFoundKeys:false, FullTextColumn:bodyCol.
|
||||
// of FieldContext. Opts: SkipFullTextFilter:false, FullTextColumn:bodyCol.
|
||||
//
|
||||
// Key behavioral rules:
|
||||
//
|
||||
@@ -732,16 +749,32 @@ func (b *resourceConditionBuilder) ConditionFor(
|
||||
_ uint64,
|
||||
_ uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.FilterOperator,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
_ any,
|
||||
_ *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
) ([]string, []string, error) {
|
||||
|
||||
if key.FieldContext != telemetrytypes.FieldContextResource {
|
||||
return SkipConditionLiteral, nil
|
||||
// mirror the real resource builder: function operators never apply to resources
|
||||
if operator.IsFunctionOperator() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_cond", key.Name), nil
|
||||
keys, warning := ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
var conds []string
|
||||
for _, k := range keys {
|
||||
// only resource keys contribute; others (and unknown keys) are ignored
|
||||
if k.FieldContext != telemetrytypes.FieldContextResource {
|
||||
continue
|
||||
}
|
||||
conds = append(conds, fmt.Sprintf("%s_cond", k.Name))
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
type conditionBuilder struct{}
|
||||
@@ -751,19 +784,44 @@ func (b *conditionBuilder) ConditionFor(
|
||||
_ uint64,
|
||||
_ uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
_ qbtypes.FilterOperator,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
_ any,
|
||||
_ *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
) ([]string, []string, error) {
|
||||
|
||||
return fmt.Sprintf("%s_cond", key.Name), nil
|
||||
// has/hasAny/hasAll/hasToken only support body fields; mirror the real
|
||||
// condition builder which now owns this validation and errors for non-body keys.
|
||||
switch operator {
|
||||
case qbtypes.FilterOperatorHas, qbtypes.FilterOperatorHasAny, qbtypes.FilterOperatorHasAll, qbtypes.FilterOperatorHasToken:
|
||||
if key.FieldContext != telemetrytypes.FieldContextBody {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "function supports only body JSON search")
|
||||
}
|
||||
return []string{fmt.Sprintf("%s_cond", key.Name)}, nil, nil
|
||||
}
|
||||
|
||||
keys, warning := ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
// errors on unknown keys (no IgnoreNotFoundKeys equivalent for this builder)
|
||||
return nil, warnings, NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
conds = append(conds, fmt.Sprintf("%s_cond", k.Name))
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
// visitComparisonCase is a single test case for the TestVisitComparison_* family.
|
||||
// Each case is run under two independent configurations:
|
||||
//
|
||||
// - rsbOpts (resourceConditionBuilder): skips full-text and function calls,
|
||||
// ignores unknown keys, produces conditions only for resource-context keys.
|
||||
// - rsbOpts (resourceConditionBuilder): skips full-text; its builder skips function
|
||||
// calls and unknown keys, producing conditions only for resource-context keys.
|
||||
//
|
||||
// - sbOpts (conditionBuilder): skips resource-context keys (unless OR is present),
|
||||
// evaluates full-text, errors on unknown keys.
|
||||
@@ -799,8 +857,6 @@ func visitComparisonOpts(t *testing.T) (rsbOpts, sbOpts FilterExprVisitorOpts) {
|
||||
Variables: allVariable,
|
||||
SkipResourceFilter: false,
|
||||
SkipFullTextFilter: true,
|
||||
SkipFunctionCalls: true,
|
||||
IgnoreNotFoundKeys: true,
|
||||
}
|
||||
sbOpts = FilterExprVisitorOpts{
|
||||
Context: t.Context(),
|
||||
@@ -809,8 +865,6 @@ func visitComparisonOpts(t *testing.T) (rsbOpts, sbOpts FilterExprVisitorOpts) {
|
||||
Variables: allVariable,
|
||||
SkipResourceFilter: true,
|
||||
SkipFullTextFilter: false,
|
||||
SkipFunctionCalls: false,
|
||||
IgnoreNotFoundKeys: false,
|
||||
FullTextColumn: bodyCol,
|
||||
}
|
||||
return
|
||||
@@ -1537,9 +1591,9 @@ func TestVisitComparison_AllVariable(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestVisitComparison_FunctionCalls covers function call expressions (has, hasAny, hasAll).
|
||||
// rsbOpts has SkipFunctionCalls=true → TrueConditionLiteral (function never evaluated).
|
||||
// sbOpts has SkipFunctionCalls=false; has/hasAny/hasAll only support FieldContextBody,
|
||||
// so calls on attribute/resource keys return an error.
|
||||
// The resource builder skips function operators, so they yield no condition in RSB.
|
||||
// In SB, has/hasAny/hasAll only support FieldContextBody, so calls on attribute/resource
|
||||
// keys return an error.
|
||||
func TestVisitComparison_FunctionCalls(t *testing.T) {
|
||||
rsbOpts, sbOpts := visitComparisonOpts(t)
|
||||
tests := []visitComparisonCase{
|
||||
|
||||
@@ -171,6 +171,42 @@ func (c *conditionBuilder) conditionFor(
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for audit.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
|
||||
@@ -49,7 +49,6 @@ func NewAuditQueryStatementBuilder(
|
||||
telemetrytypes.SourceAudit,
|
||||
metadataStore,
|
||||
fullTextColumn,
|
||||
jsonKeyToKey,
|
||||
flagger,
|
||||
)
|
||||
|
||||
@@ -551,7 +550,6 @@ func (b *auditQueryStatementBuilder) addFilterCondition(
|
||||
FieldKeys: keys,
|
||||
SkipResourceFilter: true,
|
||||
FullTextColumn: b.fullTextColumn,
|
||||
JsonKeyToKey: b.jsonKeyToKey,
|
||||
Variables: variables,
|
||||
StartNs: start,
|
||||
EndNs: end,
|
||||
|
||||
@@ -25,6 +25,97 @@ func NewConditionBuilder(fm qbtypes.FieldMapper, fl flagger.Flagger) *conditionB
|
||||
return &conditionBuilder{fm: fm, fl: fl}
|
||||
}
|
||||
|
||||
// isBodyJSONSearch reports whether a key addresses a path within the body JSON. Only
|
||||
// an explicit Body context qualifies; a bare, context-less `body` (e.g. full-text
|
||||
// `count_distinct(body)` or `body EXISTS`) is a full-text match, not a `$.body` path.
|
||||
func isBodyJSONSearch(key *telemetrytypes.TelemetryFieldKey, columns []*schema.Column) bool {
|
||||
if key.FieldContext != telemetrytypes.FieldContextBody {
|
||||
return false
|
||||
}
|
||||
for _, column := range columns {
|
||||
if column.Name == LogsV2BodyColumn || column.Name == LogsV2BodyV2Column {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// conditionForArrayFunction builds `has/hasAny/hasAll(<arrayFieldExpr>, value)` over a
|
||||
// body JSON array field. The field expression uses the JSON accessor (flag on) or
|
||||
// legacy string extraction (flag off); value[0] is the needle.
|
||||
func (c *conditionBuilder) conditionForArrayFunction(
|
||||
ctx context.Context,
|
||||
startNs, endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
columns []*schema.Column,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
if !isBodyJSONSearch(key, columns) {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `%s` supports only body JSON search", operator.FunctionName()).WithUrl(functionBodyJSONSearchDocURL)
|
||||
}
|
||||
|
||||
needle := value
|
||||
if args, ok := value.([]any); ok && len(args) > 0 {
|
||||
needle = args[0]
|
||||
}
|
||||
|
||||
var fieldExpr string
|
||||
if c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
fe, err := c.fm.FieldFor(ctx, startNs, endNs, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fieldExpr = fe
|
||||
} else {
|
||||
// legacy string-body path; value drives array-type inference (e.g. `[*]` paths)
|
||||
fieldExpr, _ = GetBodyJSONKey(ctx, key, qbtypes.FilterOperatorUnknown, value)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s(%s, %s)", operator.FunctionName(), fieldExpr, sb.Var(needle)), nil
|
||||
}
|
||||
|
||||
// conditionForHasToken builds `hasToken(LOWER(<bodyColumn>), LOWER(<needle>))`, a
|
||||
// full-text token search over the body column. It resolves the column from the key
|
||||
// name + use_json_body flag, validates the field/value, and tags errors with the doc URL.
|
||||
func (c *conditionBuilder) conditionForHasToken(
|
||||
ctx context.Context,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
// hasToken takes a single needle; unwrap it from the function-argument slice.
|
||||
needle := value
|
||||
if args, ok := value.([]any); ok && len(args) > 0 {
|
||||
needle = args[0]
|
||||
}
|
||||
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
columnName := LogsV2BodyColumn
|
||||
if bodyJSONEnabled {
|
||||
if key.Name != LogsV2BodyColumn && key.Name != bodyMessageField {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` only supports body/body.message field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
columnName = bodyMessageField
|
||||
} else if key.Name != LogsV2BodyColumn {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` only supports body field as first parameter").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
|
||||
// hasToken matches string tokens only.
|
||||
if _, ok := needle.(string); !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `hasToken` expects value parameter to be a string").WithUrl(hasTokenFunctionDocURL)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", columnName, sb.Var(needle)), nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionFor(
|
||||
ctx context.Context,
|
||||
startNs, endNs uint64,
|
||||
@@ -33,15 +124,27 @@ func (c *conditionBuilder) conditionFor(
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
// hasToken is a token search over the body column resolved purely from the key
|
||||
// name + flag, independent of column resolution, so handle it before anything else.
|
||||
if operator == qbtypes.FilterOperatorHasToken {
|
||||
return c.conditionForHasToken(ctx, key, value, sb)
|
||||
}
|
||||
|
||||
columns, err := c.fm.ColumnFor(ctx, startNs, endNs, key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// has/hasAny/hasAll build `has(<arrayFieldExpr>, value)` over body JSON arrays
|
||||
// rather than going through the normal operator paths, so handle them up front.
|
||||
if operator.IsArrayFunctionOperator() {
|
||||
return c.conditionForArrayFunction(ctx, startNs, endNs, key, operator, value, columns, sb)
|
||||
}
|
||||
|
||||
// TODO(Piyush): Update this to support multiple JSON columns based on evolutions
|
||||
for _, column := range columns {
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
if column.Type.GetType() == schema.ColumnTypeEnumJSON && key.FieldContext == telemetrytypes.FieldContextBody && c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) && key.Name != messageSubField {
|
||||
if column.Type.GetType() == schema.ColumnTypeEnumJSON && isBodyJSONSearch(key, columns) && c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) && key.Name != messageSubField {
|
||||
valueType, value := InferDataType(value, operator, key)
|
||||
cond, err := NewJSONConditionBuilder(key, valueType).buildJSONCondition(operator, value, sb)
|
||||
if err != nil {
|
||||
@@ -60,9 +163,9 @@ func (c *conditionBuilder) conditionFor(
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if this is a body JSON search - either by FieldContext
|
||||
// Check if this is a body JSON search (legacy string-body path, JSON flag off).
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody && !c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
if isBodyJSONSearch(key, columns) && !c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
fieldExpression, value = GetBodyJSONKey(ctx, key, operator, value)
|
||||
}
|
||||
|
||||
@@ -174,7 +277,7 @@ func (c *conditionBuilder) conditionFor(
|
||||
// key membership checks, so depending on the column type, the condition changes
|
||||
case qbtypes.FilterOperatorExists, qbtypes.FilterOperatorNotExists:
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
if key.FieldContext == telemetrytypes.FieldContextBody && !c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
if isBodyJSONSearch(key, columns) && !c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
if operator == qbtypes.FilterOperatorExists {
|
||||
return GetBodyJSONKeyForExists(ctx, key, operator, value), nil
|
||||
} else {
|
||||
@@ -268,6 +371,82 @@ func (c *conditionBuilder) conditionFor(
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
// No known field key matched. Legacy string-body mode still searches unknown
|
||||
// Body-context keys as body JSON paths; JSON-body mode requires a metadata match.
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
switch {
|
||||
case key.FieldContext == telemetrytypes.FieldContextBody && key.Name == "":
|
||||
return nil, warnings, errors.NewInvalidInputf(errors.CodeInvalidInput, "missing key for body json search - expected key of the form `body.key` (ex: `body.status`)")
|
||||
case key.FieldContext == telemetrytypes.FieldContextBody &&
|
||||
!c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})):
|
||||
keys = []*telemetrytypes.TelemetryFieldKey{key}
|
||||
default:
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// has/hasAny/hasAll need an array field: in JSON-body mode drop non-array matches so a
|
||||
// scalar errors clearly instead of failing at ClickHouse runtime (legacy mode skips this).
|
||||
if operator.IsArrayFunctionOperator() &&
|
||||
c.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{})) {
|
||||
arrayKeys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k.FieldDataType.IsArray() {
|
||||
arrayKeys = append(arrayKeys, k)
|
||||
}
|
||||
}
|
||||
if len(arrayKeys) == 0 {
|
||||
return nil, warnings, errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"function `%s` expects key parameter to be an array field; no array fields found", operator.FunctionName())
|
||||
}
|
||||
keys = arrayKeys
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
if w := c.bodyFullTextDefaultWarning(ctx, startNs, endNs, k, operator); w != "" {
|
||||
warnings = append(warnings, w)
|
||||
}
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
// bodyFullTextDefaultWarning returns the advisory shown when a regexp full-text
|
||||
// search on `body` resolves to the body.message sub-field (JSON mode), else "". This
|
||||
// keeps the JSON-vs-legacy decision in the builder rather than the filter visitor.
|
||||
func (c *conditionBuilder) bodyFullTextDefaultWarning(ctx context.Context, startNs, endNs uint64, key *telemetrytypes.TelemetryFieldKey, operator qbtypes.FilterOperator) string {
|
||||
if operator != qbtypes.FilterOperatorRegexp || key.Name != LogsV2BodyColumn {
|
||||
return ""
|
||||
}
|
||||
if field, err := c.fm.FieldFor(ctx, startNs, endNs, key); err == nil && field == messageSubColumn {
|
||||
return querybuilder.BodyFullTextSearchDefaultWarning
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
|
||||
@@ -131,8 +131,8 @@ func TestExistsConditionForWithEvolutions(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, tc.startTs, tc.endTs, &tc.key, tc.operator, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, tc.startTs, tc.endTs, &tc.key, []*telemetrytypes.TelemetryFieldKey{&tc.key}, tc.operator, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
|
||||
if tc.expectedError != nil {
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
@@ -522,8 +522,8 @@ func TestConditionFor(t *testing.T) {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.key.Evolutions = tc.evolutions
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, tc.operator, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, []*telemetrytypes.TelemetryFieldKey{&tc.key}, tc.operator, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
|
||||
if tc.expectedError != nil {
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
|
||||
@@ -44,6 +44,16 @@ const (
|
||||
messageSubField = "message"
|
||||
messageSubColumn = "body_v2.message"
|
||||
bodySearchDefaultWarning = "body searches default to `body.message:string`. Use `body.<key>` to search a different field inside body"
|
||||
|
||||
// bodyMessageField is the field name addressing the message sub-field of the
|
||||
// body when use_json_body is enabled (i.e. `body` + `.` + `message`). hasToken
|
||||
// targets this column in that mode.
|
||||
bodyMessageField = "body.message"
|
||||
|
||||
// Documentation URLs attached to function-call errors so the visitor can
|
||||
// surface them to the user without knowing function-specific details.
|
||||
hasTokenFunctionDocURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
|
||||
functionBodyJSONSearchDocURL = "https://signoz.io/docs/userguide/search-troubleshooting/#function-supports-only-body-json-search"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -28,7 +28,6 @@ func TestLikeAndILikeWithoutWildcards_Warns(t *testing.T) {
|
||||
ConditionBuilder: cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: DefaultFullTextColumn,
|
||||
JsonKeyToKey: GetBodyJSONKey,
|
||||
}
|
||||
|
||||
tests := []string{
|
||||
@@ -66,9 +65,7 @@ func TestLikeAndILikeWithWildcards_NoWarn(t *testing.T) {
|
||||
FieldMapper: fm,
|
||||
ConditionBuilder: cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: DefaultFullTextColumn,
|
||||
JsonKeyToKey: GetBodyJSONKey,
|
||||
}
|
||||
FullTextColumn: DefaultFullTextColumn}
|
||||
|
||||
tests := []string{
|
||||
"service.name LIKE 'demo-%'",
|
||||
|
||||
@@ -30,7 +30,6 @@ func TestFilterExprLogsBodyJSON(t *testing.T) {
|
||||
ConditionBuilder: cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: &telemetrytypes.TelemetryFieldKey{Name: "body"},
|
||||
JsonKeyToKey: GetBodyJSONKey,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
|
||||
@@ -50,7 +50,6 @@ func TestFilterExprLogs(t *testing.T) {
|
||||
ConditionBuilder: cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: DefaultFullTextColumn,
|
||||
JsonKeyToKey: GetBodyJSONKey,
|
||||
StartNs: uint64(releaseTime.Add(-5 * time.Minute).UnixNano()),
|
||||
EndNs: uint64(releaseTime.Add(5 * time.Minute).UnixNano()),
|
||||
}
|
||||
@@ -2467,7 +2466,6 @@ func TestFilterExprLogsConflictNegation(t *testing.T) {
|
||||
ConditionBuilder: cb,
|
||||
FieldKeys: keys,
|
||||
FullTextColumn: DefaultFullTextColumn,
|
||||
JsonKeyToKey: GetBodyJSONKey,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
|
||||
@@ -58,7 +58,6 @@ func NewLogQueryStatementBuilder(
|
||||
telemetrytypes.SourceUnspecified,
|
||||
metadataStore,
|
||||
fullTextColumn,
|
||||
jsonKeyToKey,
|
||||
fl,
|
||||
telemetryStore,
|
||||
skipResourceFingerprintThreshold,
|
||||
@@ -659,8 +658,6 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
|
||||
var preparedWhereClause querybuilder.PreparedWhereClause
|
||||
var err error
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := b.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
// add filter expression
|
||||
@@ -670,10 +667,8 @@ func (b *logQueryStatementBuilder) addFilterCondition(
|
||||
FieldMapper: b.fm,
|
||||
ConditionBuilder: b.cb,
|
||||
FieldKeys: keys,
|
||||
BodyJSONEnabled: bodyJSONEnabled,
|
||||
SkipResourceFilter: skipResourceFilter,
|
||||
FullTextColumn: b.fullTextColumn,
|
||||
JsonKeyToKey: b.jsonKeyToKey,
|
||||
Variables: variables,
|
||||
StartNs: start,
|
||||
EndNs: end,
|
||||
|
||||
@@ -20,6 +20,40 @@ func NewConditionBuilder(fm qbtypes.FieldMapper) *conditionBuilder {
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
tsStart, tsEnd uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject to avoid malformed related-values SQL.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// metadata builds best-effort filters for related-values lookups; an unknown key
|
||||
// simply yields no condition rather than an error.
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, tsStart, tsEnd, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
tsStart, tsEnd uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
|
||||
@@ -53,8 +53,8 @@ func TestConditionFor(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, tc.operator, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, []*telemetrytypes.TelemetryFieldKey{&tc.key}, tc.operator, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
|
||||
if tc.expectedError != nil {
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
|
||||
@@ -1372,9 +1372,6 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
|
||||
instrumentationtypes.CodeFunctionName: "getRelatedValues",
|
||||
})
|
||||
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := t.fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
// nothing to return as "related" value if there is nothing to filter on
|
||||
if fieldValueSelector.ExistingQuery == "" {
|
||||
return nil, true, nil
|
||||
@@ -1424,7 +1421,6 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
|
||||
FieldMapper: t.fm,
|
||||
ConditionBuilder: t.conditionBuilder,
|
||||
FieldKeys: keys,
|
||||
BodyJSONEnabled: bodyJSONEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
t.logger.WarnContext(ctx, "error parsing existing query for related values", errors.Attr(err))
|
||||
@@ -1450,22 +1446,22 @@ func (t *telemetryMetaStore) getRelatedValues(ctx context.Context, fieldValueSel
|
||||
|
||||
// search on attributes
|
||||
key.FieldContext = telemetrytypes.FieldContextAttribute
|
||||
cond, err := t.conditionBuilder.ConditionFor(ctx, 0, 0, key, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
|
||||
attrConds, _, err := t.conditionBuilder.ConditionFor(ctx, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
|
||||
if err == nil {
|
||||
conds = append(conds, cond)
|
||||
conds = append(conds, attrConds...)
|
||||
}
|
||||
|
||||
// search on resource
|
||||
key.FieldContext = telemetrytypes.FieldContextResource
|
||||
cond, err = t.conditionBuilder.ConditionFor(ctx, 0, 0, key, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
|
||||
resourceConds, _, err := t.conditionBuilder.ConditionFor(ctx, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
|
||||
if err == nil {
|
||||
conds = append(conds, cond)
|
||||
conds = append(conds, resourceConds...)
|
||||
}
|
||||
key.FieldContext = origContext
|
||||
} else {
|
||||
cond, err := t.conditionBuilder.ConditionFor(ctx, 0, 0, key, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
|
||||
keyConds, _, err := t.conditionBuilder.ConditionFor(ctx, 0, 0, key, []*telemetrytypes.TelemetryFieldKey{key}, qbtypes.FilterOperatorContains, fieldValueSelector.Value, sb)
|
||||
if err == nil {
|
||||
conds = append(conds, cond)
|
||||
conds = append(conds, keyConds...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,42 @@ func (c *conditionBuilder) conditionFor(
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for metrics.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
|
||||
@@ -234,8 +234,8 @@ func TestConditionFor(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, tc.operator, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, 0, 0, &tc.key, []*telemetrytypes.TelemetryFieldKey{&tc.key}, tc.operator, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
|
||||
if tc.expectedError != nil {
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
@@ -289,8 +289,8 @@ func TestConditionForMultipleKeys(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var err error
|
||||
for _, key := range tc.keys {
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, 0, 0, &key, tc.operator, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, 0, 0, &key, []*telemetrytypes.TelemetryFieldKey{&key}, tc.operator, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting condition for key %s: %v", key.Name, err)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,46 @@ func keyIndexFilter(key *telemetrytypes.TelemetryFieldKey) any {
|
||||
}
|
||||
|
||||
func (b *defaultConditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
op qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only functions; they never apply to the
|
||||
// resource fingerprint table, so skip them (the main query still evaluates them).
|
||||
if op.IsFunctionOperator() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
// the resource fingerprint table only stores resource attributes; keys from
|
||||
// any other context contribute no condition and are omitted. An empty result
|
||||
// (including an unknown key) lets the caller skip this filter entirely.
|
||||
if k.FieldContext != telemetrytypes.FieldContextResource {
|
||||
continue
|
||||
}
|
||||
cond, err := b.conditionForKey(ctx, startNs, endNs, k, op, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (b *defaultConditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
@@ -53,10 +93,6 @@ func (b *defaultConditionBuilder) ConditionFor(
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) (string, error) {
|
||||
|
||||
if key.FieldContext != telemetrytypes.FieldContextResource {
|
||||
return querybuilder.SkipConditionLiteral, nil
|
||||
}
|
||||
|
||||
// except for in, not in, between, not between all other operators should have formatted value
|
||||
// as we store resource values as string
|
||||
formattedValue := querybuilder.FormatValueForContains(value)
|
||||
|
||||
@@ -205,8 +205,8 @@ func TestConditionBuilder(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cond, err := conditionBuilder.ConditionFor(context.Background(), 0, 0, tc.key, tc.op, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(context.Background(), 0, 0, tc.key, []*telemetrytypes.TelemetryFieldKey{tc.key}, tc.op, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
|
||||
if tc.expectedErr != nil {
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -24,7 +24,6 @@ func NewResolver[T any](
|
||||
source telemetrytypes.Source,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
fl flagger.Flagger,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
threshold uint64,
|
||||
@@ -38,7 +37,6 @@ func NewResolver[T any](
|
||||
source,
|
||||
metadataStore,
|
||||
fullTextColumn,
|
||||
jsonKeyToKey,
|
||||
fl,
|
||||
),
|
||||
telemetryStore: telemetryStore,
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/flagger"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
)
|
||||
|
||||
@@ -28,7 +26,6 @@ type resourceFilterStatementBuilder[T any] struct {
|
||||
flagger flagger.Flagger
|
||||
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc
|
||||
}
|
||||
|
||||
// Ensure interface compliance at compile time.
|
||||
@@ -45,7 +42,6 @@ func New[T any](
|
||||
source telemetrytypes.Source,
|
||||
metadataStore telemetrytypes.MetadataStore,
|
||||
fullTextColumn *telemetrytypes.TelemetryFieldKey,
|
||||
jsonKeyToKey qbtypes.JsonKeyToFieldFunc,
|
||||
fl flagger.Flagger,
|
||||
) *resourceFilterStatementBuilder[T] {
|
||||
set := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/pkg/telemetryresourcefilter")
|
||||
@@ -62,7 +58,6 @@ func New[T any](
|
||||
source: source,
|
||||
flagger: fl,
|
||||
fullTextColumn: fullTextColumn,
|
||||
jsonKeyToKey: jsonKeyToKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +153,6 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) (bool, error) {
|
||||
|
||||
// TODO(Tushar): thread orgID here to evaluate correctly
|
||||
bodyJSONEnabled := b.flagger.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, featuretypes.NewFlaggerEvaluationContext(valuer.UUID{}))
|
||||
|
||||
// Add filter condition if present
|
||||
if query.Filter != nil && query.Filter.Expression != "" {
|
||||
|
||||
@@ -171,16 +163,13 @@ func (b *resourceFilterStatementBuilder[T]) addConditions(
|
||||
FieldMapper: b.fieldMapper,
|
||||
ConditionBuilder: b.conditionBuilder,
|
||||
FieldKeys: keys,
|
||||
BodyJSONEnabled: bodyJSONEnabled,
|
||||
FullTextColumn: b.fullTextColumn,
|
||||
JsonKeyToKey: b.jsonKeyToKey,
|
||||
SkipFullTextFilter: true,
|
||||
SkipFunctionCalls: true,
|
||||
// there is no need for "key" not found error for resource filtering
|
||||
IgnoreNotFoundKeys: true,
|
||||
Variables: variables,
|
||||
StartNs: start,
|
||||
EndNs: end,
|
||||
// the resource-filter condition builder ignores keys it can't resolve (and
|
||||
// skips function calls), so no "key not found" error arises here.
|
||||
Variables: variables,
|
||||
StartNs: start,
|
||||
EndNs: end,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -361,7 +361,6 @@ func TestResourceFilterStatementBuilder_Traces(t *testing.T) {
|
||||
telemetrytypes.SourceUnspecified,
|
||||
mockMetadataStore,
|
||||
nil,
|
||||
nil,
|
||||
flaggertest.New(t),
|
||||
)
|
||||
|
||||
@@ -555,7 +554,6 @@ func TestResourceFilterStatementBuilder_Logs(t *testing.T) {
|
||||
telemetrytypes.SourceUnspecified,
|
||||
mockMetadataStore,
|
||||
nil,
|
||||
nil,
|
||||
flaggertest.New(t),
|
||||
)
|
||||
|
||||
@@ -623,7 +621,6 @@ func TestResourceFilterStatementBuilder_Variables(t *testing.T) {
|
||||
telemetrytypes.SourceUnspecified,
|
||||
mockMetadataStore,
|
||||
nil,
|
||||
nil,
|
||||
flaggertest.New(t),
|
||||
)
|
||||
|
||||
|
||||
@@ -255,6 +255,42 @@ func (c *conditionBuilder) conditionFor(
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) ConditionFor(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
key *telemetrytypes.TelemetryFieldKey,
|
||||
fieldKeysForName []*telemetrytypes.TelemetryFieldKey,
|
||||
operator qbtypes.FilterOperator,
|
||||
value any,
|
||||
sb *sqlbuilder.SelectBuilder,
|
||||
) ([]string, []string, error) {
|
||||
|
||||
// has/hasAny/hasAll/hasToken are logs-body-only; reject for traces.
|
||||
if err := querybuilder.NewFunctionUnsupportedError(operator); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keys, warning := querybuilder.ResolveKeys(key, fieldKeysForName)
|
||||
var warnings []string
|
||||
if warning != "" {
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil, warnings, querybuilder.NewKeyNotFoundError(key.Name)
|
||||
}
|
||||
|
||||
conds := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
cond, err := c.conditionForKey(ctx, startNs, endNs, k, operator, value, sb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
return conds, warnings, nil
|
||||
}
|
||||
|
||||
func (c *conditionBuilder) conditionForKey(
|
||||
ctx context.Context,
|
||||
startNs uint64,
|
||||
endNs uint64,
|
||||
|
||||
@@ -293,8 +293,8 @@ func TestConditionFor(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, 1761437108000000000, 1761458708000000000, &tc.key, tc.operator, tc.value, sb)
|
||||
sb.Where(cond)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, 1761437108000000000, 1761458708000000000, &tc.key, []*telemetrytypes.TelemetryFieldKey{&tc.key}, tc.operator, tc.value, sb)
|
||||
sb.Where(cond...)
|
||||
|
||||
if tc.expectedError != nil {
|
||||
assert.Equal(t, tc.expectedError, err)
|
||||
@@ -380,9 +380,9 @@ func TestConditionForResourceWithEvolution(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cond, err := conditionBuilder.ConditionFor(ctx, tc.tsStart, tc.tsEnd, &tc.key, tc.operator, nil, sb)
|
||||
cond, _, err := conditionBuilder.ConditionFor(ctx, tc.tsStart, tc.tsEnd, &tc.key, []*telemetrytypes.TelemetryFieldKey{&tc.key}, tc.operator, nil, sb)
|
||||
require.NoError(t, err)
|
||||
sb.Where(cond)
|
||||
sb.Where(cond...)
|
||||
sql, _ := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
assert.Contains(t, sql, tc.expectedSQL)
|
||||
})
|
||||
|
||||
@@ -54,7 +54,6 @@ func NewTraceQueryStatementBuilder(
|
||||
telemetrytypes.SourceUnspecified,
|
||||
metadataStore,
|
||||
nil,
|
||||
nil,
|
||||
flagger,
|
||||
telemetryStore,
|
||||
skipResourceFingerprintThreshold,
|
||||
|
||||
@@ -44,7 +44,6 @@ func NewTraceOperatorStatementBuilder(
|
||||
telemetrytypes.SourceUnspecified,
|
||||
metadataStore,
|
||||
nil,
|
||||
nil,
|
||||
flagger,
|
||||
)
|
||||
|
||||
|
||||
@@ -111,6 +111,13 @@ const (
|
||||
|
||||
FilterOperatorContains
|
||||
FilterOperatorNotContains
|
||||
|
||||
// has/hasAny/hasAll are array membership functions; hasToken is a full-text token
|
||||
// search (not array membership), so it is excluded from IsArrayFunctionOperator.
|
||||
FilterOperatorHas
|
||||
FilterOperatorHasToken
|
||||
FilterOperatorHasAny
|
||||
FilterOperatorHasAll
|
||||
)
|
||||
|
||||
var operatorInverseMapping = map[FilterOperator]FilterOperator{
|
||||
@@ -224,6 +231,45 @@ func (f FilterOperator) IsArrayOperator() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsArrayFunctionOperator reports whether the operator is one of the array
|
||||
// membership functions (has/hasAny/hasAll) that operate over array fields.
|
||||
func (f FilterOperator) IsArrayFunctionOperator() bool {
|
||||
switch f {
|
||||
case FilterOperatorHas, FilterOperatorHasAny, FilterOperatorHasAll:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsFunctionOperator reports whether the operator is a query function
|
||||
// (has/hasAny/hasAll/hasToken); these apply to the logs body column only.
|
||||
func (f FilterOperator) IsFunctionOperator() bool {
|
||||
switch f {
|
||||
case FilterOperatorHas, FilterOperatorHasAny, FilterOperatorHasAll, FilterOperatorHasToken:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FunctionName returns the query-text name of a function operator
|
||||
// (has/hasAny/hasAll/hasToken), or "" for any non-function operator.
|
||||
func (f FilterOperator) FunctionName() string {
|
||||
switch f {
|
||||
case FilterOperatorHas:
|
||||
return "has"
|
||||
case FilterOperatorHasAny:
|
||||
return "hasAny"
|
||||
case FilterOperatorHasAll:
|
||||
return "hasAll"
|
||||
case FilterOperatorHasToken:
|
||||
return "hasToken"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type OrderDirection struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrColumnNotFound = errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "field not found")
|
||||
ErrColumnNotFound = errors.NewNotFoundf(errors.CodeNotFound, "field not found")
|
||||
ErrBetweenValues = errors.NewInvalidInputf(errors.CodeInvalidInput, "(not) between operator requires two values")
|
||||
ErrBetweenValuesType = errors.NewInvalidInputf(errors.CodeInvalidInput, "(not) between operator requires two values of the number type")
|
||||
ErrInValues = errors.NewInvalidInputf(errors.CodeInvalidInput, "(not) in operator requires a list of values")
|
||||
@@ -29,10 +29,14 @@ type FieldMapper interface {
|
||||
ColumnExpressionFor(ctx context.Context, tsStart, tsEnd uint64, key *telemetrytypes.TelemetryFieldKey, keys map[string][]*telemetrytypes.TelemetryFieldKey) (string, error)
|
||||
}
|
||||
|
||||
// ConditionBuilder builds the condition for the filter.
|
||||
// ConditionBuilder builds the conditions for the filter.
|
||||
type ConditionBuilder interface {
|
||||
// ConditionFor returns the condition for the given key, operator and value.
|
||||
ConditionFor(ctx context.Context, startNs uint64, endNs uint64, key *telemetrytypes.TelemetryFieldKey, operator FilterOperator, value any, sb *sqlbuilder.SelectBuilder) (string, error)
|
||||
// ConditionFor returns the conditions and any advisory warnings for a filter
|
||||
// term. key is the field key as parsed from the query text; fieldKeysForName is
|
||||
// the set of known field keys matching it (may be empty). The builder owns the
|
||||
// decision of what to do — resolve ambiguity, fall back to a body JSON search,
|
||||
// emit a "not found" error, or skip — and which errors/warnings are apt.
|
||||
ConditionFor(ctx context.Context, startNs uint64, endNs uint64, key *telemetrytypes.TelemetryFieldKey, fieldKeysForName []*telemetrytypes.TelemetryFieldKey, operator FilterOperator, value any, sb *sqlbuilder.SelectBuilder) ([]string, []string, error)
|
||||
}
|
||||
|
||||
type AggExprRewriter interface {
|
||||
|
||||
@@ -172,58 +172,13 @@ func (f *TelemetryFieldKey) Normalize() {
|
||||
|
||||
}
|
||||
|
||||
// GetFieldKeyFromKeyText returns a TelemetryFieldKey from a key text.
|
||||
// The key text is expected to be in the format of `fieldContext.fieldName:fieldDataType` in the search query.
|
||||
// Both fieldContext and :fieldDataType are optional.
|
||||
// fieldName can contain dots and can start with a dot (e.g., ".http_code").
|
||||
// Special cases:
|
||||
// - When key exactly matches a field context name (e.g., "body", "attribute"), use unspecified context.
|
||||
// - When key starts with "body." prefix, use "body" as context with remainder as field name.
|
||||
// GetFieldKeyFromKeyText returns a TelemetryFieldKey parsed from a key text of the
|
||||
// form `fieldContext.fieldName:fieldDataType` (context and :dataType optional). It
|
||||
// delegates to Normalize; see Normalize for the parsing rules and special cases.
|
||||
func GetFieldKeyFromKeyText(key string) TelemetryFieldKey {
|
||||
var explicitFieldDataType = FieldDataTypeUnspecified
|
||||
var fieldName string
|
||||
|
||||
// Step 1: Parse data type from the right (after the last ":")
|
||||
var keyWithoutDataType string
|
||||
if colonIdx := strings.LastIndex(key, ":"); colonIdx != -1 {
|
||||
potentialDataType := key[colonIdx+1:]
|
||||
if dt, ok := fieldDataTypes[potentialDataType]; ok && dt != FieldDataTypeUnspecified {
|
||||
explicitFieldDataType = dt
|
||||
keyWithoutDataType = key[:colonIdx]
|
||||
} else {
|
||||
// No valid data type found, treat the entire key as the field name
|
||||
keyWithoutDataType = key
|
||||
}
|
||||
} else {
|
||||
keyWithoutDataType = key
|
||||
}
|
||||
|
||||
// Step 2: Parse field context from the left
|
||||
if dotIdx := strings.Index(keyWithoutDataType, "."); dotIdx != -1 {
|
||||
potentialContext := keyWithoutDataType[:dotIdx]
|
||||
if fc, ok := fieldContexts[potentialContext]; ok && fc != FieldContextUnspecified {
|
||||
fieldName = keyWithoutDataType[dotIdx+1:]
|
||||
|
||||
// Step 2a: Handle special case for log.body.* fields
|
||||
if fc == FieldContextLog && strings.HasPrefix(fieldName, BodyJSONStringSearchPrefix) {
|
||||
fc = FieldContextBody
|
||||
fieldName = strings.TrimPrefix(fieldName, BodyJSONStringSearchPrefix)
|
||||
}
|
||||
|
||||
return TelemetryFieldKey{
|
||||
Name: fieldName,
|
||||
FieldContext: fc,
|
||||
FieldDataType: explicitFieldDataType,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: No context found, entire key is the field name
|
||||
return TelemetryFieldKey{
|
||||
Name: keyWithoutDataType,
|
||||
FieldContext: FieldContextUnspecified,
|
||||
FieldDataType: explicitFieldDataType,
|
||||
}
|
||||
f := TelemetryFieldKey{Name: key}
|
||||
f.Normalize()
|
||||
return f
|
||||
}
|
||||
|
||||
func TelemetryFieldKeyToText(key *TelemetryFieldKey) string {
|
||||
|
||||
Reference in New Issue
Block a user