blob: 5c7b2e9c8626707c9dc4dda796cfb5b3fc8c6891 [file] [log] [blame]
package alerts
import (
"fmt"
"net/url"
"strconv"
"strings"
"go.skia.org/infra/go/paramtools"
"go.skia.org/infra/perf/go/types"
)
const (
INVALID_ID = -1
)
var (
// The default value for Config.Sparse.
DefaultSparse = false
)
// Config represents the configuration for one alert.
type Config struct {
ID int64 `json:"id" datastore:",noindex"`
DisplayName string `json:"display_name" datastore:",noindex"`
Query string `json:"query" datastore:",noindex"` // The query to perform on the trace store to select the traces to alert on.
Alert string `json:"alert" datastore:",noindex"` // Email address or id of a chat room to send alerts to.
Interesting float32 `json:"interesting" datastore:",noindex"` // The regression interestingness threshold.
BugURITemplate string `json:"bug_uri_template" datastore:",noindex"` // URI Template used for reporting bugs. Format TBD.
Algo types.ClusterAlgo `json:"algo" datastore:",noindex"` // Which clustering algorithm to use.
State ConfigState `json:"state"` // The state of the config.
Owner string `json:"owner" datastore:",noindex"` // Email address of the person that owns this alert.
StepUpOnly bool `json:"step_up_only" datastore:",noindex"` // If true then only steps up will trigger an alert. [Deprecated, use Direction.]
Direction Direction `json:"direction" datastore:",noindex"` // Which direction will trigger an alert.
Radius int `json:"radius" datastore:",noindex"` // How many commits to each side of a commit to consider when looking for a step. 0 means use the server default.
K int `json:"k" datastore:",noindex"` // The K in k-means clustering. 0 means use an algorithmically chosen value based on the data.
GroupBy string `json:"group_by" datastore:",noindex"` // A comma separated list of keys in the paramset that all Clustering should be broken up across. Keys must not appear in Query.
Sparse bool `json:"sparse" datastore:",noindex"` // Data is sparse, so only include commits that have data.
MinimumNum int `json:"minimum_num" datastore:",noindex"` // How many traces need to be found interesting before an alert is fired.
Category string `json:"category" datastore:",noindex"` // Which category this alert falls into.
}
func (c *Config) IdAsString() string {
return fmt.Sprintf("%d", c.ID)
}
func (c *Config) StringToId(s string) {
if i, err := strconv.ParseInt(s, 10, 64); err != nil {
c.ID = -1
} else {
c.ID = i
}
}
// GroupedBy returns the parsed GroupBy value as a slice of strings.
func (c *Config) GroupedBy() []string {
ret := []string{}
for _, s := range strings.Split(c.GroupBy, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
ret = append(ret, s)
}
return ret
}
// KeyValue holds a single Params key and value, used in 'Combination'.
type KeyValue struct {
Key string
Value string
}
// Combination is a slice of KeyValue's, returned from GroupCombinations.
type Combination []KeyValue
// equal returns true if the two slices of ints are equal.
func equal(sliceA, sliceB []int) bool {
for i, a := range sliceA {
if a != sliceB[i] {
return false
}
}
return true
}
// inc() will cycle through all combinations of slices with integer values <=
// the values in limits.
//
// I.e. inc increments the values in 'a' up the to maximum values in 'limits',
// effectively counting as if each column in the slice was a different base.
//
// I.e. inc([0,0,1], [1,1,1]) would return [0,1,0].
//
// See the unit tests for more examples.
func inc(a, limits []int) []int {
ret := make([]int, len(a))
_ = copy(ret, a)
for i := len(a) - 1; i >= 0; i-- {
ret[i] = ret[i] + 1
if ret[i] <= limits[i] {
break
}
ret[i] = 0
}
return ret
}
// toCombination converts the slice of offsets into a Combination.
func toCombination(offsets []int, keys []string, ps paramtools.ParamSet) (Combination, error) {
ret := Combination{}
for i, offset := range offsets {
key := keys[i]
values, ok := ps[key]
if !ok {
return nil, fmt.Errorf("Key %q not found in ParamSet %#v", key, ps)
}
ret = append(ret, KeyValue{
Key: key,
Value: values[offset],
})
}
return ret, nil
}
// GroupCombinations returns a slice of Combinations that represent
// all the GroupBy combinations possible for the given ParamSet.
//
// I.e. for:
// ps := paramtools.ParamSet{
// "model": []string{"nexus4", "nexus6", "nexus6"},
// "config": []string{"565", "8888", "nvpr"},
// "arch": []string{"ARM", "x86"},
// }
//
// the GroupCombinations for a GroupBy of "config, arch" would be:
//
// []Combination{
// Combination{KeyValue{"arch", "ARM"}, KeyValue{"config", "565"}},
// Combination{KeyValue{"arch", "ARM"}, KeyValue{"config", "8888"}},
// Combination{KeyValue{"arch", "ARM"}, KeyValue{"config", "nvpr"}},
// Combination{KeyValue{"arch", "x86"}, KeyValue{"config", "565"}},
// Combination{KeyValue{"arch", "x86"}, KeyValue{"config", "8888"}},
// Combination{KeyValue{"arch", "x86"}, KeyValue{"config", "nvpr"}},
// }
//
func (c *Config) GroupCombinations(ps paramtools.ParamSet) ([]Combination, error) {
limits := []int{}
keys := c.GroupedBy()
for _, key := range keys {
limits = append(limits, len(ps[key])-1)
}
ret := []Combination{}
zeroes := make([]int, len(limits))
cfg := make([]int, len(limits))
for {
comb, err := toCombination(cfg, keys, ps)
if err != nil {
return nil, fmt.Errorf("Failed to build combination: %s", err)
}
ret = append(ret, comb)
cfg = inc(cfg, limits)
if equal(cfg, zeroes) {
break
}
}
return ret, nil
}
// QueriesFromParamset uses GroupCombinations to produce the full set of
// queries that this Config represents.
func (c *Config) QueriesFromParamset(paramset paramtools.ParamSet) ([]string, error) {
ret := []string{}
if len(c.GroupBy) != 0 {
allCombinations, err := c.GroupCombinations(paramset)
if err != nil {
return nil, fmt.Errorf("Failed to build GroupBy combinations: %s", err)
}
for _, combo := range allCombinations {
parsed, err := url.ParseQuery(c.Query)
if err != nil {
return nil, fmt.Errorf("Found invalid query %q: %s", c.Query, err)
}
for _, kv := range combo {
parsed[kv.Key] = []string{kv.Value}
}
ret = append(ret, parsed.Encode())
}
} else {
ret = append(ret, c.Query)
}
return ret, nil
}
func (c *Config) Validate() error {
parsed, err := url.ParseQuery(c.Query)
if err != nil {
return fmt.Errorf("Invalid Config: Invalid Query: %s", err)
}
if c.GroupBy != "" {
for _, groupParam := range c.GroupedBy() {
if _, ok := parsed[groupParam]; ok {
return fmt.Errorf("Invalid Config: GroupBy must not appear in Query: %q %q ", c.GroupBy, c.Query)
}
}
}
if c.StepUpOnly {
c.StepUpOnly = false
c.Direction = UP
}
return nil
}
// NewConfig creates a new Config properly initialized.
func NewConfig() *Config {
return &Config{
ID: INVALID_ID,
Algo: types.KMEANS_ALGO,
State: ACTIVE,
Sparse: DefaultSparse,
}
}