diff --git a/docs/victoriatraces/changelog/CHANGELOG.md b/docs/victoriatraces/changelog/CHANGELOG.md index 7906db50a..1c16a4265 100644 --- a/docs/victoriatraces/changelog/CHANGELOG.md +++ b/docs/victoriatraces/changelog/CHANGELOG.md @@ -12,6 +12,8 @@ The following `tip` changes can be tested by building VictoriaTraces components ## tip +* BUGFIX: [Single-node VictoriaTraces](https://docs.victoriametrics.com/victoriatraces/) and vtselect in [VictoriaTraces cluster](https://docs.victoriametrics.com/victoriatraces/cluster/): support unscoped attributes in TraceQL queries for the Tempo search API. Previously such queries always returned empty results. See [this issue #198](https://github.com/VictoriaMetrics/VictoriaTraces/issues/198). + ## [v0.12.0](https://github.com/VictoriaMetrics/VictoriaTraces/releases/tag/v0.12.0) Released at 2026-09-29 diff --git a/lib/traceql/filter_general.go b/lib/traceql/filter_general.go index 8cb230072..74067cd96 100644 --- a/lib/traceql/filter_general.go +++ b/lib/traceql/filter_general.go @@ -111,6 +111,15 @@ func (fc *filterCommon) String() string { return otelpb.ParentSpanIDField + `:=""` } + // Unscoped attribute `.foo` reads the span attribute if the span has it, and the resource attribute otherwise, + // `.service.name` is mapped to the resource stream field below. + if attr, ok := strings.CutPrefix(fc.fieldName, "."); ok && fc.fieldName != ".service.name" { + spanFilter := filterCommon{fieldName: "span." + attr, op: fc.op, value: fc.value} + resourceFilter := filterCommon{fieldName: "resource." + attr, op: fc.op, value: fc.value} + spanField := quoteFieldNameIfNeeded(otelpb.SpanAttrPrefixField + attr) + return "(" + spanField + ":* and " + spanFilter.String() + " or " + spanField + `:"" and ` + resourceFilter.String() + ")" + } + // TraceQL's `attr = nil` / `attr != nil` map to LogsQL's empty-value and // any-value filters respectively — the canonical forms per // https://docs.victoriametrics.com/victorialogs/logsql/#empty-value-filter diff --git a/lib/traceql/parser_test.go b/lib/traceql/parser_test.go index ca93a3707..f2557386a 100644 --- a/lib/traceql/parser_test.go +++ b/lib/traceql/parser_test.go @@ -107,6 +107,12 @@ func TestParseQuery(t *testing.T) { // Regex match operator `=~` must become LogsQL `~` (not `=~`). f(`{resource.host.name =~ "kimi-k2-a.*"}`, `"resource_attr:host.name":~"kimi-k2-a.*"`) + + // unscoped attributes read the span attribute if present, and the resource attribute otherwise. + f(`{.istio.cluster_id = "c1"}`, `("span_attr:istio.cluster_id":* and "span_attr:istio.cluster_id":=c1 or "span_attr:istio.cluster_id":"" and "resource_attr:istio.cluster_id":=c1)`) + f(`{.service.name = "x" && .istio.cluster_id = "c1"}`, `{"resource_attr:service.name"=x} and ("span_attr:istio.cluster_id":* and "span_attr:istio.cluster_id":=c1 or "span_attr:istio.cluster_id":"" and "resource_attr:istio.cluster_id":=c1)`) + f(`{.foo !~ "a.*"}`, `("span_attr:foo":* and "span_attr:foo":!~"a.*" or "span_attr:foo":"" and "resource_attr:foo":!~"a.*")`) + f(`{.foo != nil}`, `("span_attr:foo":* and "span_attr:foo":* or "span_attr:foo":"" and "resource_attr:foo":*)`) } // TestParseQueryInvalid asserts that malformed inputs return an error