From 7b47a91627d5f3991984bdfe7988f66039b6a297 Mon Sep 17 00:00:00 2001 From: Vandit Singh Date: Sat, 3 Oct 2026 05:03:07 +0530 Subject: [PATCH] vtselect: fix attribute name autocomplete for scoped Tempo tag search --- app/vtselect/traces/tempo/tempo.go | 6 +- apptest/model.go | 35 +++++++++ apptest/tests/tempo_search_tags_test.go | 87 ++++++++++++++++++++++ apptest/vtsingle.go | 20 ++++- docs/victoriatraces/changelog/CHANGELOG.md | 1 + 5 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 apptest/tests/tempo_search_tags_test.go diff --git a/app/vtselect/traces/tempo/tempo.go b/app/vtselect/traces/tempo/tempo.go index a07b015d3..f8746f53a 100644 --- a/app/vtselect/traces/tempo/tempo.go +++ b/app/vtselect/traces/tempo/tempo.go @@ -387,11 +387,11 @@ func searchTags(ctx context.Context, cp *tracecommon.CommonParams, traceQLStr st pipeLimit := limit switch scope { case "instrumentation": - scopes = fmt.Sprintf(`| filter name:"%s:"*`, otelpb.InstrumentationScopeAttrPrefix) + scopes = fmt.Sprintf(`| filter name:"%s"*`, otelpb.InstrumentationScopeAttrPrefix) case "resource": - scopes = fmt.Sprintf(`| filter name:"%s:"*`, otelpb.ResourceAttrPrefix) + scopes = fmt.Sprintf(`| filter name:"%s"*`, otelpb.ResourceAttrPrefix) case "span": - scopes = fmt.Sprintf(`| filter name:"%s:"*`, otelpb.SpanAttrPrefixField) + scopes = fmt.Sprintf(`| filter name:"%s"*`, otelpb.SpanAttrPrefixField) case "event": //scopes = fmt.Sprintf(`| filter name:"%s:"*`, otelpb.EventPrefix+otelpb.EventAttrPrefix) return nil, errors.New("scope: event is not supported yet") diff --git a/apptest/model.go b/apptest/model.go index 070345fd3..80e6ed8ae 100644 --- a/apptest/model.go +++ b/apptest/model.go @@ -289,6 +289,41 @@ func NewTempoAPISearchResponse(t *testing.T, s string) *TempoAPISearchResponse { return res } +// TempoAPISearchTagsResponse is an in-memory representation of the +// /select/tempo/api/v2/search/tags response. +type TempoAPISearchTagsResponse struct { + Scopes []TempoTagScope `json:"scopes"` +} + +// TempoTagScope is the structure of an item in the `scopes` field of the +// /select/tempo/api/v2/search/tags response. +type TempoTagScope struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +// Tags returns the tag names of the given scope. +func (r *TempoAPISearchTagsResponse) Tags(scope string) []string { + for _, s := range r.Scopes { + if s.Name == scope { + return s.Tags + } + } + return nil +} + +// NewTempoAPISearchTagsResponse is a test helper function that creates a new +// instance of TempoAPISearchTagsResponse by unmarshalling a json string. +func NewTempoAPISearchTagsResponse(t *testing.T, s string) *TempoAPISearchTagsResponse { + t.Helper() + + res := &TempoAPISearchTagsResponse{} + if err := json.Unmarshal([]byte(s), res); err != nil { + t.Fatalf("could not unmarshal query response data=\n%s\n: %v", s, err) + } + return res +} + // NewJaegerAPITraceResponse is a test helper function that creates a new // instance of JaegerAPITraceResponse by unmarshalling a json string. func NewJaegerAPITraceResponse(t *testing.T, s string) *JaegerAPITraceResponse { diff --git a/apptest/tests/tempo_search_tags_test.go b/apptest/tests/tempo_search_tags_test.go new file mode 100644 index 000000000..3c30ae6c6 --- /dev/null +++ b/apptest/tests/tempo_search_tags_test.go @@ -0,0 +1,87 @@ +package tests + +import ( + "testing" + "time" + + at "github.com/VictoriaMetrics/VictoriaTraces/apptest" + otelpb "github.com/VictoriaMetrics/VictoriaTraces/lib/protoparser/opentelemetry/pb" +) + +// TestSingleTempoSearchTagsByScope verifies that /select/tempo/api/v2/search/tags +// returns the attribute names of the requested scope. +func TestSingleTempoSearchTagsByScope(t *testing.T) { + tc := at.NewTestCase(t) + defer tc.Stop() + + sut := tc.MustStartVtsingle("vtsingle", []string{ + "-storageDataPath=" + tc.Dir() + "/vtsingle", + "-retentionPeriod=100y", + }) + + serviceName := "testSearchTagsService" + clusterName := "testCluster" + libLang := "go" + httpMethod := "GET" + spanTime := time.Now().Add(-time.Hour) + req := &otelpb.ExportTraceServiceRequest{ + ResourceSpans: []*otelpb.ResourceSpans{ + { + Resource: otelpb.Resource{ + Attributes: []*otelpb.KeyValue{ + {Key: "service.name", Value: &otelpb.AnyValue{StringValue: &serviceName}}, + {Key: "k8s.cluster.name", Value: &otelpb.AnyValue{StringValue: &clusterName}}, + }, + }, + ScopeSpans: []*otelpb.ScopeSpans{ + { + Scope: otelpb.InstrumentationScope{ + Name: "testScope", + Attributes: []*otelpb.KeyValue{ + {Key: "library.language", Value: &otelpb.AnyValue{StringValue: &libLang}}, + }, + }, + Spans: []*otelpb.Span{ + { + TraceID: "7c2e8d1a4b9f0e3d5a6b7c8d9e0f1a2b", + SpanID: "1a2b3c4d5e6f7a8b", + Name: "testSearchTagsSpan", + StartTimeUnixNano: uint64(spanTime.UnixNano()), + EndTimeUnixNano: uint64(spanTime.Add(time.Millisecond).UnixNano()), + Attributes: []*otelpb.KeyValue{ + {Key: "http.method", Value: &otelpb.AnyValue{StringValue: &httpMethod}}, + }, + }, + }, + }, + }, + }, + }, + } + sut.OTLPHTTPExportTraces(t, req, at.QueryOpts{}) + sut.ForceFlush(t) + + startTime := spanTime.Add(-time.Hour) + endTime := time.Now() + + resourceTags := []string{"k8s.cluster.name", "service.name"} + spanTags := []string{"http.method"} + scopeTags := []string{"library.language"} + + f := func(scope string, wantResource, wantSpan, wantScope []string) { + t.Helper() + tc.Assert(&at.AssertOptions{ + Msg: "unexpected tags for scope=" + scope + " from /select/tempo/api/v2/search/tags", + Got: func() any { + res := sut.TempoAPISearchTags(t, scope, startTime, endTime) + return [][]string{res.Tags("resource"), res.Tags("span"), res.Tags("instrumentation")} + }, + Want: [][]string{wantResource, wantSpan, wantScope}, + }) + } + + f("resource", resourceTags, []string{}, []string{}) + f("span", []string{}, spanTags, []string{}) + f("instrumentation", []string{}, []string{}, scopeTags) + f("all", resourceTags, spanTags, scopeTags) +} diff --git a/apptest/vtsingle.go b/apptest/vtsingle.go index f7be749b1..1f1f201a3 100644 --- a/apptest/vtsingle.go +++ b/apptest/vtsingle.go @@ -33,7 +33,8 @@ type Vtsingle struct { jaegerAPITraceURL string jaegerAPIDependenciesURL string - tempoAPISearchURL string + tempoAPISearchURL string + tempoAPISearchTagsURL string logsQLQueryURL string @@ -84,7 +85,8 @@ func StartVtsingle(instance string, flags, env []string, cli *Client) (*Vtsingle jaegerAPITraceURL: fmt.Sprintf("http://%s/select/jaeger/api/traces/%%s", stderrExtracts[1]), jaegerAPIDependenciesURL: fmt.Sprintf("http://%s/select/jaeger/api/dependencies", stderrExtracts[1]), - tempoAPISearchURL: fmt.Sprintf("http://%s/select/tempo/api/search", stderrExtracts[1]), + tempoAPISearchURL: fmt.Sprintf("http://%s/select/tempo/api/search", stderrExtracts[1]), + tempoAPISearchTagsURL: fmt.Sprintf("http://%s/select/tempo/api/v2/search/tags", stderrExtracts[1]), logsQLQueryURL: fmt.Sprintf("http://%s/select/logsql/query", stderrExtracts[1]), @@ -178,6 +180,20 @@ func (app *Vtsingle) TempoAPISearch(t *testing.T, q string, start, end time.Time return NewTempoAPISearchResponse(t, res) } +// TempoAPISearchTags is a test helper function that lists attribute names of the given scope +// by sending an HTTP GET request to /select/tempo/api/v2/search/tags Vtsingle endpoint. +func (app *Vtsingle) TempoAPISearchTags(t *testing.T, scope string, start, end time.Time) *TempoAPISearchTagsResponse { + t.Helper() + + uv := url.Values{ + "scope": {scope}, + "start": {strconv.FormatInt(start.Unix(), 10)}, + "end": {strconv.FormatInt(end.Unix(), 10)}, + } + res, _ := app.cli.Get(t, app.tempoAPISearchTagsURL+"?"+uv.Encode()) + return NewTempoAPISearchTagsResponse(t, res) +} + // JaegerAPIDependencies is a test helper function that queries for the dependencies. // This method is not implemented in Vtsingle and this test is no-op for now. func (app *Vtsingle) JaegerAPIDependencies(t *testing.T, param JaegerDependenciesParam, opts QueryOpts) *JaegerAPIDependenciesResponse { diff --git a/docs/victoriatraces/changelog/CHANGELOG.md b/docs/victoriatraces/changelog/CHANGELOG.md index 4e98dfd22..9105bd04c 100644 --- a/docs/victoriatraces/changelog/CHANGELOG.md +++ b/docs/victoriatraces/changelog/CHANGELOG.md @@ -14,6 +14,7 @@ The following `tip` changes can be tested by building VictoriaTraces components * SECURITY: upgrade Go builder from Go1.26.6 to Go1.27.2. See the list of issues addressed in [Go1.26.7](https://github.com/golang/go/issues?q=milestone%3AGo1.26.7+label%3ACherryPickApproved), [Go1.27.1](https://github.com/golang/go/issues?q=milestone%3AGo1.27.1+label%3ACherryPickApproved) and [Go1.27.2](https://github.com/golang/go/issues?q=milestone%3AGo1.27.2+label%3ACherryPickApproved). +* BUGFIX: [Single-node VictoriaTraces](https://docs.victoriametrics.com/victoriatraces/) and vtselect in [VictoriaTraces cluster](https://docs.victoriametrics.com/victoriatraces/cluster/): return attribute names for `scope=resource`, `scope=span` and `scope=instrumentation` in the Tempo `/api/v2/search/tags` API. Previously these scopes returned empty lists, so TraceQL editors such as Perses showed no attribute names after `resource.` or `span.`. See [issue #268](https://github.com/VictoriaMetrics/VictoriaTraces/issues/268). * BUGFIX: [Single-node VictoriaTraces](https://docs.victoriametrics.com/victoriatraces/) and vtselect in [VictoriaTraces cluster](https://docs.victoriametrics.com/victoriatraces/cluster/): apply correct time‑range filter and return proper traces from Jaeger and Tempo search APIs when VictoriaTraces runs in a non‑UTC time zone. Previously, searches could return fewer traces or empty results due to incorrect time‑zone conversion for the time‑range filter, especially for time zones west of UTC such as `TZ=America/New_York`, which impacted the end‑time boundary. See [issue #260](https://github.com/VictoriaMetrics/VictoriaTraces/issues/260). Thank @Vandit1604 for [pull request #262](https://github.com/VictoriaMetrics/VictoriaTraces/pull/262). ## [v0.12.0](https://github.com/VictoriaMetrics/VictoriaTraces/releases/tag/v0.12.0)