grafana/pkg/tsdb/prometheus/models/query.go
Owen Williams 7b347fd12a
Prometheus: Add $__rate_interval_ms to go along with $__interval_ms (#69582)
* Add $__rate_interval_ms to go along with $__interval_ms

Fixes https://github.com/grafana/grafana/issues/69581

* update doc

* PoC add rate_interval_ms as rate range in prometheus

* Revert "PoC add rate_interval_ms as rate range in prometheus"

This reverts commit 7d4ec700d7.

* Update docs/sources/dashboards/variables/add-template-variables/index.md

grammar nit

Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>

* Update docs/sources/dashboards/variables/add-template-variables/index.md

---------

Co-authored-by: Galen <galen.kistler@grafana.com>
Co-authored-by: Isabel <76437239+imatwawana@users.noreply.github.com>
Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
2023-08-27 16:44:58 +02:00

262 lines
8.6 KiB
Go

package models
import (
"encoding/json"
"math"
"strconv"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
"github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery"
)
// Internal interval and range variables
const (
varInterval = "$__interval"
varIntervalMs = "$__interval_ms"
varRange = "$__range"
varRangeS = "$__range_s"
varRangeMs = "$__range_ms"
varRateInterval = "$__rate_interval"
varRateIntervalMs = "$__rate_interval_ms"
)
// Internal interval and range variables with {} syntax
// Repetitive code, we should have functionality to unify these
const (
varIntervalAlt = "${__interval}"
varIntervalMsAlt = "${__interval_ms}"
varRangeAlt = "${__range}"
varRangeSAlt = "${__range_s}"
varRangeMsAlt = "${__range_ms}"
varRateIntervalAlt = "${__rate_interval}"
varRateIntervalMsAlt = "${__rate_interval_ms}"
)
type TimeSeriesQueryType string
const (
RangeQueryType TimeSeriesQueryType = "range"
InstantQueryType TimeSeriesQueryType = "instant"
ExemplarQueryType TimeSeriesQueryType = "exemplar"
UnknownQueryType TimeSeriesQueryType = "unknown"
)
var safeResolution = 11000
type QueryModel struct {
dataquery.PrometheusDataQuery
// The following properties may be part of the request payload, however they are not saved in panel JSON
// Timezone offset to align start & end time on backend
UtcOffsetSec int64 `json:"utcOffsetSec,omitempty"`
LegendFormat string `json:"legendFormat,omitempty"`
Interval string `json:"interval,omitempty"`
IntervalMs int64 `json:"intervalMs,omitempty"`
IntervalFactor int64 `json:"intervalFactor,omitempty"`
}
type TimeRange struct {
Start time.Time
End time.Time
Step time.Duration
}
type Query struct {
Expr string
Step time.Duration
LegendFormat string
Start time.Time
End time.Time
RefId string
InstantQuery bool
RangeQuery bool
ExemplarQuery bool
UtcOffsetSec int64
}
func Parse(query backend.DataQuery, timeInterval string, intervalCalculator intervalv2.Calculator, fromAlert bool) (*Query, error) {
model := &QueryModel{}
if err := json.Unmarshal(query.JSON, model); err != nil {
return nil, err
}
// Final interval value
interval, err := calculatePrometheusInterval(model.Interval, timeInterval, model.IntervalMs, model.IntervalFactor, query, intervalCalculator)
if err != nil {
return nil, err
}
// Interpolate variables in expr
timeRange := query.TimeRange.To.Sub(query.TimeRange.From)
expr := interpolateVariables(model.Expr, model.Interval, interval, timeRange, intervalCalculator, timeInterval)
var rangeQuery, instantQuery bool
if model.Instant == nil {
instantQuery = false
} else {
instantQuery = *model.Instant
}
if model.Range == nil {
rangeQuery = false
} else {
rangeQuery = *model.Range
}
if !instantQuery && !rangeQuery {
// In older dashboards, we were not setting range query param and !range && !instant was run as range query
rangeQuery = true
}
// We never want to run exemplar query for alerting
exemplarQuery := false
if model.Exemplar != nil {
exemplarQuery = *model.Exemplar
}
if fromAlert {
exemplarQuery = false
}
return &Query{
Expr: expr,
Step: interval,
LegendFormat: model.LegendFormat,
Start: query.TimeRange.From,
End: query.TimeRange.To,
RefId: query.RefID,
InstantQuery: instantQuery,
RangeQuery: rangeQuery,
ExemplarQuery: exemplarQuery,
UtcOffsetSec: model.UtcOffsetSec,
}, nil
}
func (query *Query) Type() TimeSeriesQueryType {
if query.InstantQuery {
return InstantQueryType
}
if query.RangeQuery {
return RangeQueryType
}
if query.ExemplarQuery {
return ExemplarQueryType
}
return UnknownQueryType
}
func (query *Query) TimeRange() TimeRange {
return TimeRange{
Step: query.Step,
// Align query range to step. It rounds start and end down to a multiple of step.
Start: AlignTimeRange(query.Start, query.Step, query.UtcOffsetSec),
End: AlignTimeRange(query.End, query.Step, query.UtcOffsetSec),
}
}
func calculatePrometheusInterval(
queryInterval, timeInterval string,
intervalMs, intervalFactor int64,
query backend.DataQuery,
intervalCalculator intervalv2.Calculator,
) (time.Duration, error) {
// we need to compare the original query model after it is overwritten below to variables so that we can
// calculate the rateInterval if it is equal to $__rate_interval or ${__rate_interval}
originalQueryInterval := queryInterval
// If we are using variable for interval/step, we will replace it with calculated interval
if isVariableInterval(queryInterval) {
queryInterval = ""
}
minInterval, err := intervalv2.GetIntervalFrom(timeInterval, queryInterval, intervalMs, 15*time.Second)
if err != nil {
return time.Duration(0), err
}
calculatedInterval := intervalCalculator.Calculate(query.TimeRange, minInterval, query.MaxDataPoints)
safeInterval := intervalCalculator.CalculateSafeInterval(query.TimeRange, int64(safeResolution))
adjustedInterval := safeInterval.Value
if calculatedInterval.Value > safeInterval.Value {
adjustedInterval = calculatedInterval.Value
}
// here is where we compare for $__rate_interval or ${__rate_interval}
if originalQueryInterval == varRateInterval || originalQueryInterval == varRateIntervalAlt {
// Rate interval is final and is not affected by resolution
return calculateRateInterval(adjustedInterval, timeInterval, intervalCalculator), nil
} else {
queryIntervalFactor := intervalFactor
if queryIntervalFactor == 0 {
queryIntervalFactor = 1
}
return time.Duration(int64(adjustedInterval) * queryIntervalFactor), nil
}
}
func calculateRateInterval(
interval time.Duration,
scrapeInterval string,
intervalCalculator intervalv2.Calculator,
) time.Duration {
scrape := scrapeInterval
if scrape == "" {
scrape = "15s"
}
scrapeIntervalDuration, err := intervalv2.ParseIntervalStringToTimeDuration(scrape)
if err != nil {
return time.Duration(0)
}
rateInterval := time.Duration(int64(math.Max(float64(interval+scrapeIntervalDuration), float64(4)*float64(scrapeIntervalDuration))))
return rateInterval
}
func interpolateVariables(expr, queryInterval string, interval time.Duration,
timeRange time.Duration,
intervalCalculator intervalv2.Calculator, timeInterval string) string {
rangeMs := timeRange.Milliseconds()
rangeSRounded := int64(math.Round(float64(rangeMs) / 1000.0))
var rateInterval time.Duration
if queryInterval == varRateInterval || queryInterval == varRateIntervalAlt {
rateInterval = interval
} else {
rateInterval = calculateRateInterval(interval, timeInterval, intervalCalculator)
}
expr = strings.ReplaceAll(expr, varIntervalMs, strconv.FormatInt(int64(interval/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varInterval, intervalv2.FormatDuration(interval))
expr = strings.ReplaceAll(expr, varRangeMs, strconv.FormatInt(rangeMs, 10))
expr = strings.ReplaceAll(expr, varRangeS, strconv.FormatInt(rangeSRounded, 10))
expr = strings.ReplaceAll(expr, varRange, strconv.FormatInt(rangeSRounded, 10)+"s")
expr = strings.ReplaceAll(expr, varRateIntervalMs, strconv.FormatInt(int64(rateInterval/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varRateInterval, rateInterval.String())
// Repetitive code, we should have functionality to unify these
expr = strings.ReplaceAll(expr, varIntervalMsAlt, strconv.FormatInt(int64(interval/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varIntervalAlt, intervalv2.FormatDuration(interval))
expr = strings.ReplaceAll(expr, varRangeMsAlt, strconv.FormatInt(rangeMs, 10))
expr = strings.ReplaceAll(expr, varRangeSAlt, strconv.FormatInt(rangeSRounded, 10))
expr = strings.ReplaceAll(expr, varRangeAlt, strconv.FormatInt(rangeSRounded, 10)+"s")
expr = strings.ReplaceAll(expr, varRateIntervalMsAlt, strconv.FormatInt(int64(rateInterval/time.Millisecond), 10))
expr = strings.ReplaceAll(expr, varRateIntervalAlt, rateInterval.String())
return expr
}
func isVariableInterval(interval string) bool {
if interval == varInterval || interval == varIntervalMs || interval == varRateInterval || interval == varRateIntervalMs {
return true
}
// Repetitive code, we should have functionality to unify these
if interval == varIntervalAlt || interval == varIntervalMsAlt || interval == varRateIntervalAlt || interval == varRateIntervalMsAlt {
return true
}
return false
}
func AlignTimeRange(t time.Time, step time.Duration, offset int64) time.Time {
offsetNano := float64(offset * 1e9)
stepNano := float64(step.Nanoseconds())
return time.Unix(0, int64(math.Floor((float64(t.UnixNano())+offsetNano)/stepNano)*stepNano-offsetNano)).UTC()
}