Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 cmd/pyroscope/help-all.txt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ Usage of ./pyroscope:
Limit how far back in profiling data can be queried, up until lookback duration ago. This limit is enforced in the query frontend. If the requested time range is outside the allowed range, the request will not fail, but will be modified to only query data within the allowed time range. 0 to disable, default to 7d. (default 1w)
-querier.max-query-parallelism int
Maximum number of queries that will be scheduled in parallel by the frontend.
-querier.min-step-duration duration
The minimum step duration for range queries. (default 15s)
-querier.query-analysis-enabled
Whether query analysis is enabled in the query frontend. If disabled, the /AnalyzeQuery endpoint will return an empty response. (default true)
-querier.query-analysis-series-enabled
Expand Down
2 changes: 2 additions & 0 deletions cmd/pyroscope/help.txt.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ Usage of ./pyroscope:
Limit how far back in profiling data can be queried, up until lookback duration ago. This limit is enforced in the query frontend. If the requested time range is outside the allowed range, the request will not fail, but will be modified to only query data within the allowed time range. 0 to disable, default to 7d. (default 1w)
-querier.max-query-parallelism int
Maximum number of queries that will be scheduled in parallel by the frontend.
-querier.min-step-duration duration
The minimum step duration for range queries. (default 15s)
-querier.query-analysis-enabled
Whether query analysis is enabled in the query frontend. If disabled, the /AnalyzeQuery endpoint will return an empty response. (default true)
-querier.query-analysis-series-enabled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2293,6 +2293,10 @@ distributor_usage_groups:
# CLI flag: -querier.query-analysis-series-enabled
[query_analysis_series_enabled: <boolean> | default = false]

# The minimum step duration for range queries.
# CLI flag: -querier.min-step-duration
[min_step_duration: <duration> | default = 15s]

# Maximum number of flame graph nodes by default. 0 to disable.
# CLI flag: -querier.max-flamegraph-nodes-default
[max_flamegraph_nodes_default: <int> | default = 8192]
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,8 @@ func (a *API) RegisterFeatureFlagsServiceHandler(svc capabilitiesv1connect.Featu
capabilitiesv1connect.RegisterFeatureFlagsServiceHandler(a.server.HTTP, svc, a.connectOptionsAuthLogRecovery()...)
}

func (a *API) RegisterPyroscopeHandlers(client querierv1connect.QuerierServiceClient) {
handlers := querier.NewHTTPHandlers(client)
func (a *API) RegisterPyroscopeHandlers(client querierv1connect.QuerierServiceClient, limits querier.Limits) {
handlers := querier.NewHTTPHandlers(client, limits)
a.RegisterRoute("/pyroscope/render", http.HandlerFunc(handlers.Render), a.registerOptionsReadPath()...)
a.RegisterRoute("/pyroscope/render-diff", http.HandlerFunc(handlers.RenderDiff), a.registerOptionsReadPath()...)
a.RegisterRoute("/pyroscope/label-values", http.HandlerFunc(handlers.LabelValues), a.registerOptionsReadPath()...)
Expand Down
2 changes: 1 addition & 1 deletion pkg/pyroscope/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func (f *Pyroscope) initQuerier() (services.Service, error) {
}

if !f.isModuleActive(QueryFrontend) {
f.API.RegisterPyroscopeHandlers(querierSvc)
f.API.RegisterPyroscopeHandlers(querierSvc, f.Overrides)
f.API.RegisterQuerierServiceHandler(querierSvc)
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/pyroscope/modules_experimental.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (f *Pyroscope) initQueryFrontendV1() (services.Service, error) {
}
f.API.RegisterFrontendForQuerierHandler(f.frontend)
f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger))
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger))
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(f.frontend, queryFrontendLogger), f.Overrides)
f.API.RegisterVCSServiceHandler(f.frontend)
return f.frontend, nil
}
Expand All @@ -104,7 +104,7 @@ func (f *Pyroscope) initQueryFrontendV2() (services.Service, error) {
)

f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger))
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger))
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(queryFrontend, queryFrontendLogger), f.Overrides)
f.API.RegisterVCSServiceHandler(vcsService)

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

f.API.RegisterFrontendForQuerierHandler(f.frontend)
f.API.RegisterQuerierServiceHandler(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger))
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger))
f.API.RegisterPyroscopeHandlers(spanlogger.NewLogSpanParametersWrapper(handler, queryFrontendLogger), f.Overrides)
f.API.RegisterVCSServiceHandler(vcsService)

return f.frontend, nil
Expand Down
17 changes: 14 additions & 3 deletions pkg/querier/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"connectrpc.com/connect"
"github.com/gogo/status"
"github.com/google/pprof/profile"
"github.com/grafana/dskit/tenant"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
Expand All @@ -31,12 +32,16 @@ import (
httputil "github.com/grafana/pyroscope/pkg/util/http"
)

