Make 2nd arg to delta optional. Add a deriv() function.

The 2nd isCounter argument to delta is ugly, make it optional as the first step
of deprecating it. This will makes delta only ever applied to gauges.

Add a deriv function to calculate the least squares
slope of a gauge. This is more useful for prediction than delta,
as it isn't as heavily influenced by outliers at the boundaries.
This commit is contained in:
Brian Brazil 2015-01-22 11:52:30 +00:00
parent 01f2bc4ee7
commit a31730e88b
2 changed files with 109 additions and 32 deletions

View File

@ -30,6 +30,7 @@ import (
type Function struct { type Function struct {
name string name string
argTypes []ExprType argTypes []ExprType
optionalArgs int
returnType ExprType returnType ExprType
callFn func(timestamp clientmodel.Timestamp, args []Node) interface{} callFn func(timestamp clientmodel.Timestamp, args []Node) interface{}
} }
@ -37,28 +38,34 @@ type Function struct {
// CheckArgTypes returns a non-nil error if the number or types of // CheckArgTypes returns a non-nil error if the number or types of
// passed in arg nodes do not match the function's expectations. // passed in arg nodes do not match the function's expectations.
func (function *Function) CheckArgTypes(args []Node) error { func (function *Function) CheckArgTypes(args []Node) error {
if len(function.argTypes) != len(args) { if len(function.argTypes) < len(args) {
return fmt.Errorf( return fmt.Errorf(
"wrong number of arguments to function %v(): %v expected, %v given", "too many arguments to function %v(): %v expected at most, %v given",
function.name, len(function.argTypes), len(args), function.name, len(function.argTypes), len(args),
) )
} }
for idx, argType := range function.argTypes { if len(function.argTypes) - function.optionalArgs > len(args) {
return fmt.Errorf(
"too few arguments to function %v(): %v expected at least, %v given",
function.name, len(function.argTypes)-function.optionalArgs, len(args),
)
}
for idx, arg := range args {
invalidType := false invalidType := false
var expectedType string var expectedType string
if _, ok := args[idx].(ScalarNode); argType == ScalarType && !ok { if _, ok := arg.(ScalarNode); function.argTypes[idx] == ScalarType && !ok {
invalidType = true invalidType = true
expectedType = "scalar" expectedType = "scalar"
} }
if _, ok := args[idx].(VectorNode); argType == VectorType && !ok { if _, ok := arg.(VectorNode); function.argTypes[idx] == VectorType && !ok {
invalidType = true invalidType = true
expectedType = "vector" expectedType = "vector"
} }
if _, ok := args[idx].(MatrixNode); argType == MatrixType && !ok { if _, ok := arg.(MatrixNode); function.argTypes[idx] == MatrixType && !ok {
invalidType = true invalidType = true
expectedType = "matrix" expectedType = "matrix"
} }
if _, ok := args[idx].(StringNode); argType == StringType && !ok { if _, ok := arg.(StringNode); function.argTypes[idx] == StringType && !ok {
invalidType = true invalidType = true
expectedType = "string" expectedType = "string"
} }
@ -78,10 +85,10 @@ func timeImpl(timestamp clientmodel.Timestamp, args []Node) interface{} {
return clientmodel.SampleValue(timestamp.Unix()) return clientmodel.SampleValue(timestamp.Unix())
} }
// === delta(matrix MatrixNode, isCounter ScalarNode) Vector === // === delta(matrix MatrixNode, isCounter=0 ScalarNode) Vector ===
func deltaImpl(timestamp clientmodel.Timestamp, args []Node) interface{} { func deltaImpl(timestamp clientmodel.Timestamp, args []Node) interface{} {
matrixNode := args[0].(MatrixNode) matrixNode := args[0].(MatrixNode)
isCounter := args[1].(ScalarNode).Eval(timestamp) > 0 isCounter := len(args) >= 2 && args[1].(ScalarNode).Eval(timestamp) > 0
resultVector := Vector{} resultVector := Vector{}
// If we treat these metrics as counters, we need to fetch all values // If we treat these metrics as counters, we need to fetch all values
@ -406,6 +413,49 @@ func absentImpl(timestamp clientmodel.Timestamp, args []Node) interface{} {
} }
} }
// === deriv(node MatrixNode) Vector ===
func derivImpl(timestamp clientmodel.Timestamp, args []Node) interface{} {
matrixNode := args[0].(MatrixNode)
resultVector := Vector{}
matrixValue := matrixNode.Eval(timestamp)
for _, samples := range matrixValue {
// No sense in trying to compute a derivative without at least two points.
// Drop this vector element.
if len(samples.Values) < 2 {
continue
}
// Least squares.
n := clientmodel.SampleValue(0)
sumY := clientmodel.SampleValue(0)
sumX := clientmodel.SampleValue(0)
sumXY := clientmodel.SampleValue(0)
sumX2 := clientmodel.SampleValue(0)
for _, sample := range samples.Values {
x := clientmodel.SampleValue(sample.Timestamp.UnixNano() / 1e9)
n += 1.0
sumY += sample.Value
sumX += x
sumXY += x * sample.Value
sumX2 += x * x
}
numerator := sumXY - sumX*sumY/n
denominator := sumX2 - (sumX*sumX)/n
resultValue := numerator / denominator
resultSample := &Sample{
Metric: samples.Metric,
Value: resultValue,
Timestamp: timestamp,
}
resultSample.Metric.Delete(clientmodel.MetricNameLabel)
resultVector = append(resultVector, resultSample)
}
return resultVector
}
var functions = map[string]*Function{ var functions = map[string]*Function{
"abs": { "abs": {
name: "abs", name: "abs",
@ -446,6 +496,7 @@ var functions = map[string]*Function{
"delta": { "delta": {
name: "delta", name: "delta",
argTypes: []ExprType{MatrixType, ScalarType}, argTypes: []ExprType{MatrixType, ScalarType},
optionalArgs: 1, // The 2nd argument is deprecated.
returnType: VectorType, returnType: VectorType,
callFn: deltaImpl, callFn: deltaImpl,
}, },
@ -509,6 +560,12 @@ var functions = map[string]*Function{
returnType: VectorType, returnType: VectorType,
callFn: topkImpl, callFn: topkImpl,
}, },
"deriv": {
name: "deriv",
argTypes: []ExprType{MatrixType},
returnType: VectorType,
callFn: derivImpl,
},
} }
// GetFunction returns a predefined Function object for the given // GetFunction returns a predefined Function object for the given

View File

@ -260,13 +260,27 @@ func TestExpressions(t *testing.T) {
fullRanges: 0, fullRanges: 0,
intervalRanges: 2, intervalRanges: 2,
}, { }, {
expr: `http_requests{job="api-server", group="canary"} + delta(http_requests{job="api-server"}[5m], 1)`, expr: `http_requests{job="api-server", group="canary"} + rate(http_requests{job="api-server"}[5m]) * 5 * 60`,
output: []string{ output: []string{
`{group="canary", instance="0", job="api-server"} => 330 @[%v]`, `{group="canary", instance="0", job="api-server"} => 330 @[%v]`,
`{group="canary", instance="1", job="api-server"} => 440 @[%v]`, `{group="canary", instance="1", job="api-server"} => 440 @[%v]`,
}, },
fullRanges: 4, fullRanges: 4,
intervalRanges: 0, intervalRanges: 0,
}, {
expr: `rate(http_requests[25m]) * 25 * 60`,
output: []string{
`{group="canary", instance="0", job="api-server"} => 150 @[%v]`,
`{group="canary", instance="0", job="app-server"} => 350 @[%v]`,
`{group="canary", instance="1", job="api-server"} => 200 @[%v]`,
`{group="canary", instance="1", job="app-server"} => 400 @[%v]`,
`{group="production", instance="0", job="api-server"} => 50 @[%v]`,
`{group="production", instance="0", job="app-server"} => 249.99999999999997 @[%v]`,
`{group="production", instance="1", job="api-server"} => 100 @[%v]`,
`{group="production", instance="1", job="app-server"} => 300 @[%v]`,
},
fullRanges: 8,
intervalRanges: 0,
}, { }, {
expr: `delta(http_requests[25m], 1)`, expr: `delta(http_requests[25m], 1)`,
output: []string{ output: []string{
@ -368,38 +382,44 @@ func TestExpressions(t *testing.T) {
intervalRanges: 8, intervalRanges: 8,
}, { }, {
// Deltas should be adjusted for target interval vs. samples under target interval. // Deltas should be adjusted for target interval vs. samples under target interval.
expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m], 1)`, expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m])`,
output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`}, output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`},
fullRanges: 1, fullRanges: 1,
intervalRanges: 0, intervalRanges: 0,
}, { }, {
// Rates should transform per-interval deltas to per-second rates. // Deltas should perform the same operation when 2nd argument is 0.
expr: `rate(http_requests{group="canary", instance="1", job="app-server"}[10m])`, expr: `delta(http_requests{group="canary", instance="1", job="app-server"}[18m], 0)`,
output: []string{`{group="canary", instance="1", job="app-server"} => 288 @[%v]`},
fullRanges: 1,
intervalRanges: 0,
}, {
// Rates should calculate per-second rates.
expr: `rate(http_requests{group="canary", instance="1", job="app-server"}[60m])`,
output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`}, output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`},
fullRanges: 1, fullRanges: 1,
intervalRanges: 0, intervalRanges: 0,
}, { }, {
// Counter resets in middle of range are ignored by delta() if counter == 1. // Deriv should return the same as rate in simple cases.
expr: `delta(testcounter_reset_middle[50m], 1)`, expr: `deriv(http_requests{group="canary", instance="1", job="app-server"}[60m])`,
output: []string{`{} => 90 @[%v]`}, output: []string{`{group="canary", instance="1", job="app-server"} => 0.26666666666666666 @[%v]`},
fullRanges: 1, fullRanges: 1,
intervalRanges: 0, intervalRanges: 0,
}, { }, {
// Counter resets in middle of range are not ignored by delta() if counter == 0. // Counter resets at in the middle of range are handled correctly by rate().
expr: `delta(testcounter_reset_middle[50m], 0)`, expr: `rate(testcounter_reset_middle[60m])`,
output: []string{`{} => 50 @[%v]`}, output: []string{`{} => 0.03 @[%v]`},
fullRanges: 1, fullRanges: 1,
intervalRanges: 0, intervalRanges: 0,
}, { }, {
// Counter resets at end of range are ignored by delta() if counter == 1. // Counter resets at end of range are ignored by rate().
expr: `delta(testcounter_reset_end[5m], 1)`, expr: `rate(testcounter_reset_end[5m])`,
output: []string{`{} => 0 @[%v]`}, output: []string{`{} => 0 @[%v]`},
fullRanges: 1, fullRanges: 1,
intervalRanges: 0, intervalRanges: 0,
}, { }, {
// Counter resets at end of range are not ignored by delta() if counter == 0. // Deriv should return correct result.
expr: `delta(testcounter_reset_end[5m], 0)`, expr: `deriv(testcounter_reset_middle[100m])`,
output: []string{`{} => -90 @[%v]`}, output: []string{`{} => 0.010606060606060607 @[%v]`},
fullRanges: 1, fullRanges: 1,
intervalRanges: 0, intervalRanges: 0,
}, { }, {