mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-07 02:30:42 +01:00
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description - A logs filter on a bare key that lives in **both** resource and another context (body or scope) ANDed the two: the resource candidate built the `__resource_filter` fingerprint CTE while the other candidate landed as a required main-query term, so the query matched almost nothing. - `ResolveLogicalFields` only preferred resource over `attribute`. Generalized it to prefer resource over **any** other context (attribute, body, scope, …); other contexts stay reachable via their qualified names (e.g. `body.service.name`). #### Issues closed by this PR Closes SigNoz/engineering-pod#6086 Part of https://github.com/SigNoz/platform-pod/issues/3158 #### Additional Information Generalized rather than special-casing body/scope, since any future context would hit the same fingerprint-CTE trap.
221 lines
9.4 KiB
Go
221 lines
9.4 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
schema "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
|
"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/#q-im-getting-key-fieldname-not-found--why-cant-it-find-my-field"
|
|
|
|
// Doc URLs for the has/hasAny/hasAll and hasToken "unsupported" errors.
|
|
functionBodyJSONSearchDocURL = "https://signoz.io/docs/userguide/search-troubleshooting/#q-im-getting-function-supports-only-body-json-search--can-i-use-functions-on-other-fields"
|
|
hasTokenFunctionDocURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
|
|
)
|
|
|
|
// ResolveLogicalFields picks which logical fields a filter term builds conditions
|
|
// for. With 0 or 1 field it returns the input unchanged and no warning. When a
|
|
// name is ambiguous (several logical fields — a family is one field and never
|
|
// ambiguous with itself) it returns a warning; a resource + other-context mix
|
|
// (attribute, body, scope, …) defaults to the resource fields (the common
|
|
// intent), noted in the warning.
|
|
func ResolveLogicalFields(field *telemetrytypes.TelemetryFieldKey, logicalFields []*telemetrytypes.LogicalField) ([]*telemetrytypes.LogicalField, string) {
|
|
if len(logicalFields) <= 1 {
|
|
return logicalFields, ""
|
|
}
|
|
|
|
warning := fmt.Sprintf(
|
|
"Key `%s` is ambiguous, found %d different combinations of field context / data type: %v.",
|
|
field.Name,
|
|
len(logicalFields),
|
|
logicalFields,
|
|
)
|
|
|
|
hasResource, hasOther := false, false
|
|
for _, item := range logicalFields {
|
|
if item.FieldContext == telemetrytypes.FieldContextResource {
|
|
hasResource = true
|
|
} else {
|
|
hasOther = true
|
|
}
|
|
}
|
|
|
|
// with resource and any other context, default to resource only
|
|
if hasResource && hasOther {
|
|
filtered := make([]*telemetrytypes.LogicalField, 0, len(logicalFields))
|
|
for _, item := range logicalFields {
|
|
if item.FieldContext == telemetrytypes.FieldContextResource {
|
|
filtered = append(filtered, item)
|
|
}
|
|
}
|
|
logicalFields = filtered
|
|
warning += " " + "Using `resource` context by default. To query another context explicitly, " +
|
|
fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s' or 'body.%s')", field.Name, field.Name)
|
|
}
|
|
|
|
return logicalFields, warning
|
|
}
|
|
|
|
// ColumnDataType is the field data type a table column reads as. A storage
|
|
// stamps it on the column key its Fallback returns. The intrinsic-column
|
|
// step can then drop a same-named metadata key of a contradicting type. A
|
|
// time column has no field data type and matches none.
|
|
func ColumnDataType(column *schema.Column) telemetrytypes.FieldDataType {
|
|
switch column.Type.GetType() {
|
|
case schema.ColumnTypeEnumBool:
|
|
return telemetrytypes.FieldDataTypeBool
|
|
case schema.ColumnTypeEnumInt8, schema.ColumnTypeEnumInt16, schema.ColumnTypeEnumInt32, schema.ColumnTypeEnumInt64,
|
|
schema.ColumnTypeEnumUInt8, schema.ColumnTypeEnumUInt16, schema.ColumnTypeEnumUInt32, schema.ColumnTypeEnumUInt64,
|
|
schema.ColumnTypeEnumFloat32, schema.ColumnTypeEnumFloat64:
|
|
return telemetrytypes.FieldDataTypeNumber
|
|
case schema.ColumnTypeEnumString, schema.ColumnTypeEnumFixedString:
|
|
return telemetrytypes.FieldDataTypeString
|
|
case schema.ColumnTypeEnumLowCardinality:
|
|
if lc, ok := column.Type.(schema.LowCardinalityColumnType); ok && lc.ElementType.GetType() == schema.ColumnTypeEnumString {
|
|
return telemetrytypes.FieldDataTypeString
|
|
}
|
|
}
|
|
return telemetrytypes.FieldDataTypeUnspecified
|
|
}
|
|
|
|
// WrapAsLogicalFields wraps physical keys (candidate or synthesized) as
|
|
// single-member logical fields addressed by the requested spelling.
|
|
func WrapAsLogicalFields(requestedName string, keys []*telemetrytypes.TelemetryFieldKey) []*telemetrytypes.LogicalField {
|
|
fields := make([]*telemetrytypes.LogicalField, 0, len(keys))
|
|
for _, key := range keys {
|
|
fields = append(fields, telemetrytypes.SingleLogicalField(requestedName, key))
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// NewKeyNotFoundError builds the error for a key that neither metadata nor
|
|
// the storage can serve, with the closest known names as suggestions.
|
|
func NewKeyNotFoundError(name string, known []string) error {
|
|
err := errors.NewInvalidInputf(errors.CodeInvalidInput, "key `%s` not found", name).WithUrl(KeyNotFoundDocURL)
|
|
if len(known) == 0 {
|
|
return err
|
|
}
|
|
return err.WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(name, errors.NounKeys, known)...)
|
|
}
|
|
|
|
// NewKeyNotFoundWarning is the warning surfaced when a referenced key is absent from
|
|
// metadata and the query falls back to synthesized keys.
|
|
func NewKeyNotFoundWarning(name string) string {
|
|
return fmt.Sprintf("key `%s` not found in metadata; querying the underlying data directly. If this is unexpected, check the key name for typos.", name)
|
|
}
|
|
|
|
// SynthesizeKeys builds the field keys to query when metadata has no match: a qualified
|
|
// key is honored as-is; a bare key defaults to attribute context with the data type
|
|
// inferred from the operand, or fanned out across string/number/bool without one.
|
|
func SynthesizeKeys(field *telemetrytypes.TelemetryFieldKey, value any) []*telemetrytypes.TelemetryFieldKey {
|
|
fieldContext := field.FieldContext
|
|
if fieldContext == telemetrytypes.FieldContextUnspecified {
|
|
fieldContext = telemetrytypes.FieldContextAttribute
|
|
}
|
|
fieldDataType := field.FieldDataType
|
|
// Resource and scope values are strings; pin the type so operand coercion applies.
|
|
if (fieldContext == telemetrytypes.FieldContextResource || fieldContext == telemetrytypes.FieldContextScope) &&
|
|
fieldDataType == telemetrytypes.FieldDataTypeUnspecified {
|
|
fieldDataType = telemetrytypes.FieldDataTypeString
|
|
}
|
|
|
|
// A set data type needs only one synthesized key. It keeps the request
|
|
// key's physical data (evolutions, materialization, JSON plan): a key the
|
|
// caller decorated reads through those even when metadata is silent.
|
|
if fieldDataType != telemetrytypes.FieldDataTypeUnspecified {
|
|
key := *field
|
|
key.FieldContext = fieldContext
|
|
key.FieldDataType = fieldDataType
|
|
return []*telemetrytypes.TelemetryFieldKey{&key}
|
|
}
|
|
|
|
dataTypes := inferDataTypesFromOperand(value)
|
|
keys := make([]*telemetrytypes.TelemetryFieldKey, 0, len(dataTypes))
|
|
for _, dt := range dataTypes {
|
|
keys = append(keys, telemetrytypes.NewTelemetryFieldKey(field.Name, fieldContext, dt))
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// allVariantDataTypes is the fanout used when no operand pins the type (exists / group by).
|
|
func allVariantDataTypes() []telemetrytypes.FieldDataType {
|
|
return []telemetrytypes.FieldDataType{
|
|
telemetrytypes.FieldDataTypeString,
|
|
telemetrytypes.FieldDataTypeNumber,
|
|
telemetrytypes.FieldDataTypeBool,
|
|
}
|
|
}
|
|
|
|
// inferDataTypesFromOperand maps an operand value to the data type(s) to query. With no
|
|
// operand or an unrecognized type it fans out across all variants.
|
|
func inferDataTypesFromOperand(value any) []telemetrytypes.FieldDataType {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return allVariantDataTypes()
|
|
case string:
|
|
return []telemetrytypes.FieldDataType{telemetrytypes.FieldDataTypeString}
|
|
case bool:
|
|
return []telemetrytypes.FieldDataType{telemetrytypes.FieldDataTypeBool}
|
|
case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, json.Number:
|
|
return []telemetrytypes.FieldDataType{telemetrytypes.FieldDataTypeNumber}
|
|
case []any:
|
|
return inferDataTypesFromList(v)
|
|
default:
|
|
return allVariantDataTypes()
|
|
}
|
|
}
|
|
|
|
// inferDataTypesFromList derives the distinct data types present in an `in` list, kept
|
|
// in string/number/bool order; an empty or all-unknown list fans out.
|
|
func inferDataTypesFromList(values []any) []telemetrytypes.FieldDataType {
|
|
var hasString, hasNumber, hasBool bool
|
|
for _, v := range values {
|
|
switch v.(type) {
|
|
case string:
|
|
hasString = true
|
|
case bool:
|
|
hasBool = true
|
|
case float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, json.Number:
|
|
hasNumber = true
|
|
}
|
|
}
|
|
var out []telemetrytypes.FieldDataType
|
|
if hasString {
|
|
out = append(out, telemetrytypes.FieldDataTypeString)
|
|
}
|
|
if hasNumber {
|
|
out = append(out, telemetrytypes.FieldDataTypeNumber)
|
|
}
|
|
if hasBool {
|
|
out = append(out, telemetrytypes.FieldDataTypeBool)
|
|
}
|
|
if len(out) == 0 {
|
|
return allVariantDataTypes()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// NewFunctionUnsupportedError returns the error for a has/hasAny/hasAll/hasToken/search
|
|
// operator on a builder that doesn't support it (logs 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)
|
|
case qbtypes.FilterOperatorSearch:
|
|
return errors.NewInvalidInputf(errors.CodeInvalidInput, "function `search` is only supported for logs")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|