Permalink
Cannot retrieve contributors at this time
| package graphite | |
| import ( | |
| "fmt" | |
| "math" | |
| "strings" | |
| ) | |
| // Response is a convenience type: | |
| // it provides original http and json decode errors, if applicable | |
| // and also the decoded response body, if any | |
| type Response struct { | |
| HTTPErr error | |
| DecodeErr error | |
| Code int | |
| TraceID string | |
| Decoded Data | |
| BodyErr string // set if we could not decode json | |
| } | |
| func (r Response) StringWithoutData() string { | |
| data := "{" | |
| for i, serie := range r.Decoded { | |
| if i > 0 { | |
| data += "," | |
| } | |
| data += fmt.Sprintf("%q", serie.Target) | |
| } | |
| data += "}" | |
| return fmt.Sprintf("<Response>{HTTPErr: %v, DecodeErr: %v, Code: %d, TraceID: %s, Decoded: %s}", r.HTTPErr, r.DecodeErr, r.Code, r.TraceID, data) | |
| } | |
| type Validator struct { | |
| Name string | |
| Fn func(resp Response) bool | |
| } | |
| // Equal returns whether v1 equals v2 | |
| // workaround for go-cmp which treats Validators unequal even if they have the same function pointer. not sure why | |
| func (v1 Validator) Equal(v2 Validator) bool { | |
| return v1.Name == v2.Name | |
| } | |
| // ValidateTargets returns a function that validates that the response contains exactly all named targets | |
| func ValidateTargets(targets []string) Validator { | |
| return Validator{ | |
| Name: fmt.Sprintf("ValidateTargets(%s)", strings.Join(targets, ",")), | |
| Fn: func(resp Response) bool { | |
| if resp.HTTPErr != nil || resp.DecodeErr != nil || resp.Code != 200 { | |
| return false | |
| } | |
| if len(resp.Decoded) != len(targets) { | |
| return false | |
| } | |
| for i, r := range resp.Decoded { | |
| if r.Target != targets[i] { | |
| return false | |
| } | |
| } | |
| return true | |
| }, | |
| } | |
| } | |
| // ValidateCorrect returns a validator with a min number, | |
| // which will validate whether we received a "sufficiently correct" | |
| // response. We assume the response corresponds to a sumSeries() query | |
| // of multiple series, typically across shards across different instances. | |
| // the number denotes the minimum accepted value. | |
| // e.g. with 12, all points must equal 12 | |
| // (i.e. for the use case of 12 series (1 for each shard, and each series valued at 1) | |
| // all data from all shards is incorporated) | |
| // to allow 4 shards being down and unaccounted for, pass 8. | |
| // NOTE: 8 points are ignored (see comments further down) so you should only call this | |
| // for sufficiently long series, e.g. 15 points or so. | |
| func ValidateCorrect(num float64) Validator { | |
| return Validator{ | |
| Name: fmt.Sprintf("ValidateCorrect(%f)", num), | |
| Fn: func(resp Response) bool { | |
| if resp.HTTPErr != nil || resp.DecodeErr != nil || resp.Code != 200 { | |
| return false | |
| } | |
| if len(resp.Decoded) != 1 { | |
| return false | |
| } | |
| points := resp.Decoded[0].Datapoints | |
| // first 4 points can sometimes be null (or some of them, so that sums don't add up) | |
| // We should at some point be more strict and clean that up, | |
| // but that's not in scope for these tests which focus on cluster related problems | |
| // last 2 points may be NaN or incomplete sums because some terms are NaN | |
| // this is standard graphite behavior unlike the faulty cluster behavior. | |
| // the faulty cluster behavior we're looking for is where terms are missing across the time range | |
| // (because entire shards and their series are not taken into account) | |
| for _, p := range points[5 : len(points)-3] { | |
| if math.IsNaN(p.Val) { | |
| return false | |
| } | |
| if p.Val > 12 || p.Val < num { | |
| return false | |
| } | |
| } | |
| return true | |
| }, | |
| } | |
| } | |
| // ValidaterCode returns a validator that validates whether the response has the given code | |
| func ValidateCode(code int) Validator { | |
| return Validator{ | |
| Name: fmt.Sprintf("ValidateCode(%d)", code), | |
| Fn: func(resp Response) bool { | |
| return resp.Code == code | |
| }, | |
| } | |
| } | |
| // ValidatorAvgWindowed returns a validator that validates the number of series and the avg value of each series | |
| // it is windowed to allow the dataset to include one or two values that would be evened out by a value | |
| // just outside of the response. For example: | |
| // response: NaN 4 4 4 5 3 4 4 4 5 | |
| // clearly here we can trust that if the avg value should be 4, that there would be a 3 coming after the response | |
| // but we don't want to wait for that. | |
| // NOTE: ignores up to 2 points from each series, adjust your input size accordingly for desired confidence | |
| func ValidatorAvgWindowed(numPoints int, cmp Comparator) Validator { | |
| try := func(datapoints []Point) bool { | |
| for i := 0; i <= 1; i++ { | |
| Try: | |
| for j := len(datapoints); j >= len(datapoints)-1; j-- { | |
| points := datapoints[i:j] | |
| sum := float64(0) | |
| for _, p := range points { | |
| if math.IsNaN(p.Val) { | |
| continue Try | |
| } | |
| sum += p.Val | |
| } | |
| if cmp.Fn(sum / float64(len(points))) { | |
| return true | |
| } | |
| } | |
| } | |
| return false | |
| } | |
| return Validator{ | |
| Name: fmt.Sprintf("ValidatorAvgWindowed(numPoints=%d, cmp=%s)", numPoints, cmp.Name), | |
| Fn: func(resp Response) bool { | |
| for _, series := range resp.Decoded { | |
| if len(series.Datapoints) != numPoints { | |
| return false | |
| } | |
| if !try(series.Datapoints) { | |
| return false | |
| } | |
| } | |
| return true | |
| }, | |
| } | |
| } | |
| // ValidatorLenNulls returns a validator that validates that any of the series contained | |
| // within the response, has a length of l and no more than prefix nulls up front. | |
| func ValidatorLenNulls(prefix, l int) Validator { | |
| return Validator{ | |
| Name: fmt.Sprintf("ValidatorLenNulls(prefix=%d, l=%d)", prefix, l), | |
| Fn: func(resp Response) bool { | |
| for _, series := range resp.Decoded { | |
| if len(series.Datapoints) != l { | |
| return false | |
| } | |
| for i, dp := range series.Datapoints { | |
| if math.IsNaN(dp.Val) && i+1 > prefix { | |
| return false | |
| } | |
| } | |
| } | |
| return true | |
| }, | |
| } | |
| } | |
| // ValidatorAnd returns a validator that returns whether all given validators return true | |
| // it runs them in the order given and returns as soon as one fails. | |
| func ValidatorAnd(vals ...Validator) Validator { | |
| var name string | |
| for i, val := range vals { | |
| if i > 0 { | |
| name += " && " | |
| name += val.Name | |
| } | |
| } | |
| return Validator{ | |
| Name: name, | |
| Fn: func(resp Response) bool { | |
| for _, val := range vals { | |
| if !val.Fn(resp) { | |
| return false | |
| } | |
| } | |
| return true | |
| }, | |
| } | |
| } | |
| // ValidatorAndExhaustive returns a validator that returns whether all given validators return true | |
| // it runs them in the order given, always runs all of them, and prints a log at each run showing the intermediate status | |
| func ValidatorAndExhaustive(vals ...Validator) Validator { | |
| var name string | |
| for i, val := range vals { | |
| if i > 0 { | |
| name += " && " | |
| name += val.Name | |
| } | |
| } | |
| return Validator{ | |
| Name: name, | |
| Fn: func(resp Response) bool { | |
| msg := "ValidatorAndExhaustive: " | |
| ret := true | |
| for i, val := range vals { | |
| ok := val.Fn(resp) | |
| if !ok { | |
| ret = false | |
| } | |
| if i > 0 { | |
| msg += ", " | |
| } | |
| if ok { | |
| msg += vals[i].Name + ": OK" | |
| } else { | |
| msg += vals[i].Name + ":FAIL" | |
| } | |
| } | |
| fmt.Println(msg) | |
| return ret | |
| }, | |
| } | |
| } |