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.
114 lines
4.4 KiB
Go
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
|
|
}
|