Skip to content

Commit 930de2f

Browse files
committed
make the minStepDuration of the querier configurable per tenant from limits configuration
1 parent c0df078 commit 930de2f

File tree

8 files changed

+121
-16
lines changed

8 files changed

+121
-16
lines changed

pkg/api/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ func (a *API) RegisterFeatureFlagsServiceHandler(svc capabilitiesv1connect.Featu
232232
capabilitiesv1connect.RegisterFeatureFlagsServiceHandler(a.server.HTTP, svc, a.connectOptionsAuthLogRecovery()...)
233233
}
234234

235-
func (a *API) RegisterPyroscopeHandlers(client querierv1connect.QuerierServiceClient) {
236-
handlers := querier.NewHTTPHandlers(client)
235+
func (a *API) RegisterPyroscopeHandlers(client querierv1connect.QuerierServiceClient, limits querier.Limits) {
236+
handlers := querier.NewHTTPHandlers(client, limits)
237237
a.RegisterRoute("/pyroscope/render", http.HandlerFunc(handlers.Render), a.registerOptionsReadPath()...)
238238
a.RegisterRoute("/pyroscope/render-diff", http.HandlerFunc(handlers.RenderDiff), a.registerOptionsReadPath()...)
239239
a.RegisterRoute("/pyroscope/label-values", http.HandlerFunc(handlers.LabelValues), a.registerOptionsReadPath()...)

pkg/pyroscope/modules.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ func (f *Pyroscope) initQuerier() (services.Service, error) {
262262
}
263263

264264
if !f.isModuleActive(QueryFrontend) {
265-
f.API.RegisterPyroscopeHandlers(querierSvc)
265+
f.API.RegisterPyroscopeHandlers(querierSvc, f.Overrides)
266266
f.API.RegisterQuerierServiceHandler(querierSvc)
267267
}
268268

pkg/pyroscope/modules_experimental.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (f *Pyroscope) initQueryFrontendV1() (services.Service, error) {
8282
}
8383
f.API.RegisterFrontendForQuerierHandler(f.frontend)
8484
f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger))
85-
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger))
85+
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger), f.Overrides)
8686
f.API.RegisterVCSServiceHandler(f.frontend)
8787
return f.frontend, nil
8888
}
@@ -104,7 +104,7 @@ func (f *Pyroscope) initQueryFrontendV2() (services.Service, error) {
104104
)
105105

106106
f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger))
107-
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger))
107+
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger), f.Overrides)
108108
f.API.RegisterVCSServiceHandler(vcsService)
109109

110110
// New query frontend does not have any state.
@@ -148,7 +148,7 @@ func (f *Pyroscope) initQueryFrontendV12() (services.Service, error) {
148148

149149
f.API.RegisterFrontendForQuerierHandler(f.frontend)
150150
f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger))
151-
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger))
151+
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger), f.Overrides)
152152
f.API.RegisterVCSServiceHandler(vcsService)
153153

154154
return f.frontend, nil

pkg/querier/http.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"connectrpc.com/connect"
1313
"github.com/gogo/status"
1414
"github.com/google/pprof/profile"
15+
"github.com/grafana/dskit/tenant"
1516
"github.com/prometheus/common/model"
1617
"github.com/prometheus/prometheus/model/labels"
1718
"github.com/prometheus/prometheus/promql/parser"
@@ -31,12 +32,16 @@ import (
3132
httputil "github.com/grafana/pyroscope/pkg/util/http"
3233
)
3334

34-
func NewHTTPHandlers(client querierv1connect.QuerierServiceClient) *QueryHandlers {
35-
return &QueryHandlers{client}
35+
func NewHTTPHandlers(client querierv1connect.QuerierServiceClient, limits Limits) *QueryHandlers {
36+
return &QueryHandlers{
37+
client: client,
38+
limits: limits,
39+
}
3640
}
3741

3842
type QueryHandlers struct {
3943
client querierv1connect.QuerierServiceClient
44+
limits Limits
4045
}
4146

4247
// LabelValues only returns the label values for the given label name.
@@ -186,7 +191,13 @@ func (q *QueryHandlers) Render(w http.ResponseWriter, req *http.Request) {
186191
return err
187192
})
188193

