app/vmselect/promql: duration handling improvements in MetricsQL queries

- Support durations anywhere in MetricsQL queries. E.g. sum_over_time(m[1h])/1h is equivalent to sum_over_time(m[1h])/3600
- Support durations without suffix. E.g. rate(m[300]) is equivalent to rate(m[5m])
This commit is contained in:
Aliaksandr Valialkin 2021-07-12 17:16:38 +03:00
parent f9de546139
commit a925d5a3e1
12 changed files with 176 additions and 128 deletions

View file

@ -25,7 +25,6 @@ import (
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/storage" "github.com/VictoriaMetrics/VictoriaMetrics/lib/storage"
"github.com/VictoriaMetrics/metrics" "github.com/VictoriaMetrics/metrics"
"github.com/VictoriaMetrics/metricsql"
"github.com/valyala/fastjson/fastfloat" "github.com/valyala/fastjson/fastfloat"
"github.com/valyala/quicktemplate" "github.com/valyala/quicktemplate"
) )
@ -991,15 +990,9 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
if err != nil { if err != nil {
return err return err
} }
if childQuery, windowStr, offsetStr := promql.IsMetricSelectorWithRollup(query); childQuery != "" { if childQuery, windowExpr, offsetExpr := promql.IsMetricSelectorWithRollup(query); childQuery != "" {
window, err := parsePositiveDuration(windowStr, step) window := windowExpr.Duration(step)
if err != nil { offset := offsetExpr.Duration(step)
return fmt.Errorf("cannot parse window: %w", err)
}
offset, err := parseDuration(offsetStr, step)
if err != nil {
return fmt.Errorf("cannot parse offset: %w", err)
}
start -= offset start -= offset
end := start end := start
start = end - window start = end - window
@ -1014,22 +1007,13 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
queryDuration.UpdateDuration(startTime) queryDuration.UpdateDuration(startTime)
return nil return nil
} }
if childQuery, windowStr, stepStr, offsetStr := promql.IsRollup(query); childQuery != "" { if childQuery, windowExpr, stepExpr, offsetExpr := promql.IsRollup(query); childQuery != "" {
newStep, err := parsePositiveDuration(stepStr, step) newStep := stepExpr.Duration(step)
if err != nil {
return fmt.Errorf("cannot parse step: %w", err)
}
if newStep > 0 { if newStep > 0 {
step = newStep step = newStep
} }
window, err := parsePositiveDuration(windowStr, step) window := windowExpr.Duration(step)
if err != nil { offset := offsetExpr.Duration(step)
return fmt.Errorf("cannot parse window: %w", err)
}
offset, err := parseDuration(offsetStr, step)
if err != nil {
return fmt.Errorf("cannot parse offset: %w", err)
}
start -= offset start -= offset
end := start end := start
start = end - window start = end - window
@ -1086,20 +1070,6 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
var queryDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/query"}`) var queryDuration = metrics.NewSummary(`vm_request_duration_seconds{path="/api/v1/query"}`)
func parseDuration(s string, step int64) (int64, error) {
if len(s) == 0 {
return 0, nil
}
return metricsql.DurationValue(s, step)
}
func parsePositiveDuration(s string, step int64) (int64, error) {
if len(s) == 0 {
return 0, nil
}
return metricsql.PositiveDurationValue(s, step)
}
// QueryRangeHandler processes /api/v1/query_range request. // QueryRangeHandler processes /api/v1/query_range request.
// //
// See https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries // See https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries

View file

@ -336,6 +336,12 @@ func evalExpr(ec *EvalConfig, e metricsql.Expr) ([]*timeseries, error) {
rv := evalString(ec, se.S) rv := evalString(ec, se.S)
return rv, nil return rv, nil
} }
if de, ok := e.(*metricsql.DurationExpr); ok {
d := de.Duration(ec.Step)
dSec := float64(d) / 1000
rv := evalNumber(ec, dSec)
return rv, nil
}
return nil, fmt.Errorf("unexpected expression %q", e.AppendString(nil)) return nil, fmt.Errorf("unexpected expression %q", e.AppendString(nil))
} }
@ -473,12 +479,8 @@ func getRollupExprArg(arg metricsql.Expr) *metricsql.RollupExpr {
func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, expr metricsql.Expr, re *metricsql.RollupExpr, iafc *incrementalAggrFuncContext) ([]*timeseries, error) { func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, expr metricsql.Expr, re *metricsql.RollupExpr, iafc *incrementalAggrFuncContext) ([]*timeseries, error) {
ecNew := ec ecNew := ec
var offset int64 var offset int64
if len(re.Offset) > 0 { if re.Offset != nil {
var err error offset = re.Offset.Duration(ec.Step)
offset, err = metricsql.DurationValue(re.Offset, ec.Step)
if err != nil {
return nil, err
}
ecNew = newEvalConfig(ecNew) ecNew = newEvalConfig(ecNew)
ecNew.Start -= offset ecNew.Start -= offset
ecNew.End -= offset ecNew.End -= offset
@ -526,24 +528,11 @@ func evalRollupFunc(ec *EvalConfig, name string, rf rollupFunc, expr metricsql.E
func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, expr metricsql.Expr, re *metricsql.RollupExpr) ([]*timeseries, error) { func evalRollupFuncWithSubquery(ec *EvalConfig, name string, rf rollupFunc, expr metricsql.Expr, re *metricsql.RollupExpr) ([]*timeseries, error) {
// TODO: determine whether to use rollupResultCacheV here. // TODO: determine whether to use rollupResultCacheV here.
var step int64 step := re.Step.Duration(ec.Step)
if len(re.Step) > 0 { if step == 0 {
var err error
step, err = metricsql.PositiveDurationValue(re.Step, ec.Step)
if err != nil {
return nil, err
}
} else {
step = ec.Step step = ec.Step
} }
var window int64 window := re.Window.Duration(ec.Step)
if len(re.Window) > 0 {
var err error
window, err = metricsql.PositiveDurationValue(re.Window, ec.Step)
if err != nil {
return nil, err
}
}
ecSQ := newEvalConfig(ec) ecSQ := newEvalConfig(ec)
ecSQ.Start -= window + maxSilenceInterval + step ecSQ.Start -= window + maxSilenceInterval + step
@ -652,18 +641,11 @@ var (
) )
func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc, func evalRollupFuncWithMetricExpr(ec *EvalConfig, name string, rf rollupFunc,
expr metricsql.Expr, me *metricsql.MetricExpr, iafc *incrementalAggrFuncContext, windowStr string) ([]*timeseries, error) { expr metricsql.Expr, me *metricsql.MetricExpr, iafc *incrementalAggrFuncContext, windowExpr *metricsql.DurationExpr) ([]*timeseries, error) {
if me.IsEmpty() { if me.IsEmpty() {
return evalNumber(ec, nan), nil return evalNumber(ec, nan), nil
} }
var window int64 window := windowExpr.Duration(ec.Step)
if len(windowStr) > 0 {
var err error
window, err = metricsql.PositiveDurationValue(windowStr, ec.Step)
if err != nil {
return nil, err
}
}
// Search for partial results in cache. // Search for partial results in cache.
tssCached, start := rollupResultCacheV.Get(ec, expr, window) tssCached, start := rollupResultCacheV.Get(ec, expr, window)

View file

@ -232,6 +232,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run("time()[:100] offset 0", func(t *testing.T) {
t.Parallel()
q := `time()[:100] offset 0`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{1000, 1200, 1400, 1600, 1800, 2000},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("time() offset 1h40s0ms", func(t *testing.T) { t.Run("time() offset 1h40s0ms", func(t *testing.T) {
t.Parallel() t.Parallel()
q := `time() offset 1h40s0ms` q := `time() offset 1h40s0ms`
@ -243,6 +254,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run("time() offset 3640", func(t *testing.T) {
t.Parallel()
q := `time() offset 3640`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{-2800, -2600, -2400, -2200, -2000, -1800},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("time() offset -1h40s0ms", func(t *testing.T) { t.Run("time() offset -1h40s0ms", func(t *testing.T) {
t.Parallel() t.Parallel()
q := `time() offset -1h40s0ms` q := `time() offset -1h40s0ms`
@ -361,6 +383,28 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r1, r2} resultExpected := []netstorage.Result{r1, r2}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run("1h", func(t *testing.T) {
t.Parallel()
q := `1h`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{3600, 3600, 3600, 3600, 3600, 3600},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("sum_over_time(time()[1h]) / 1h", func(t *testing.T) {
t.Parallel()
q := `sum_over_time(time()[1h]) / 1h`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{-3.5, -2.5, -1.5, -0.5, 0.5, 1.5},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("time()[:100s] offset 100s", func(t *testing.T) { t.Run("time()[:100s] offset 100s", func(t *testing.T) {
t.Parallel() t.Parallel()
q := `time()[:100s] offset 100s` q := `time()[:100s] offset 100s`
@ -383,6 +427,17 @@ func TestExecSuccess(t *testing.T) {
resultExpected := []netstorage.Result{r} resultExpected := []netstorage.Result{r}
f(q, resultExpected) f(q, resultExpected)
}) })
t.Run("time()[300:100] offset 100", func(t *testing.T) {
t.Parallel()
q := `time()[300:100] offset 100`
r := netstorage.Result{
MetricName: metricNameExpected,
Values: []float64{900, 1100, 1300, 1500, 1700, 1900},
Timestamps: timestampsExpected,
}
resultExpected := []netstorage.Result{r}
f(q, resultExpected)
})
t.Run("time()[1.5i:0.5i] offset 0.5i", func(t *testing.T) { t.Run("time()[1.5i:0.5i] offset 0.5i", func(t *testing.T) {
t.Parallel() t.Parallel()
q := `time()[1.5i:0.5i] offset 0.5i` q := `time()[1.5i:0.5i] offset 0.5i`

View file

@ -10,13 +10,13 @@ import (
// IsRollup verifies whether s is a rollup with non-empty window. // IsRollup verifies whether s is a rollup with non-empty window.
// //
// It returns the wrapped query with the corresponding window, step and offset. // It returns the wrapped query with the corresponding window, step and offset.
func IsRollup(s string) (childQuery string, window, step, offset string) { func IsRollup(s string) (childQuery string, window, step, offset *metricsql.DurationExpr) {
expr, err := parsePromQLWithCache(s) expr, err := parsePromQLWithCache(s)
if err != nil { if err != nil {
return return
} }
re, ok := expr.(*metricsql.RollupExpr) re, ok := expr.(*metricsql.RollupExpr)
if !ok || len(re.Window) == 0 { if !ok || re.Window == nil {
return return
} }
wrappedQuery := re.Expr.AppendString(nil) wrappedQuery := re.Expr.AppendString(nil)
@ -27,13 +27,13 @@ func IsRollup(s string) (childQuery string, window, step, offset string) {
// wrapped into rollup. // wrapped into rollup.
// //
// It returns the wrapped query with the corresponding window with offset. // It returns the wrapped query with the corresponding window with offset.
func IsMetricSelectorWithRollup(s string) (childQuery string, window, offset string) { func IsMetricSelectorWithRollup(s string) (childQuery string, window, offset *metricsql.DurationExpr) {
expr, err := parsePromQLWithCache(s) expr, err := parsePromQLWithCache(s)
if err != nil { if err != nil {
return return
} }
re, ok := expr.(*metricsql.RollupExpr) re, ok := expr.(*metricsql.RollupExpr)
if !ok || len(re.Window) == 0 || len(re.Step) > 0 { if !ok || re.Window == nil || re.Step != nil {
return return
} }
me, ok := re.Expr.(*metricsql.MetricExpr) me, ok := re.Expr.(*metricsql.MetricExpr)

View file

@ -13,6 +13,8 @@ sort: 15
* FEATURE: vmselect: embed [vmui](https://github.com/VictoriaMetrics/vmui) into a single-node VictoriaMetrics and into `vmselect` component of cluster version. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1413). The web interface is available at the following paths: * FEATURE: vmselect: embed [vmui](https://github.com/VictoriaMetrics/vmui) into a single-node VictoriaMetrics and into `vmselect` component of cluster version. See [this feature request](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1413). The web interface is available at the following paths:
* `/vmui/` for a single-node VictoriaMetrics * `/vmui/` for a single-node VictoriaMetrics
* `/select/<accountID>/vmui/` for `vmselect` at cluster version of VictoriaMetrics * `/select/<accountID>/vmui/` for `vmselect` at cluster version of VictoriaMetrics
* FEATURE: support durations anywhere in [MetricsQL queries](https://docs.victoriametrics.com/MetricsQL.html). For example, `sum_over_time(m[1h]) / 1h` is a valid query, which is equivalent to `sum_over_time(m[1h]) / 3600`.
* FEATURE: support durations without suffxies in [MetricsQL queries](https://docs.victoriametrics.com/MetricsQL.html). For example, `rate(m[3600])` is a valid query, which is equivalent to `rate(m[1h])`.
* BUGFIX: vmagent: remove `{ %space %}` typo in `/targets` output. The typo has been introduced in v1.62.0. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1408). * BUGFIX: vmagent: remove `{ %space %}` typo in `/targets` output. The typo has been introduced in v1.62.0. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1408).
* BUGFIX: vmagent: fix CSS styles on `/targets` page. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1422). * BUGFIX: vmagent: fix CSS styles on `/targets` page. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1422).

View file

@ -44,6 +44,8 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g
- `offset` may be put anywere in the query. For instance, `sum(foo) offset 24h`. - `offset` may be put anywere in the query. For instance, `sum(foo) offset 24h`.
- `offset` may be negative. For example, `q offset -1h`. - `offset` may be negative. For example, `q offset -1h`.
- [Range duration](https://prometheus.io/docs/prometheus/latest/querying/basics/#range-vector-selectors) and [offset](https://prometheus.io/docs/prometheus/latest/querying/basics/#offset-modifier) may be fractional. For instance, `rate(node_network_receive_bytes_total[1.5m] offset 0.5d)`. - [Range duration](https://prometheus.io/docs/prometheus/latest/querying/basics/#range-vector-selectors) and [offset](https://prometheus.io/docs/prometheus/latest/querying/basics/#offset-modifier) may be fractional. For instance, `rate(node_network_receive_bytes_total[1.5m] offset 0.5d)`.
- The duration suffix is optional. The duration is in seconds if the suffix is missing. For example, `rate(m[300] offset 1800)` is equivalent to `rate(m[5m]) offset 30m`.
- The duration can be placed anywhere in the query. For example, `sum_over_time(m[1h]) / 1h` is equivalent to `sum_over_time(m[1h]) / 3600`.
- `default` binary operator. `q1 default q2` fills gaps in `q1` with the corresponding values from `q2`. - `default` binary operator. `q1 default q2` fills gaps in `q1` with the corresponding values from `q2`.
- Most aggregate functions accept arbitrary number of args. For example, `avg(q1, q2, q3)` would return the average values for every point across `q1`, `q2` and `q3`. - Most aggregate functions accept arbitrary number of args. For example, `avg(q1, q2, q3)` would return the average values for every point across `q1`, `q2` and `q3`.
- `histogram_quantile` accepts optional third arg - `boundsLabel`. In this case it returns `lower` and `upper` bounds for the estimated percentile. See [this issue for details](https://github.com/prometheus/prometheus/issues/5706). - `histogram_quantile` accepts optional third arg - `boundsLabel`. In this case it returns `lower` and `upper` bounds for the estimated percentile. See [this issue for details](https://github.com/prometheus/prometheus/issues/5706).

2
go.mod
View file

@ -9,7 +9,7 @@ require (
// like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b // like https://github.com/valyala/fasthttp/commit/996610f021ff45fdc98c2ce7884d5fa4e7f9199b
github.com/VictoriaMetrics/fasthttp v1.0.16 github.com/VictoriaMetrics/fasthttp v1.0.16
github.com/VictoriaMetrics/metrics v1.17.3 github.com/VictoriaMetrics/metrics v1.17.3
github.com/VictoriaMetrics/metricsql v0.15.0 github.com/VictoriaMetrics/metricsql v0.16.0
github.com/VividCortex/ewma v1.2.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect
github.com/aws/aws-sdk-go v1.39.1 github.com/aws/aws-sdk-go v1.39.1
github.com/cespare/xxhash/v2 v2.1.1 github.com/cespare/xxhash/v2 v2.1.1

4
go.sum
View file

@ -105,8 +105,8 @@ github.com/VictoriaMetrics/fasthttp v1.0.16/go.mod h1:s9o5H4T58Kt4CTrdyJp4RorBKC
github.com/VictoriaMetrics/metrics v1.12.2/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE= github.com/VictoriaMetrics/metrics v1.12.2/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
github.com/VictoriaMetrics/metrics v1.17.3 h1:QPUakR6JRy8BhL2C2kOgYKLuoPDwtJQ+7iKIZSjt1A4= github.com/VictoriaMetrics/metrics v1.17.3 h1:QPUakR6JRy8BhL2C2kOgYKLuoPDwtJQ+7iKIZSjt1A4=
github.com/VictoriaMetrics/metrics v1.17.3/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE= github.com/VictoriaMetrics/metrics v1.17.3/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
github.com/VictoriaMetrics/metricsql v0.15.0 h1:7tveqhA0xLumXhokcmgxiUn90VlKqiVtYZQ3p6y9Fu4= github.com/VictoriaMetrics/metricsql v0.16.0 h1:YzrMnGUs6Y6f5LxsH8eSAoik98aEzlc1TiYgOONgr3Q=
github.com/VictoriaMetrics/metricsql v0.15.0/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94bGoGtzWfLGqFhMIg8= github.com/VictoriaMetrics/metricsql v0.16.0/go.mod h1:ylO7YITho/Iw6P71oEaGyHbO94bGoGtzWfLGqFhMIg8=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=

View file

@ -445,7 +445,12 @@ func DurationValue(s string, step int64) (int64, error) {
if len(s) == 0 { if len(s) == 0 {
return 0, fmt.Errorf("duration cannot be empty") return 0, fmt.Errorf("duration cannot be empty")
} }
var d float64 // Try parsing floating-point duration
d, err := strconv.ParseFloat(s, 64)
if err == nil {
// Convert the duration to milliseconds.
return int64(d * 1000), nil
}
isMinus := false isMinus := false
for len(s) > 0 { for len(s) > 0 {
n := scanSingleDuration(s, true) n := scanSingleDuration(s, true)

View file

@ -423,12 +423,15 @@ func (p *parser) parseSingleExpr() (Expr, error) {
} }
func (p *parser) parseSingleExprWithoutRollupSuffix() (Expr, error) { func (p *parser) parseSingleExprWithoutRollupSuffix() (Expr, error) {
if isPositiveNumberPrefix(p.lex.Token) || isInfOrNaN(p.lex.Token) { if isPositiveDuration(p.lex.Token) {
return p.parsePositiveNumberExpr() return p.parsePositiveDuration()
} }
if isStringPrefix(p.lex.Token) { if isStringPrefix(p.lex.Token) {
return p.parseStringExpr() return p.parseStringExpr()
} }
if isPositiveNumberPrefix(p.lex.Token) || isInfOrNaN(p.lex.Token) {
return p.parsePositiveNumberExpr()
}
if isIdentPrefix(p.lex.Token) { if isIdentPrefix(p.lex.Token) {
return p.parseIdentExpr() return p.parseIdentExpr()
} }
@ -1221,29 +1224,29 @@ func (lfe *labelFilterExpr) toLabelFilter() (*LabelFilter, error) {
return &lf, nil return &lf, nil
} }
func (p *parser) parseWindowAndStep() (string, string, bool, error) { func (p *parser) parseWindowAndStep() (*DurationExpr, *DurationExpr, bool, error) {
if p.lex.Token != "[" { if p.lex.Token != "[" {
return "", "", false, fmt.Errorf(`windowAndStep: unexpected token %q; want "["`, p.lex.Token) return nil, nil, false, fmt.Errorf(`windowAndStep: unexpected token %q; want "["`, p.lex.Token)
} }
err := p.lex.Next() err := p.lex.Next()
if err != nil { if err != nil {
return "", "", false, err return nil, nil, false, err
} }
var window string var window *DurationExpr
if !strings.HasPrefix(p.lex.Token, ":") { if !strings.HasPrefix(p.lex.Token, ":") {
window, err = p.parsePositiveDuration() window, err = p.parsePositiveDuration()
if err != nil { if err != nil {
return "", "", false, err return nil, nil, false, err
} }
} }
var step string var step *DurationExpr
inheritStep := false inheritStep := false
if strings.HasPrefix(p.lex.Token, ":") { if strings.HasPrefix(p.lex.Token, ":") {
// Parse step // Parse step
p.lex.Token = p.lex.Token[1:] p.lex.Token = p.lex.Token[1:]
if p.lex.Token == "" { if p.lex.Token == "" {
if err := p.lex.Next(); err != nil { if err := p.lex.Next(); err != nil {
return "", "", false, err return nil, nil, false, err
} }
if p.lex.Token == "]" { if p.lex.Token == "]" {
inheritStep = true inheritStep = true
@ -1252,63 +1255,94 @@ func (p *parser) parseWindowAndStep() (string, string, bool, error) {
if p.lex.Token != "]" { if p.lex.Token != "]" {
step, err = p.parsePositiveDuration() step, err = p.parsePositiveDuration()
if err != nil { if err != nil {
return "", "", false, err return nil, nil, false, err
} }
} }
} }
if p.lex.Token != "]" { if p.lex.Token != "]" {
return "", "", false, fmt.Errorf(`windowAndStep: unexpected token %q; want "]"`, p.lex.Token) return nil, nil, false, fmt.Errorf(`windowAndStep: unexpected token %q; want "]"`, p.lex.Token)
} }
if err := p.lex.Next(); err != nil { if err := p.lex.Next(); err != nil {
return "", "", false, err return nil, nil, false, err
} }
return window, step, inheritStep, nil return window, step, inheritStep, nil
} }
func (p *parser) parseOffset() (string, error) { func (p *parser) parseOffset() (*DurationExpr, error) {
if !isOffset(p.lex.Token) { if !isOffset(p.lex.Token) {
return "", fmt.Errorf(`offset: unexpected token %q; want "offset"`, p.lex.Token) return nil, fmt.Errorf(`offset: unexpected token %q; want "offset"`, p.lex.Token)
} }
if err := p.lex.Next(); err != nil { if err := p.lex.Next(); err != nil {
return "", err return nil, err
} }
d, err := p.parseDuration() de, err := p.parseDuration()
if err != nil { if err != nil {
return "", err return nil, err
} }
return d, nil return de, nil
} }
func (p *parser) parseDuration() (string, error) { func (p *parser) parseDuration() (*DurationExpr, error) {
isNegative := false isNegative := p.lex.Token == "-"
if p.lex.Token == "-" { if isNegative {
isNegative = true
if err := p.lex.Next(); err != nil { if err := p.lex.Next(); err != nil {
return "", err return nil, err
} }
} }
if !isPositiveDuration(p.lex.Token) { de, err := p.parsePositiveDuration()
return "", fmt.Errorf(`duration: unexpected token %q; want "duration"`, p.lex.Token) if err != nil {
} return nil, err
d := p.lex.Token
if err := p.lex.Next(); err != nil {
return "", err
} }
if isNegative { if isNegative {
d = "-" + d de.s = "-" + de.s
} }
return d, nil return de, nil
} }
func (p *parser) parsePositiveDuration() (string, error) { func (p *parser) parsePositiveDuration() (*DurationExpr, error) {
d, err := p.parseDuration() s := p.lex.Token
if isPositiveDuration(s) {
if err := p.lex.Next(); err != nil {
return nil, err
}
} else {
if !isPositiveNumberPrefix(s) {
return nil, fmt.Errorf(`duration: unexpected token %q; want "duration"`, s)
}
// Verify the duration in seconds without explicit suffix.
if _, err := p.parsePositiveNumberExpr(); err != nil {
return nil, fmt.Errorf(`duration: parse error: %s`, err)
}
}
de := &DurationExpr{
s: s,
}
return de, nil
}
// DurationExpr contains the duration
type DurationExpr struct {
s string
}
// AppendString appends string representation of de to dst and returns the result.
func (de *DurationExpr) AppendString(dst []byte) []byte {
if de == nil {
return dst
}
return append(dst, de.s...)
}
// Duration returns the duration from de in milliseconds.
func (de *DurationExpr) Duration(step int64) int64 {
if de == nil {
return 0
}
d, err := DurationValue(de.s, step)
if err != nil { if err != nil {
return "", err panic(fmt.Errorf("BUG: cannot parse duration %q: %s", de.s, err))
} }
if strings.HasPrefix(d, "-") { return d
return "", fmt.Errorf("positiveDuration: expecting positive duration; got %q", d)
}
return d, nil
} }
// parseIdentExpr parses expressions starting with `ident` token. // parseIdentExpr parses expressions starting with `ident` token.
@ -1628,17 +1662,17 @@ type RollupExpr struct {
// Window contains optional window value from square brackets // Window contains optional window value from square brackets
// //
// For example, `http_requests_total[5m]` will have Window value `5m`. // For example, `http_requests_total[5m]` will have Window value `5m`.
Window string Window *DurationExpr
// Offset contains optional value from `offset` part. // Offset contains optional value from `offset` part.
// //
// For example, `foobar{baz="aa"} offset 5m` will have Offset value `5m`. // For example, `foobar{baz="aa"} offset 5m` will have Offset value `5m`.
Offset string Offset *DurationExpr
// Step contains optional step value from square brackets. // Step contains optional step value from square brackets.
// //
// For example, `foobar[1h:3m]` will have Step value '3m'. // For example, `foobar[1h:3m]` will have Step value '3m'.
Step string Step *DurationExpr
// If set to true, then `foo[1h:]` would print the same // If set to true, then `foo[1h:]` would print the same
// instead of `foo[1h]`. // instead of `foo[1h]`.
@ -1647,7 +1681,7 @@ type RollupExpr struct {
// ForSubquery returns true if re represents subquery. // ForSubquery returns true if re represents subquery.
func (re *RollupExpr) ForSubquery() bool { func (re *RollupExpr) ForSubquery() bool {
return len(re.Step) > 0 || re.InheritStep return re.Step != nil || re.InheritStep
} }
// AppendString appends string representation of re to dst and returns the result. // AppendString appends string representation of re to dst and returns the result.
@ -1671,22 +1705,20 @@ func (re *RollupExpr) AppendString(dst []byte) []byte {
if needParens { if needParens {
dst = append(dst, ')') dst = append(dst, ')')
} }
if len(re.Window) > 0 || re.InheritStep || len(re.Step) > 0 { if re.Window != nil || re.InheritStep || re.Step != nil {
dst = append(dst, '[') dst = append(dst, '[')
if len(re.Window) > 0 { dst = re.Window.AppendString(dst)
dst = append(dst, re.Window...) if re.Step != nil {
}
if len(re.Step) > 0 {
dst = append(dst, ':') dst = append(dst, ':')
dst = append(dst, re.Step...) dst = re.Step.AppendString(dst)
} else if re.InheritStep { } else if re.InheritStep {
dst = append(dst, ':') dst = append(dst, ':')
} }
dst = append(dst, ']') dst = append(dst, ']')
} }
if len(re.Offset) > 0 { if re.Offset != nil {
dst = append(dst, " offset "...) dst = append(dst, " offset "...)
dst = append(dst, re.Offset...) dst = re.Offset.AppendString(dst)
} }
return dst return dst
} }

View file

@ -52,7 +52,7 @@ var transformFuncs = map[string]bool{
"label_match": true, "label_match": true,
"label_mismatch": true, "label_mismatch": true,
"union": true, "union": true,
"": true, // empty func is a synonim to union "": true, // empty func is a synonym to union
"keep_last_value": true, "keep_last_value": true,
"keep_next_value": true, "keep_next_value": true,
"interpolate": true, "interpolate": true,

2
vendor/modules.txt vendored
View file

@ -21,7 +21,7 @@ github.com/VictoriaMetrics/fasthttp/stackless
# github.com/VictoriaMetrics/metrics v1.17.3 # github.com/VictoriaMetrics/metrics v1.17.3
## explicit ## explicit
github.com/VictoriaMetrics/metrics github.com/VictoriaMetrics/metrics
# github.com/VictoriaMetrics/metricsql v0.15.0 # github.com/VictoriaMetrics/metricsql v0.16.0
## explicit ## explicit
github.com/VictoriaMetrics/metricsql github.com/VictoriaMetrics/metricsql
github.com/VictoriaMetrics/metricsql/binaryop github.com/VictoriaMetrics/metricsql/binaryop