Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
06f8233
split tag req with ORs into groups of conditions
ie-pham Mar 29, 2026
f502a20
claude reviews
ie-pham Mar 30, 2026
9af75f1
fix when there is no op group
ie-pham Mar 31, 2026
6348607
claude told me this is good
ie-pham Mar 31, 2026
e8e83af
fix test and address comments
ie-pham Mar 31, 2026
371464d
move block read outside of loop
ie-pham Mar 31, 2026
f3c0f49
add docs
ie-pham Mar 31, 2026
001d5ec
more optimization
ie-pham Mar 31, 2026
18766fb
add strict to req
ie-pham Apr 1, 2026
da1f9da
add strict param handling
ie-pham Apr 1, 2026
d21c8a7
fix duplicatebranch
ie-pham Apr 1, 2026
40e3e53
add strict test
ie-pham Apr 1, 2026
d3149de
address comments
ie-pham Apr 2, 2026
55d176e
fix test
ie-pham Apr 2, 2026
0ec0cdb
more comments
ie-pham Apr 2, 2026
e3f07ea
remove strict mode and add config to set max groups
ie-pham Apr 4, 2026
d6babb0
some more optimization
ie-pham Apr 6, 2026
9ef76b4
rename
ie-pham Apr 6, 2026
b4846fb
remove strict
ie-pham Apr 6, 2026
e583a7c
missed gen-proto
ie-pham Apr 6, 2026
ed7a695
error handling and tests
ie-pham Apr 7, 2026
f8252d1
lint
ie-pham Apr 8, 2026
9781179
typo
ie-pham Apr 8, 2026
59ff266
review comments
ie-pham Apr 8, 2026
8db9d5f
fix
ie-pham Apr 8, 2026
2be38f7
fix test
ie-pham Apr 9, 2026
30269d9
return ops in flatten function
ie-pham Apr 9, 2026
e683bb9
make tag value searches work for unscoped tag
ie-pham Apr 14, 2026
138ac02
lint
ie-pham Apr 15, 2026
a56474a
changelog
ie-pham Apr 15, 2026
c0baf86
update doc for unscoped
ie-pham Apr 15, 2026
2a544f4
handle when max conditions is set to zero
ie-pham Apr 15, 2026
06d5d2b
lint
ie-pham Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## main / unreleased

