diff --git a/main.go b/main.go index e72c30bd..ac5d0058 100644 --- a/main.go +++ b/main.go @@ -14,17 +14,37 @@ package main import ( + "flag" + "fmt" "net/http" "github.com/prometheus/common/route" + // "gopkg.in/yaml.v2" + + "github.com/prometheus/alertmanager/manager" +) + +var ( + configFile = flag.String("config.file", "config.yml", "The configuration file") ) func main() { - state := NewMemState() + conf, err := manager.LoadFile(*configFile) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(conf) + + for _, r := range conf.Routes { + fmt.Println(r) + } + + state := manager.NewMemState() router := route.New() - NewAPI(router.WithPrefix("/api"), state) + manager.NewAPI(router.WithPrefix("/api"), state) http.ListenAndServe(":9091", router) } diff --git a/api.go b/manager/api.go similarity index 99% rename from api.go rename to manager/api.go index 5ab3a02d..d8219597 100644 --- a/api.go +++ b/manager/api.go @@ -1,4 +1,4 @@ -package main +package manager import ( "encoding/json" diff --git a/config/config.go b/manager/config.go similarity index 99% rename from config/config.go rename to manager/config.go index 5afae3e2..e4f2dbde 100644 --- a/config/config.go +++ b/manager/config.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package manager import ( "fmt" @@ -64,7 +64,7 @@ func LoadFile(filename string) (*Config, error) { // Config is the top-level configuration for Alertmanager's config files. type Config struct { - AggrRules []*AggrRule `yaml:"aggregation_rules,omitempty"` + Routes []*Route `yaml:"routes,omitempty"` InhibitRules []*InhibitRule `yaml:"inhibit_rules,omitempty"` NotificationConfigs []*NotificationConfig `yaml:"notification_configs,omitempty"` diff --git a/match.go b/manager/match.go similarity index 91% rename from match.go rename to manager/match.go index 7c621ae3..e57e788e 100644 --- a/match.go +++ b/manager/match.go @@ -11,9 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package manager import ( + "fmt" "regexp" "github.com/prometheus/common/model" @@ -28,6 +29,13 @@ type Matcher struct { regex *regexp.Regexp } +func (m *Matcher) String() string { + if m.isRegex { + return fmt.Sprintf("", m.Name, m.regex) + } + return fmt.Sprintf("", m.Name, m.Value) +} + // IsRegex returns true of the matcher compares against a regular expression. func (m *Matcher) IsRegex() bool { return m.isRegex diff --git a/manager/route.go b/manager/route.go new file mode 100644 index 00000000..5b9aa847 --- /dev/null +++ b/manager/route.go @@ -0,0 +1,153 @@ +package manager + +import ( + "fmt" + "time" + + "github.com/prometheus/common/model" +) + +// A Route is a node that contains definitions of how to handle alerts. +type Route struct { + // The configuration parameters for matches of this route. + RouteOpts RouteOpts + + // Equality or regex matchers an alert has to fulfill to match + // this route. + Matchers Matchers + // If true, an alert matches further routes on the same level. + Continue bool + + // Children routes of this route. + Routes []*Route +} + +// Match does a depth-first left-to-right search through the route tree +// and returns the flattened configuration for the reached node. +func (r *Route) Match(lset model.LabelSet) []*RouteOpts { + if !r.Matchers.MatchAll(lset) { + return nil + } + + var allMatches []*RouteOpts + + for _, cr := range r.Routes { + matches := cr.Match(lset) + + for _, ro := range matches { + ro.populateDefault(&r.RouteOpts) + } + + allMatches = append(allMatches, matches...) + + if matches != nil && !r.Continue { + break + } + } + + if len(allMatches) == 0 { + allMatches = append(allMatches, &r.RouteOpts) + } + + return allMatches +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { + type route struct { + SendTo string `yaml:"send_to,omitempty"` + GroupBy []model.LabelName `yaml:"group_by,omitempty"` + GroupWait *model.Duration `yaml:"group_wait,omitempty"` + + Match map[string]string `yaml:"match,omitempty"` + MatchRE map[string]string `yaml:"match_re,omitempty"` + Continue bool `yaml:"continue,omitempty"` + Routes []*Route `yaml:"routes,omitempty"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` + } + var v route + if err := unmarshal(&v); err != nil { + return err + } + + for k, val := range v.Match { + if !model.LabelNameRE.MatchString(k) { + fmt.Errorf("invalid label name %q", k) + } + ln := model.LabelName(k) + r.Matchers = append(r.Matchers, NewMatcher(ln, val)) + } + + for k, val := range v.MatchRE { + if !model.LabelNameRE.MatchString(k) { + fmt.Errorf("invalid label name %q", k) + } + ln := model.LabelName(k) + + m, err := NewRegexMatcher(ln, val) + if err != nil { + return err + } + r.Matchers = append(r.Matchers, m) + } + + r.RouteOpts.GroupBy = make(map[model.LabelName]struct{}, len(v.GroupBy)) + + for _, ln := range v.GroupBy { + if _, ok := r.RouteOpts.GroupBy[ln]; ok { + return fmt.Errorf("duplicated label %q in group_by", ln) + } + r.RouteOpts.GroupBy[ln] = struct{}{} + } + + r.RouteOpts.groupWait = (*time.Duration)(v.GroupWait) + r.RouteOpts.SendTo = v.SendTo + + r.Continue = v.Continue + r.Routes = v.Routes + + return checkOverflow(v.XXX, "route") +} + +type RouteOpts struct { + // The identifier of the associated notification configuration + SendTo string + + // What labels to group alerts by for notifications. + GroupBy map[model.LabelName]struct{} + + // How long to wait to group matching alerts before sending + // a notificaiton + groupWait *time.Duration +} + +func (ro *RouteOpts) String() string { + var labels []model.LabelName + for ln := range ro.GroupBy { + labels = append(labels, ln) + } + return fmt.Sprintf("", ro.SendTo, labels, ro.groupWait) +} + +func (ro *RouteOpts) GroupWait() time.Duration { + if ro.groupWait == nil { + return 0 + } + return *ro.groupWait +} + +func (ro *RouteOpts) populateDefault(parent *RouteOpts) { + for ln := range parent.GroupBy { + if _, ok := ro.GroupBy[ln]; !ok { + ro.GroupBy[ln] = struct{}{} + } + } + if ro.SendTo == "" { + ro.SendTo = parent.SendTo + } + if ro.groupWait == nil { + ro.groupWait = parent.groupWait + } +} diff --git a/manager/route_test.go b/manager/route_test.go new file mode 100644 index 00000000..3b81e8df --- /dev/null +++ b/manager/route_test.go @@ -0,0 +1,58 @@ +package manager + +import ( + "reflect" + "testing" + + "github.com/prometheus/common/model" + "gopkg.in/yaml.v2" +) + +func TestRouteMatch(t *testing.T) { + in := ` +send_to: 'notify-def' + +routes: +- match: + owner: 'team-A' + + send_to: 'notify-A' + +- match_re: + owner: '^team-(B|C)$' + + send_to: 'notify-BC' +` + + var tree Route + if err := yaml.Unmarshal([]byte(in), &tree); err != nil { + t.Fatal(err) + } + + var emptySet = map[model.LabelName]struct{}{} + + tests := []struct { + input model.LabelSet + result []*RouteOpts + }{ + { + input: model.LabelSet{ + "owner": "team-A", + }, + result: []*RouteOpts{ + { + SendTo: "notify-A", + GroupBy: emptySet, + }, + }, + }, + } + + for _, test := range tests { + matches := tree.Match(test.input) + + if !reflect.DeepEqual(matches, test.result) { + t.Errorf("expected:\n%v\n\ngot:\n%v", test.result, matches) + } + } +} diff --git a/silence.go b/manager/silence.go similarity index 98% rename from silence.go rename to manager/silence.go index 8ff5f9e8..5760654d 100644 --- a/silence.go +++ b/manager/silence.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package manager import ( "time" diff --git a/state.go b/manager/state.go similarity index 99% rename from state.go rename to manager/state.go index 4e39705c..aacaa71e 100644 --- a/state.go +++ b/manager/state.go @@ -1,4 +1,4 @@ -package main +package manager import ( "fmt"