From ac8e300fd264a51e84934fb5c540d418f11bbacb Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Wed, 15 May 2019 15:14:06 +0200 Subject: [PATCH] Implement histogram_quantile() Signed-off-by: Julius Volz --- promflux/call.go | 16 + promflux/query_test.go | 15 + stdlib/promql/flux_gen.go | 190 +++++---- stdlib/promql/histogram_quantile.go | 396 +++++++++++++++++++ stdlib/promql/histogram_quantile_test.go | 483 +++++++++++++++++++++++ stdlib/promql/promql.flux | 1 + 6 files changed, 1023 insertions(+), 78 deletions(-) create mode 100644 stdlib/promql/histogram_quantile.go create mode 100644 stdlib/promql/histogram_quantile_test.go diff --git a/promflux/call.go b/promflux/call.go index 8e58c7ac49..8f6f8e839d 100644 --- a/promflux/call.go +++ b/promflux/call.go @@ -426,6 +426,22 @@ func (t *transpiler) transpileCall(c *promql.Call) (ast.Expression, error) { "fn": setConstValueFn(args[0]), }), ), nil + case "histogram_quantile": + if yieldsTable(c.Args[0]) { + return nil, fmt.Errorf("non-const scalar expressions not supported yet") + } + + return buildPipeline( + args[1], + call("group", map[string]ast.Expression{ + "columns": columnList("_time", "_value", "le"), + "mode": &ast.StringLiteral{Value: "except"}, + }), + call("promql.promHistogramQuantile", map[string]ast.Expression{ + "quantile": args[0], + }), + dropMeasurementCall, + ), nil default: return nil, fmt.Errorf("PromQL function %q is not supported yet", c.Func.Name) } diff --git a/promflux/query_test.go b/promflux/query_test.go index 0ec328a8ef..c60d26bfd8 100644 --- a/promflux/query_test.go +++ b/promflux/query_test.go @@ -354,6 +354,21 @@ var queries = []struct { { query: `vector(time())`, }, + { + query: `histogram_quantile({{.quantile}}, rate(demo_api_request_duration_seconds_bucket[1m]))`, + variantArgs: []string{"quantile"}, + }, + { + query: `histogram_quantile(0.9, nonexistent_metric)`, + }, + { + // Missing "le" label. + query: `histogram_quantile(0.9, demo_cpu_usage_seconds_total)`, + }, + { + // Missing "le" label only in some series of the same grouping. + query: `histogram_quantile(0.9, {__name__=~"demo_api_request_duration_seconds_.+"})`, + }, // Subqueries. Comparisons are skipped since the implementation cannot guarantee completely identical results. { diff --git a/stdlib/promql/flux_gen.go b/stdlib/promql/flux_gen.go index 5f1651c923..df5e777909 100644 --- a/stdlib/promql/flux_gen.go +++ b/stdlib/promql/flux_gen.go @@ -22,10 +22,10 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 2, - Line: 27, + Line: 28, }, File: "promql.flux", - Source: "package promql\n\nbuiltin changes\nbuiltin dayOfMonth\nbuiltin dayOfWeek\nbuiltin daysInMonth\nbuiltin linearRegression\nbuiltin emptyTable\nbuiltin extrapolatedRate\nbuiltin hour\nbuiltin instantRate\nbuiltin minute\nbuiltin month\nbuiltin resets\nbuiltin timestamp\nbuiltin year\n\n// hack to simulate an imported promql package\npromql = {\n dayOfMonth:dayOfMonth,\n dayOfWeek:dayOfWeek,\n daysInMonth:daysInMonth,\n hour:hour,\n minute:minute,\n month:month,\n year:year,\n}", + Source: "package promql\n\nbuiltin changes\nbuiltin dayOfMonth\nbuiltin dayOfWeek\nbuiltin daysInMonth\nbuiltin linearRegression\nbuiltin emptyTable\nbuiltin extrapolatedRate\nbuiltin promHistogramQuantile\nbuiltin hour\nbuiltin instantRate\nbuiltin minute\nbuiltin month\nbuiltin resets\nbuiltin timestamp\nbuiltin year\n\n// hack to simulate an imported promql package\npromql = {\n dayOfMonth:dayOfMonth,\n dayOfWeek:dayOfWeek,\n daysInMonth:daysInMonth,\n hour:hour,\n minute:minute,\n month:month,\n year:year,\n}", Start: ast.Position{ Column: 1, Line: 1, @@ -275,11 +275,11 @@ var pkgAST = &ast.Package{ Errors: nil, Loc: &ast.SourceLocation{ End: ast.Position{ - Column: 13, + Column: 30, Line: 10, }, File: "promql.flux", - Source: "builtin hour", + Source: "builtin promHistogramQuantile", Start: ast.Position{ Column: 1, Line: 10, @@ -291,17 +291,51 @@ var pkgAST = &ast.Package{ Errors: nil, Loc: &ast.SourceLocation{ End: ast.Position{ - Column: 13, + Column: 30, Line: 10, }, File: "promql.flux", - Source: "hour", + Source: "promHistogramQuantile", Start: ast.Position{ Column: 9, Line: 10, }, }, }, + Name: "promHistogramQuantile", + }, + }, &ast.BuiltinStatement{ + BaseNode: ast.BaseNode{ + Errors: nil, + Loc: &ast.SourceLocation{ + End: ast.Position{ + Column: 13, + Line: 11, + }, + File: "promql.flux", + Source: "builtin hour", + Start: ast.Position{ + Column: 1, + Line: 11, + }, + }, + }, + ID: &ast.Identifier{ + BaseNode: ast.BaseNode{ + Errors: nil, + Loc: &ast.SourceLocation{ + End: ast.Position{ + Column: 13, + Line: 11, + }, + File: "promql.flux", + Source: "hour", + Start: ast.Position{ + Column: 9, + Line: 11, + }, + }, + }, Name: "hour", }, }, &ast.BuiltinStatement{ @@ -310,13 +344,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 20, - Line: 11, + Line: 12, }, File: "promql.flux", Source: "builtin instantRate", Start: ast.Position{ Column: 1, - Line: 11, + Line: 12, }, }, }, @@ -326,13 +360,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 20, - Line: 11, + Line: 12, }, File: "promql.flux", Source: "instantRate", Start: ast.Position{ Column: 9, - Line: 11, + Line: 12, }, }, }, @@ -344,13 +378,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 15, - Line: 12, + Line: 13, }, File: "promql.flux", Source: "builtin minute", Start: ast.Position{ Column: 1, - Line: 12, + Line: 13, }, }, }, @@ -360,13 +394,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 15, - Line: 12, + Line: 13, }, File: "promql.flux", Source: "minute", Start: ast.Position{ Column: 9, - Line: 12, + Line: 13, }, }, }, @@ -378,13 +412,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 14, - Line: 13, + Line: 14, }, File: "promql.flux", Source: "builtin month", Start: ast.Position{ Column: 1, - Line: 13, + Line: 14, }, }, }, @@ -394,13 +428,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 14, - Line: 13, + Line: 14, }, File: "promql.flux", Source: "month", Start: ast.Position{ Column: 9, - Line: 13, + Line: 14, }, }, }, @@ -412,13 +446,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 15, - Line: 14, + Line: 15, }, File: "promql.flux", Source: "builtin resets", Start: ast.Position{ Column: 1, - Line: 14, + Line: 15, }, }, }, @@ -428,13 +462,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 15, - Line: 14, + Line: 15, }, File: "promql.flux", Source: "resets", Start: ast.Position{ Column: 9, - Line: 14, + Line: 15, }, }, }, @@ -446,13 +480,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 18, - Line: 15, + Line: 16, }, File: "promql.flux", Source: "builtin timestamp", Start: ast.Position{ Column: 1, - Line: 15, + Line: 16, }, }, }, @@ -462,13 +496,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 18, - Line: 15, + Line: 16, }, File: "promql.flux", Source: "timestamp", Start: ast.Position{ Column: 9, - Line: 15, + Line: 16, }, }, }, @@ -480,13 +514,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 13, - Line: 16, + Line: 17, }, File: "promql.flux", Source: "builtin year", Start: ast.Position{ Column: 1, - Line: 16, + Line: 17, }, }, }, @@ -496,13 +530,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 13, - Line: 16, + Line: 17, }, File: "promql.flux", Source: "year", Start: ast.Position{ Column: 9, - Line: 16, + Line: 17, }, }, }, @@ -514,13 +548,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 2, - Line: 27, + Line: 28, }, File: "promql.flux", Source: "promql = {\n dayOfMonth:dayOfMonth,\n dayOfWeek:dayOfWeek,\n daysInMonth:daysInMonth,\n hour:hour,\n minute:minute,\n month:month,\n year:year,\n}", Start: ast.Position{ Column: 1, - Line: 19, + Line: 20, }, }, }, @@ -530,13 +564,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 7, - Line: 19, + Line: 20, }, File: "promql.flux", Source: "promql", Start: ast.Position{ Column: 1, - Line: 19, + Line: 20, }, }, }, @@ -548,13 +582,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 2, - Line: 27, + Line: 28, }, File: "promql.flux", Source: "{\n dayOfMonth:dayOfMonth,\n dayOfWeek:dayOfWeek,\n daysInMonth:daysInMonth,\n hour:hour,\n minute:minute,\n month:month,\n year:year,\n}", Start: ast.Position{ Column: 10, - Line: 19, + Line: 20, }, }, }, @@ -564,13 +598,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 24, - Line: 20, + Line: 21, }, File: "promql.flux", Source: "dayOfMonth:dayOfMonth", Start: ast.Position{ Column: 3, - Line: 20, + Line: 21, }, }, }, @@ -580,13 +614,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 13, - Line: 20, + Line: 21, }, File: "promql.flux", Source: "dayOfMonth", Start: ast.Position{ Column: 3, - Line: 20, + Line: 21, }, }, }, @@ -598,13 +632,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 24, - Line: 20, + Line: 21, }, File: "promql.flux", Source: "dayOfMonth", Start: ast.Position{ Column: 14, - Line: 20, + Line: 21, }, }, }, @@ -616,13 +650,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 22, - Line: 21, + Line: 22, }, File: "promql.flux", Source: "dayOfWeek:dayOfWeek", Start: ast.Position{ Column: 3, - Line: 21, + Line: 22, }, }, }, @@ -632,13 +666,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 12, - Line: 21, + Line: 22, }, File: "promql.flux", Source: "dayOfWeek", Start: ast.Position{ Column: 3, - Line: 21, + Line: 22, }, }, }, @@ -650,13 +684,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 22, - Line: 21, + Line: 22, }, File: "promql.flux", Source: "dayOfWeek", Start: ast.Position{ Column: 13, - Line: 21, + Line: 22, }, }, }, @@ -668,13 +702,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 26, - Line: 22, + Line: 23, }, File: "promql.flux", Source: "daysInMonth:daysInMonth", Start: ast.Position{ Column: 3, - Line: 22, + Line: 23, }, }, }, @@ -684,13 +718,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 14, - Line: 22, + Line: 23, }, File: "promql.flux", Source: "daysInMonth", Start: ast.Position{ Column: 3, - Line: 22, + Line: 23, }, }, }, @@ -702,13 +736,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 26, - Line: 22, + Line: 23, }, File: "promql.flux", Source: "daysInMonth", Start: ast.Position{ Column: 15, - Line: 22, + Line: 23, }, }, }, @@ -720,13 +754,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 12, - Line: 23, + Line: 24, }, File: "promql.flux", Source: "hour:hour", Start: ast.Position{ Column: 3, - Line: 23, + Line: 24, }, }, }, @@ -736,13 +770,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 7, - Line: 23, + Line: 24, }, File: "promql.flux", Source: "hour", Start: ast.Position{ Column: 3, - Line: 23, + Line: 24, }, }, }, @@ -754,13 +788,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 12, - Line: 23, + Line: 24, }, File: "promql.flux", Source: "hour", Start: ast.Position{ Column: 8, - Line: 23, + Line: 24, }, }, }, @@ -772,13 +806,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 16, - Line: 24, + Line: 25, }, File: "promql.flux", Source: "minute:minute", Start: ast.Position{ Column: 3, - Line: 24, + Line: 25, }, }, }, @@ -788,13 +822,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 9, - Line: 24, + Line: 25, }, File: "promql.flux", Source: "minute", Start: ast.Position{ Column: 3, - Line: 24, + Line: 25, }, }, }, @@ -806,13 +840,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 16, - Line: 24, + Line: 25, }, File: "promql.flux", Source: "minute", Start: ast.Position{ Column: 10, - Line: 24, + Line: 25, }, }, }, @@ -824,13 +858,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 14, - Line: 25, + Line: 26, }, File: "promql.flux", Source: "month:month", Start: ast.Position{ Column: 3, - Line: 25, + Line: 26, }, }, }, @@ -840,13 +874,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 8, - Line: 25, + Line: 26, }, File: "promql.flux", Source: "month", Start: ast.Position{ Column: 3, - Line: 25, + Line: 26, }, }, }, @@ -858,13 +892,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 14, - Line: 25, + Line: 26, }, File: "promql.flux", Source: "month", Start: ast.Position{ Column: 9, - Line: 25, + Line: 26, }, }, }, @@ -876,13 +910,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 12, - Line: 26, + Line: 27, }, File: "promql.flux", Source: "year:year", Start: ast.Position{ Column: 3, - Line: 26, + Line: 27, }, }, }, @@ -892,13 +926,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 7, - Line: 26, + Line: 27, }, File: "promql.flux", Source: "year", Start: ast.Position{ Column: 3, - Line: 26, + Line: 27, }, }, }, @@ -910,13 +944,13 @@ var pkgAST = &ast.Package{ Loc: &ast.SourceLocation{ End: ast.Position{ Column: 12, - Line: 26, + Line: 27, }, File: "promql.flux", Source: "year", Start: ast.Position{ Column: 8, - Line: 26, + Line: 27, }, }, }, diff --git a/stdlib/promql/histogram_quantile.go b/stdlib/promql/histogram_quantile.go new file mode 100644 index 0000000000..0dc59fde0c --- /dev/null +++ b/stdlib/promql/histogram_quantile.go @@ -0,0 +1,396 @@ +package promql + +import ( + "fmt" + "math" + "sort" + "strconv" + + "github.com/influxdata/flux" + "github.com/influxdata/flux/execute" + "github.com/influxdata/flux/plan" + "github.com/influxdata/flux/semantic" +) + +// TODO: Added "prom" prefix to avoid duplicate registration error. Decide whether to +// remove universe version of this function. +const HistogramQuantileKind = "promHistogramQuantile" + +const DefaultUpperBoundColumnLabel = "le" + +type HistogramQuantileOpSpec struct { + Quantile float64 `json:"quantile"` + CountColumn string `json:"countColumn"` + UpperBoundColumn string `json:"upperBoundColumn"` + ValueColumn string `json:"valueColumn"` +} + +func init() { + histogramQuantileSignature := flux.FunctionSignature(map[string]semantic.PolyType{ + "quantile": semantic.Float, + "countColumn": semantic.String, + "upperBoundColumn": semantic.String, + "valueColumn": semantic.String, + }, nil) + + flux.RegisterPackageValue("promql", HistogramQuantileKind, flux.FunctionValue(HistogramQuantileKind, createHistogramQuantileOpSpec, histogramQuantileSignature)) + flux.RegisterOpSpec(HistogramQuantileKind, newHistogramQuantileOp) + plan.RegisterProcedureSpec(HistogramQuantileKind, newHistogramQuantileProcedure, HistogramQuantileKind) + execute.RegisterTransformation(HistogramQuantileKind, createHistogramQuantileTransformation) +} +func createHistogramQuantileOpSpec(args flux.Arguments, a *flux.Administration) (flux.OperationSpec, error) { + if err := a.AddParentFromArgs(args); err != nil { + return nil, err + } + + s := new(HistogramQuantileOpSpec) + q, err := args.GetRequiredFloat("quantile") + if err != nil { + return nil, err + } + s.Quantile = q + + if col, ok, err := args.GetString("countColumn"); err != nil { + return nil, err + } else if ok { + s.CountColumn = col + } else { + s.CountColumn = execute.DefaultValueColLabel + } + + if col, ok, err := args.GetString("upperBoundColumn"); err != nil { + return nil, err + } else if ok { + s.UpperBoundColumn = col + } else { + s.UpperBoundColumn = DefaultUpperBoundColumnLabel + } + + if col, ok, err := args.GetString("valueColumn"); err != nil { + return nil, err + } else if ok { + s.ValueColumn = col + } else { + s.ValueColumn = execute.DefaultValueColLabel + } + + return s, nil +} + +func newHistogramQuantileOp() flux.OperationSpec { + return new(HistogramQuantileOpSpec) +} + +func (s *HistogramQuantileOpSpec) Kind() flux.OperationKind { + return HistogramQuantileKind +} + +type HistogramQuantileProcedureSpec struct { + plan.DefaultCost + Quantile float64 `json:"quantile"` + CountColumn string `json:"countColumn"` + UpperBoundColumn string `json:"upperBoundColumn"` + ValueColumn string `json:"valueColumn"` +} + +func newHistogramQuantileProcedure(qs flux.OperationSpec, a plan.Administration) (plan.ProcedureSpec, error) { + spec, ok := qs.(*HistogramQuantileOpSpec) + if !ok { + return nil, fmt.Errorf("invalid spec type %T", qs) + } + return &HistogramQuantileProcedureSpec{ + Quantile: spec.Quantile, + CountColumn: spec.CountColumn, + UpperBoundColumn: spec.UpperBoundColumn, + ValueColumn: spec.ValueColumn, + }, nil +} + +func (s *HistogramQuantileProcedureSpec) Kind() plan.ProcedureKind { + return HistogramQuantileKind +} +func (s *HistogramQuantileProcedureSpec) Copy() plan.ProcedureSpec { + ns := new(HistogramQuantileProcedureSpec) + *ns = *s + return ns +} + +type histogramQuantileTransformation struct { + d execute.Dataset + cache execute.TableBuilderCache + + spec HistogramQuantileProcedureSpec +} + +type bucket struct { + count float64 + upperBound float64 +} + +func createHistogramQuantileTransformation(id execute.DatasetID, mode execute.AccumulationMode, spec plan.ProcedureSpec, a execute.Administration) (execute.Transformation, execute.Dataset, error) { + s, ok := spec.(*HistogramQuantileProcedureSpec) + if !ok { + return nil, nil, fmt.Errorf("invalid spec type %T", spec) + } + cache := execute.NewTableBuilderCache(a.Allocator()) + d := execute.NewDataset(id, mode, cache) + t := NewHistogramQuantileTransformation(d, cache, s) + return t, d, nil +} + +func NewHistogramQuantileTransformation( + d execute.Dataset, + cache execute.TableBuilderCache, + spec *HistogramQuantileProcedureSpec, +) execute.Transformation { + return &histogramQuantileTransformation{ + d: d, + cache: cache, + spec: *spec, + } +} + +func (t histogramQuantileTransformation) RetractTable(id execute.DatasetID, key flux.GroupKey) error { + // TODO + return nil +} + +func (t histogramQuantileTransformation) Process(id execute.DatasetID, tbl flux.Table) error { + builder, created := t.cache.TableBuilder(tbl.Key()) + if !created { + return fmt.Errorf("histogramQuantile found duplicate table with key: %v", tbl.Key()) + } + + if err := execute.AddTableKeyCols(tbl.Key(), builder); err != nil { + return err + } + valueIdx, err := builder.AddCol(flux.ColMeta{ + Label: t.spec.ValueColumn, + Type: flux.TFloat, + }) + if err != nil { + return err + } + + countIdx := execute.ColIdx(t.spec.CountColumn, tbl.Cols()) + if countIdx < 0 { + return fmt.Errorf("table is missing count column %q", t.spec.CountColumn) + } + if tbl.Cols()[countIdx].Type != flux.TFloat { + return fmt.Errorf("count column %q must be of type float", t.spec.CountColumn) + } + upperBoundIdx := execute.ColIdx(t.spec.UpperBoundColumn, tbl.Cols()) + if upperBoundIdx < 0 { + // No "le" labels present at all, return empty result. + return nil + } + if tbl.Cols()[upperBoundIdx].Type != flux.TString { + return fmt.Errorf("upper bound column %q must be of type string", t.spec.UpperBoundColumn) + } + // Read buckets + var cdf []bucket + sorted := true //track if the cdf was naturally sorted + if err := tbl.Do(func(cr flux.ColReader) error { + offset := len(cdf) + // Grow cdf by number of rows + l := offset + cr.Len() + if cap(cdf) < l { + cpy := make([]bucket, l, l*2) + // Copy existing buckets to new slice + copy(cpy, cdf) + cdf = cpy + } else { + cdf = cdf[:l] + } + for i := 0; i < cr.Len(); i++ { + curr := i + offset + prev := curr - 1 + + b := bucket{} + if vs := cr.Floats(countIdx); vs.IsValid(i) { + b.count = vs.Value(i) + } else { + return fmt.Errorf("unexpected null in the countColumn") + } + if vs := cr.Strings(upperBoundIdx); vs.IsValid(i) { + upperBound, err := strconv.ParseFloat(string(vs.Value(i)), 64) + if err != nil { + // "le" label value invalid, skip. + continue + } + b.upperBound = upperBound + } else { + // "le" label missing, skip. + continue + } + cdf[curr] = b + if prev >= 0 { + sorted = sorted && cdf[prev].upperBound <= cdf[curr].upperBound + } + } + return nil + }); err != nil { + return err + } + + if !sorted { + sort.Slice(cdf, func(i, j int) bool { + return cdf[i].upperBound < cdf[j].upperBound + }) + } + + q := bucketQuantile(t.spec.Quantile, cdf) + if err := execute.AppendKeyValues(tbl.Key(), builder); err != nil { + return err + } + if err := builder.AppendFloat(valueIdx, q); err != nil { + return err + } + return nil +} + +// Taken from Prometheus. +// TODO: Include some kind of copyright notice? +// +// bucketQuantile calculates the quantile 'q' based on the given buckets. The +// buckets will be sorted by upperBound by this function (i.e. no sorting +// needed before calling this function). The quantile value is interpolated +// assuming a linear distribution within a bucket. However, if the quantile +// falls into the highest bucket, the upper bound of the 2nd highest bucket is +// returned. A natural lower bound of 0 is assumed if the upper bound of the +// lowest bucket is greater 0. In that case, interpolation in the lowest bucket +// happens linearly between 0 and the upper bound of the lowest bucket. +// However, if the lowest bucket has an upper bound less or equal 0, this upper +// bound is returned if the quantile falls into the lowest bucket. +// +// There are a number of special cases (once we have a way to report errors +// happening during evaluations of AST functions, we should report those +// explicitly): +// +// If 'buckets' has fewer than 2 elements, NaN is returned. +// +// If the highest bucket is not +Inf, NaN is returned. +// +// If q<0, -Inf is returned. +// +// If q>1, +Inf is returned. +func bucketQuantile(q float64, buckets []bucket) float64 { + if q < 0 { + return math.Inf(-1) + } + if q > 1 { + return math.Inf(+1) + } + if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) { + return math.NaN() + } + + buckets = coalesceBuckets(buckets) + ensureMonotonic(buckets) + + if len(buckets) < 2 { + return math.NaN() + } + + rank := q * buckets[len(buckets)-1].count + b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank }) + + if b == len(buckets)-1 { + return buckets[len(buckets)-2].upperBound + } + if b == 0 && buckets[0].upperBound <= 0 { + return buckets[0].upperBound + } + var ( + bucketStart float64 + bucketEnd = buckets[b].upperBound + count = buckets[b].count + ) + if b > 0 { + bucketStart = buckets[b-1].upperBound + count -= buckets[b-1].count + rank -= buckets[b-1].count + } + return bucketStart + (bucketEnd-bucketStart)*(rank/count) +} + +// Taken from Prometheus. +// TODO: Include some kind of copyright notice? +// +// coalesceBuckets merges buckets with the same upper bound. +// +// The input buckets must be sorted. +func coalesceBuckets(buckets []bucket) []bucket { + last := buckets[0] + i := 0 + for _, b := range buckets[1:] { + if b.upperBound == last.upperBound { + last.count += b.count + } else { + buckets[i] = last + last = b + i++ + } + } + buckets[i] = last + return buckets[:i+1] +} + +// Taken from Prometheus. +// TODO: Include some kind of copyright notice? +// +// The assumption that bucket counts increase monotonically with increasing +// upperBound may be violated during: +// +// * Recording rule evaluation of histogram_quantile, especially when rate() +// has been applied to the underlying bucket timeseries. +// * Evaluation of histogram_quantile computed over federated bucket +// timeseries, especially when rate() has been applied. +// +// This is because scraped data is not made available to rule evaluation or +// federation atomically, so some buckets are computed with data from the +// most recent scrapes, but the other buckets are missing data from the most +// recent scrape. +// +// Monotonicity is usually guaranteed because if a bucket with upper bound +// u1 has count c1, then any bucket with a higher upper bound u > u1 must +// have counted all c1 observations and perhaps more, so that c >= c1. +// +// Randomly interspersed partial sampling breaks that guarantee, and rate() +// exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from +// 4 samples but the bucket with le=2000 has a count of 7 from 3 samples. The +// monotonicity is broken. It is exacerbated by rate() because under normal +// operation, cumulative counting of buckets will cause the bucket counts to +// diverge such that small differences from missing samples are not a problem. +// rate() removes this divergence.) +// +// bucketQuantile depends on that monotonicity to do a binary search for the +// bucket with the φ-quantile count, so breaking the monotonicity +// guarantee causes bucketQuantile() to return undefined (nonsense) results. +// +// As a somewhat hacky solution until ingestion is atomic per scrape, we +// calculate the "envelope" of the histogram buckets, essentially removing +// any decreases in the count between successive buckets. +func ensureMonotonic(buckets []bucket) { + max := buckets[0].count + for i := range buckets[1:] { + switch { + case buckets[i].count > max: + max = buckets[i].count + case buckets[i].count < max: + buckets[i].count = max + } + } +} + +func (t histogramQuantileTransformation) UpdateWatermark(id execute.DatasetID, mark execute.Time) error { + return t.d.UpdateWatermark(mark) +} + +func (t histogramQuantileTransformation) UpdateProcessingTime(id execute.DatasetID, pt execute.Time) error { + return t.d.UpdateProcessingTime(pt) +} + +func (t histogramQuantileTransformation) Finish(id execute.DatasetID, err error) { + t.d.Finish(err) +} diff --git a/stdlib/promql/histogram_quantile_test.go b/stdlib/promql/histogram_quantile_test.go new file mode 100644 index 0000000000..aa67a1d4c9 --- /dev/null +++ b/stdlib/promql/histogram_quantile_test.go @@ -0,0 +1,483 @@ +package promql_test + +import ( + "math" + "testing" + + "github.com/influxdata/flux" + "github.com/influxdata/flux/execute" + "github.com/influxdata/flux/execute/executetest" + "github.com/influxdata/flux/stdlib/promql" + "github.com/pkg/errors" +) + +var linearHist = []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.1", 1.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.2", 2.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.3", 3.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.4", 4.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.5", 5.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.6", 6.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.7", 7.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.8", 8.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.9", 9.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "+Inf", 10.0}, + }, +}} +var linearHistNoMax = []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.2", 2.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.4", 4.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.6", 6.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.8", 8.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + }, +}} +var unsortedOddHist = []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.4", 4.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.6", 6.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "+Inf", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.2", 2.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.8", 10.0}, + }, +}} +var nonLinearHist = []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.1", 1.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.5", 5.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "+Inf", 11.0}, + }, +}} + +func TestHistogramQuantile_Process(t *testing.T) { + testCases := []struct { + name string + spec *promql.HistogramQuantileProcedureSpec + data []flux.Table + want []*executetest.Table + wantErr error + }{ + { + name: "90th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.9, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.9}, + }, + }}, + }, + { + name: "0th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.0, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.0}, + }, + }}, + }, + { + name: "5th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.05, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.05}, + }, + }}, + }, + { + name: "10th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.1, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.1}, + }, + }}, + }, + { + name: "95th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.95, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.95}, + }, + }}, + }, + { + name: "99.999th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.99999, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.99999}, + }, + }}, + }, + { + name: "100th linear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 1.0, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 1.0}, + }, + }}, + }, + { + name: "100th linear no max", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 1.0, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: linearHistNoMax, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), math.NaN()}, + }, + }}, + }, + { + name: "90th linear unsorted odd", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.9, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: unsortedOddHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.75}, + }, + }}, + }, + { + name: "100th linear unsorted odd", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 1.0, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: unsortedOddHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.8}, + }, + }}, + }, + { + name: "90th nonlinear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.90, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: nonLinearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.99}, + }, + }}, + }, + { + name: "highest finite upper bound nonlinear", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.99, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: nonLinearHist, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 1.0}, + }, + }}, + }, + { + name: "null in count column", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.9, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.1", 1.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.2", 2.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.3", 3.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.4", 4.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.5", 5.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.6", 6.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.7", nil}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.8", 8.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.9", 9.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "+Inf", 10.0}, + }, + }}, + wantErr: errors.New("unexpected null in the countColumn"), + }, + { + name: "null in upperBound column", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.9, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.1", 1.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.2", 2.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.3", 3.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.4", 4.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.5", 5.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.6", 6.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), nil, 7.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.8", 8.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.9", 9.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "+Inf", 10.0}, + }, + }}, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.9}, + }, + }}, + }, + { + name: "unparseable value in upperBound column", + spec: &promql.HistogramQuantileProcedureSpec{ + Quantile: 0.9, + CountColumn: "_value", + UpperBoundColumn: "le", + ValueColumn: "_value", + }, + data: []flux.Table{&executetest.Table{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_time", Type: flux.TTime}, + {Label: "le", Type: flux.TString}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), execute.Time(1), "0.1", 1.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.2", 2.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.3", 3.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.4", 4.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.5", 5.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.6", 6.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "invalid", 7.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.8", 8.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "0.9", 9.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "1.0", 10.0}, + {execute.Time(1), execute.Time(3), execute.Time(1), "+Inf", 10.0}, + }, + }}, + want: []*executetest.Table{{ + KeyCols: []string{"_start", "_stop"}, + ColMeta: []flux.ColMeta{ + {Label: "_start", Type: flux.TTime}, + {Label: "_stop", Type: flux.TTime}, + {Label: "_value", Type: flux.TFloat}, + }, + Data: [][]interface{}{ + {execute.Time(1), execute.Time(3), 0.9}, + }, + }}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + executetest.ProcessTestHelper( + t, + tc.data, + tc.want, + tc.wantErr, + func(d execute.Dataset, c execute.TableBuilderCache) execute.Transformation { + return promql.NewHistogramQuantileTransformation(d, c, tc.spec) + }, + ) + }) + } +} diff --git a/stdlib/promql/promql.flux b/stdlib/promql/promql.flux index 9dc05e6664..ea297f61f1 100644 --- a/stdlib/promql/promql.flux +++ b/stdlib/promql/promql.flux @@ -7,6 +7,7 @@ builtin daysInMonth builtin linearRegression builtin emptyTable builtin extrapolatedRate +builtin promHistogramQuantile builtin hour builtin instantRate builtin minute