* [CHANGE] **BREAKING CHANGE** Remove duplicate "compaction" prefix from CompactorConfig CLI flags. Affected flags: `compaction.block-retention`, `compaction.max-objects-per-block`, `compaction.max-block-bytes`, `compaction.compaction-window`. [#6909](https://github.com/grafana/tempo/pull/6909) (@electron0zero)
* [ENHANCEMENT] Support OR conditions for tag name and tag value autocomplete (search tags v2) [#6827](https://github.com/grafana/tempo/pull/6827) (@ie-pham)
* [ENHANCEMENT] Expose MinIO retry settings via S3 config [#6561](https://github.com/grafana/tempo/pull/6561) (@rwhitty)
* [CHANGE] **BREAKING CHANGE** Centralize block and WAL config: `block_builder` and `live_store` now always use `storage.trace.block` settings; per-module block config fields are removed. [#6647](https://github.com/grafana/tempo/pull/6647) (@stoewer)
Comment on lines +4 to 6
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

This changelog section requires entries to be grouped in the fixed category order ([SECURITY], [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]). The newly added [ENHANCEMENT] items are currently placed before existing [CHANGE] entries, which breaks that ordering. Please move the new entry into the [ENHANCEMENT] block after all [CHANGE] and [FEATURE] items in this section.

Copilot uses AI. Check for mistakes.
* [CHANGE] **BREAKING CHANGE** Remove Opencensus receiver [#6523](https://github.com/grafana/tempo/pull/6523) (@javiermolinar)
* [CHANGE] Upgrade Tempo to Go 1.26.0 [#6443](https://github.com/grafana/tempo/pull/6443) (@stoewer)
Expand Down
11 changes: 6 additions & 5 deletions docs/sources/tempo/api_docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ Parameters:
Specifies the scope of the tags, this is an optional parameter, if not specified it means all scopes.
Default = `all`
- `q = (traceql query)`
Optional. A TraceQL query to filter tag names by. Currently only works for a single spanset of `&&`ed conditions. For example: `{ span.foo = "bar" && resource.baz = "bat" ...}`. See also [Filtered tag values](#filtered-tag-values).
Optional. A TraceQL query to filter tag names by. Supports `&&` and `||` operators within a single spanset. For example: `{ span.foo = "bar" && resource.baz = "bat" }` or `{ span.foo = "bar" || resource.baz = "bat" }`. See also [Filtered tag values](#filtered-tag-values).
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The V2 tags endpoint now accepts a strict query parameter (see API parsing + proto changes), but the docs for /api/v2/search/tags don't mention it. Please document what strict does and its default (e.g., whether Tempo falls back to unfiltered results vs returning a truncated/partial interpretation when OR-expansion exceeds limits).

Suggested change
Optional. A TraceQL query to filter tag names by. Supports `&&` and `||` operators within a single spanset. For example: `{ span.foo = "bar" && resource.baz = "bat" }` or `{ span.foo = "bar" || resource.baz = "bat" }`. See also [Filtered tag values](#filtered-tag-values).
Optional. A TraceQL query to filter tag names by. Supports `&&` and `||` operators within a single spanset. For example: `{ span.foo = "bar" && resource.baz = "bat" }` or `{ span.foo = "bar" || resource.baz = "bat" }`. See also [Filtered tag values](#filtered-tag-values).
- `strict = (boolean)`
Optional. Controls how strictly the TraceQL filter is applied when it cannot be fully expanded (for example, when OR-expansion exceeds internal limits). When `strict=true`, the request fails with an error instead of relaxing the filter. When `strict=false` (the default, including when omitted), Tempo returns tag names based on the portion of the filter that can be honored and ignores parts that exceed limits; if the filter cannot be applied at all, the request falls back to returning unfiltered tag names.

Copilot uses AI. Check for mistakes.
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.

While the writing proposed appears to be okay, is the content correct?

- `start = (unix epoch seconds)`
Optional. Along with `end` define a time range from which tags should be returned.
- `end = (unix epoch seconds)`
Expand Down Expand Up @@ -608,7 +608,7 @@ Parameters:
- `end = (unix epoch seconds)`
Optional. Along with `start`, defines a time range from which tags values should be returned. Providing both `start` and `end` includes blocks for the specified time range only.
- `q = (traceql query)`
Optional. A TraceQL query to filter tag values by. Currently only works for a single spanset of `&&`ed conditions. For example: `{ span.foo = "bar" && resource.baz = "bat" ...}`. Refer to [Filtered tag values](#filtered-tag-values).
Optional. A TraceQL query to filter tag values by. Supports `&&` and `||` operators within a single spanset. For example: `{ span.foo = "bar" && resource.baz = "bat" }` or `{ span.foo = "bar" || resource.baz = "bat" }`. Refer to [Filtered tag values](#filtered-tag-values).
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Same as for tag names: the V2 tag values endpoint now supports a strict query parameter, but the docs for /api/v2/search/tag/<tag>/values don't list or explain it. Consider adding it to the parameter list and describing how it affects filtering when OR expansion hits the maxConditionGroups cap.

Suggested change
Optional. A TraceQL query to filter tag values by. Supports `&&` and `||` operators within a single spanset. For example: `{ span.foo = "bar" && resource.baz = "bat" }` or `{ span.foo = "bar" || resource.baz = "bat" }`. Refer to [Filtered tag values](#filtered-tag-values).
Optional. A TraceQL query to filter tag values by. Supports `&&` and `||` operators within a single spanset. For example: `{ span.foo = "bar" && resource.baz = "bat" }` or `{ span.foo = "bar" || resource.baz = "bat" }`. Refer to [Filtered tag values](#filtered-tag-values).
- `strict = (boolean)`
Optional. Controls how the `q` filter behaves when OR expansion exceeds the internal `maxConditionGroups` limit. When `strict=true`, the request fails instead of dropping excess OR conditions; when omitted or `false`, the query is relaxed by ignoring OR branches beyond the limit, which can return approximate results.

Copilot uses AI. Check for mistakes.
- `limit = (integer)`
Optional. Limits the maximum number of tags values
- `maxStaleValues = (integer)`
Expand All @@ -632,12 +632,13 @@ Tempo extracts only the valid matchers and builds a valid query.
If an input is invalid, Tempo doesn't provide an error. Instead,
you'll see the whole list when a failure of parsing input. This behavior helps with backwards compatibility.

Only queries with a single selector `{}` and AND `&&` operators are supported.
Queries must use a single spanset selector `{}`. Both `&&` and `||` operators are supported within the selector.

- Example supported: `{ resource.cluster = "us-east-1" && resource.service = "frontend" }`
- Example unsupported: `{ resource.cluster = "us-east-1" || resource.service = "frontend" } && { resource.cluster = "us-east-2" }`
- Example supported: `{ resource.cluster = "us-east-1" || resource.service = "frontend" }`
- Example unsupported: `{ resource.cluster = "us-east-1" } && { resource.cluster = "us-east-2" }`

Unscoped attributes aren't supported for filtered tag values.
Unscoped attributes are supported for filtered tag values. When you use an unscoped attribute name, Tempo looks for matches in both span and resource scopes.

The following request returns all discovered service names on spans with `span.http.method=GET`:

Expand Down
1 change: 1 addition & 0 deletions docs/sources/tempo/configuration/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ overrides:
retry_info_enabled: true
read:
max_bytes_per_tag_values_query: 1000000
max_condition_groups_per_tag_query: 100
metrics_generator:
generate_native_histograms: classic
native_histogram_bucket_factor: 1.1
Expand Down
188 changes: 172 additions & 16 deletions integration/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -42,6 +43,9 @@ func TestTagEndpoints(t *testing.T) {
{spanCount: 2, name: "baz", resourceAttr: "secondRes", resourceAttVal: "qux", SpanAttr: "secondSpan", spanAttVal: "qux"},
{spanCount: 2, name: "foo", resourceAttr: "twoRes", resourceAttVal: "bar", SpanAttr: "twoSpan", spanAttVal: "bar"},
{spanCount: 2, name: "baz", resourceAttr: "twoRes", resourceAttVal: "qux", SpanAttr: "twoSpan", spanAttVal: "qux"},
{spanCount: 2, name: "foo", resourceAttr: "secondRes", resourceAttVal: "qux", SpanAttr: "secondSpan", spanAttVal: "bar"},
{spanCount: 2, name: "boo", resourceAttr: "sameAttr", resourceAttVal: "qux", SpanAttr: "someSpan", spanAttVal: "bar"},
{spanCount: 2, name: "boo", resourceAttr: "someRes", resourceAttVal: "qux", SpanAttr: "sameAttr", spanAttVal: "bar"},
}

for _, b := range batches {
Expand Down Expand Up @@ -69,7 +73,7 @@ func TestTagEndpoints(t *testing.T) {

// Test V1 API endpoints (backwards compatibility)
t.Run("tags_v1_wal", func(t *testing.T) {
expectedTags := []string{"firstRes", "firstSpan", "secondRes", "secondSpan", "service.name", "twoRes", "twoSpan"}
expectedTags := []string{"firstRes", "firstSpan", "sameAttr", "secondRes", "secondSpan", "service.name", "someRes", "someSpan", "twoRes", "twoSpan"}
callSearchTagsAndAssert(t, h, expectedTags, 0, 0)
})

Expand Down Expand Up @@ -101,7 +105,7 @@ func TestTagEndpoints(t *testing.T) {

// Test V1 API endpoints on backend (backwards compatibility)
t.Run("tags_v1_backend", func(t *testing.T) {
expectedTags := []string{"firstRes", "firstSpan", "secondRes", "secondSpan", "service.name", "twoRes", "twoSpan"}
expectedTags := []string{"firstRes", "firstSpan", "sameAttr", "secondRes", "secondSpan", "service.name", "someRes", "someSpan", "twoRes", "twoSpan"}
callSearchTagsAndAssert(t, h, expectedTags, start.Unix(), end.Unix())
})

Expand Down Expand Up @@ -132,11 +136,11 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr, batches[2].SpanAttr},
Tags: []string{batches[0].SpanAttr, batches[6].SpanAttr, batches[1].SpanAttr, batches[5].SpanAttr, batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[5].resourceAttr, batches[1].resourceAttr, batches[6].resourceAttr, batches[2].resourceAttr, "service.name"},
},
},
},
Expand All @@ -150,11 +154,11 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr, batches[2].SpanAttr},
Tags: []string{batches[0].SpanAttr, batches[6].SpanAttr, batches[1].SpanAttr, batches[5].SpanAttr, batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[5].resourceAttr, batches[1].resourceAttr, batches[6].resourceAttr, batches[2].resourceAttr, "service.name"},
},
},
},
Expand All @@ -167,7 +171,7 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[2].resourceAttr, batches[4].resourceAttr, "service.name"},
},
},
},
Expand Down Expand Up @@ -246,11 +250,11 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr, batches[2].SpanAttr},
Tags: []string{batches[0].SpanAttr, batches[6].SpanAttr, batches[1].SpanAttr, batches[5].SpanAttr, batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[5].resourceAttr, batches[1].resourceAttr, batches[6].resourceAttr, batches[2].resourceAttr, "service.name"},
},
},
},
Expand All @@ -263,11 +267,11 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr, batches[2].SpanAttr},
Tags: []string{batches[0].SpanAttr, batches[6].SpanAttr, batches[1].SpanAttr, batches[5].SpanAttr, batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[5].resourceAttr, batches[1].resourceAttr, batches[6].resourceAttr, batches[2].resourceAttr, "service.name"},
},
},
},
Expand All @@ -280,11 +284,11 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr, batches[2].SpanAttr},
Tags: []string{batches[0].SpanAttr, batches[6].SpanAttr, batches[1].SpanAttr, batches[5].SpanAttr, batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[5].resourceAttr, batches[1].resourceAttr, batches[6].resourceAttr, batches[2].resourceAttr, "service.name"},
},
},
},
Expand All @@ -297,11 +301,59 @@ func buildSearchTagsV2TestCases(batches []batchTmpl) []struct {
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr, batches[2].SpanAttr},
Tags: []string{batches[0].SpanAttr, batches[6].SpanAttr, batches[1].SpanAttr, batches[5].SpanAttr, batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, batches[2].resourceAttr, "service.name"},
Tags: []string{batches[0].resourceAttr, batches[5].resourceAttr, batches[1].resourceAttr, batches[6].resourceAttr, batches[2].resourceAttr, "service.name"},
},
},
},
},
// OR conditions
{
name: "OR - two resource attrs from different batches",
query: fmt.Sprintf(`{ resource.%s="%s" || resource.%s="%s" }`, batches[0].resourceAttr, batches[0].resourceAttVal, batches[1].resourceAttr, batches[1].resourceAttVal),
scope: "none",
expected: &tempopb.SearchTagsV2Response{
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[0].SpanAttr, batches[1].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, batches[1].resourceAttr, "service.name"},
},
},
},
},
{
name: "OR - same resource attr two values",
query: fmt.Sprintf(`{ resource.%s="%s" || resource.%s="%s" }`, batches[2].resourceAttr, batches[2].resourceAttVal, batches[3].resourceAttr, batches[3].resourceAttVal),
scope: "none",
expected: &tempopb.SearchTagsV2Response{
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "span",
Tags: []string{batches[2].SpanAttr},
},
{
Name: "resource",
Tags: []string{batches[2].resourceAttr, "service.name"},
},
},
},
},
{
name: "OR - left branch matches only",
query: fmt.Sprintf(`{ resource.%s="%s" || resource.nonexistent="nope" }`, batches[0].resourceAttr, batches[0].resourceAttVal),
scope: "resource",
expected: &tempopb.SearchTagsV2Response{
Scopes: []*tempopb.SearchTagsV2Scope{
{
Name: "resource",
Tags: []string{batches[0].resourceAttr, "service.name"},
},
},
},
Expand Down Expand Up @@ -414,7 +466,7 @@ func buildSearchTagValuesV2TestCases(batches []batchTmpl) []struct {
query: `{ resource.service.name="my-service"}`,
tagName: "name",
expected: &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[3].name}, {Type: "string", Value: batches[2].name}},
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[3].name}, {Type: "string", Value: batches[5].name}, {Type: "string", Value: batches[2].name}},
},
},
{
Expand All @@ -433,6 +485,45 @@ func buildSearchTagValuesV2TestCases(batches []batchTmpl) []struct {
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[2].spanAttVal}, {Type: "string", Value: batches[3].spanAttVal}},
},
},
// OR conditions
{
name: "OR - both branches match different values",
query: fmt.Sprintf(`{ resource.%s="%s" || resource.%s="%s" }`, batches[2].resourceAttr, batches[2].resourceAttVal, batches[3].resourceAttr, batches[3].resourceAttVal),
tagName: "span.twoSpan",
expected: &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[2].spanAttVal}, {Type: "string", Value: batches[3].spanAttVal}},
},
},
{
name: "OR - only left branch matches",
query: fmt.Sprintf(`{ resource.%s="%s" || resource.nonexistent="nope" }`, batches[2].resourceAttr, batches[2].resourceAttVal),
tagName: "span.twoSpan",
expected: &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[2].spanAttVal}},
},
},
{
name: "OR - neither branch matches",
query: fmt.Sprintf(`{ resource.%s="nope" || resource.%s="nope" }`, batches[2].resourceAttr, batches[3].resourceAttr),
tagName: "span.twoSpan",
expected: &tempopb.SearchTagValuesV2Response{},
},
{
name: "OR - different attributes each matching a different batch",
query: fmt.Sprintf(`{ span:name="%s" || resource.%s="%s" }`, batches[1].name, batches[4].resourceAttr, batches[4].resourceAttVal),
tagName: "span.secondSpan",
expected: &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[4].spanAttVal}, {Type: "string", Value: batches[1].spanAttVal}},
},
},
{
name: "OR - unscoped tag with query having different attributes each matching a different batch",
query: fmt.Sprintf(`{ span.%s="%s" || resource.%s="%s" }`, batches[5].SpanAttr, batches[5].spanAttVal, batches[6].resourceAttr, batches[6].resourceAttVal),
tagName: ".sameAttr",
expected: &tempopb.SearchTagValuesV2Response{
TagValues: []*tempopb.TagValue{{Type: "string", Value: batches[6].spanAttVal}, {Type: "string", Value: batches[5].resourceAttVal}},
},
},
}
}

