square / metrics

Metrics Query Engine
Apache License 2.0
170 stars 21 forks source link

use MakeFunction in place of explicit MetricFunction everywhere possible #276

Closed Nathan-Fenner closed 8 years ago

Nathan-Fenner commented 8 years ago

To try to make it easier to define MetricFunctions without having to write repetitive evaluations and casts everywhere, a new function function.MakeFunction (possible rename to just Make) has been added.

It asks for an interface{} which is a function that acts like a metric function. It can take any of:

In addition, it can have optional arguments by requesting pointers to any of these:

Non-optional formal arguments cannot follow optional ones. (So, for example, (float, *string, float) would be illegal).

For all of these, the parameters will be evaluated before they are passed to the function except for function.Expression and *function.Expression, where it is up to the given function to evaluate them.

All evaluations are done in parallel automatically, so the function itself does not have to do any extra work here.

The argument function can also ask for an function.EvaluationContext to get the current context (or an api.Timerange to get just the current timerange).

If it wants to allow group by or collapse by clauses, it can ask for a function.Groups object.

The argument function can return either (function.Value, error) or just a function.Value (which means it never errors). Either of these can be replaced by any subtype implementing these interfaces (e.g. api.SeriesList or SyntaxError respectively).

If an input function violates any of these constraints, MakeFunction panics.

TODO: test optional arguments, cleaning up reflection in MakeFunction.

TODO: variadic functions for MakeFunction (for variadic functions in MQE)

TODO: float, string, duration returns?

Example

Consider the following toy metric function:

Old Way

// adder takes one or two numbers.
// when given only one parameter, the second defaults to "1.0".
function.MetricFunction {
    Name: "adder",
    MinArguments: 1,
    MaxArguments: 2,
    Compute: func(context function.EvaluationContext, arguments []function.Expression, groups function.Groups) (function.Value, error) {
         left, err := function.EvaluateToDuration(arguments[0], context)
         if err != nil {
             return nil, err
         }
         right := float64(1.0)
         if len(arguments) == 2 {
             right, err = function.EvaluateToDuration(arguments[1], context)
         }
         return expression.Scalar(left + right), nil
    },
}

New Way

// adder takes one or two numbers.
// when given only one parameter, the second defaults to "1.0".
function.MakeFunction(
    "adder",
    func(left float64, optionalRight *float64) float64 {
         right := float64(1.0)
         if optionalRight != nil {
             right = *optionalRight
         }
         return left + right
    },
)

in addition, the arguments will be evaluated in parallel here (there are library functions to evaluate them in parallel using the old way, but it will add several more lines for error-checking).

drcapulet commented 8 years ago

LGTM