Skip to content

[Feature] traceql comparison operators - #6474

Merged
ruslan-mikhailov merged 25 commits into
grafana:mainfrom
ruslan-mikhailov:feature/traceql-comparison
Mar 4, 2026
Merged

[Feature] traceql comparison operators#6474
ruslan-mikhailov merged 25 commits into
grafana:mainfrom
ruslan-mikhailov:feature/traceql-comparison

Conversation

@ruslan-mikhailov

@ruslan-mikhailov ruslan-mikhailov commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

What this PR does: it introduces comparison queries to TraceQL Metrics such as {} | rate() > 10. It is sensitive to step variable, especially to queries like count_over_time().
Examples:
No filters
image
More than 1
image
More than 3. In this example you can see that Grafana connects dots even when NaN is between them. This should be addressed in a separate issue (proposal is here #6486).
image
How it works if Tempo returns NaN (not included in this PR!)
image

Duration
image
Combination with topk
image

Which issue(s) this PR fixes:
Fixes # #6417

Checklist

  • Tests updated
  • Documentation added
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]
@ruslan-mikhailov ruslan-mikhailov changed the title Feature/traceql comparison Feb 16, 2026
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch 5 times, most recently from 68f1bc4 to 21cd34c Compare February 17, 2026 12:39
Comment thread pkg/traceql/expr.y
// Uses == for equality instead of = to distinguish from field-level equality
// **********************
metricsFilterOperation:
EQ { $$ = OpEqual }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I picked = and not == to align with how we compare attributes

@mdisibio mdisibio Feb 25, 2026

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 think starting with consistency is good. If for some reason it's not fitting well, we could change it or support both. Fwiw this operator is likely to not be used much.

Comment thread pkg/traceql/expr.y
%type <scalarFilterOperation> scalarFilterOperation
%type <metricsAggregation> metricsAggregation
%type <metricsSecondStage> metricsSecondStage
%type <scalarFilterOperation> metricsFilterOperation

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

After we agree on syntax, we can add the feature to documentation

@javiermolinar javiermolinar 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.

Nice feature! I missed some benchmarks