Expand Down Expand Up @@ -546,6 +637,71 @@ func TestSearchTagValuesV2_badRequest(t *testing.T) {
})
}

const configMaxConditionGroups = "./config-max-condition-groups.yaml"

// TestSearchTags_maxConditionGroupsExceeded verifies that both the HTTP and gRPC tag search
// endpoints return a 400 / InvalidArgument error when a query expands into more OR condition
// groups than the configured limit.
func TestSearchTags_maxConditionGroupsExceeded(t *testing.T) {
// Set max_condition_groups_per_tag_query to 1 so any OR query with two branches exceeds it.
util.RunIntegrationTests(t, util.TestHarnessConfig{
ConfigOverlay: configMaxConditionGroups,
}, func(h *util.TempoHarness) {
// A query with two OR branches produces 2 condition groups, exceeding the limit of 1.
overLimitQuery := `{ resource.service.name="foo" || resource.service.name="bar" }`

// --- SearchTagsV2 HTTP ---
req, err := http.NewRequest(http.MethodGet,
fmt.Sprintf("%s/api/v2/search/tags?q=%s", h.BaseURL(), url.QueryEscape(overLimitQuery)), nil)
require.NoError(t, err)
Comment thread
ie-pham marked this conversation as resolved.

res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)

// --- SearchTagsV2 gRPC ---
grpcClient, ctx, err := h.APIClientGRPC("")
require.NoError(t, err)

tagsStream, err := grpcClient.SearchTagsV2(ctx, &tempopb.SearchTagsRequest{
Query: overLimitQuery,
})
require.NoError(t, err)

_, err = tagsStream.Recv()
require.Error(t, err)

st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, codes.InvalidArgument, st.Code())

// --- SearchTagValuesV2 HTTP ---
req, err = http.NewRequest(http.MethodGet,
fmt.Sprintf("%s/api/v2/search/tag/resource.service.name/values?q=%s", h.BaseURL(), url.QueryEscape(overLimitQuery)), nil)
require.NoError(t, err)
Comment thread
ie-pham marked this conversation as resolved.

res, err = http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusBadRequest, res.StatusCode)

