diff --git a/docs/graphite.md b/docs/graphite.md index 719a4bb8b5..b089a7fb73 100644 --- a/docs/graphite.md +++ b/docs/graphite.md @@ -36,6 +36,8 @@ averageSeries(seriesLists) series | avg | Stable consolidateBy(seriesList, func) seriesList | | Stable diffSeries(seriesLists) series | | Stable divideSeries(seriesList, dividend, divisor) seriesList| | Stable +exclude(seriesList, pattern) seriesList | | Stable +grep(seriesList, pattern) seriesList | | Stable groupByTags(seriesList, func, tagList) seriesList | | Stable maxSeries(seriesList) series | max | Stable minSeries(seriesList) series | min | Stable diff --git a/expr/func_grep.go b/expr/func_grep.go new file mode 100644 index 0000000000..c653453de5 --- /dev/null +++ b/expr/func_grep.go @@ -0,0 +1,49 @@ +package expr + +import ( + "regexp" + + "github.com/grafana/metrictank/api/models" +) + +type FuncGrep struct { + in GraphiteFunc + pattern *regexp.Regexp + excludeMatches bool +} + +func NewGrep() GraphiteFunc { + return &FuncGrep{excludeMatches: false} +} + +func NewExclude() GraphiteFunc { + return &FuncGrep{excludeMatches: true} +} + +func (s *FuncGrep) Signature() ([]Arg, []Arg) { + return []Arg{ + ArgSeriesList{val: &s.in}, + ArgRegex{key: "pattern", val: &s.pattern}, + }, []Arg{ + ArgSeriesList{}, + } +} + +func (s *FuncGrep) Context(context Context) Context { + return context +} + +func (s *FuncGrep) Exec(cache map[Req][]models.Series) ([]models.Series, error) { + series, err := s.in.Exec(cache) + if err != nil { + return nil, err + } + + var outputs []models.Series + for _, serie := range series { + if s.pattern.MatchString(serie.Target) != s.excludeMatches { + outputs = append(outputs, serie) + } + } + return outputs, nil +} diff --git a/expr/func_grep_test.go b/expr/func_grep_test.go new file mode 100644 index 0000000000..cb30c5d928 --- /dev/null +++ b/expr/func_grep_test.go @@ -0,0 +1,115 @@ +package expr + +import ( + "fmt" + "regexp" + "testing" + + "github.com/grafana/metrictank/api/models" +) + +func TestGrep(t *testing.T) { + cases := []struct { + pattern string + in []string + matches []string + nonmatches []string + }{ + { + "this", + []string{"series.name.this.ok"}, + []string{"series.name.this.ok"}, + []string{}, + }, + { + `cpu\d`, + []string{"series.cpu1.ok", "series.cpu2.ok", "series.cpu.notok", "series.cpu3.ok"}, + []string{"series.cpu1.ok", "series.cpu2.ok", "series.cpu3.ok"}, + []string{"series.cpu.notok"}, + }, + { + `cpu[02468]`, + []string{"series.cpu1.ok", "series.cpu2.ok", "series.cpu.notok", "series.cpu3.ok"}, + []string{"series.cpu2.ok"}, + []string{"series.cpu1.ok", "series.cpu.notok", "series.cpu3.ok"}, + }, + } + for i, c := range cases { + var in []models.Series + for _, name := range c.in { + in = append(in, models.Series{ + Target: name, + }) + } + + { + f := NewGrep() + grep := f.(*FuncGrep) + grep.pattern = regexp.MustCompile(c.pattern) + grep.in = NewMock(in) + checkGrepOutput(t, f, i, c.matches) + } + + { + f := NewExclude() + grep := f.(*FuncGrep) + grep.pattern = regexp.MustCompile(c.pattern) + grep.in = NewMock(in) + checkGrepOutput(t, f, i, c.nonmatches) + } + } +} + +func checkGrepOutput(t *testing.T, f GraphiteFunc, i int, expected []string) { + got, err := f.Exec(make(map[Req][]models.Series)) + if err != nil { + t.Fatalf("case %d: err should be nil. got %q", i, err) + } + if len(got) != len(expected) { + t.Fatalf("case %d: expected %d output series, got %d", i, len(expected), len(got)) + } + for i, o := range expected { + g := got[i] + if o != g.Target { + t.Fatalf("case %d: expected target %q, got %q", i, o, g.Target) + } + } +} + +func BenchmarkGrep_1(b *testing.B) { + benchmarkGrep(b, 1) +} +func BenchmarkGrep_10(b *testing.B) { + benchmarkGrep(b, 10) +} +func BenchmarkGrep_100(b *testing.B) { + benchmarkGrep(b, 100) +} +func BenchmarkGrep_1000(b *testing.B) { + benchmarkGrep(b, 1000) +} +func BenchmarkGrep_100000(b *testing.B) { + benchmarkGrep(b, 100000) +} + +func benchmarkGrep(b *testing.B, numSeries int) { + var input []models.Series + for i := 0; i < numSeries; i++ { + series := models.Series{ + Target: fmt.Sprintf("metrictank.stats.env.instance.input.plugin%d.metrics_received.counter32", i), + } + input = append(input, series) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + f := NewGrep() + grep := f.(*FuncGrep) + grep.pattern = regexp.MustCompile("input.plugin[0246].metrics") + grep.in = NewMock(input) + got, err := f.Exec(make(map[Req][]models.Series)) + if err != nil { + b.Fatalf("%s", err) + } + results = got + } +} diff --git a/expr/funcs.go b/expr/funcs.go index 468424af39..15a10c0121 100644 --- a/expr/funcs.go +++ b/expr/funcs.go @@ -55,6 +55,8 @@ func init() { "consolidateBy": {NewConsolidateBy, true}, "diffSeries": {NewAggregateConstructor("diff", crossSeriesDiff), true}, "divideSeries": {NewDivideSeries, true}, + "exclude": {NewExclude, true}, + "grep": {NewGrep, true}, "groupByTags": {NewGroupByTags, true}, "max": {NewAggregateConstructor("max", crossSeriesMax), true}, "maxSeries": {NewAggregateConstructor("max", crossSeriesMax), true},