189-
timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End)
194+
// Get tenant-specific min step duration from limits
195+
tenantID, err := tenant.TenantID(req.Context())
196+
if err != nil {
197+
tenantID = "" // Use default limits if tenant ID cannot be extracted
198+
}
199+
minStepDuration := q.limits.MinStepDuration(tenantID)
200+
timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End, minStepDuration)
190201
var resSeries *connect.Response[querierv1.SelectSeriesResponse]
191202
g.Go(func() error {
192203
var err error

pkg/querier/querier.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func (cfg *Config) RegisterFlags(fs *flag.FlagSet) {
5757

5858
type Limits interface {
5959
QueryAnalysisSeriesEnabled(string) bool
60+
MinStepDuration(string) time.Duration
6061
}
6162

6263
type Querier struct {

pkg/querier/timeline/calculator.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,30 @@ import (
77
)
88

99
var (
10-
DefaultRes int64 = 1500
11-
DefaultMinInterval = time.Second * 15
10+
DefaultRes int64 = 1500
1211
)
1312

1413
// CalcPointInterval calculates the appropriate interval between each point (aka step)
1514
// Note that its main usage is with SelectSeries, therefore its
1615
// * inputs are in ms
1716
// * output is in seconds
18-
func CalcPointInterval(fromMs int64, untilMs int64) float64 {
17+
func CalcPointInterval(fromMs int64, untilMs int64, minStepDuration time.Duration) float64 {
1918
resolution := DefaultRes
2019

2120
fromNano := fromMs * 1000000
2221
untilNano := untilMs * 1000000
2322
calculatedIntervalNano := time.Duration((untilNano - fromNano) / resolution)
2423

25-
if calculatedIntervalNano < DefaultMinInterval {
26-
return DefaultMinInterval.Seconds()
24+
if calculatedIntervalNano < minStepDuration {
25+
return minStepDuration.Seconds()
2726
}
2827

2928
return roundInterval(calculatedIntervalNano).Seconds()
3029
}
3130

3231
//nolint:gocyclo
3332
func roundInterval(interval time.Duration) time.Duration {
34-
// Notice that interval may be smaller than DefaultMinInterval, and therefore some branches may never be reached
33+
// Some branches may never be reached depending on the minimum interval configured
3534
// These branches are left in case the invariant changes
3635
switch {
3736
// 0.01s

pkg/querier/timeline/calculator_test.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
func Test_CalcPointInterval(t *testing.T) {
1313
TestDate := time.Date(2023, time.April, 18, 1, 2, 3, 4, time.UTC)
14+
defaultMinStepDuration := 15 * time.Second
1415

1516
testCases := []struct {
1617
name string
@@ -31,10 +32,94 @@ func Test_CalcPointInterval(t *testing.T) {
3132

3233
for _, tc := range testCases {
3334
t.Run(tc.name, func(t *testing.T) {
34-
got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli())
35+
got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli(), defaultMinStepDuration)
3536

3637
assert.Equal(t, float64(tc.want), got)
3738
})
3839
}
3940

4041
}
42+
43+
func Test_CalcPointInterval_WithCustomMinStepDuration(t *testing.T) {
44+
TestDate := time.Date(2023, time.April, 18, 1, 2, 3, 4, time.UTC)
45+
46+
testCases := []struct {
47+
name string
48+
start time.Time
49+
end time.Time
50+
minStepDuration time.Duration
51+
want float64
52+
}{
53+
{
54+
name: "1 second with 5s min step duration",
55+
start: TestDate,
56+
end: TestDate.Add(1 * time.Second),
57+
minStepDuration: 5 * time.Second,
58+
want: 5.0,
59+
},
60+
{
61+
name: "1 second with 30s min step duration",
62+
start: TestDate,
63+
end: TestDate.Add(1 * time.Second),
64+
minStepDuration: 30 * time.Second,
65+
want: 30.0,
66+
},
67+
{
68+
name: "1 hour with 5s min step duration",
69+
start: TestDate,
70+
end: TestDate.Add(1 * time.Hour),
71+
minStepDuration: 5 * time.Second,
72+
want: 5.0,
73+
},
74+
{
75+
name: "1 hour with 1m min step duration",
76+
start: TestDate,
77+
end: TestDate.Add(1 * time.Hour),
78+
minStepDuration: 1 * time.Minute,
79+
want: 60.0,
80+
},
81+
{
82+
name: "7 days with 1m min step duration",
83+
start: TestDate,
84+
end: TestDate.Add(7 * 24 * time.Hour),
85+
minStepDuration: 1 * time.Minute,
86+
want: 300.0, // calculated interval is 5m, which is > 1m min
87+
},
88+
{
89+
name: "7 days with 10m min step duration",
90+
start: TestDate,
91+
end: TestDate.Add(7 * 24 * time.Hour),
92+
minStepDuration: 10 * time.Minute,
93+
want: 600.0, // min step duration enforced (10m)
94+
},
95+
{
96+
name: "30 days with default min step duration",
97+
start: TestDate,
98+
end: TestDate.Add(30 * 24 * time.Hour),
99+
minStepDuration: 15 * time.Second,
100+
want: 1800.0, // calculated interval is 30m
101+
},
102+
{
103+
name: "30 days with 1h min step duration",
104+
start: TestDate,
105+
end: TestDate.Add(30 * 24 * time.Hour),
106+
minStepDuration: 1 * time.Hour,
107+
want: 3600.0, // min step duration enforced (1h)
108+
},
109+
{
110+
name: "1 year with 5m min step duration",
111+
start: TestDate,
112+
end: TestDate.Add(365 * 24 * time.Hour),
113+
minStepDuration: 5 * time.Minute,
114+
want: 21600.0, // calculated interval is 6h
115+
},
116+
}
117+
118+
for _, tc := range testCases {
119+
t.Run(tc.name, func(t *testing.T) {
120+
got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli(), tc.minStepDuration)
121+
122+
assert.Equal(t, tc.want, got, "expected %v seconds, got %v seconds", tc.want, got)
123+
})
124+
}
125+
}

pkg/validation/limits.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type Limits struct {
8585
MaxQueryParallelism int `yaml:"max_query_parallelism" json:"max_query_parallelism"`
8686
QueryAnalysisEnabled bool `yaml:"query_analysis_enabled" json:"query_analysis_enabled"`
8787
QueryAnalysisSeriesEnabled bool `yaml:"query_analysis_series_enabled" json:"query_analysis_series_enabled"`
88+
MinStepDuration model.Duration `yaml:"min_step_duration" json:"min_step_duration"`
8889

8990
// Flame graph enforced limits.
9091
MaxFlameGraphNodesDefault int `yaml:"max_flamegraph_nodes_default" json:"max_flamegraph_nodes_default"`
@@ -178,6 +179,9 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) {
178179
f.BoolVar(&l.QueryAnalysisEnabled, "querier.query-analysis-enabled", true, "Whether query analysis is enabled in the query frontend. If disabled, the /AnalyzeQuery endpoint will return an empty response.")
179180
f.BoolVar(&l.QueryAnalysisSeriesEnabled, "querier.query-analysis-series-enabled", false, "Whether the series portion of query analysis is enabled. If disabled, no series data (e.g., series count) will be calculated by the /AnalyzeQuery endpoint.")
180181

182+
_ = l.MinStepDuration.Set("15s")
183+
f.Var(&l.MinStepDuration, "querier.min-step-duration", "The minimum step duration for range queries.")
184+
181185
f.IntVar(&l.MaxProfileSizeBytes, "validation.max-profile-size-bytes", 4*1024*1024, "Maximum size of a profile in bytes. This is based off the uncompressed size. 0 to disable.")
182186
f.IntVar(&l.MaxProfileStacktraceSamples, "validation.max-profile-stacktrace-samples", 16000, "Maximum number of samples in a profile. 0 to disable.")
183187
f.IntVar(&l.MaxProfileStacktraceSampleLabels, "validation.max-profile-stacktrace-sample-labels", 100, "Maximum number of labels in a profile sample. 0 to disable.")
@@ -560,6 +564,11 @@ func (o *Overrides) QueryAnalysisSeriesEnabled(tenantID string) bool {
560564
return o.getOverridesForTenant(tenantID).QueryAnalysisSeriesEnabled
561565
}
562566

567+
// MinStepDuration returns the minimum step duration for range queries.
568+
func (o *Overrides) MinStepDuration(tenantID string) time.Duration {
569+
return time.Duration(o.getOverridesForTenant(tenantID).MinStepDuration)
570+
}
571+
563572
func (o *Overrides) WritePathOverrides(tenantID string) writepath.Config {
564573
return o.getOverridesForTenant(tenantID).WritePathOverrides
565574
}

0 commit comments

Comments
 (0)