// --- SearchTagValuesV2 gRPC ---
valuesStream, err := grpcClient.SearchTagValuesV2(ctx, &tempopb.SearchTagValuesRequest{
TagName: "resource.service.name",
Query: overLimitQuery,
})
require.NoError(t, err)

_, err = valuesStream.Recv()
require.Error(t, err)

st, ok = status.FromError(err)
require.True(t, ok)
require.Equal(t, codes.InvalidArgument, st.Code())
})
}

func callSearchTagValuesV2AndAssert(t *testing.T, h *util.TempoHarness, tagName, query string, expected *tempopb.SearchTagValuesV2Response, start, end int64) {
apiClient := h.APIClientHTTP("")
response, err := apiClient.SearchTagValuesV2WithRange(tagName, query, start, end)
Expand Down
4 changes: 4 additions & 0 deletions integration/api/config-max-condition-groups.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
overrides:
defaults:
read:
max_condition_groups_per_tag_query: 1
8 changes: 4 additions & 4 deletions modules/frontend/mcp_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,10 @@ func (s *MCPServer) handleGetAttributeValues(ctx context.Context, request mcp.Ca
}

query := request.GetString("filter-query", "")
if query != "" {
extractedReq := traceql.ExtractFetchRequest(query)
if extractedReq == nil || !extractedReq.AllConditions {
return mcp.NewToolResultError("filter-query invalid. It can only have one spanset and only &&'ed conditions like { <cond> && <cond> && ... }"), nil
if !traceql.IsEmptyQuery(query) {
conditionGroups, _ := traceql.ExtractConditionGroups(query, traceql.DefaultMaxConditionGroupsPerTagQuery)
if len(conditionGroups) == 0 {
return mcp.NewToolResultError("filter-query invalid. It must have a single spanset filter with &&/|| conditions like { <cond> && <cond> } or { <cond> || <cond> }"), nil
}
Comment thread
ie-pham marked this conversation as resolved.
}

Expand Down
2 changes: 1 addition & 1 deletion modules/frontend/mcp_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ func TestHandleGetAttributeValues(t *testing.T) {
"filter-query": "{ foo bar baz }",
}),
expected: expectedResult{
err: "filter-query invalid. It can only have one spanset and only &&'ed conditions like { <cond> && <cond> && ... }",
err: "filter-query invalid. It must have a single spanset filter with &&/|| conditions like { <cond> && <cond> } or { <cond> || <cond> }",
},
},
}
Expand Down
Loading
Loading