func NewHTTPHandlers(client querierv1connect.QuerierServiceClient) *QueryHandlers {
return &QueryHandlers{client}
func NewHTTPHandlers(client querierv1connect.QuerierServiceClient, limits Limits) *QueryHandlers {
return &QueryHandlers{
client: client,
limits: limits,
}
}

type QueryHandlers struct {
client querierv1connect.QuerierServiceClient
limits Limits
}

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

timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End)
// Get tenant-specific min step duration from limits
tenantID, err := tenant.TenantID(req.Context())
if err != nil {
tenantID = "" // Use default limits if tenant ID cannot be extracted
}
minStepDuration := q.limits.MinStepDuration(tenantID)
timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End, minStepDuration)
var resSeries *connect.Response[querierv1.SelectSeriesResponse]
g.Go(func() error {
var err error
Expand Down
1 change: 1 addition & 0 deletions pkg/querier/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func (cfg *Config) RegisterFlags(fs *flag.FlagSet) {

type Limits interface {
QueryAnalysisSeriesEnabled(string) bool
MinStepDuration(string) time.Duration
}

type Querier struct {
Expand Down
11 changes: 5 additions & 6 deletions pkg/querier/timeline/calculator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,30 @@ import (
)

var (
DefaultRes int64 = 1500
DefaultMinInterval = time.Second * 15
DefaultRes int64 = 1500
)

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

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

if calculatedIntervalNano < DefaultMinInterval {
return DefaultMinInterval.Seconds()
if calculatedIntervalNano < minStepDuration {
return minStepDuration.Seconds()
}

return roundInterval(calculatedIntervalNano).Seconds()
}

//nolint:gocyclo
func roundInterval(interval time.Duration) time.Duration {
// Notice that interval may be smaller than DefaultMinInterval, and therefore some branches may never be reached
// Some branches may never be reached depending on the minimum interval configured
// These branches are left in case the invariant changes
switch {
// 0.01s
Expand Down
87 changes: 86 additions & 1 deletion pkg/querier/timeline/calculator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

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

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

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli())
got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli(), defaultMinStepDuration)

assert.Equal(t, float64(tc.want), got)
})
}

}

func Test_CalcPointInterval_WithCustomMinStepDuration(t *testing.T) {
TestDate := time.Date(2023, time.April, 18, 1, 2, 3, 4, time.UTC)

testCases := []struct {
name string
start time.Time
end time.Time
minStepDuration time.Duration
want float64
}{
{
name: "1 second with 5s min step duration",
start: TestDate,
end: TestDate.Add(1 * time.Second),
minStepDuration: 5 * time.Second,
want: 5.0,
},
{
name: "1 second with 30s min step duration",
start: TestDate,
end: TestDate.Add(1 * time.Second),
minStepDuration: 30 * time.Second,
want: 30.0,
},
{
name: "1 hour with 5s min step duration",
start: TestDate,
end: TestDate.Add(1 * time.Hour),
minStepDuration: 5 * time.Second,
want: 5.0,
},
{
name: "1 hour with 1m min step duration",
start: TestDate,
end: TestDate.Add(1 * time.Hour),
minStepDuration: 1 * time.Minute,
want: 60.0,
},
{
name: "7 days with 1m min step duration",
start: TestDate,
end: TestDate.Add(7 * 24 * time.Hour),
minStepDuration: 1 * time.Minute,
want: 300.0, // calculated interval is 5m, which is > 1m min
},
{
name: "7 days with 10m min step duration",
start: TestDate,
end: TestDate.Add(7 * 24 * time.Hour),
minStepDuration: 10 * time.Minute,
want: 600.0, // min step duration enforced (10m)
},
{
name: "30 days with default min step duration",
start: TestDate,
end: TestDate.Add(30 * 24 * time.Hour),
minStepDuration: 15 * time.Second,
want: 1800.0, // calculated interval is 30m
},
{
name: "30 days with 1h min step duration",
start: TestDate,
end: TestDate.Add(30 * 24 * time.Hour),
minStepDuration: 1 * time.Hour,
want: 3600.0, // min step duration enforced (1h)
},
{
name: "1 year with 5m min step duration",
start: TestDate,
end: TestDate.Add(365 * 24 * time.Hour),
minStepDuration: 5 * time.Minute,
want: 21600.0, // calculated interval is 6h
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := timeline.CalcPointInterval(tc.start.UnixMilli(), tc.end.UnixMilli(), tc.minStepDuration)

assert.Equal(t, tc.want, got, "expected %v seconds, got %v seconds", tc.want, got)
})
}
}
9 changes: 9 additions & 0 deletions pkg/validation/limits.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type Limits struct {
MaxQueryParallelism int `yaml:"max_query_parallelism" json:"max_query_parallelism"`
QueryAnalysisEnabled bool `yaml:"query_analysis_enabled" json:"query_analysis_enabled"`
QueryAnalysisSeriesEnabled bool `yaml:"query_analysis_series_enabled" json:"query_analysis_series_enabled"`
MinStepDuration model.Duration `yaml:"min_step_duration" json:"min_step_duration"`

// Flame graph enforced limits.
MaxFlameGraphNodesDefault int `yaml:"max_flamegraph_nodes_default" json:"max_flamegraph_nodes_default"`
Expand Down Expand Up @@ -178,6 +179,9 @@ func (l *Limits) RegisterFlags(f *flag.FlagSet) {
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.")
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.")

_ = l.MinStepDuration.Set("15s")
f.Var(&l.MinStepDuration, "querier.min-step-duration", "The minimum step duration for range queries.")

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.")
f.IntVar(&l.MaxProfileStacktraceSamples, "validation.max-profile-stacktrace-samples", 16000, "Maximum number of samples in a profile. 0 to disable.")
f.IntVar(&l.MaxProfileStacktraceSampleLabels, "validation.max-profile-stacktrace-sample-labels", 100, "Maximum number of labels in a profile sample. 0 to disable.")
Expand Down Expand Up @@ -560,6 +564,11 @@ func (o *Overrides) QueryAnalysisSeriesEnabled(tenantID string) bool {
return o.getOverridesForTenant(tenantID).QueryAnalysisSeriesEnabled
}

// MinStepDuration returns the minimum step duration for range queries.
func (o *Overrides) MinStepDuration(tenantID string) time.Duration {
return time.Duration(o.getOverridesForTenant(tenantID).MinStepDuration)
}

func (o *Overrides) WritePathOverrides(tenantID string) writepath.Config {
return o.getOverridesForTenant(tenantID).WritePathOverrides
}
Expand Down