diff --git a/app/vmselect/promql/exec_test.go b/app/vmselect/promql/exec_test.go index a46314e23..b8eb3a019 100644 --- a/app/vmselect/promql/exec_test.go +++ b/app/vmselect/promql/exec_test.go @@ -3083,6 +3083,28 @@ func TestExecSuccess(t *testing.T) { resultExpected := []netstorage.Result{r} f(q, resultExpected) }) + t.Run(`share_gt_over_time`, func(t *testing.T) { + t.Parallel() + q := `share_gt_over_time(rand(0)[200s:10s], 0.7)` + r := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{0.35, 0.3, 0.5, 0.3, 0.3, 0.25}, + Timestamps: timestampsExpected, + } + resultExpected := []netstorage.Result{r} + f(q, resultExpected) + }) + t.Run(`share_le_over_time`, func(t *testing.T) { + t.Parallel() + q := `share_le_over_time(rand(0)[200s:10s], 0.7)` + r := netstorage.Result{ + MetricName: metricNameExpected, + Values: []float64{0.65, 0.7, 0.5, 0.7, 0.7, 0.75}, + Timestamps: timestampsExpected, + } + resultExpected := []netstorage.Result{r} + f(q, resultExpected) + }) t.Run(`increases_over_time`, func(t *testing.T) { t.Parallel() q := `increases_over_time(rand(0)[200s:10s])` diff --git a/app/vmselect/promql/rollup.go b/app/vmselect/promql/rollup.go index c31886f31..1f0fa05c5 100644 --- a/app/vmselect/promql/rollup.go +++ b/app/vmselect/promql/rollup.go @@ -49,6 +49,8 @@ var rollupFuncs = map[string]newRollupFunc{ "lifetime": newRollupFuncOneArg(rollupLifetime), "lag": newRollupFuncOneArg(rollupLag), "scrape_interval": newRollupFuncOneArg(rollupScrapeInterval), + "share_le_over_time": newRollupShareLE, + "share_gt_over_time": newRollupShareGT, "rollup": newRollupFuncOneArg(rollupFake), "rollup_rate": newRollupFuncOneArg(rollupFake), // + rollupFuncsRemoveCounterResets "rollup_deriv": newRollupFuncOneArg(rollupFake), @@ -529,6 +531,56 @@ func linearRegression(rfa *rollupFuncArg) (float64, float64) { return v, k } +func newRollupShareLE(args []interface{}) (rollupFunc, error) { + return newRollupShareFilter(args, countFilterLE) +} + +func countFilterLE(values []float64, le float64) int { + n := 0 + for _, v := range values { + if v <= le { + n++ + } + } + return n +} + +func newRollupShareGT(args []interface{}) (rollupFunc, error) { + return newRollupShareFilter(args, countFilterGT) +} + +func countFilterGT(values []float64, gt float64) int { + n := 0 + for _, v := range values { + if v > gt { + n++ + } + } + return n +} + +func newRollupShareFilter(args []interface{}, countFilter func(values []float64, limit float64) int) (rollupFunc, error) { + if err := expectRollupArgsNum(args, 2); err != nil { + return nil, err + } + limits, err := getScalar(args[1], 1) + if err != nil { + return nil, err + } + rf := func(rfa *rollupFuncArg) float64 { + // There is no need in handling NaNs here, since they must be cleaned up + // before calling rollup funcs. + values := rfa.values + if len(values) == 0 { + return nan + } + limit := limits[rfa.idx] + n := countFilter(values, limit) + return float64(n) / float64(len(values)) + } + return rf, nil +} + func newRollupQuantile(args []interface{}) (rollupFunc, error) { if err := expectRollupArgsNum(args, 2); err != nil { return nil, err diff --git a/app/vmselect/promql/rollup_test.go b/app/vmselect/promql/rollup_test.go index f7a274082..323f33569 100644 --- a/app/vmselect/promql/rollup_test.go +++ b/app/vmselect/promql/rollup_test.go @@ -192,6 +192,52 @@ func testRollupFunc(t *testing.T, funcName string, args []interface{}, meExpecte } } +func TestRollupShareLEOverTime(t *testing.T) { + f := func(le, vExpected float64) { + t.Helper() + les := []*timeseries{{ + Values: []float64{le}, + Timestamps: []int64{123}, + }} + var me metricsql.MetricExpr + args := []interface{}{&metricsql.RollupExpr{Expr: &me}, les} + testRollupFunc(t, "share_le_over_time", args, &me, vExpected) + } + + f(-123, 0) + f(0, 0) + f(10, 0) + f(12, 0.08333333333333333) + f(30, 0.16666666666666666) + f(50, 0.75) + f(100, 0.9166666666666666) + f(123, 1) + f(1000, 1) +} + +func TestRollupShareGTOverTime(t *testing.T) { + f := func(gt, vExpected float64) { + t.Helper() + gts := []*timeseries{{ + Values: []float64{gt}, + Timestamps: []int64{123}, + }} + var me metricsql.MetricExpr + args := []interface{}{&metricsql.RollupExpr{Expr: &me}, gts} + testRollupFunc(t, "share_gt_over_time", args, &me, vExpected) + } + + f(-123, 1) + f(0, 1) + f(10, 1) + f(12, 0.9166666666666666) + f(30, 0.8333333333333334) + f(50, 0.25) + f(100, 0.08333333333333333) + f(123, 0) + f(1000, 0) +} + func TestRollupQuantileOverTime(t *testing.T) { f := func(phi, vExpected float64) { t.Helper() diff --git a/docs/ExtendedPromQL.md b/docs/ExtendedPromQL.md index be09c368d..3f8c91937 100644 --- a/docs/ExtendedPromQL.md +++ b/docs/ExtendedPromQL.md @@ -89,3 +89,7 @@ This functionality can be tried at [an editable Grafana dashboard](http://play-g - `bottomk_max(k, q)` - returns bottom K time series with the min maximums on the given time range - `bottomk_avg(k, q)` - returns bottom K time series with the min averages on the given time range - `bottomk_median(k, q)` - returns bottom K time series with the min medians on the given time range +- `share_le_over_time(m[d], le)` - returns share (in the range 0..1) of values in `m` over `d`, which are smaller or equal to `le`. Useful for calculating SLI and SLO. + Example: `share_le_over_time(memory_usage_bytes[24h], 100*1024*1024)` returns the share of time series values for the last 24 hours when memory usage was below or equal to 100MB. +- `share_gt_over_time(m[d], gt)` - returns share (in the range 0..1) of values in `m` over `d`, which are bigger than `gt`. Useful for calculating SLI and SLO. + Example: `share_gt_over_time(up[24h], 0)` - returns service availability for the last 24 hours. diff --git a/lib/metricsql/rollup.go b/lib/metricsql/rollup.go index 178fec783..6206907ca 100644 --- a/lib/metricsql/rollup.go +++ b/lib/metricsql/rollup.go @@ -41,6 +41,8 @@ var rollupFuncs = map[string]bool{ "lifetime": true, "lag": true, "scrape_interval": true, + "share_le_over_time": true, + "share_gt_over_time": true, "rollup": true, "rollup_rate": true, "rollup_deriv": true,