Files
signoz/pkg/querybuilder/logical_expr.go
Srikanth Chekuri 1643df620b
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
feat: resolve semconv families across logs and metrics (#12870)
#### 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.
2026-10-01 20:28:15 +00:00

114 lines
4.4 KiB
Go

package querybuilder
import (
"context"
"fmt"
"strings"
"github.com/SigNoz/signoz/pkg/clickhousesql"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
// LogicalRead is the only place family expressions are built. It composes
// exclusively from the storage's per-key Read, so every member honors its
// own storage: materialized columns, evolutions, and JSON plans ride the
// member keys, and a signal supports families the moment its reads are
// correct.
//
// A single-member field reads through its member. A family merges the
// member reads, current member first. It is present when any member is
// present, and absent when no member is present. A row without any member
// reads what the tail of the merge reads: the sentinel for a string family,
// NULL for the others. When every member reads its sentinel as a value, so
// does the family, and the keyless contract of the signal survives the
// merge. A member with a value map reads in the current vocabulary.
func LogicalRead(ctx context.Context, q qbtypes.QueryInfo, storage qbtypes.Storage, logical *telemetrytypes.LogicalField) (qbtypes.Read, error) {
if !logical.IsFamily() {
return memberRead(ctx, q, storage, logical, 0)
}
reads := make([]qbtypes.Read, 0, len(logical.Members))
for i := range logical.Members {
read, err := memberRead(ctx, q, storage, logical, i)
if err != nil {
return qbtypes.Read{}, err
}
reads = append(reads, read)
}
merged := qbtypes.Read{WhenAbsent: familyAbsence(logical, reads)}
guards := make([]string, 0, len(reads))
for _, read := range reads {
guards = append(guards, read.Presence)
merged.KeepType = merged.KeepType || read.KeepType
}
merged.Presence = "(" + strings.Join(guards, " OR ") + ")"
merged.Absence = "NOT " + merged.Presence
merged.FilterOnly = true
for _, read := range reads {
merged.FilterOnly = merged.FilterOnly && read.FilterOnly
}
if logical.FieldDataType == telemetrytypes.FieldDataTypeString {
// The trailing '' keeps single-key semantics for rows without any
// member: string maps read '' for an absent key, and negative
// operators must keep including such rows. A NULL tail would drop
// them: NULL != 'x' evaluates to NULL, and the row falls out of the
// result.
values := make([]string, 0, len(reads))
for _, read := range reads {
values = append(values, fmt.Sprintf("NULLIF(%s, '')", read.SQL))
}
merged.SQL = "COALESCE(" + strings.Join(values, ", ") + ", '')"
return merged, nil
}
// Numeric and boolean maps return zero for an absent key. If a family of
// either type is enabled, this tail must become zero too.
branches := make([]string, 0, len(reads)*2)
for _, read := range reads {
branches = append(branches, read.Presence, read.SQL)
}
merged.SQL = "multiIf(" + strings.Join(branches, ", ") + ", NULL)"
return merged, nil
}
func memberRead(ctx context.Context, q qbtypes.QueryInfo, storage qbtypes.Storage, logical *telemetrytypes.LogicalField, i int) (qbtypes.Read, error) {
read, err := storage.Read(ctx, q, logical.Members[i])
if err != nil {
return qbtypes.Read{}, err
}
if i < len(logical.ValueMaps) && logical.ValueMaps[i] != nil {
read.SQL = TransformRead(read.SQL, logical.ValueMaps[i])
}
return read, nil
}
// TransformRead brings a member's read into the current vocabulary: a stored
// value maps to its current value, any other value reads as it is.
func TransformRead(read string, valueMap *telemetrytypes.ValueMap) string {
return fmt.Sprintf("transform(%s, %s, %s, %s)", read, clickHouseStringArray(valueMap.Stored), clickHouseStringArray(valueMap.Current), read)
}
func clickHouseStringArray(values []string) string {
items := make([]string, 0, len(values))
for _, value := range values {
items = append(items, clickhousesql.StringLiteral(value))
}
return "[" + strings.Join(items, ", ") + "]"
}
// familyAbsence is what the merged read yields for a row without any
// member: the sentinel tail of a string family, NULL for the others. When
// every member's sentinel is a value, the tail is one too.
func familyAbsence(logical *telemetrytypes.LogicalField, reads []qbtypes.Read) qbtypes.Absent {
if logical.FieldDataType != telemetrytypes.FieldDataTypeString {
return qbtypes.AbsentIsNull
}
for _, read := range reads {
if read.WhenAbsent != qbtypes.AbsentIsValue {
return qbtypes.AbsentIsSentinel
}
}
return qbtypes.AbsentIsValue
}