mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-07 02:30:42 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description Phase 2 of #6143: semantic-convention families resolve on logs and metrics, behind the `resolve_semconv_families` flag (default off), on the storage contract of #12802. - Registry: each member carries the scope of its own rename edges, so a fan-out keeps one membership per target and an ambiguous name stays literal. - Logs and metrics families need no family code of their own. The gate applies to every signal, and `LogicalRead` merges members through each storage's `Read`. - Metric-name families union the storage names in every `metric_name` filter, and the querier reads type, temporality, and the reduced flag across the family. - Span-metrics labels: the metrics the processor emits, listed by name, also read each family member with the `resource_` prefix. Requested names are never rewritten. - Values suggestions and related values cover every spelling of the family. - `deployment.environment.name` resolves on all three signals. `db.system.name` stays off until a value-mapping reader exists. #### Additional Information - `pkg/semconv.Family` fields are now unexported, and `transition.go` is removed. #12446 reads the old API and needs an update when stacked. - A target that emits both names of a metric-name family double-counts in `sum()` during the overlap window. Reading both names is the feature. Pinned by a test. - A family of metrics labels keeps the keyless contract of a single label: no guard, no NULL group.
218 lines
8.1 KiB
Go
218 lines
8.1 KiB
Go
package querybuilder
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
|
|
"github.com/SigNoz/signoz/pkg/flagger"
|
|
"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"
|
|
"golang.org/x/exp/maps"
|
|
)
|
|
|
|
// NewQueryInfo binds the context of one query and evaluates the query-path
|
|
// flags one time. A nil flagger keeps resolution literal and the log body in
|
|
// its legacy column.
|
|
func NewQueryInfo(ctx context.Context, orgID valuer.UUID, fl flagger.Flagger, signal telemetrytypes.Signal, metric *telemetrytypes.MetricContext, startNs, endNs uint64) qbtypes.QueryInfo {
|
|
q := qbtypes.QueryInfo{
|
|
StartNs: startNs,
|
|
EndNs: endNs,
|
|
Signal: signal,
|
|
Metric: metric,
|
|
FamiliesOn: SemconvFamiliesEnabled(ctx, orgID, fl),
|
|
}
|
|
if fl != nil {
|
|
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
|
|
q.BodyJSONOn = fl.BooleanOrEmpty(ctx, flagger.FeatureUseJSONBody, evalCtx)
|
|
q.TraceAttrsJSONOn = fl.BooleanOrEmpty(ctx, flagger.FeatureUseTraceAttributesJSON, evalCtx)
|
|
}
|
|
return q
|
|
}
|
|
|
|
// Resolve turns one requested key into its meanings, one time for each
|
|
// use. The order is the same for every storage, in the filter and in every
|
|
// select field, group by, order by, and aggregation:
|
|
//
|
|
// 1. matches: the metadata keys under the key's spellings, grouped into
|
|
// families when the flag is on. A key under one of the storage's own
|
|
// contexts matches its own context first, then a column the storage
|
|
// knows under that context, and only then as if it had no context.
|
|
// 2. ambiguity: in a filter, several interpretations settle by the
|
|
// resource-over-attribute policy, with a warning. A select field, group
|
|
// by, order by, or aggregation keeps every interpretation in metadata
|
|
// order and folds them.
|
|
// 3. intrinsic column first: for a bare key, a column every row has leads.
|
|
// Metadata can report the column, or the storage's own tables can. When
|
|
// only the storage knows the column, same-named metadata keys of a
|
|
// contradicting type drop.
|
|
// 4. fallback: with no match, the storage's fallback keys. The not-found
|
|
// warning fires only when every one of them is a guess.
|
|
func Resolve(
|
|
ctx context.Context,
|
|
q qbtypes.QueryInfo,
|
|
storage qbtypes.Storage,
|
|
key *telemetrytypes.TelemetryFieldKey,
|
|
operator qbtypes.FilterOperator,
|
|
value any,
|
|
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
) (qbtypes.Resolved, error) {
|
|
traits := storage.Traits()
|
|
|
|
lookup := key
|
|
matches := matchingLogicalFields(q.FamiliesOn, q.Signal, q.Metric, key, fieldKeys)
|
|
if len(matches) == 0 && slices.Contains(traits.OwnContexts, key.FieldContext) {
|
|
// a column the storage knows under the key's own context is the key
|
|
// as written, and only a miss corrects to the bare spelling
|
|
if fallback, err := storage.Fallback(ctx, q, key, operator, value); err == nil {
|
|
if column := alwaysPresent(ctx, q, storage, fallback); column != nil {
|
|
return qbtypes.Resolved{Key: key, Fields: []*telemetrytypes.LogicalField{column}, FromFallback: true}, nil
|
|
}
|
|
}
|
|
lookup = telemetrytypes.NewTelemetryFieldKey(key.Name, telemetrytypes.FieldContextUnspecified, key.FieldDataType)
|
|
matches = matchingLogicalFields(q.FamiliesOn, q.Signal, q.Metric, lookup, fieldKeys)
|
|
}
|
|
|
|
resolved := qbtypes.Resolved{Key: key, Ambiguous: len(matches) > 1}
|
|
fields := matches
|
|
if operator != qbtypes.FilterOperatorUnknown {
|
|
var warning string
|
|
fields, warning = ResolveLogicalFields(key, matches)
|
|
if warning != "" {
|
|
resolved.Warnings = append(resolved.Warnings, warning)
|
|
}
|
|
}
|
|
if lookup.FieldContext == telemetrytypes.FieldContextUnspecified && len(fields) > 0 {
|
|
fields = intrinsicColumnFirst(ctx, q, storage, key, operator, value, fields)
|
|
}
|
|
if len(fields) > 0 {
|
|
resolved.Fields = fields
|
|
return resolved, nil
|
|
}
|
|
|
|
fallback, err := storage.Fallback(ctx, q, key, operator, value)
|
|
if err != nil {
|
|
return qbtypes.Resolved{}, err
|
|
}
|
|
if len(fallback) == 0 {
|
|
if traits.UnknownKey == qbtypes.IgnoreUnknownKey {
|
|
resolved.Skipped = true
|
|
return resolved, nil
|
|
}
|
|
return qbtypes.Resolved{}, NewKeyNotFoundError(key.Name, maps.Keys(fieldKeys))
|
|
}
|
|
resolved.FromFallback = true
|
|
resolved.Fields = fallback
|
|
if fallbackIsGuess(ctx, q, storage, fallback) {
|
|
resolved.Warnings = append(resolved.Warnings, NewKeyNotFoundWarning(key.Name))
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
// ResolveColumn resolves one key for a select field, group by, order by, or
|
|
// aggregation, and renders its column expression.
|
|
func ResolveColumn(
|
|
ctx context.Context,
|
|
q qbtypes.QueryInfo,
|
|
storage qbtypes.Storage,
|
|
key *telemetrytypes.TelemetryFieldKey,
|
|
target telemetrytypes.FieldDataType,
|
|
fieldKeys map[string][]*telemetrytypes.TelemetryFieldKey,
|
|
) (string, error) {
|
|
resolved, err := Resolve(ctx, q, storage, key, qbtypes.FilterOperatorUnknown, nil, fieldKeys)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return Column(ctx, q, storage, resolved, target)
|
|
}
|
|
|
|
// intrinsicColumnFirst puts a column every row has first for a bare key.
|
|
// The column comes from the matches when metadata reports it. Otherwise it
|
|
// comes from the storage's own fallback. A metadata gap then degrades to
|
|
// the correct column, never to a corrupt metadata key. Only in that gap
|
|
// does a match of a contradicting data type drop. When metadata reports the
|
|
// column too, every match reads.
|
|
func intrinsicColumnFirst(
|
|
ctx context.Context,
|
|
q qbtypes.QueryInfo,
|
|
storage qbtypes.Storage,
|
|
key *telemetrytypes.TelemetryFieldKey,
|
|
operator qbtypes.FilterOperator,
|
|
value any,
|
|
fields []*telemetrytypes.LogicalField,
|
|
) []*telemetrytypes.LogicalField {
|
|
column := alwaysPresent(ctx, q, storage, fields)
|
|
fromFallback := false
|
|
if column == nil {
|
|
// a fallback that cannot answer this term has no column for it, and
|
|
// its error belongs to the no-match path
|
|
if fallback, err := storage.Fallback(ctx, q, key, operator, value); err == nil {
|
|
column = alwaysPresent(ctx, q, storage, fallback)
|
|
fromFallback = column != nil
|
|
}
|
|
}
|
|
if column == nil {
|
|
return fields
|
|
}
|
|
out := make([]*telemetrytypes.LogicalField, 0, len(fields)+1)
|
|
out = append(out, column)
|
|
for _, logical := range fields {
|
|
if logical == column || sameFact(logical, column) {
|
|
continue
|
|
}
|
|
if fromFallback && !dataTypesConsistent(column.FieldDataType, logical.FieldDataType) {
|
|
continue
|
|
}
|
|
out = append(out, logical)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// alwaysPresent returns the first field every row has. A field the storage
|
|
// cannot test for presence is not that field.
|
|
func alwaysPresent(ctx context.Context, q qbtypes.QueryInfo, storage qbtypes.Storage, fields []*telemetrytypes.LogicalField) *telemetrytypes.LogicalField {
|
|
for _, logical := range fields {
|
|
if logical.IsFamily() {
|
|
continue
|
|
}
|
|
read, err := storage.Read(ctx, q, logical.Single())
|
|
if err == nil && read.WhenAbsent == qbtypes.AlwaysPresent {
|
|
return logical
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sameFact(a, b *telemetrytypes.LogicalField) bool {
|
|
return !a.IsFamily() && !b.IsFamily() &&
|
|
a.FieldContext == b.FieldContext && a.Single().Name == b.Single().Name
|
|
}
|
|
|
|
// dataTypesConsistent reports whether a metadata key's data type can
|
|
// describe the same stored value as the column's. An untyped metadata key
|
|
// matches every type. A column without a field data type (a time column)
|
|
// matches no type. The numeric kinds match each other.
|
|
func dataTypesConsistent(column, entry telemetrytypes.FieldDataType) bool {
|
|
if entry == telemetrytypes.FieldDataTypeUnspecified {
|
|
return true
|
|
}
|
|
if column == telemetrytypes.FieldDataTypeUnspecified {
|
|
return false
|
|
}
|
|
if column == entry {
|
|
return true
|
|
}
|
|
return isNumber(column) && isNumber(entry)
|
|
}
|
|
|
|
func isNumber(dt telemetrytypes.FieldDataType) bool {
|
|
return dt == telemetrytypes.FieldDataTypeInt64 || dt == telemetrytypes.FieldDataTypeFloat64 || dt == telemetrytypes.FieldDataTypeNumber
|
|
}
|
|
|
|
// fallbackIsGuess reports whether every fallback key is a guess. A column
|
|
// the storage knows is not one, and its presence means the key was found.
|
|
func fallbackIsGuess(ctx context.Context, q qbtypes.QueryInfo, storage qbtypes.Storage, fields []*telemetrytypes.LogicalField) bool {
|
|
return alwaysPresent(ctx, q, storage, fields) == nil
|
|
}
|