diff --git a/docs/querying/functions.md b/docs/querying/functions.md index c9e65fe6c..461bdc2b6 100644 --- a/docs/querying/functions.md +++ b/docs/querying/functions.md @@ -421,6 +421,11 @@ by the number of seconds under the specified time range window, and should be used primarily for human readability. Use `rate` in recording rules so that increases are tracked consistently on a per-second basis. +## `info()` + +For each time series in `v`, `info(v instant-vector, [label-selector string])` finds all info metrics with corresponding +identifying labels, and adds the union of their data labels to the time series, that gets returned. + ## `irate()` `irate(v range-vector)` calculates the per-second instant rate of increase of diff --git a/promql/engine.go b/promql/engine.go index 1e880410a..61a13cf33 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -707,6 +707,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval lookbackDelta: s.LookbackDelta, samplesStats: query.sampleStats, noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn, + querier: querier, } query.sampleStats.InitStepTracking(start, start, 1) @@ -764,6 +765,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval lookbackDelta: s.LookbackDelta, samplesStats: query.sampleStats, noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn, + querier: querier, } query.sampleStats.InitStepTracking(evaluator.startTimestamp, evaluator.endTimestamp, evaluator.interval) val, warnings, err := evaluator.Eval(s.Expr) @@ -1017,6 +1019,8 @@ type evaluator struct { lookbackDelta time.Duration samplesStats *stats.QuerySamples noStepSubqueryIntervalFn func(rangeMillis int64) int64 + + querier storage.Querier } // errorf causes a panic with the input formatted into an error. @@ -1092,6 +1096,8 @@ type EvalNodeHelper struct { rightSigs map[string]Sample matchedSigs map[string]map[uint64]struct{} resultMetric map[string]labels.Labels + + Querier storage.Querier } func (enh *EvalNodeHelper) resetBuilder(lbls labels.Labels) { @@ -1141,7 +1147,7 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) biggestLen = len(matrixes[i]) } } - enh := &EvalNodeHelper{Out: make(Vector, 0, biggestLen)} + enh := &EvalNodeHelper{Out: make(Vector, 0, biggestLen), Querier: ev.querier} type seriesAndTimestamp struct { Series ts int64 @@ -1170,6 +1176,9 @@ func (ev *evaluator) rangeEval(prepSeries func(labels.Labels, *EvalSeriesHelper) } } + // For each timestamp, iterate over the expressions, and for each series in each expression's matrix, + // append a corresponding sample to the expression's input vector. + // Then, call funcCall. for ts := ev.startTimestamp; ts <= ev.endTimestamp; ts += ev.interval { if err := contextDone(ev.ctx, "expression evaluation"); err != nil { ev.error(err) diff --git a/promql/functions.go b/promql/functions.go index fe1a5644e..0c83f13a6 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -14,6 +14,7 @@ package promql import ( + "context" "fmt" "math" "sort" @@ -30,6 +31,7 @@ import ( "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/promql/parser/posrange" + "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/util/annotations" ) @@ -1417,7 +1419,6 @@ func funcLabelJoin(vals []parser.Value, args parser.Expressions, enh *EvalNodeHe if l, ok := enh.Dmn[h]; ok { outMetric = l } else { - for i, src := range srcLabels { srcVals[i] = el.Metric.Get(src) } @@ -1520,6 +1521,126 @@ func funcYear(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) }), nil } +// === info(vector parser.ValueTypeVector, [ls label-selector]) (Vector, Annotations) === +func funcInfo(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + vector := vals[0].(Vector) + var dataLabelMatchers []*labels.Matcher + if len(args) > 1 { + // TODO: Introduce a dedicated LabelSelector type + labelSelector := args[1].(*parser.VectorSelector) + dataLabelMatchers = labelSelector.LabelMatchers + } + + if len(vector) == 0 { + return enh.Out, nil + } + + if enh.Dmn == nil { + enh.Dmn = make(map[uint64]labels.Labels, len(enh.Out)) + } + + ts := vector[0].T + hints := &storage.SelectHints{ + Start: ts, + End: ts, + Func: "info", + } + selectors := []*labels.Matcher{ + labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".+_info"), + } + selectors = append(selectors, dataLabelMatchers...) + + it := enh.Querier.Select(context.TODO(), false, hints, selectors...) + var infoSeries []storage.Series + for it.Next() { + infoSeries = append(infoSeries, it.At()) + } + if it.Err() != nil { + var annots annotations.Annotations + annots.Add(fmt.Errorf("querying for info metrics: %w", it.Err())) + return nil, annots + } + + for _, s := range vector { + // Hash of sample's label set (the label set identifies the metric) + h := s.Metric.Hash() + if ls, ok := enh.Dmn[h]; ok { + // We've encountered this label set/metric before and can re-use the previous result + enh.Out = append(enh.Out, Sample{ + Metric: ls, + F: s.F, + H: s.H, + }) + continue + } + + lb := labels.NewBuilder(s.Metric) + + // Find info series with matching labels, these are identifying labels + addedDataLabels := map[string]struct{}{} + for _, series := range infoSeries { + infoLbls := series.Labels() + + identifyingLabels := map[string]string{} + dataLabels := map[string]string{} + infoLbls.Range(func(l labels.Label) { + if l.Name == "__name__" { + return + } + + if s.Metric.Has(l.Name) { + identifyingLabels[l.Name] = l.Value + return + } + + if len(dataLabelMatchers) > 0 { + // Keep only data labels among dataLabelMatchers + found := false + for _, m := range dataLabelMatchers { + if m.Name == l.Name { + found = true + break + } + } + if !found { + return + } + } + dataLabels[l.Name] = l.Value + }) + if len(identifyingLabels) == 0 { + continue + } + + name := infoLbls.Get("__name__") + fmt.Printf("Identifying labels for info metric %s: %#v, data labels: %#v\n", name, identifyingLabels, dataLabels) + for n, v := range dataLabels { + if lb.Get(n) == "" { + lb.Set(n, v) + } + + addedDataLabels[n] = struct{}{} + } + } + + // If info metric data label matchers are specified, we should only include series where + // info metric data labels are found + if len(dataLabelMatchers) > 0 && len(addedDataLabels) == 0 { + continue + } + + ls := lb.Labels() + enh.Dmn[h] = ls + enh.Out = append(enh.Out, Sample{ + Metric: ls, + F: s.F, + H: s.H, + }) + } + + return enh.Out, nil +} + // FunctionCalls is a list of all functions supported by PromQL, including their types. var FunctionCalls = map[string]FunctionCall{ "abs": funcAbs, @@ -1560,6 +1681,7 @@ var FunctionCalls = map[string]FunctionCall{ "hour": funcHour, "idelta": funcIdelta, "increase": funcIncrease, + "info": funcInfo, "irate": funcIrate, "label_replace": funcLabelReplace, "label_join": funcLabelJoin, diff --git a/promql/parser/functions.go b/promql/parser/functions.go index 99b41321f..434d3cdc1 100644 --- a/promql/parser/functions.go +++ b/promql/parser/functions.go @@ -223,6 +223,13 @@ var Functions = map[string]*Function{ ArgTypes: []ValueType{ValueTypeMatrix}, ReturnType: ValueTypeVector, }, + "info": { + Name: "info", + ArgTypes: []ValueType{ValueTypeVector, ValueTypeVector}, + ReturnType: ValueTypeVector, + Experimental: true, + Variadic: 1, + }, "irate": { Name: "irate", ArgTypes: []ValueType{ValueTypeMatrix}, diff --git a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts index 9d5d55f60..55082939e 100644 --- a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts +++ b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts @@ -281,6 +281,12 @@ export const functionIdentifierTerms = [ info: 'Calculate the increase in value over a range of time (for counters)', type: 'function', }, + { + label: 'info', + detail: 'function', + info: 'Add data labels from corresponding info metrics', + type: 'function', + }, { label: 'irate', detail: 'function', diff --git a/web/ui/module/codemirror-promql/src/types/function.ts b/web/ui/module/codemirror-promql/src/types/function.ts index 2505edc22..50fa400a3 100644 --- a/web/ui/module/codemirror-promql/src/types/function.ts +++ b/web/ui/module/codemirror-promql/src/types/function.ts @@ -50,6 +50,7 @@ import { Hour, Idelta, Increase, + Info, Irate, LabelJoin, LabelReplace, @@ -336,6 +337,12 @@ const promqlFunctions: { [key: number]: PromQLFunction } = { variadic: 0, returnType: ValueType.vector, }, + [Info]: { + name: 'info', + argTypes: [ValueType.vector, ValueType.vector], + variadic: 0, + returnType: ValueType.vector, + }, [Irate]: { name: 'irate', argTypes: [ValueType.matrix], diff --git a/web/ui/module/lezer-promql/src/promql.grammar b/web/ui/module/lezer-promql/src/promql.grammar index 496648317..4e8789594 100644 --- a/web/ui/module/lezer-promql/src/promql.grammar +++ b/web/ui/module/lezer-promql/src/promql.grammar @@ -143,6 +143,7 @@ FunctionIdentifier { Hour | Idelta | Increase | + Info | Irate | LabelReplace | LabelJoin | @@ -376,6 +377,7 @@ NumberLiteral { Hour { condFn<"hour"> } Idelta { condFn<"idelta"> } Increase { condFn<"increase"> } + Info { condFn<"info"> } Irate { condFn<"irate"> } LabelReplace { condFn<"label_replace"> } LabelJoin { condFn<"label_join"> }