Comment thread pkg/traceql/parse_test.go
},
// Combined second stage tests (topk/bottomk with filter)
{
in: `{ } | rate() | topk(5) > 10`,

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.

Can we invert the order?

{ } | rate() > 10 | topk(5) 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In the current solution, it is not possible. I can implement, but do we actually need this feature? 🤔

@javiermolinar javiermolinar Feb 18, 2026

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 guess so? I think allowing both is the best user experience. Right now it will be confussing why we allow one form and not the other

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.

Yes agree, I would expect to be able to use the scalar filter on any part of the pipeline which is metrics. Also the queries { } | rate() | topk(5) > 10 and { } | rate() > 10 | topk(5) are meaningfully different.

I think we could add the scalar filter as another second stage function, so that { } | rate() > 10 | topk(5) < 11 is parsed as:

spanpipeine: { }
first stage: rate()
second stages: > 10, topk(5), < 11

An argument could be made to add the scalar filter inside a stage. Depending on if that has very large implementation benefits.

If this helps, here is a query with multiple metrics stages and regrouping that I would like to support eventually:

{ } | rate() by (pod) > 10 | topk(5) by (namespace) | sum() by (cluster) > 20

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 concur with the above. This feature would benefit from more time on drawing board to ensure more cases are supported, or there is a concrete plan on how to achieve that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added support for { } | rate() > 10 | topk(5) and minor refactoring for better support

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.

This is so close to being generalized for N stages:

// ChainedSecondStage chains two second stage elements together.
// The first element is processed, then its output is passed to the second.
// Example: {status=error} | rate() | topk(5) > 10
type ChainedSecondStage struct {
	first  secondStageElement
	second secondStageElement
}

Encountered this somewhat quickly by trying to do a query with rate() > X | topk(5) < Y. I do think the PR as-is could be confusing to users, and it is worth generalizing. For example it is not clear why a comparison operator works with topk in the first query but not the second, without understanding the current parsing:

rate() | topk(5) < 100
rate() > 0 | topk(5) < 100

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh, I misunderstood you. Added, now it's even more clean

Comment thread pkg/traceql/ast_metrics.go
Comment thread pkg/traceql/ast_metrics.go Outdated

exemplars := make([]Exemplar, 0, len(series.Exemplars))
for i, e := range series.Exemplars {
if m.compare(e.Value) {

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.

We need to guard here against NaN too since NaN != 10 is true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice, catch! Fixed by dropping such exemplars

Comment thread pkg/traceql/expr.y Outdated
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from 06a732d to a5ebfea Compare February 18, 2026 12:19
@ruslan-mikhailov

Copy link
Copy Markdown
Contributor Author

+ review fixes
+ tests for .String()

Comment thread pkg/traceql/parse_test.go
Comment thread pkg/traceql/test_examples.yaml
Comment thread pkg/traceql/test_examples.yaml
Comment thread tempodb/tempodb_metrics_test.go
Comment thread pkg/traceql/engine_metrics_test.go
Comment thread pkg/traceql/ast_metrics.go
Comment thread pkg/traceql/ast_metrics.go
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from a5ebfea to bd1f47c Compare February 23, 2026 10:41
@ruslan-mikhailov

Copy link
Copy Markdown
Contributor Author

+ review fixes

@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from ca50186 to 75aea14 Compare February 23, 2026 13:07
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from 75aea14 to 17b15ff Compare February 23, 2026 14:56
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from 5177cf6 to 858a04a Compare February 25, 2026 08:28
@knylander-grafana

Copy link
Copy Markdown
Contributor

Do we need docs for this?

@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.

This is looking great. 95% lgtm, just want to see if we can update a few small places.

Comment thread pkg/traceql/ast_metrics.go Outdated

for key, series := range input {
hasValue := false
newValues := make([]float64, len(series.Values))

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.

For performance, I think it would be ok to edit the input series here. Is there any case where the same series is reused elsewhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think they should not? Calling Result twice with MetricsFilter will also have the same result due to IsNaN check.

Pushed in a separate commit

Comment thread pkg/traceql/ast_metrics.go Outdated
}

func (c ChainedSecondStage) separator() string {
return c[0].separator()

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.

This isn't used, which makes me consider if separator() shouldn't be part of the interface of individual elements. But part of the second stage, to be used in String() function above only.

i.e. instead of appending directly in expr.y with append($1, $3), we could add a helper that appends with a given separator.

Comment thread pkg/traceql/expr.y Outdated
| spansetPipelineExpression { yylex.(*lexer).expr = newRootExpr($1) }
| scalarPipelineExpressionFilter { yylex.(*lexer).expr = newRootExpr($1) }
| spansetPipeline PIPE metricsAggregation { yylex.(*lexer).expr = newRootExprWithMetrics($1, $3) }
| spansetPipeline PIPE metricsAggregation metricsSecondStagePipeline { yylex.(*lexer).expr = newRootExprWithMetricsTwoStage($1, $3, secondStagePipeline($4)) }

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.

This is fantastic 👍 One of these days we could rename metricsAggregation to metricsFirstStagePipeline and I think it would be very easy to understand.

Comment thread pkg/traceql/test_examples.yaml
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from 858a04a to 33b0c79 Compare March 3, 2026 09:25
@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from 33b0c79 to d0d43a4 Compare March 3, 2026 12:57

@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.

I'm loving this PR. BTW this works too {} | rate() by (resource.service.name) > 20 < 60 🚀

LGTM

@ruslan-mikhailov
ruslan-mikhailov force-pushed the feature/traceql-comparison branch from d0d43a4 to b92c0bf Compare March 4, 2026 09:07
@ruslan-mikhailov

ruslan-mikhailov commented Mar 4, 2026

Copy link
Copy Markdown
Contributor Author

No-op push to retrigger CI pipeline

@ruslan-mikhailov
ruslan-mikhailov enabled auto-merge (squash) March 4, 2026 09:16
@ruslan-mikhailov
ruslan-mikhailov merged commit 3d3f747 into grafana:main Mar 4, 2026
24 checks passed
@ruslan-mikhailov ruslan-mikhailov mentioned this pull request Mar 12, 2026
3 tasks
mattdurham pushed a commit to mattdurham/tempo that referenced this pull request Jun 18, 2026
Introduce comparison operators to TraceQL Metrics.

Examples:
{} | rate() by (resource.service.name) < 0.01
{} | avg_over_time(span:duration) > 1s
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

6 participants