Skip to content

[bugfix] Add nil support for tags lookup - #5967

Merged
joe-elliott merged 6 commits into
grafana:mainfrom
ie-pham:niltags
Nov 24, 2025
Merged

[bugfix] Add nil support for tags lookup#5967
joe-elliott merged 6 commits into
grafana:mainfrom
ie-pham:niltags

Conversation

@ie-pham

@ie-pham ie-pham commented Nov 18, 2025

Copy link
Copy Markdown
Contributor

What this PR does: Add attr = nil support for tags look up. Without this, tags endpoint would panic when attr = nil is used in the query. This is mostly just applying the logic from block_traceql.go

Which issue(s) this PR fixes:
Fixes #

Checklist

  • Tests updated
  • Documentation added
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]
// be sped up by searching for traceDuration first. note that we can only set the predicates if all conditions is true.
// otherwise we just pass the info up to the engine to make a choice
for _, cond := range conds {
if cond.Op == traceql.OpNotExists {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is inconsistent with the way other things are handled but i prefer this. for instance in createSpanIterator we interpret span:name = nil to be span:name = "" which seems wrong.

https://github.com/grafana/tempo/blob/main/tempodb/encoding/vparquet5/block_traceql.go#L1910-L1927

honestly I think neither path is correct. { span:name = nil } is simply an invalid query and should not be executed. an intrinsic cannot be nil. only attributes can.

let's get the panic fixed in this PR, but i'd like the change above. it should also simplify quite a bit of the code.

Comment thread tempodb/encoding/vparquet4/block_autocomplete.go Outdated
Signed-off-by: Joe Elliott <number101010@gmail.com>
@joe-elliott

Copy link
Copy Markdown
Collaborator
Original panic
panic: runtime error: index out of range [0] with length 0 [recovered, repanicked]

goroutine 203122 [running]:
go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End.deferwrap1()
	/home/runner/work/tempo/tempo/vendor/go.opentelemetry.io/otel/sdk/trace/span.go:468 +0x25
go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End(0xc0673f34a0, {0x0, 0x0, 0x0?})
	/home/runner/work/tempo/tempo/vendor/go.opentelemetry.io/otel/sdk/trace/span.go:517 +0xbf2
panic({0x3970a80?, 0xc06c027d88?})
	/home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.linux-amd64/src/runtime/panic.go:783 +0x132
go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End.deferwrap1()
	/home/runner/work/tempo/tempo/vendor/go.opentelemetry.io/otel/sdk/trace/span.go:468 +0x25
go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End(0xc0673f3860, {0x0, 0x0, 0xc061e9a700?})
	/home/runner/work/tempo/tempo/vendor/go.opentelemetry.io/otel/sdk/trace/span.go:517 +0xbf2
panic({0x3970a80?, 0xc06c027d88?})
	/home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.linux-amd64/src/runtime/panic.go:783 +0x132
go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End.deferwrap1()
	/home/runner/work/tempo/tempo/vendor/go.opentelemetry.io/otel/sdk/trace/span.go:468 +0x25
go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End(0xc0673f3a40, {0x0, 0x0, 0xc06c0b0728?})
	/home/runner/work/tempo/tempo/vendor/go.opentelemetry.io/otel/sdk/trace/span.go:517 +0xbf2
panic({0x3970a80?, 0xc06c027d88?})
	/home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.1.linux-amd64/src/runtime/panic.go:783 +0x132
github.com/grafana/tempo/tempodb/encoding/vparquet4.createDistinctAttributeIterator(0xc06c0b04f0, {{0xc067812e10, 0x2, 0x2}, 0x0, {{0xc06c064976, 0x6}, 0x2, 0x0, 0x0}}, ...)
	/home/runner/work/tempo/tempo/tempodb/encoding/vparquet4/block_autocomplete.go:613 +0x26d9
github.com/grafana/tempo/tempodb/encoding/vparquet4.createDistinctSpanIterator(0xc06c0b04f0, {{0xc067812e10, 0x2, 0x2}, 0x0, {{0xc06c064976, 0x6}, 0x2, 0x0, 0x0}}, ...)
	/home/runner/work/tempo/tempo/tempodb/encoding/vparquet4/block_autocomplete.go:544 +0x24d4
github.com/grafana/tempo/tempodb/encoding/vparquet4.autocompleteIter({0x425d378, 0xc06c0197d0}, {{0xc067812e10, 0x2, 0x2}, 0x0, {{0xc06c064976, 0x6}, 0x2, 0x0, ...}}, ...)
	/home/runner/work/tempo/tempo/tempodb/encoding/vparquet4/block_autocomplete.go:234 +0x674
github.com/grafana/tempo/tempodb/encoding/vparquet4.(*walBlock).FetchTagValues(0xc06e9ffb00, {0x425d378?, 0xc06c019710?}, {{0xc067812e10, 0x2, 0x2}, {{0xc06c064976, 0x6}, 0x2, 0x0, ...}}, ...)
	/home/runner/work/tempo/tempo/tempodb/encoding/vparquet4/wal_block.go:751 +0x50e
github.com/grafana/tempo/modules/livestore.(*instance).SearchTagValuesV2.func2.1({0x425d378, 0xc06c019710}, {{0xc067812e10, 0x2, 0x2}, {{0xc06c064976, 0x6}, 0x2, 0x0, 0x0}}, ...)
	/home/runner/work/tempo/tempo/modules/livestore/instance_search.go:479 +0x158
github.com/grafana/tempo/pkg/traceql.TagValuesFetcherWrapper.Fetch({0xc040f02cd0?}, {0x425d378?, 0xc06c019710?}, {{0xc067812e10, 0x2, 0x2}, {{0xc06c064976, 0x6}, 0x2, 0x0, ...}}, ...)
	/home/runner/work/tempo/tempo/pkg/traceql/storage.go:288 +0x4c
github.com/grafana/tempo/pkg/traceql.(*Engine).ExecuteTagValues(0x669ca20, {0x425d378?, 0xc06c0196e0?}, {{0xc06c064976?, 0x7fad7d0633e8?}, 0x30?, 0x0?, 0x0?}, {0xc06c027398, 0x13}, ...)
	/home/runner/work/tempo/tempo/pkg/traceql/engine.go:220 +0x954
github.com/grafana/tempo/modules/livestore.(*instance).SearchTagValuesV2.func2({0x425d378, 0xc06c0196e0}, {0x7fad33de6328, 0xc06e9ffb00})
	/home/runner/work/tempo/tempo/modules/livestore/instance_search.go:482 +0x431
github.com/grafana/tempo/modules/livestore.(*instance).SearchTagValuesV2.func3({0x425d378, 0xc06c0196e0}, 0x1?, {0x7fad341ccc48?, 0xc06e9ffb00?})
	/home/runner/work/tempo/tempo/modules/livestore/instance_search.go:489 +0xa62
github.com/grafana/tempo/modules/livestore.(*instance).iterateBlocks(0xc047383108, {0x425d378, 0xc06c019620}, {0xc045231280?, 0x0?, 0x6669520?}, {0xb?, 0x4?, 0x6669520?}, 0xc0676bb860)
	/home/runner/work/tempo/tempo/modules/livestore/instance_search.go:86 +0xbf8
github.com/grafana/tempo/modules/livestore.(*instance).SearchTagValuesV2(0xc047383108, {0x425d378, 0xc06c0195f0}, 0xc0676bb770)
	/home/runner/work/tempo/tempo/modules/livestore/instance_search.go:557 +0x779
github.com/grafana/tempo/modules/livestore.(*LiveStore).SearchTagValuesV2.func1(0x425d378?)
	/home/runner/work/tempo/tempo/modules/livestore/live_store.go:567 +0x1f
github.com/grafana/tempo/modules/livestore.withInstance[...]({0x425d378?, 0xc06c0195f0}, 0xc0004cce08, 0xc045231350)
	/home/runner/work/tempo/tempo/modules/livestore/live_store.go:604 +0x73
github.com/grafana/tempo/modules/livestore.(*LiveStore).SearchTagValuesV2(0x6518500?, {0x425d378?, 0xc06c0195f0?}, 0xc06c0619c0?)
	/home/runner/work/tempo/tempo/modules/livestore/live_store.go:566 +0x47
github.com/grafana/tempo/pkg/tempopb._Querier_SearchTagValuesV2_Handler.func1({0x425d378?, 0xc06c0195f0?}, {0x3b1f880?, 0xc0676bb770?})
	/home/runner/work/tempo/tempo/pkg/tempopb/tempo.pb.go:4422 +0xcb
github.com/grafana/dskit/middleware.ServerUserHeaderInterceptor({0x425d378?, 0xc06c019590?}, {0x3b1f880, 0xc0676bb770}, 0xc045231468?, 0xc06c05d4d0)
	/home/runner/work/tempo/tempo/vendor/github.com/grafana/dskit/middleware/grpc_auth.go:43 +0x5b
github.com/grafana/tempo/cmd/tempo/app.(*App).setupAuthMiddleware.func1({0x425d378, 0xc06c019590}, {0x3b1f880, 0xc0676bb770}, 0xc05ec3b5a0, 0xc06c05d4d0)
	/home/runner/work/tempo/tempo/cmd/tempo/app/app.go:150 +0x94
google.golang.org/grpc.getChainUnaryHandler.func1({0x425d378?, 0xc06c019590?}, {0x3b1f880?, 0xc0676bb770?})
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1243 +0x153
github.com/grafana/dskit/middleware.UnaryServerInstrumentInterceptor.func1({0x425d378, 0xc06c019590}, {0x3b1f880, 0xc0676bb770}, 0xc05ec3b5a0, 0xc06c023180)
	/home/runner/work/tempo/tempo/vendor/github.com/grafana/dskit/middleware/grpc_instrumentation.go:51 +0xa7
google.golang.org/grpc.getChainUnaryHandler.func1.getChainUnaryHandler.1({0x425d378, 0xc06c019590}, {0x3b1f880, 0xc0676bb770})
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1243 +0xa8
github.com/grafana/dskit/server.newServer.HTTPGRPCTracingInterceptor.func3({0x425d378?, 0xc06c019590?}, {0x3b1f880?, 0xc0676bb770?}, 0xb928bb96f24f40c5?, 0xc045231728?)
	/home/runner/work/tempo/tempo/vendor/github.com/grafana/dskit/middleware/http_tracing.go:182 +0x182
google.golang.org/grpc.getChainUnaryHandler.func1({0x425d378?, 0xc06c019590?}, {0x3b1f880?, 0xc0676bb770?})
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1243 +0x153
github.com/grafana/dskit/middleware.GRPCServerLog.UnaryServerInterceptor({{0x42148c0?, 0xc000744690?}, 0xa0?, 0x3?}, {0x425d378, 0xc06c019590}, {0x3b1f880, 0xc0676bb770}, 0xc05ec3b5a0, 0xc06c023100)
	/home/runner/work/tempo/tempo/vendor/github.com/grafana/dskit/middleware/grpc_logging.go:54 +0xb2
google.golang.org/grpc.NewServer.chainUnaryServerInterceptors.chainUnaryInterceptors.func1({0x425d378, 0xc06c019590}, {0x3b1f880, 0xc0676bb770}, 0xc05ec3b5a0, 0x80?)
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1234 +0x7c
github.com/grafana/tempo/pkg/tempopb._Querier_SearchTagValuesV2_Handler({0x3ba3b00, 0xc0004cce08}, {0x425d378, 0xc06c019590}, 0xc06c06a880, 0xc0008cb6c0)
	/home/runner/work/tempo/tempo/pkg/tempopb/tempo.pb.go:4424 +0x143
google.golang.org/grpc.(*Server).processUnaryRPC(0xc00061b800, {0x425d378, 0xc06c019410}, 0xc06785a300, 0xc0417d9980, 0x65ce8f0, 0x0)
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1431 +0xfe8
google.golang.org/grpc.(*Server).handleStream(0xc00061b800, {0x425e928, 0xc00020d6c0}, 0xc06785a300)
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1842 +0xb95
google.golang.org/grpc.(*Server).serveStreams.func2.1()
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1061 +0x7f
created by google.golang.org/grpc.(*Server).serveStreams.func2 in goroutine 872
	/home/runner/work/tempo/tempo/vendor/google.golang.org/grpc/server.go:1072 +0x11d
// = nil ?
if isNotExistSearch {
pred := parquetquery.NewIncludeNilStringEqualPredicate([]byte(cond.Attribute.Name))
iters = append(iters, makeNilIter(columnPathSpanAttrKey, pred, "")) // don't select just filter nils

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming my understanding: this line (and the others like it) prevent the panic by not dropping into the genericConditions for =nil checks on generic attrs. Because the Operands[0].Type lookup is still in createDistinctAttributeCollector, instead we are catching that one layer higher here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the = nil operation breaks expectations by not passing in an operand, but instead only passing an operator of NotExists. This special handling is also in the normal iterator construction as well.

// tag requested. only used for tag values. if populated then return tag values for this tag, otherwise return tag names.
tag traceql.Attribute
// existsTagName is a callback to check if a tag name has already been seen
existsTagName func(key tagNameKey) bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change of moving the cache one layer up, but is there a performance or correctness difference? It looks like the data by each distinct attr collector doesn't overlap because they are different scopes anyway. So a map here of all data, is the same as smaller maps in the individual collectors.

Update: OK I think I understand this IS a correctness fix. The cache inside each distinct attr collector is too low, it's caching every individual foo and bar, but we are looking for foo && bar. The top-level map is only populated after that. 👍

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so if you were searching for values of span.foo where resource.baz = "bat" then the distrinctAttrCollector would only surface each value of span.foo the first time it was found due to the local map. If this value happened to be eliminated by the higher level resource condition we would never attempt to collect it again.

for _, cond := range conds {
if cond.Op == traceql.OpNotExists {
// trace-level intrinsic can't be nil
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a return instead? Continuing like the condition doesn't exist I think makes ... && trace:id = nil return everything instead of nothing. (Specifically it returns the resourceIter with no extra filtering). We have been chatting that handling of intrinsics in general is not where we want in this PR, so if we want to include this in the follow up that is fine too.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think to catch other trace level conditions it needs to be a continue as written. However it's in a weird place b/c it's not asserting a condition and just forcing the engine to evaluate intrinsic = nil. I did some basic testing and this seems to always evaluate to false, but haven't dug deeper.

What it should really do is just return an error b/c this is an invalid condition. We should also include protections higher up to presumably never pass this condition into the fetch layer.

)

for _, cond := range conditions {
isNotExistSearch := len(cond.Operands) == 0 && cond.Op == traceql.OpNotExists

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to generate an OpNotExists with operands or does it matter? We are checking this in each place, and wondering if we can simplify.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can likely simplify. I didn't write the original functionality so would have to dig deeper.

@mdisibio mdisibio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted offline and although there are still outstanding changes we want, this is a significant improvement in correctness and fixes the panic, which would be great to get in.

@joe-elliott
joe-elliott merged commit 258c551 into grafana:main Nov 24, 2025
36 of 37 checks passed
mattdurham pushed a commit to mattdurham/tempo that referenced this pull request Jun 18, 2026
* [bugfix] support attr = nil for tags

* add other vparquet versions

* added tests

* changelog

* Accumulate distinct results at a higher level

Signed-off-by: Joe Elliott <number101010@gmail.com>

* Add logic to wal blocks

Signed-off-by: Joe Elliott <number101010@gmail.com>

---------

Signed-off-by: Joe Elliott <number101010@gmail.com>
Co-authored-by: Joe Elliott <number101010@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants