Skip to content

Commit 67fe233

Browse files
committed
Improve resolution of gcp metrics, enable $__interval $__rate_interval
1 parent cc663aa commit 67fe233

4 files changed

Lines changed: 195 additions & 24 deletions

File tree

gcp/cloudmonitoring/time_series_fetcher.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,35 @@ package cloudmonitoring
22

33
import (
44
"context"
5-
"github.com/nullstone-io/deployment-sdk/prometheus"
6-
"github.com/nullstone-io/deployment-sdk/workspace"
75
"sync"
86
"time"
7+
8+
"github.com/nullstone-io/deployment-sdk/prometheus"
9+
"github.com/nullstone-io/deployment-sdk/workspace"
910
)
1011

1112
// TimeSeriesFetcherFromMapping creates an interface for fetching the metrics in a background goroutine
12-
// This fetcher utilizes the QueryClient to fetch metrics using MQL
13+
// This fetcher uses the QueryClient to fetch metrics using MQL
1314
func TimeSeriesFetcherFromMapping(mapping MetricMapping, options workspace.MetricsGetterOptions, series *workspace.MetricSeries) *TimeSeriesFetcher {
14-
interval := CalcPeriod(options.StartTime, options.EndTime)
15-
steps := int(interval / time.Second)
16-
qo := prometheus.QueryOptions{
17-
Start: options.StartTime,
18-
End: options.EndTime,
19-
Step: steps,
15+
end := time.Now()
16+
start := end.Add(-time.Hour)
17+
if options.StartTime != nil {
18+
start = *options.StartTime
2019
}
21-
if qo.Start == nil {
22-
start := time.Now().Add(-time.Hour)
23-
qo.Start = &start
20+
if options.EndTime != nil {
21+
end = *options.EndTime
2422
}
2523

24+
qo := prometheus.QueryOptions{
25+
Start: start,
26+
End: end,
27+
IntervalCalculator: prometheus.NewIntervalCalculator(prometheus.IntervalOptions{
28+
Start: start,
29+
End: end,
30+
PanelWidth: options.PanelWidth,
31+
ScrapeInterval: options.ScrapeInterval,
32+
}),
33+
}
2634
return &TimeSeriesFetcher{
2735
Query: mapping.Query,
2836
Options: qo,

prometheus/interval_calculator.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package prometheus
2+
3+
import (
4+
"fmt"
5+
"math"
6+
"regexp"
7+
"time"
8+
)
9+
10+
const (
11+
DefaultPanelWidth = 1200
12+
DefaultScrapeInterval = 60
13+
)
14+
15+
type IntervalOptions struct {
16+
Start time.Time
17+
End time.Time
18+
// PanelWidth refers to the width of the panel, measured in pixels
19+
// This is used to
20+
PanelWidth int
21+
// ScrapeInterval refers to the interval at which the metric is scraped, measured in number of seconds
22+
ScrapeInterval int
23+
// MaxDataPoints refers to the maximum number of data points to plot (if 0, use PanelWidth)
24+
MaxDataPoints int
25+
// MinInterval refers to the minimum interval to use (if 0, use ScrapeInterval)
26+
MinInterval int
27+
}
28+
29+
// IntervalCalculator handles interval calculations and query substitution
30+
type IntervalCalculator struct {
31+
interval int // calculated $__interval in seconds
32+
rateInterval int // calculated $__rate_interval in seconds
33+
}
34+
35+
// NewIntervalCalculator creates a new calculator with the given parameters
36+
func NewIntervalCalculator(opts IntervalOptions) *IntervalCalculator {
37+
calc := &IntervalCalculator{}
38+
calc.calculate(opts)
39+
return calc
40+
}
41+
42+
// calculate computes $__interval and $__rate_interval
43+
func (c *IntervalCalculator) calculate(opts IntervalOptions) {
44+
// Set defaults
45+
width := opts.PanelWidth
46+
if width == 0 {
47+
width = DefaultPanelWidth
48+
}
49+
50+
scrapeInterval := opts.ScrapeInterval
51+
if scrapeInterval == 0 {
52+
scrapeInterval = DefaultScrapeInterval
53+
}
54+
55+
maxDataPoints := opts.MaxDataPoints
56+
if maxDataPoints == 0 {
57+
maxDataPoints = width
58+
}
59+
60+
minInterval := opts.MinInterval
61+
if minInterval == 0 {
62+
minInterval = scrapeInterval
63+
}
64+
65+
// Calculate range in seconds
66+
rangeSeconds := int(opts.End.Sub(opts.Start).Seconds())
67+
68+
// Calculate $__interval (basic interval)
69+
// interval = range / desired_points
70+
interval := rangeSeconds / maxDataPoints
71+
72+
// Ensure minimum interval
73+
if interval < minInterval {
74+
interval = minInterval
75+
}
76+
77+
c.interval = interval
78+
79+
// Calculate $__rate_interval
80+
// rate_interval = max(interval + scrape_interval, 4 * scrape_interval)
81+
optionA := interval + scrapeInterval
82+
optionB := 4 * scrapeInterval
83+
84+
c.rateInterval = int(math.Max(float64(optionA), float64(optionB)))
85+
}
86+
87+
// GetInterval returns the calculated $__interval in seconds
88+
func (c *IntervalCalculator) GetInterval() int {
89+
return c.interval
90+
}
91+
92+
// GetRateInterval returns the calculated $__rate_interval in seconds
93+
func (c *IntervalCalculator) GetRateInterval() int {
94+
return c.rateInterval
95+
}
96+
97+
// GetIntervalString returns $__interval as a duration string (e.g., "60s")
98+
func (c *IntervalCalculator) GetIntervalString() string {
99+
return formatDuration(c.interval)
100+
}
101+
102+
// GetRateIntervalString returns $__rate_interval as a duration string
103+
func (c *IntervalCalculator) GetRateIntervalString() string {
104+
return formatDuration(c.rateInterval)
105+
}
106+
107+
// SubstituteVariables replaces ${__interval} and ${__rate_interval} in the query
108+
func (c *IntervalCalculator) SubstituteVariables(query string) string {
109+
// Replace ${__interval} and $__interval
110+
query = replaceVariable(query, "__interval", c.GetIntervalString())
111+
112+
// Replace ${__rate_interval} and $__rate_interval
113+
query = replaceVariable(query, "__rate_interval", c.GetRateIntervalString())
114+
115+
return query
116+
}
117+
118+
// replaceVariable replaces both ${var} and $var patterns
119+
func replaceVariable(query, varName, value string) string {
120+
// Pattern 1: ${__variable}
121+
pattern1 := regexp.MustCompile(`\$\{` + varName + `\}`)
122+
query = pattern1.ReplaceAllString(query, value)
123+
124+
// Pattern 2: $__variable (without braces)
125+
pattern2 := regexp.MustCompile(`\$` + varName + `\b`)
126+
query = pattern2.ReplaceAllString(query, value)
127+
128+
return query
129+
}
130+
131+
// formatDuration converts seconds to Prometheus duration string
132+
func formatDuration(seconds int) string {
133+
if seconds < 60 {
134+
return fmt.Sprintf("%ds", seconds)
135+
} else if seconds < 3600 {
136+
minutes := seconds / 60
137+
remainder := seconds % 60
138+
if remainder == 0 {
139+
return fmt.Sprintf("%dm", minutes)
140+
}
141+
return fmt.Sprintf("%dm%ds", minutes, remainder)
142+
} else if seconds < 86400 {
143+
hours := seconds / 3600
144+
remainder := seconds % 3600
145+
if remainder == 0 {
146+
return fmt.Sprintf("%dh", hours)
147+
}
148+
minutes := remainder / 60
149+
return fmt.Sprintf("%dh%dm", hours, minutes)
150+
} else {
151+
days := seconds / 86400
152+
remainder := seconds % 86400
153+
if remainder == 0 {
154+
return fmt.Sprintf("%dd", days)
155+
}
156+
hours := remainder / 3600
157+
return fmt.Sprintf("%dd%dh", days, hours)
158+
}
159+
}

prometheus/query_client.go

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,21 @@ type QueryClient struct {
2121
}
2222

2323
type QueryOptions struct {
24-
Start *time.Time
25-
End *time.Time
26-
// Step refers to the resolution of datapoints, measured in number of seconds
27-
Step int
24+
Start time.Time
25+
End time.Time
26+
IntervalCalculator *IntervalCalculator
2827
}
2928

3029
func (c *QueryClient) Query(ctx context.Context, query string, options QueryOptions) (*QueryResponse, error) {
30+
finalQuery := options.IntervalCalculator.SubstituteVariables(query)
31+
3132
u := *c.BaseUrl
3233
u.Path = path.Join(c.BaseUrl.Path, "api/v1/query_range")
3334
params := url.Values{}
34-
params.Add("query", query)
35-
if options.Start != nil {
36-
params.Add("start", options.Start.Format(time.RFC3339))
37-
}
38-
if options.End != nil {
39-
params.Add("end", options.End.Format(time.RFC3339))
40-
}
41-
params.Add("step", fmt.Sprint(options.Step))
35+
params.Add("query", finalQuery)
36+
params.Add("start", options.Start.Format(time.RFC3339))
37+
params.Add("end", options.End.Format(time.RFC3339))
38+
params.Add("step", fmt.Sprint(options.IntervalCalculator.GetInterval()))
4239
u.RawQuery = params.Encode()
4340

4441
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)

workspace/metrics_getter.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ type MetricsGetterOptions struct {
1515
StartTime *time.Time
1616
EndTime *time.Time
1717

18+
// PanelWidth refers to the width of the panel, measured in pixels
19+
// This is used to calculate the number of data points to plot
20+
PanelWidth int
21+
// ScrapeInterval is how often metrics are collected from targets, in seconds.
22+
// Determines the minimum window size for rate() to have enough data points (default: 60).
23+
ScrapeInterval int
24+
1825
Metrics []string
1926
}
2027

0 commit comments

Comments
 (0)