Skip to content

Commit a078f64

Browse files
committed
address pr comments
1 parent 648cb56 commit a078f64

File tree

5 files changed

+50
-28
lines changed

5 files changed

+50
-28
lines changed

modules/frontend/metrics_query_range_handler.go

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ func newQueryRangeStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripp
5151
return err
5252
}
5353

54+
// Normalize exemplars before combiner creation: 0 (unspecified) or above the configured
55+
// maximum defaults to MaxExemplars.
56+
if req.Exemplars == 0 || req.Exemplars > cfg.Metrics.Sharder.MaxExemplars {
57+
req.Exemplars = cfg.Metrics.Sharder.MaxExemplars
58+
}
59+
5460
traceql.AlignRequest(req)
5561

5662
// the end time cutoff is applied here because it has to be done before combiner creation
@@ -75,14 +81,6 @@ func newQueryRangeStreamingGRPCHandler(cfg Config, next pipeline.AsyncRoundTripp
7581
tenant, _ := user.ExtractOrgID(ctx)
7682
start := time.Now()
7783

78-
// Normalize exemplars before combiner creation: 0 (unspecified) or above the configured
79-
// maximum defaults to MaxExemplars. The sharder applies the same normalization to sub-requests,
80-
// but runs after the combiner is created, so we must mirror it here.
81-
if cfg.Metrics.Sharder.MaxExemplars > 0 {
82-
if req.Exemplars == 0 || int(req.Exemplars) > cfg.Metrics.Sharder.MaxExemplars {
83-
req.Exemplars = uint32(cfg.Metrics.Sharder.MaxExemplars) //nolint:gosec // G115
84-
}
85-
}
8684
var finalResponse *tempopb.QueryRangeResponse
8785
c, err := combiner.NewTypedQueryRange(req, cfg.Metrics.Sharder.MaxResponseSeries)
8886
if err != nil {
@@ -141,6 +139,12 @@ func newMetricsQueryRangeHTTPHandler(cfg Config, next pipeline.AsyncRoundTripper
141139
return httpInvalidRequest(err), nil
142140
}
143141

142+
// Normalize exemplars before combiner creation: 0 (unspecified) or above the configured
143+
// maximum defaults to MaxExemplars.
144+
if queryRangeReq.Exemplars == 0 || queryRangeReq.Exemplars > cfg.Metrics.Sharder.MaxExemplars {
145+
queryRangeReq.Exemplars = cfg.Metrics.Sharder.MaxExemplars
146+
}
147+
144148
traceql.AlignRequest(queryRangeReq)
145149

146150
// the end time cutoff is applied here because it has to be done before combiner creation
@@ -156,14 +160,6 @@ func newMetricsQueryRangeHTTPHandler(cfg Config, next pipeline.AsyncRoundTripper
156160
}
157161
req = api.BuildQueryRangeRequest(req, queryRangeReq, "")
158162

159-
// Normalize exemplars before combiner creation: 0 (unspecified) or above the configured
160-
// maximum defaults to MaxExemplars. The sharder applies the same normalization to sub-requests,
161-
// but runs after the combiner is created, so we must mirror it here.
162-
if cfg.Metrics.Sharder.MaxExemplars > 0 {
163-
if queryRangeReq.Exemplars == 0 || int(queryRangeReq.Exemplars) > cfg.Metrics.Sharder.MaxExemplars {
164-
queryRangeReq.Exemplars = uint32(cfg.Metrics.Sharder.MaxExemplars) //nolint:gosec // G115
165-
}
166-
}
167163
// build and use roundtripper
168164
combiner, err := combiner.NewTypedQueryRange(queryRangeReq, cfg.Metrics.Sharder.MaxResponseSeries)
169165
if err != nil {

modules/frontend/metrics_query_range_handler_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,26 @@ func TestQueryRangeHandlerExemplarNormalization(t *testing.T) {
592592
}
593593
})
594594

595+
t.Run("client requests exemplars but MaxExemplars is zero disables them", func(t *testing.T) {
596+
f := frontendWithSettings(t, &mockRoundTripper{
597+
responseFn: func() proto.Message { return mockResp },
598+
}, nil, nil, nil, func(c *Config, _ *overrides.Config) {
599+
c.Metrics.Sharder.Interval = time.Hour
600+
c.Metrics.Sharder.MaxExemplars = 0
601+
})
602+
603+
httpResp := httptest.NewRecorder()
604+
f.MetricsQueryRangeHandler.ServeHTTP(httpResp, makeRequest(5)) // client requests exemplars
605+
require.Equal(t, 200, httpResp.Code)
606+
607+
actualResp := &tempopb.QueryRangeResponse{}
608+
require.NoError(t, jsonpb.Unmarshal(httpResp.Body, actualResp))
609+
610+
for _, s := range actualResp.Series {
611+
assert.Empty(t, s.Exemplars, "exemplars should be empty when MaxExemplars is zero even if client requests them")
612+
}
613+
})
614+
595615
t.Run("client-specified exemplars capped to cfg.MaxExemplars", func(t *testing.T) {
596616
f := frontendWithSettings(t, &mockRoundTripper{
597617
responseFn: func() proto.Message { return mockResp },

modules/frontend/metrics_query_range_sharder.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type QueryRangeSharderConfig struct {
4848
// QueryBackendAfter determines when to query backend storage vs ingesters only.
4949
QueryBackendAfter time.Duration `yaml:"query_backend_after,omitempty"`
5050
Interval time.Duration `yaml:"interval,omitempty"`
51-
MaxExemplars int `yaml:"max_exemplars,omitempty"`
51+
MaxExemplars uint32 `yaml:"max_exemplars,omitempty"`
5252
MaxResponseSeries int `yaml:"max_response_series,omitempty"`
5353
StreamingShards int `yaml:"streaming_shards,omitempty"`
5454
}
@@ -115,16 +115,11 @@ func (s queryRangeSharder) RoundTrip(pipelineRequest pipeline.Request) (pipeline
115115
}
116116

117117
traceql.AlignRequest(req)
118-
119-
var maxExemplars uint32
118+
120119
// Instant queries must not compute exemplars
121-
if !s.instantMode && s.cfg.MaxExemplars > 0 {
122-
maxExemplars = req.Exemplars
123-
if maxExemplars == 0 || maxExemplars > uint32(s.cfg.MaxExemplars) {
124-
maxExemplars = uint32(s.cfg.MaxExemplars) // Enforce configuration
125-
}
120+
if s.instantMode {
121+
req.Exemplars = 0
126122
}
127-
req.Exemplars = maxExemplars
128123

129124
// if a limit is being enforced, honor the request if it is less than the limit
130125
// else set it to max limit

modules/frontend/metrics_query_range_sharder_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,17 @@ func TestExemplarsCutoff(t *testing.T) {
307307
expectedBeforeCut uint32
308308
expectedAfterCut uint32
309309
}{
310+
{
311+
// Instant queries zero exemplars before calling exemplarsCutoff; both sides must be 0.
312+
name: "zero exemplars (instant mode) spanning cutoff",
313+
req: tempopb.QueryRangeRequest{
314+
Start: uint64(cutoff.Add(-20 * time.Minute).UnixNano()),
315+
End: uint64(now.UnixNano()),
316+
Exemplars: 0,
317+
},
318+
expectedBeforeCut: 0,
319+
expectedAfterCut: 0,
320+
},
310321
{
311322
// When all data is after the cutoff, all exemplars should go to the 'after' portion
312323
name: "all data after cutoff",

pkg/traceql/engine_metrics_average.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ type averageSeries struct {
202202
Exemplars []Exemplar
203203
}
204204

205-
func newAverageSeries(l int, lenExemplars int, labels Labels) averageSeries {
205+
func newAverageSeries(l int, lenExemplars uint32, labels Labels) averageSeries {
206206
s := averageSeries{
207207
values: make([]averageValue, l),
208208
labels: labels,
@@ -301,7 +301,7 @@ func (b *averageOverTimeSeriesAggregator) Combine(in []*tempopb.TimeSeries) {
301301
countPosMapper[key] = i
302302
} else if !ok {
303303
lbls := getLabels(ts.Labels, "")
304-
s := newAverageSeries(b.len, len(ts.Exemplars), lbls)
304+
s := newAverageSeries(b.len, uint32(len(ts.Exemplars)), lbls)
305305
b.weightedAverageSeries[key] = &s
306306
}
307307
}
@@ -583,7 +583,7 @@ func (g *avgOverTimeSpanAggregator[F, S]) getSeries(span Span) avgOverTimeSeries
583583
intervals := g.intervalMapper.IntervalCount()
584584
s = avgOverTimeSeries[S]{
585585
vals: g.buf.vals,
586-
average: newAverageSeries(intervals, int(g.exemplars), nil),
586+
average: newAverageSeries(intervals, g.exemplars, nil),
587587
exemplarBuckets: newExemplarBucketSet(g.exemplars, g.start, g.end, g.step, g.instant),
588588
initialized: true,
589589
}

0 commit comments

Comments
 (0)