diff --git a/promql/functions.go b/promql/functions.go index 607421470..d496fb958 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -18,6 +18,7 @@ import ( "regexp" "sort" "strconv" + "strings" "time" "github.com/prometheus/common/model" @@ -28,11 +29,11 @@ import ( // Function represents a function of the expression language and is // used by function nodes. type Function struct { - Name string - ArgTypes []model.ValueType - OptionalArgs int - ReturnType model.ValueType - Call func(ev *evaluator, args Expressions) model.Value + Name string + ArgTypes []model.ValueType + Variadic int + ReturnType model.ValueType + Call func(ev *evaluator, args Expressions) model.Value } // === time() model.SampleValue === @@ -849,6 +850,50 @@ func funcLabelReplace(ev *evaluator, args Expressions) model.Value { return vector } +// === label_join(vector model.ValVector, dest_labelname, separator, src_labelname...) Vector === +func funcLabelJoin(ev *evaluator, args Expressions) model.Value { + var ( + vector = ev.evalVector(args[0]) + dst = model.LabelName(ev.evalString(args[1]).Value) + sep = ev.evalString(args[2]).Value + srcLabels = make([]model.LabelName, len(args)-3) + ) + for i := 3; i < len(args); i++ { + src := model.LabelName(ev.evalString(args[i]).Value) + if !model.LabelNameRE.MatchString(string(src)) { + ev.errorf("invalid source label name in label_join(): %s", src) + } + srcLabels[i-3] = src + } + + if !model.LabelNameRE.MatchString(string(dst)) { + ev.errorf("invalid destination label name in label_join(): %s", dst) + } + + outSet := make(map[model.Fingerprint]struct{}, len(vector)) + for _, el := range vector { + srcVals := make([]string, len(srcLabels)) + for i, src := range srcLabels { + srcVals[i] = string(el.Metric.Metric[src]) + } + + strval := strings.Join(srcVals, sep) + if strval == "" { + el.Metric.Del(dst) + } else { + el.Metric.Set(dst, model.LabelValue(strval)) + } + + fp := el.Metric.Metric.Fingerprint() + if _, exists := outSet[fp]; exists { + ev.errorf("duplicated label set in output of label_join(): %s", el.Metric.Metric) + } else { + outSet[fp] = struct{}{} + } + } + return vector +} + // === vector(s scalar) Vector === func funcVector(ev *evaluator, args Expressions) model.Value { return vector{ @@ -986,25 +1031,25 @@ var functions = map[string]*Function{ Call: funcCountScalar, }, "days_in_month": { - Name: "days_in_month", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcDaysInMonth, + Name: "days_in_month", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcDaysInMonth, }, "day_of_month": { - Name: "day_of_month", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcDayOfMonth, + Name: "day_of_month", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcDayOfMonth, }, "day_of_week": { - Name: "day_of_week", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcDayOfWeek, + Name: "day_of_week", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcDayOfWeek, }, "delta": { Name: "delta", @@ -1049,11 +1094,11 @@ var functions = map[string]*Function{ Call: funcHoltWinters, }, "hour": { - Name: "hour", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcHour, + Name: "hour", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcHour, }, "idelta": { Name: "idelta", @@ -1079,6 +1124,13 @@ var functions = map[string]*Function{ ReturnType: model.ValVector, Call: funcLabelReplace, }, + "label_join": { + Name: "label_join", + ArgTypes: []model.ValueType{model.ValVector, model.ValString, model.ValString, model.ValString}, + Variadic: -1, + ReturnType: model.ValVector, + Call: funcLabelJoin, + }, "ln": { Name: "ln", ArgTypes: []model.ValueType{model.ValVector}, @@ -1110,18 +1162,18 @@ var functions = map[string]*Function{ Call: funcMinOverTime, }, "minute": { - Name: "minute", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcMinute, + Name: "minute", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcMinute, }, "month": { - Name: "month", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcMonth, + Name: "month", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcMonth, }, "predict_linear": { Name: "predict_linear", @@ -1148,11 +1200,11 @@ var functions = map[string]*Function{ Call: funcResets, }, "round": { - Name: "round", - ArgTypes: []model.ValueType{model.ValVector, model.ValScalar}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcRound, + Name: "round", + ArgTypes: []model.ValueType{model.ValVector, model.ValScalar}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcRound, }, "scalar": { Name: "scalar", @@ -1209,11 +1261,11 @@ var functions = map[string]*Function{ Call: funcVector, }, "year": { - Name: "year", - ArgTypes: []model.ValueType{model.ValVector}, - OptionalArgs: 1, - ReturnType: model.ValVector, - Call: funcYear, + Name: "year", + ArgTypes: []model.ValueType{model.ValVector}, + Variadic: 1, + ReturnType: model.ValVector, + Call: funcYear, }, } diff --git a/promql/parse.go b/promql/parse.go index 891d46c90..6a0ecc8dd 100644 --- a/promql/parse.go +++ b/promql/parse.go @@ -1086,13 +1086,23 @@ func (p *parser) checkType(node Node) (typ model.ValueType) { case *Call: nargs := len(n.Func.ArgTypes) - if na := nargs - n.Func.OptionalArgs; na > len(n.Args) { - p.errorf("expected at least %d argument(s) in call to %q, got %d", na, n.Func.Name, len(n.Args)) - } - if nargs < len(n.Args) { - p.errorf("expected at most %d argument(s) in call to %q, got %d", nargs, n.Func.Name, len(n.Args)) + if n.Func.Variadic == 0 { + if nargs != len(n.Args) { + p.errorf("expected %d argument(s) in call to %q, got %d", nargs, n.Func.Name, len(n.Args)) + } + } else { + na := nargs - 1 + if na > len(n.Args) { + p.errorf("expected at least %d argument(s) in call to %q, got %d", na, n.Func.Name, len(n.Args)) + } else if nargsmax := na + n.Func.Variadic; n.Func.Variadic > 0 && nargsmax < len(n.Args) { + p.errorf("expected at most %d argument(s) in call to %q, got %d", nargsmax, n.Func.Name, len(n.Args)) + } } + for i, arg := range n.Args { + if i >= len(n.Func.ArgTypes) { + i = len(n.Func.ArgTypes) - 1 + } p.expectType(arg, n.Func.ArgTypes[i], fmt.Sprintf("call to function %q", n.Func.Name)) } diff --git a/promql/parse_test.go b/promql/parse_test.go index 38f4c1f19..df4e3353d 100644 --- a/promql/parse_test.go +++ b/promql/parse_test.go @@ -1356,11 +1356,11 @@ var testExpr = []struct { }, { input: "floor()", fail: true, - errMsg: "expected at least 1 argument(s) in call to \"floor\", got 0", + errMsg: "expected 1 argument(s) in call to \"floor\", got 0", }, { input: "floor(some_metric, other_metric)", fail: true, - errMsg: "expected at most 1 argument(s) in call to \"floor\", got 2", + errMsg: "expected 1 argument(s) in call to \"floor\", got 2", }, { input: "floor(1)", fail: true, diff --git a/promql/testdata/functions.test b/promql/testdata/functions.test index a04933110..745fa9a76 100644 --- a/promql/testdata/functions.test +++ b/promql/testdata/functions.test @@ -223,6 +223,43 @@ eval_fail instant at 0m label_replace(testmetric, "src", "", "", "") clear +# Tests for label_join. +load 5m + testmetric{src="a",src1="b",src2="c",dst="original-destination-value"} 0 + testmetric{src="d",src1="e",src2="f",dst="original-destination-value"} 1 + +# label_join joins all src values in order. +eval instant at 0m label_join(testmetric, "dst", "-", "src", "src1", "src2") + testmetric{src="a",src1="b",src2="c",dst="a-b-c"} 0 + testmetric{src="d",src1="e",src2="f",dst="d-e-f"} 1 + +# label_join treats non existent src labels as empty strings. +eval instant at 0m label_join(testmetric, "dst", "-", "src", "src3", "src1") + testmetric{src="a",src1="b",src2="c",dst="a--b"} 0 + testmetric{src="d",src1="e",src2="f",dst="d--e"} 1 + +# label_join overwrites the destination label even if the resulting dst label is empty string +eval instant at 0m label_join(testmetric, "dst", "", "emptysrc", "emptysrc1", "emptysrc2") + testmetric{src="a",src1="b",src2="c"} 0 + testmetric{src="d",src1="e",src2="f"} 1 + +# test without src label for label_join +eval instant at 0m label_join(testmetric, "dst", ", ") + testmetric{src="a",src1="b",src2="c"} 0 + testmetric{src="d",src1="e",src2="f"} 1 + +# test without dst label for label_join +load 5m + testmetric1{src="foo",src1="bar",src2="foobar"} 0 + testmetric1{src="fizz",src1="buzz",src2="fizzbuzz"} 1 + +# label_join creates dst label if not present. +eval instant at 0m label_join(testmetric1, "dst", ", ", "src", "src1", "src2") + testmetric1{src="foo",src1="bar",src2="foobar",dst="foo, bar, foobar"} 0 + testmetric1{src="fizz",src1="buzz",src2="fizzbuzz",dst="fizz, buzz, fizzbuzz"} 1 + +clear + # Tests for vector. eval instant at 0m vector(1) {} 1