From 03264c9eef94e9795c0b0509849fab6b6340239b Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 27 May 2016 14:35:00 -0400 Subject: [PATCH 01/14] cmd/bosun: API route to list summaries of open incidents --- cmd/bosun/conf/conf.go | 31 +++++++++++++++++++++++--- cmd/bosun/web/web.go | 49 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/cmd/bosun/conf/conf.go b/cmd/bosun/conf/conf.go index 92987e1d25..4da4517def 100644 --- a/cmd/bosun/conf/conf.go +++ b/cmd/bosun/conf/conf.go @@ -295,6 +295,31 @@ func (ns *Notifications) Get(c *Conf, tags opentsdb.TagSet) map[string]*Notifica return nots } +func GetNotificationChains(c *Conf, n map[string]*Notification) ([][]string) { + chains := [][]string{} + for _, root := range n { + chain := []string{} + seen := make(map[string]bool) + var walkChain func(next *Notification) + walkChain = func(next *Notification) { + if (next == nil) { + chains = append(chains, chain) + return + } + if (seen[next.Name]) { + chain = append(chain, fmt.Sprintf("...%v", next.Name)) + chains = append(chains, chain) + return + } + chain = append(chain, next.Name) + seen[next.Name] = true + walkChain(next.Next) + } + walkChain(root) + } + return chains +} + // parseNotifications parses the comma-separated string v for notifications and // returns them. func (c *Conf) parseNotifications(v string) (map[string]*Notification, error) { @@ -340,9 +365,9 @@ type Notification struct { body string } -func (n *Notification) MarshalJSON() ([]byte, error) { - return nil, fmt.Errorf("conf: cannot json marshal notifications") -} +// func (n *Notification) MarshalJSON() ([]byte, error) { +// return nil, fmt.Errorf("conf: cannot json marshal notifications") +// } type Vars map[string]string diff --git a/cmd/bosun/web/web.go b/cmd/bosun/web/web.go index 89c6526788..267993cb6a 100644 --- a/cmd/bosun/web/web.go +++ b/cmd/bosun/web/web.go @@ -105,6 +105,7 @@ func Listen(listenAddr string, devMode bool, tsdbHost string) error { router.Handle("/api/host", JSON(Host)) router.Handle("/api/last", JSON(Last)) router.Handle("/api/incidents", JSON(Incidents)) + router.Handle("/api/incidents/open", JSON(ListOpenIncidents)) router.Handle("/api/incidents/events", JSON(IncidentEvents)) router.Handle("/api/metadata/get", JSON(GetMetadata)) router.Handle("/api/metadata/metrics", JSON(MetadataMetrics)) @@ -477,6 +478,54 @@ func IncidentEvents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request return schedule.DataAccess.State().GetIncidentState(num) } +type IncidentSummary struct { + Id int64 + Subject string + Start int64 + Alert string + AlertName string + Tags opentsdb.TagSet + CurrentStatus models.Status + WorstStatus models.Status + LastAbnormalStatus models.Status + LastAbnormalTime int64 + Unevaluated bool + NeedAck bool + WarnNotificationChains [][]string + CritNotificationChains [][]string +} + +func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { + // TODO: Retune this when we no longer store templates with incidents + list, err := schedule.DataAccess.State().GetAllOpenIncidents() + if err != nil { + return nil, err + } + summaries := make([]IncidentSummary, len(list)) + for i, iState := range list { + warnNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].WarnNotification.Get(schedule.Conf, iState.AlertKey.Group()) + critNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].CritNotification.Get(schedule.Conf, iState.AlertKey.Group()) + summaries[i] = IncidentSummary{ + Id: iState.Id, + Subject: iState.Subject, + Start: iState.Start.Unix(), + Alert: iState.Alert, + AlertName: iState.AlertKey.Name(), + Tags: iState.AlertKey.Group(), + CurrentStatus: iState.CurrentStatus, + WorstStatus: iState.WorstStatus, + LastAbnormalStatus: iState.LastAbnormalStatus, + LastAbnormalTime: iState.LastAbnormalTime, + Unevaluated: iState.Unevaluated, + NeedAck: iState.NeedAck, + WarnNotificationChains: conf.GetNotificationChains(schedule.Conf, warnNotifications), + CritNotificationChains: conf.GetNotificationChains(schedule.Conf, critNotifications), + } + } + return summaries, nil + //return summaries, nil +} + func Incidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { // TODO: Incident Search return nil, nil From a64f343573770ef883587b6a9bda1c0d74c8d676 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 27 May 2016 17:44:47 -0400 Subject: [PATCH 02/14] add silences --- cmd/bosun/web/web.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/bosun/web/web.go b/cmd/bosun/web/web.go index 267993cb6a..68590fbec1 100644 --- a/cmd/bosun/web/web.go +++ b/cmd/bosun/web/web.go @@ -485,12 +485,14 @@ type IncidentSummary struct { Alert string AlertName string Tags opentsdb.TagSet + TagsString string CurrentStatus models.Status WorstStatus models.Status LastAbnormalStatus models.Status LastAbnormalTime int64 Unevaluated bool NeedAck bool + Silenced bool WarnNotificationChains [][]string CritNotificationChains [][]string } @@ -501,6 +503,10 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ if err != nil { return nil, err } + suppressor := schedule.Silenced() + if suppressor == nil { + return nil, fmt.Errorf("failed to get silences") + } summaries := make([]IncidentSummary, len(list)) for i, iState := range list { warnNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].WarnNotification.Get(schedule.Conf, iState.AlertKey.Group()) @@ -512,18 +518,19 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ Alert: iState.Alert, AlertName: iState.AlertKey.Name(), Tags: iState.AlertKey.Group(), + TagsString: iState.AlertKey.Group().String(), CurrentStatus: iState.CurrentStatus, WorstStatus: iState.WorstStatus, LastAbnormalStatus: iState.LastAbnormalStatus, LastAbnormalTime: iState.LastAbnormalTime, Unevaluated: iState.Unevaluated, NeedAck: iState.NeedAck, + Silenced: suppressor(iState.AlertKey) != nil, WarnNotificationChains: conf.GetNotificationChains(schedule.Conf, warnNotifications), CritNotificationChains: conf.GetNotificationChains(schedule.Conf, critNotifications), } } return summaries, nil - //return summaries, nil } func Incidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { From 4ed60ed3aab25fe62a8430d29e03502143df0dcb Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 31 May 2016 17:47:34 -0400 Subject: [PATCH 03/14] wip: filter param using boolq and glob --- cmd/bosun/web/incident.go | 140 ++++++++++++ cmd/bosun/web/web.go | 55 ----- vendor/github.com/kylebrandt/boolq/LICENSE | 21 ++ vendor/github.com/kylebrandt/boolq/README.md | 41 ++++ vendor/github.com/kylebrandt/boolq/boolq.go | 77 +++++++ .../github.com/kylebrandt/boolq/parse/lex.go | 192 ++++++++++++++++ .../github.com/kylebrandt/boolq/parse/node.go | 126 +++++++++++ .../kylebrandt/boolq/parse/parse.go | 207 ++++++++++++++++++ vendor/github.com/ryanuber/go-glob/LICENSE | 21 ++ vendor/github.com/ryanuber/go-glob/README.md | 29 +++ vendor/github.com/ryanuber/go-glob/glob.go | 51 +++++ vendor/vendor.json | 15 ++ 12 files changed, 920 insertions(+), 55 deletions(-) create mode 100644 cmd/bosun/web/incident.go create mode 100644 vendor/github.com/kylebrandt/boolq/LICENSE create mode 100644 vendor/github.com/kylebrandt/boolq/README.md create mode 100644 vendor/github.com/kylebrandt/boolq/boolq.go create mode 100644 vendor/github.com/kylebrandt/boolq/parse/lex.go create mode 100644 vendor/github.com/kylebrandt/boolq/parse/node.go create mode 100644 vendor/github.com/kylebrandt/boolq/parse/parse.go create mode 100644 vendor/github.com/ryanuber/go-glob/LICENSE create mode 100644 vendor/github.com/ryanuber/go-glob/README.md create mode 100644 vendor/github.com/ryanuber/go-glob/glob.go diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go new file mode 100644 index 0000000000..948187d98c --- /dev/null +++ b/cmd/bosun/web/incident.go @@ -0,0 +1,140 @@ +package web + +import ( + "fmt" + "net/http" + "strings" + + "bosun.org/cmd/bosun/conf" + "bosun.org/models" + "bosun.org/opentsdb" + "github.com/MiniProfiler/go/miniprofiler" + "github.com/kylebrandt/boolq" + "github.com/kylebrandt/boolq/parse" + "github.com/ryanuber/go-glob" +) + +type IncidentSummary struct { + Id int64 + Subject string + Start int64 + Alert string + AlertName string + Tags opentsdb.TagSet + TagsString string + CurrentStatus models.Status + WorstStatus models.Status + LastAbnormalStatus models.Status + LastAbnormalTime int64 + Unevaluated bool + NeedAck bool + Silenced bool + WarnNotificationChains [][]string + CritNotificationChains [][]string +} + +func (is IncidentSummary) Ask(filter string) (bool, error) { + sp := strings.SplitN(filter, ":", 2) + if len(sp) != 2 { + return false, fmt.Errorf("bad ask length: sp is %v", sp) + } + key := sp[0] + value := sp[1] + switch key { + case "ack": + switch value { + case "true": + return is.NeedAck == false, nil + case "false": + return is.NeedAck == true, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "notify": + //fmt.Println("notify", key, value) + for _, chain := range is.WarnNotificationChains { + for _, wn := range chain { + if glob.Glob(value, wn) { + return true, nil + } + } + } + for _, chain := range is.CritNotificationChains { + for _, cn := range chain { + if glob.Glob(value, cn) { + return true, nil + } + } + } + return false, nil + case "silence": + switch value { + case "true": + return is.Silenced == true, nil + case "false": + return is.Silenced == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "status": // CurrentStatus + if is.CurrentStatus.String() == value { + return true, nil + } + return false, nil + } + return false, nil +} + +func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { + // TODO: Retune this when we no longer store email bodies with incidents + list, err := schedule.DataAccess.State().GetAllOpenIncidents() + if err != nil { + return nil, err + } + suppressor := schedule.Silenced() + if suppressor == nil { + return nil, fmt.Errorf("failed to get silences") + } + summaries := []IncidentSummary{} + // filter, err := sched.MakeFilter(r.FormValue("filter")) + // if err != nil { + // return nil, fmt.Errorf("bad filter: %v", err) + // } + parsedExpr, err := parse.Parse(r.FormValue("filter")) + if err != nil { + return nil, fmt.Errorf("bad filter: %v", err) + } + for _, iState := range list { + // if !filter(schedule.Conf, schedule.Conf.Alerts[iState.Alert], iState) { + // continue + // } + warnNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].WarnNotification.Get(schedule.Conf, iState.AlertKey.Group()) + critNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].CritNotification.Get(schedule.Conf, iState.AlertKey.Group()) + is := IncidentSummary{ + Id: iState.Id, + Subject: iState.Subject, + Start: iState.Start.Unix(), + Alert: iState.Alert, + AlertName: iState.AlertKey.Name(), + Tags: iState.AlertKey.Group(), + TagsString: iState.AlertKey.Group().String(), + CurrentStatus: iState.CurrentStatus, + WorstStatus: iState.WorstStatus, + LastAbnormalStatus: iState.LastAbnormalStatus, + LastAbnormalTime: iState.LastAbnormalTime, + Unevaluated: iState.Unevaluated, + NeedAck: iState.NeedAck, + Silenced: suppressor(iState.AlertKey) != nil, + WarnNotificationChains: conf.GetNotificationChains(schedule.Conf, warnNotifications), + CritNotificationChains: conf.GetNotificationChains(schedule.Conf, critNotifications), + } + match, err := boolq.AskParsedExpr(*parsedExpr, is) + if err != nil { + return nil, err + } + if match { + summaries = append(summaries, is) + } + } + return summaries, nil +} diff --git a/cmd/bosun/web/web.go b/cmd/bosun/web/web.go index 68590fbec1..93ad727871 100644 --- a/cmd/bosun/web/web.go +++ b/cmd/bosun/web/web.go @@ -478,61 +478,6 @@ func IncidentEvents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request return schedule.DataAccess.State().GetIncidentState(num) } -type IncidentSummary struct { - Id int64 - Subject string - Start int64 - Alert string - AlertName string - Tags opentsdb.TagSet - TagsString string - CurrentStatus models.Status - WorstStatus models.Status - LastAbnormalStatus models.Status - LastAbnormalTime int64 - Unevaluated bool - NeedAck bool - Silenced bool - WarnNotificationChains [][]string - CritNotificationChains [][]string -} - -func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { - // TODO: Retune this when we no longer store templates with incidents - list, err := schedule.DataAccess.State().GetAllOpenIncidents() - if err != nil { - return nil, err - } - suppressor := schedule.Silenced() - if suppressor == nil { - return nil, fmt.Errorf("failed to get silences") - } - summaries := make([]IncidentSummary, len(list)) - for i, iState := range list { - warnNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].WarnNotification.Get(schedule.Conf, iState.AlertKey.Group()) - critNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].CritNotification.Get(schedule.Conf, iState.AlertKey.Group()) - summaries[i] = IncidentSummary{ - Id: iState.Id, - Subject: iState.Subject, - Start: iState.Start.Unix(), - Alert: iState.Alert, - AlertName: iState.AlertKey.Name(), - Tags: iState.AlertKey.Group(), - TagsString: iState.AlertKey.Group().String(), - CurrentStatus: iState.CurrentStatus, - WorstStatus: iState.WorstStatus, - LastAbnormalStatus: iState.LastAbnormalStatus, - LastAbnormalTime: iState.LastAbnormalTime, - Unevaluated: iState.Unevaluated, - NeedAck: iState.NeedAck, - Silenced: suppressor(iState.AlertKey) != nil, - WarnNotificationChains: conf.GetNotificationChains(schedule.Conf, warnNotifications), - CritNotificationChains: conf.GetNotificationChains(schedule.Conf, critNotifications), - } - } - return summaries, nil -} - func Incidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { // TODO: Incident Search return nil, nil diff --git a/vendor/github.com/kylebrandt/boolq/LICENSE b/vendor/github.com/kylebrandt/boolq/LICENSE new file mode 100644 index 0000000000..55117fd357 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Kyle Brandt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/kylebrandt/boolq/README.md b/vendor/github.com/kylebrandt/boolq/README.md new file mode 100644 index 0000000000..a0ab765684 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/README.md @@ -0,0 +1,41 @@ +# boolq +build simple bool expressions. Supports `!`, `AND`, `OR`, and `()` grouping. Individual items start with a letter and continue until whitespace. How you treat those items is up to you and is based on the Ask method. + +# Example: + +``` +package main + +import ( + "fmt" + "log" + + "github.com/kylebrandt/boolq" +) + +func init() { + log.SetFlags(log.LstdFlags | log.Lshortfile) +} + +func main() { + f := foo{} + ask := "(true AND true) AND !false" + q, err := boolq.AskExpr(ask, f) + if err != nil { + log.Fatal(err) + } + log.Println(q) +} + +type foo struct{} + +func (f foo) Ask(ask string) (bool, error) { + switch ask { + case "true": + return true, nil + case "false": + return false, nil + } + return false, fmt.Errorf("couldn't parse ask arg") +} +``` \ No newline at end of file diff --git a/vendor/github.com/kylebrandt/boolq/boolq.go b/vendor/github.com/kylebrandt/boolq/boolq.go new file mode 100644 index 0000000000..8f072b7241 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/boolq.go @@ -0,0 +1,77 @@ +// Package boolq lets you build generic query expressions. + +package boolq + +import ( + "fmt" + + "github.com/kylebrandt/boolq/parse" +) + +// An Asker is something that can be queried using boolq. The string passed +// to Ask will be the component in an expression. For example with the expression +// `(foo:bar AND baz:biz)` Ask will be called twice, once with the argument "foo:bar" +// and another time with the argument "baz:biz" +type Asker interface { + Ask(string) (bool, error) +} + +// AskExpr takes an expression and an Asker. It then parses the expression +// calling the Asker's Ask on expressions AskNodes and returns if the +// expression is true or not for the given asker. +func AskExpr(expr string, asker Asker) (bool, error) { + q, err := parse.Parse(expr) + if err != nil { + return false, err + } + return walk(q.Root, asker) +} + +// AskParsedExpr is like AskExpr but takes an expression that has already +// been parsed by parse.Parse on the expression. This is useful if you are calling +// the same expression multiple times. +func AskParsedExpr(q parse.Tree, asker Asker) (bool, error) { + return walk(q.Root, asker) +} + +func walk(node parse.Node, asker Asker) (bool, error) { + switch node := node.(type) { + case *parse.AskNode: + return asker.Ask(node.Text) + case *parse.BinaryNode: + return walkBinary(node, asker) + case *parse.UnaryNode: + return walkUnary(node, asker) + default: + return false, fmt.Errorf("can not walk type %v", node.Type()) + } +} + +func walkBinary(node *parse.BinaryNode, asker Asker) (bool, error) { + l, err := walk(node.Args[0], asker) + if err != nil { + return false, err + } + r, err := walk(node.Args[1], asker) + if err != nil { + return false, err + } + if node.OpStr == "AND" { + return l && r, nil + } + if node.OpStr == "OR" { + return l || r, nil + } + return false, fmt.Errorf("Unrecognized operator: %v", node.OpStr) +} + +func walkUnary(node *parse.UnaryNode, asker Asker) (bool, error) { + r, err := walk(node.Arg, asker) + if err != nil { + return false, err + } + if node.OpStr == "!" { + return !r, nil + } + return false, fmt.Errorf("unknown unary operator: %v", node.OpStr) +} diff --git a/vendor/github.com/kylebrandt/boolq/parse/lex.go b/vendor/github.com/kylebrandt/boolq/parse/lex.go new file mode 100644 index 0000000000..9f2cf30192 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/parse/lex.go @@ -0,0 +1,192 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package parse + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +// item represents a token or text string returned from the scanner. +type item struct { + typ itemType // The type of this item. + pos Pos // The starting position, in bytes, of this item in the input string. + val string // The value of this item. +} + +type itemType int + +const ( + itemError itemType = iota // error occurred; value is text of error + itemEOF + + // Literals + itemAsk // field:query + + itemNot // '!' + itemAnd // AND + itemOr // OR + itemLeftParen // ( + itemRightParen // ) +) + +const eof = -1 + +// stateFn represents the state of the scanner as a function that returns the next state. +type stateFn func(*lexer) stateFn + +// lexer holds the state of the scanner. +type lexer struct { + input string // the string being scanned + state stateFn // the next lexing function to enter + pos Pos // current position in the input + start Pos // start position of this item + width Pos // width of last rune read from input + lastPos Pos // position of most recent item returned by nextItem + items chan item // channel of scanned items +} + +// next returns the next rune in the input. +func (l *lexer) next() rune { + if int(l.pos) >= len(l.input) { + l.width = 0 + return eof + } + r, w := utf8.DecodeRuneInString(l.input[l.pos:]) + l.width = Pos(w) + l.pos += l.width + return r +} + +// peek returns but does not consume the next rune in the input. +func (l *lexer) peek() rune { + r := l.next() + l.backup() + return r +} + +// backup steps back one rune. Can only be called once per call of next. +func (l *lexer) backup() { + l.pos -= l.width +} + +// emit passes an item back to the client. +func (l *lexer) emit(t itemType) { + l.items <- item{t, l.start, l.input[l.start:l.pos]} + l.start = l.pos +} + +// accept consumes the next rune if it's from the valid set. +func (l *lexer) accept(valid string) bool { + if strings.IndexRune(valid, l.next()) >= 0 { + return true + } + l.backup() + return false +} + +// acceptRun consumes a run of runes from the valid set. +func (l *lexer) acceptRun(valid string) { + for strings.IndexRune(valid, l.next()) >= 0 { + } + l.backup() +} + +// ignore skips over the pending input before this point. +func (l *lexer) ignore() { + l.start = l.pos +} + +// lineNumber reports which line we're on, based on the position of +// the previous item returned by nextItem. Doing it this way +// means we don't have to worry about peek double counting. +func (l *lexer) lineNumber() int { + return 1 + strings.Count(l.input[:l.lastPos], "\n") +} + +// errorf returns an error token and terminates the scan by passing +// back a nil pointer that will be the next state, terminating l.nextItem. +func (l *lexer) errorf(format string, args ...interface{}) stateFn { + l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} + return nil +} + +// nextItem returns the next item from the input. +func (l *lexer) nextItem() item { + item := <-l.items + l.lastPos = item.pos + return item +} + +// lex creates a new scanner for the input string. +func lex(input string) *lexer { + l := &lexer{ + input: input, + items: make(chan item), + } + go l.run() + return l +} + +// run runs the state machine for the lexer. +func (l *lexer) run() { + for l.state = lexItem; l.state != nil; { + l.state = l.state(l) + } +} + +// state functions + +func lexItem(l *lexer) stateFn { +Loop: + for { + switch r := l.next(); { + case unicode.IsLetter(r): + return lexWord + case r == '(': + l.emit(itemLeftParen) + case r == ')': + l.emit(itemRightParen) + case r == '!': + l.emit(itemNot) + case isSpace(r): + l.ignore() + case r == eof: + l.emit(itemEOF) + break Loop + default: + return l.errorf("invalid character: %s", string(r)) + } + } + return nil +} + +func lexWord(l *lexer) stateFn { + for { + switch r := l.next(); { + case !isSpace(r) && r != eof && r != ')': + // absorb + default: + l.backup() + s := l.input[l.start:l.pos] + switch s { + case "AND": + l.emit(itemAnd) + case "OR": + l.emit(itemOr) + default: + l.emit(itemAsk) + } + return lexItem + } + } +} + +// isSpace reports whether r is a space character. +func isSpace(r rune) bool { + return unicode.IsSpace(r) +} diff --git a/vendor/github.com/kylebrandt/boolq/parse/node.go b/vendor/github.com/kylebrandt/boolq/parse/node.go new file mode 100644 index 0000000000..6acd20ef15 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/parse/node.go @@ -0,0 +1,126 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parse nodes. +package parse + +import "fmt" + +var textFormat = "%s" // Changed to "%q" in tests for better error messages. + +// A Node is an element in the parse tree. The interface is trivial. +// The interface contains an unexported method so that only +// types local to this package can satisfy it. +type Node interface { + Type() NodeType + String() string + Position() Pos // byte position of start of node in full original input string + // Make sure only functions in this package can create Nodes. + unexported() +} + +// NodeType identifies the type of a parse tree node. +type NodeType int + +// Pos represents a byte position in the original input text from which +// this template was parsed. +type Pos int + +func (p Pos) Position() Pos { + return p +} + +// unexported keeps Node implementations local to the package. +// All implementations embed Pos, so this takes care of it. +func (Pos) unexported() { +} + +// Type returns itself and provides an easy default implementation +// for embedding in a Node. Embedded in all non-trivial Nodes. +func (t NodeType) Type() NodeType { + return t +} + +const ( + NodeAsk NodeType = iota // key:value expression. + NodeBinary NodeType = iota // + NodeUnary NodeType = iota // +) + +// BinaryNode holds two arguments and an operator. +type BinaryNode struct { + NodeType + Pos + Args [2]Node + Operator item + OpStr string +} + +func newBinary(operator item, arg1, arg2 Node) *BinaryNode { + return &BinaryNode{NodeType: NodeBinary, Pos: operator.pos, Args: [2]Node{arg1, arg2}, Operator: operator, OpStr: operator.val} +} + +func (b *BinaryNode) String() string { + return fmt.Sprintf("%s %s %s", b.Args[0], b.Operator.val, b.Args[1]) +} + +func (b *BinaryNode) StringAST() string { + return fmt.Sprintf("%s(%s, %s)", b.Operator.val, b.Args[0], b.Args[1]) +} + +// UnaryNode holds one argument and an operator. +type UnaryNode struct { + NodeType + Pos + Arg Node + Operator item + OpStr string +} + +func newUnary(operator item, arg Node) *UnaryNode { + return &UnaryNode{NodeType: NodeUnary, Pos: operator.pos, Arg: arg, Operator: operator, OpStr: operator.val} +} + +func (u *UnaryNode) String() string { + return fmt.Sprintf("%s%s", u.Operator.val, u.Arg) +} + +func (u *UnaryNode) StringAST() string { + return fmt.Sprintf("%s(%s)", u.Operator.val, u.Arg) +} + +// Walk invokes f on n and sub-nodes of n. +func Walk(n Node, f func(Node)) { + f(n) + switch n := n.(type) { + case *BinaryNode: + Walk(n.Args[0], f) + Walk(n.Args[1], f) + case *AskNode: + // Ignore. + case *UnaryNode: + Walk(n.Arg, f) + default: + panic(fmt.Errorf("other type: %T", n)) + } +} + +// AskNode holds a filter invocation. +type AskNode struct { + NodeType + Pos + Text string +} + +func (a *AskNode) String() string { + return fmt.Sprintf("%s", a.Text) +} + +func newAsk(pos Pos, text string) *AskNode { + return &AskNode{ + NodeType: NodeAsk, + Pos: pos, + Text: text, + } +} \ No newline at end of file diff --git a/vendor/github.com/kylebrandt/boolq/parse/parse.go b/vendor/github.com/kylebrandt/boolq/parse/parse.go new file mode 100644 index 0000000000..b57a5c37a3 --- /dev/null +++ b/vendor/github.com/kylebrandt/boolq/parse/parse.go @@ -0,0 +1,207 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package parse builds parse trees for expressions as defined by expr. Clients +// should use that package to construct expressions rather than this one, which +// provides shared internal data structures not intended for general use. +package parse + +import ( + "fmt" + "runtime" + "runtime/debug" +) + +// Tree is the representation of a single parsed expression. +type Tree struct { + Text string // text parsed to create the expression. + Root Node // top-level root of the tree, returns a number. + + // Parsing only; cleared after parse. + lex *lexer + token [1]item // one-token lookahead for parser. + peekCount int +} + +// Parse returns a Tree, created by parsing the expression described in the +// argument string. If an error is encountered, parsing stops and an empty Tree +// is returned with the error. +func Parse(text string) (t *Tree, err error) { + t = New() + t.Text = text + err = t.Parse(text) + return +} + +// next returns the next token. +func (t *Tree) next() item { + if t.peekCount > 0 { + t.peekCount-- + } else { + t.token[0] = t.lex.nextItem() + } + return t.token[t.peekCount] +} + +// backup backs the input stream up one token. +func (t *Tree) backup() { + t.peekCount++ +} + +// peek returns but does not consume the next token. +func (t *Tree) peek() item { + if t.peekCount > 0 { + return t.token[t.peekCount-1] + } + t.peekCount = 1 + t.token[0] = t.lex.nextItem() + return t.token[0] +} + +// Parsing. + +// New allocates a new parse tree with the given name. +func New() *Tree { + return &Tree{} +} + +// errorf formats the error and terminates processing. +func (t *Tree) errorf(format string, args ...interface{}) { + t.Root = nil + format = fmt.Sprintf("expr: %s", format) + panic(fmt.Errorf(format, args...)) +} + +// error terminates processing. +func (t *Tree) error(err error) { + t.errorf("%s", err) +} + +// expect consumes the next token and guarantees it has the required type. +func (t *Tree) expect(expected itemType, context string) item { + token := t.next() + if token.typ != expected { + t.unexpected(token, context) + } + return token +} + +// expectOneOf consumes the next token and guarantees it has one of the required types. +func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item { + token := t.next() + if token.typ != expected1 && token.typ != expected2 { + t.unexpected(token, context) + } + return token +} + +// unexpected complains about the token and terminates processing. +func (t *Tree) unexpected(token item, context string) { + debug.PrintStack() + t.errorf("unexpected %s in %s", token, context) +} + +// recover is the handler that turns panics into returns from the top level of Parse. +func (t *Tree) recover(errp *error) { + e := recover() + if e != nil { + if _, ok := e.(runtime.Error); ok { + panic(e) + } + if t != nil { + t.stopParse() + } + *errp = e.(error) + } + return +} + +// startParse initializes the parser, using the lexer. +func (t *Tree) startParse(lex *lexer) { + t.Root = nil + t.lex = lex +} + +// stopParse terminates parsing. +func (t *Tree) stopParse() { + t.lex = nil +} + +// Parse parses the expression definition string to construct a representation +// of the expression for execution. +func (t *Tree) Parse(text string) (err error) { + defer t.recover(&err) + t.startParse(lex(text)) + t.Text = text + t.parse() + t.stopParse() + return nil +} + +// parse is the top-level parser for an expression. +// It runs to EOF. +func (t *Tree) parse() { + t.Root = t.O() + t.expect(itemEOF, "input") +} + +/* Grammar: +O -> A {"AND" A} +A -> C {"OR" C} +C -> v | "(" O ")" | "!" O +v -> ask +*/ + +// expr: +func (t *Tree) O() Node { + n := t.A() + for { + switch t.peek().typ { + case itemOr: + n = newBinary(t.next(), n, t.A()) + default: + return n + } + } +} + +func (t *Tree) A() Node { + n := t.C() + for { + switch t.peek().typ { + case itemAnd: + n = newBinary(t.next(), n, t.C()) + default: + return n + } + } +} + +func (t *Tree) C() Node { + switch token := t.peek(); token.typ { + case itemAsk: + return t.v() + case itemNot: + return newUnary(t.next(), t.C()) + case itemLeftParen: + t.next() + n := t.O() + t.expect(itemRightParen, "input") + return n + default: + t.unexpected(token, "input") + } + return nil +} + +func (t *Tree) v() Node { + switch token := t.next(); token.typ { + case itemAsk: + return newAsk(token.pos, token.val) + default: + t.unexpected(token, "input") + } + return nil +} + diff --git a/vendor/github.com/ryanuber/go-glob/LICENSE b/vendor/github.com/ryanuber/go-glob/LICENSE new file mode 100644 index 0000000000..bdfbd95149 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan Uber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/ryanuber/go-glob/README.md b/vendor/github.com/ryanuber/go-glob/README.md new file mode 100644 index 0000000000..48f7fcb05a --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/README.md @@ -0,0 +1,29 @@ +# String globbing in golang [![Build Status](https://travis-ci.org/ryanuber/go-glob.svg)](https://travis-ci.org/ryanuber/go-glob) + +`go-glob` is a single-function library implementing basic string glob support. + +Globs are an extremely user-friendly way of supporting string matching without +requiring knowledge of regular expressions or Go's particular regex engine. Most +people understand that if you put a `*` character somewhere in a string, it is +treated as a wildcard. Surprisingly, this functionality isn't found in Go's +standard library, except for `path.Match`, which is intended to be used while +comparing paths (not arbitrary strings), and contains specialized logic for this +use case. A better solution might be a POSIX basic (non-ERE) regular expression +engine for Go, which doesn't exist currently. + +Example +======= + +``` +package main + +import "github.com/ryanuber/go-glob" + +func main() { + glob.Glob("*World!", "Hello, World!") // true + glob.Glob("Hello,*", "Hello, World!") // true + glob.Glob("*ello,*", "Hello, World!") // true + glob.Glob("World!", "Hello, World!") // false + glob.Glob("/home/*", "/home/ryanuber/.bashrc") // true +} +``` diff --git a/vendor/github.com/ryanuber/go-glob/glob.go b/vendor/github.com/ryanuber/go-glob/glob.go new file mode 100644 index 0000000000..d9d46379a8 --- /dev/null +++ b/vendor/github.com/ryanuber/go-glob/glob.go @@ -0,0 +1,51 @@ +package glob + +import "strings" + +// The character which is treated like a glob +const GLOB = "*" + +// Glob will test a string pattern, potentially containing globs, against a +// subject string. The result is a simple true/false, determining whether or +// not the glob pattern matched the subject text. +func Glob(pattern, subj string) bool { + // Empty pattern can only match empty subject + if pattern == "" { + return subj == pattern + } + + // If the pattern _is_ a glob, it matches everything + if pattern == GLOB { + return true + } + + parts := strings.Split(pattern, GLOB) + + if len(parts) == 1 { + // No globs in pattern, so test for equality + return subj == pattern + } + + leadingGlob := strings.HasPrefix(pattern, GLOB) + trailingGlob := strings.HasSuffix(pattern, GLOB) + end := len(parts) - 1 + + // Check the first section. Requires special handling. + if !leadingGlob && !strings.HasPrefix(subj, parts[0]) { + return false + } + + // Go over the middle parts and ensure they match. + for i := 1; i < end; i++ { + if !strings.Contains(subj, parts[i]) { + return false + } + + // Trim evaluated text from subj as we loop over the pattern. + idx := strings.Index(subj, parts[i]) + len(parts[i]) + subj = subj[idx:] + } + + // Reached the last section. Requires special handling. + return trailingGlob || strings.HasSuffix(subj, parts[end]) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 231f7c163a..171b78651c 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -402,6 +402,16 @@ "revision": "f61123ea07e1921ac4f174e8c42225fd39bd6c6a", "revisionTime": "2015-10-16T12:35:57-05:00" }, + { + "path": "github.com/kylebrandt/boolq", + "revision": "9cb634993c9b0e2280559a7c45735985216e6486", + "revisionTime": "2016-05-31T17:44:58-04:00" + }, + { + "path": "github.com/kylebrandt/boolq/parse", + "revision": "9cb634993c9b0e2280559a7c45735985216e6486", + "revisionTime": "2016-05-31T17:44:58-04:00" + }, { "path": "github.com/kylebrandt/gohop", "revision": "605b5abd5cb7b630eb91cf1f35d365121ccdb6fd", @@ -432,6 +442,11 @@ "revision": "9fdb4e763e833f166e76009e5a33132877c32bfb", "revisionTime": "2015-11-28T12:32:46+01:00" }, + { + "path": "github.com/ryanuber/go-glob", + "revision": "572520ed46dbddaed19ea3d9541bdd0494163693", + "revisionTime": "2016-02-26T00:37:05-08:00" + }, { "path": "github.com/siddontang/go/bson", "revision": "b151716326d7c7faf22473c0b04fb7ceac88b587", From 59736a66c289ac83c5d1c040fde1127bd5b65610 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 31 May 2016 23:10:24 -0400 Subject: [PATCH 04/14] more work on incidentsummary ask --- cmd/bosun/web/incident.go | 44 ++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go index 948187d98c..1ab5883efe 100644 --- a/cmd/bosun/web/incident.go +++ b/cmd/bosun/web/incident.go @@ -50,8 +50,39 @@ func (is IncidentSummary) Ask(filter string) (bool, error) { default: return false, fmt.Errorf("unknown %s value: %s", key, value) } + case "hasTag": + if strings.Contains(value, "=") { + if strings.HasPrefix(value, "=") { + q := strings.TrimLeft(value, "=") + for _, v := range is.Tags { + if glob.Glob(q, v) { + return true, nil + } + } + return false, nil + } + if strings.HasSuffix(value, "=") { + q := strings.TrimRight(value, "=") + _, ok := is.Tags[q] + return ok, nil + } + sp := strings.Split(value, "=") + if len(sp) != 2 { + return false, fmt.Errorf("unexpected tag specification: %v", value) + } + for k, v := range is.Tags { + if k == sp[0] && glob.Glob(sp[1], v) { + return true, nil + } + return false, nil + } + } + q := strings.TrimRight(value, "=") + _, ok := is.Tags[q] + return ok, nil + case "name": + return glob.Glob(value, is.AlertName), nil case "notify": - //fmt.Println("notify", key, value) for _, chain := range is.WarnNotificationChains { for _, wn := range chain { if glob.Glob(value, wn) { @@ -77,10 +108,13 @@ func (is IncidentSummary) Ask(filter string) (bool, error) { return false, fmt.Errorf("unknown %s value: %s", key, value) } case "status": // CurrentStatus - if is.CurrentStatus.String() == value { - return true, nil - } - return false, nil + return is.CurrentStatus.String() == value, nil + case "worstStatus": + return is.WorstStatus.String() == value, nil + case "lastAbnormalStatus": + return is.LastAbnormalStatus.String() == value, nil + case "subject": + return glob.Glob(value, is.Subject), nil } return false, nil } From 6bf8c5af8596310d4de067a36f304eca098434ed Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 1 Jun 2016 11:53:21 -0400 Subject: [PATCH 05/14] add ability to filter on start time (relative to now) --- cmd/bosun/web/incident.go | 79 +++++++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go index 1ab5883efe..47c92ad364 100644 --- a/cmd/bosun/web/incident.go +++ b/cmd/bosun/web/incident.go @@ -5,6 +5,8 @@ import ( "net/http" "strings" + "time" + "bosun.org/cmd/bosun/conf" "bosun.org/models" "bosun.org/opentsdb" @@ -18,7 +20,6 @@ type IncidentSummary struct { Id int64 Subject string Start int64 - Alert string AlertName string Tags opentsdb.TagSet TagsString string @@ -36,7 +37,7 @@ type IncidentSummary struct { func (is IncidentSummary) Ask(filter string) (bool, error) { sp := strings.SplitN(filter, ":", 2) if len(sp) != 2 { - return false, fmt.Errorf("bad ask length: sp is %v", sp) + return false, fmt.Errorf("bad filter, filter must be in k:v format, got %v", filter) } key := sp[0] value := sp[1] @@ -53,7 +54,7 @@ func (is IncidentSummary) Ask(filter string) (bool, error) { case "hasTag": if strings.Contains(value, "=") { if strings.HasPrefix(value, "=") { - q := strings.TrimLeft(value, "=") + q := strings.TrimPrefix(value, "=") for _, v := range is.Tags { if glob.Glob(q, v) { return true, nil @@ -62,7 +63,7 @@ func (is IncidentSummary) Ask(filter string) (bool, error) { return false, nil } if strings.HasSuffix(value, "=") { - q := strings.TrimRight(value, "=") + q := strings.TrimSuffix(value, "=") _, ok := is.Tags[q] return ok, nil } @@ -80,6 +81,16 @@ func (is IncidentSummary) Ask(filter string) (bool, error) { q := strings.TrimRight(value, "=") _, ok := is.Tags[q] return ok, nil + case "hidden": + hide := is.Silenced || is.Unevaluated + switch value { + case "true": + return hide == true, nil + case "false": + return hide == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } case "name": return glob.Glob(value, is.AlertName), nil case "notify": @@ -107,6 +118,44 @@ func (is IncidentSummary) Ask(filter string) (bool, error) { default: return false, fmt.Errorf("unknown %s value: %s", key, value) } + case "start": + var op string + val := value + if strings.HasPrefix(value, "<") { + op = "<" + val = strings.TrimLeft(value, op) + } + if strings.HasPrefix(value, ">") { + op = ">" + val = strings.TrimLeft(value, op) + } + d, err := opentsdb.ParseDuration(val) + if err != nil { + return false, err + } + startTime := time.Unix(is.Start, 0) + // might want to make Now a property of incident summary for viewing things in the past + // but not going there at the moment. This is because right now I'm working with open + // incidents. And "What did incidents look like at this time?" is a different question + // since those incidents will no longer be open. + relativeTime := time.Now().UTC().Add(time.Duration(-d)) + switch op { + case ">", "": + return startTime.After(relativeTime), nil + case "<": + return startTime.Before(relativeTime), nil + default: + return false, fmt.Errorf("unexpected op: %v", op) + } + case "unevaluated": + switch value { + case "true": + return is.Unevaluated == true, nil + case "false": + return is.Unevaluated == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } case "status": // CurrentStatus return is.CurrentStatus.String() == value, nil case "worstStatus": @@ -130,25 +179,21 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ return nil, fmt.Errorf("failed to get silences") } summaries := []IncidentSummary{} - // filter, err := sched.MakeFilter(r.FormValue("filter")) - // if err != nil { - // return nil, fmt.Errorf("bad filter: %v", err) - // } - parsedExpr, err := parse.Parse(r.FormValue("filter")) - if err != nil { - return nil, fmt.Errorf("bad filter: %v", err) + filterText := r.FormValue("filter") + var parsedExpr *parse.Tree + if filterText != "" { + parsedExpr, err = parse.Parse(filterText) + if err != nil { + return nil, fmt.Errorf("bad filter: %v", err) + } } for _, iState := range list { - // if !filter(schedule.Conf, schedule.Conf.Alerts[iState.Alert], iState) { - // continue - // } warnNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].WarnNotification.Get(schedule.Conf, iState.AlertKey.Group()) critNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].CritNotification.Get(schedule.Conf, iState.AlertKey.Group()) is := IncidentSummary{ Id: iState.Id, Subject: iState.Subject, Start: iState.Start.Unix(), - Alert: iState.Alert, AlertName: iState.AlertKey.Name(), Tags: iState.AlertKey.Group(), TagsString: iState.AlertKey.Group().String(), @@ -162,6 +207,10 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ WarnNotificationChains: conf.GetNotificationChains(schedule.Conf, warnNotifications), CritNotificationChains: conf.GetNotificationChains(schedule.Conf, critNotifications), } + if parsedExpr == nil { + summaries = append(summaries, is) + continue + } match, err := boolq.AskParsedExpr(*parsedExpr, is) if err != nil { return nil, err From 3e37c06489b3424cf74c8b449a86706acb44bbb4 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 1 Jun 2016 15:14:46 -0400 Subject: [PATCH 06/14] have dashboard (marshal groups) use new filter method --- cmd/bosun/sched/filter.go | 112 ----------------------- cmd/bosun/sched/sched.go | 28 ++++-- cmd/bosun/sched/views.go | 187 ++++++++++++++++++++++++++++++++++++++ cmd/bosun/web/incident.go | 181 +----------------------------------- 4 files changed, 212 insertions(+), 296 deletions(-) delete mode 100644 cmd/bosun/sched/filter.go create mode 100644 cmd/bosun/sched/views.go diff --git a/cmd/bosun/sched/filter.go b/cmd/bosun/sched/filter.go deleted file mode 100644 index c53b7da088..0000000000 --- a/cmd/bosun/sched/filter.go +++ /dev/null @@ -1,112 +0,0 @@ -package sched - -import ( - "fmt" - "strings" - - "bosun.org/cmd/bosun/conf" - "bosun.org/models" -) - -func makeFilter(filter string) (func(*conf.Conf, *conf.Alert, *models.IncidentState) bool, error) { - fields := strings.Fields(filter) - if len(fields) == 0 { - return func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - return true - }, nil - } - fs := make(map[string][]func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool) - for _, f := range fields { - negate := strings.HasPrefix(f, "!") - if negate { - f = f[1:] - } - if f == "" { - return nil, fmt.Errorf("filter required") - } - sp := strings.SplitN(f, ":", 2) - value := sp[len(sp)-1] - key := sp[0] - if len(sp) == 1 { - key = "" - } - add := func(fn func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool) { - fs[key] = append(fs[key], func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - v := fn(c, a, s) - if negate { - v = !v - } - return v - }) - } - switch key { - case "": - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - ak := s.AlertKey - return strings.Contains(string(ak), value) || strings.Contains(string(s.Subject), value) - }) - case "ack": - var v bool - switch value { - case "true": - v = true - case "false": - v = false - default: - return nil, fmt.Errorf("unknown %s value: %s", key, value) - } - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - return s.NeedAck != v - }) - case "notify": - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - r := false - f := func(ns *conf.Notifications) { - for k := range ns.Get(c, s.AlertKey.Group()) { - if strings.Contains(k, value) { - r = true - break - } - } - } - f(a.CritNotification) - f(a.WarnNotification) - return r - }) - case "status": - var v models.Status - switch value { - case "normal": - v = models.StNormal - case "warning": - v = models.StWarning - case "critical": - v = models.StCritical - case "unknown": - v = models.StUnknown - default: - return nil, fmt.Errorf("unknown %s value: %s", key, value) - } - add(func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - return s.LastAbnormalStatus == v - }) - default: - return nil, fmt.Errorf("unknown filter key: %s", key) - } - } - return func(c *conf.Conf, a *conf.Alert, s *models.IncidentState) bool { - for _, ors := range fs { - match := false - for _, f := range ors { - if f(c, a, s) { - match = true - break - } - } - if !match { - return false - } - } - return true - }, nil -} diff --git a/cmd/bosun/sched/sched.go b/cmd/bosun/sched/sched.go index c0bdf85eab..74c0b5019e 100644 --- a/cmd/bosun/sched/sched.go +++ b/cmd/bosun/sched/sched.go @@ -22,6 +22,8 @@ import ( "github.com/MiniProfiler/go/miniprofiler" "github.com/boltdb/bolt" "github.com/bradfitz/slice" + "github.com/kylebrandt/boolq" + "github.com/kylebrandt/boolq/parse" "github.com/tatsushid/go-fastping" ) @@ -372,16 +374,19 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro } t.FailingAlerts, t.UnclosedErrors = s.getErrorCounts() T.Step("Setup", func(miniprofiler.Timer) { - matches, err2 := makeFilter(filter) - if err2 != nil { - err = err2 - return - } status2, err2 := s.GetOpenStates() if err2 != nil { err = err2 return } + var parsedExpr *parse.Tree + if filter != "" { + parsedExpr, err2 = parse.Parse(filter) + if err2 != nil { + err = err2 + return + } + } for k, v := range status2 { a := s.Conf.Alerts[k.Name()] if a == nil { @@ -391,7 +396,18 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro } continue } - if matches(s.Conf, a, v) { + if parsedExpr == nil { + status[k] = v + continue + } + is := MakeIncidentSummary(s.Conf, silenced, v) + match := false + match, err2 = boolq.AskParsedExpr(*parsedExpr, is) + if err2 != nil { + err = err2 + return + } + if match { status[k] = v } } diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go new file mode 100644 index 0000000000..ad03e6a5ba --- /dev/null +++ b/cmd/bosun/sched/views.go @@ -0,0 +1,187 @@ +package sched + +import ( + "fmt" + "strings" + "time" + + "bosun.org/cmd/bosun/conf" + "bosun.org/models" + "bosun.org/opentsdb" + "github.com/ryanuber/go-glob" +) + +// Views +type IncidentSummaryView struct { + Id int64 + Subject string + Start int64 + AlertName string + Tags opentsdb.TagSet + TagsString string + CurrentStatus models.Status + WorstStatus models.Status + LastAbnormalStatus models.Status + LastAbnormalTime int64 + Unevaluated bool + NeedAck bool + Silenced bool + WarnNotificationChains [][]string + CritNotificationChains [][]string +} + +func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState) IncidentSummaryView { + warnNotifications := c.Alerts[is.AlertKey.Name()].WarnNotification.Get(c, is.AlertKey.Group()) + critNotifications := c.Alerts[is.AlertKey.Name()].CritNotification.Get(c, is.AlertKey.Group()) + return IncidentSummaryView{ + Id: is.Id, + Subject: is.Subject, + Start: is.Start.Unix(), + AlertName: is.AlertKey.Name(), + Tags: is.AlertKey.Group(), + TagsString: is.AlertKey.Group().String(), + CurrentStatus: is.CurrentStatus, + WorstStatus: is.WorstStatus, + LastAbnormalStatus: is.LastAbnormalStatus, + LastAbnormalTime: is.LastAbnormalTime, + Unevaluated: is.Unevaluated, + NeedAck: is.NeedAck, + Silenced: s(is.AlertKey) != nil, + WarnNotificationChains: conf.GetNotificationChains(c, warnNotifications), + CritNotificationChains: conf.GetNotificationChains(c, critNotifications), + } +} + +func (is IncidentSummaryView) Ask(filter string) (bool, error) { + sp := strings.SplitN(filter, ":", 2) + if len(sp) != 2 { + return false, fmt.Errorf("bad filter, filter must be in k:v format, got %v", filter) + } + key := sp[0] + value := sp[1] + switch key { + case "ack": + switch value { + case "true": + return is.NeedAck == false, nil + case "false": + return is.NeedAck == true, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "hasTag": + if strings.Contains(value, "=") { + if strings.HasPrefix(value, "=") { + q := strings.TrimPrefix(value, "=") + for _, v := range is.Tags { + if glob.Glob(q, v) { + return true, nil + } + } + return false, nil + } + if strings.HasSuffix(value, "=") { + q := strings.TrimSuffix(value, "=") + _, ok := is.Tags[q] + return ok, nil + } + sp := strings.Split(value, "=") + if len(sp) != 2 { + return false, fmt.Errorf("unexpected tag specification: %v", value) + } + for k, v := range is.Tags { + if k == sp[0] && glob.Glob(sp[1], v) { + return true, nil + } + return false, nil + } + } + q := strings.TrimRight(value, "=") + _, ok := is.Tags[q] + return ok, nil + case "hidden": + hide := is.Silenced || is.Unevaluated + switch value { + case "true": + return hide == true, nil + case "false": + return hide == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "name": + return glob.Glob(value, is.AlertName), nil + case "notify": + for _, chain := range is.WarnNotificationChains { + for _, wn := range chain { + if glob.Glob(value, wn) { + return true, nil + } + } + } + for _, chain := range is.CritNotificationChains { + for _, cn := range chain { + if glob.Glob(value, cn) { + return true, nil + } + } + } + return false, nil + case "silence": + switch value { + case "true": + return is.Silenced == true, nil + case "false": + return is.Silenced == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "start": + var op string + val := value + if strings.HasPrefix(value, "<") { + op = "<" + val = strings.TrimLeft(value, op) + } + if strings.HasPrefix(value, ">") { + op = ">" + val = strings.TrimLeft(value, op) + } + d, err := opentsdb.ParseDuration(val) + if err != nil { + return false, err + } + startTime := time.Unix(is.Start, 0) + // might want to make Now a property of incident summary for viewing things in the past + // but not going there at the moment. This is because right now I'm working with open + // incidents. And "What did incidents look like at this time?" is a different question + // since those incidents will no longer be open. + relativeTime := time.Now().UTC().Add(time.Duration(-d)) + switch op { + case ">", "": + return startTime.After(relativeTime), nil + case "<": + return startTime.Before(relativeTime), nil + default: + return false, fmt.Errorf("unexpected op: %v", op) + } + case "unevaluated": + switch value { + case "true": + return is.Unevaluated == true, nil + case "false": + return is.Unevaluated == false, nil + default: + return false, fmt.Errorf("unknown %s value: %s", key, value) + } + case "status": // CurrentStatus + return is.CurrentStatus.String() == value, nil + case "worstStatus": + return is.WorstStatus.String() == value, nil + case "lastAbnormalStatus": + return is.LastAbnormalStatus.String() == value, nil + case "subject": + return glob.Glob(value, is.Subject), nil + } + return false, nil +} diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go index 47c92ad364..86bb219359 100644 --- a/cmd/bosun/web/incident.go +++ b/cmd/bosun/web/incident.go @@ -3,171 +3,14 @@ package web import ( "fmt" "net/http" - "strings" - "time" + "bosun.org/cmd/bosun/sched" - "bosun.org/cmd/bosun/conf" - "bosun.org/models" - "bosun.org/opentsdb" "github.com/MiniProfiler/go/miniprofiler" "github.com/kylebrandt/boolq" "github.com/kylebrandt/boolq/parse" - "github.com/ryanuber/go-glob" ) -type IncidentSummary struct { - Id int64 - Subject string - Start int64 - AlertName string - Tags opentsdb.TagSet - TagsString string - CurrentStatus models.Status - WorstStatus models.Status - LastAbnormalStatus models.Status - LastAbnormalTime int64 - Unevaluated bool - NeedAck bool - Silenced bool - WarnNotificationChains [][]string - CritNotificationChains [][]string -} - -func (is IncidentSummary) Ask(filter string) (bool, error) { - sp := strings.SplitN(filter, ":", 2) - if len(sp) != 2 { - return false, fmt.Errorf("bad filter, filter must be in k:v format, got %v", filter) - } - key := sp[0] - value := sp[1] - switch key { - case "ack": - switch value { - case "true": - return is.NeedAck == false, nil - case "false": - return is.NeedAck == true, nil - default: - return false, fmt.Errorf("unknown %s value: %s", key, value) - } - case "hasTag": - if strings.Contains(value, "=") { - if strings.HasPrefix(value, "=") { - q := strings.TrimPrefix(value, "=") - for _, v := range is.Tags { - if glob.Glob(q, v) { - return true, nil - } - } - return false, nil - } - if strings.HasSuffix(value, "=") { - q := strings.TrimSuffix(value, "=") - _, ok := is.Tags[q] - return ok, nil - } - sp := strings.Split(value, "=") - if len(sp) != 2 { - return false, fmt.Errorf("unexpected tag specification: %v", value) - } - for k, v := range is.Tags { - if k == sp[0] && glob.Glob(sp[1], v) { - return true, nil - } - return false, nil - } - } - q := strings.TrimRight(value, "=") - _, ok := is.Tags[q] - return ok, nil - case "hidden": - hide := is.Silenced || is.Unevaluated - switch value { - case "true": - return hide == true, nil - case "false": - return hide == false, nil - default: - return false, fmt.Errorf("unknown %s value: %s", key, value) - } - case "name": - return glob.Glob(value, is.AlertName), nil - case "notify": - for _, chain := range is.WarnNotificationChains { - for _, wn := range chain { - if glob.Glob(value, wn) { - return true, nil - } - } - } - for _, chain := range is.CritNotificationChains { - for _, cn := range chain { - if glob.Glob(value, cn) { - return true, nil - } - } - } - return false, nil - case "silence": - switch value { - case "true": - return is.Silenced == true, nil - case "false": - return is.Silenced == false, nil - default: - return false, fmt.Errorf("unknown %s value: %s", key, value) - } - case "start": - var op string - val := value - if strings.HasPrefix(value, "<") { - op = "<" - val = strings.TrimLeft(value, op) - } - if strings.HasPrefix(value, ">") { - op = ">" - val = strings.TrimLeft(value, op) - } - d, err := opentsdb.ParseDuration(val) - if err != nil { - return false, err - } - startTime := time.Unix(is.Start, 0) - // might want to make Now a property of incident summary for viewing things in the past - // but not going there at the moment. This is because right now I'm working with open - // incidents. And "What did incidents look like at this time?" is a different question - // since those incidents will no longer be open. - relativeTime := time.Now().UTC().Add(time.Duration(-d)) - switch op { - case ">", "": - return startTime.After(relativeTime), nil - case "<": - return startTime.Before(relativeTime), nil - default: - return false, fmt.Errorf("unexpected op: %v", op) - } - case "unevaluated": - switch value { - case "true": - return is.Unevaluated == true, nil - case "false": - return is.Unevaluated == false, nil - default: - return false, fmt.Errorf("unknown %s value: %s", key, value) - } - case "status": // CurrentStatus - return is.CurrentStatus.String() == value, nil - case "worstStatus": - return is.WorstStatus.String() == value, nil - case "lastAbnormalStatus": - return is.LastAbnormalStatus.String() == value, nil - case "subject": - return glob.Glob(value, is.Subject), nil - } - return false, nil -} - func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { // TODO: Retune this when we no longer store email bodies with incidents list, err := schedule.DataAccess.State().GetAllOpenIncidents() @@ -178,7 +21,7 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ if suppressor == nil { return nil, fmt.Errorf("failed to get silences") } - summaries := []IncidentSummary{} + summaries := []sched.IncidentSummaryView{} filterText := r.FormValue("filter") var parsedExpr *parse.Tree if filterText != "" { @@ -188,25 +31,7 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ } } for _, iState := range list { - warnNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].WarnNotification.Get(schedule.Conf, iState.AlertKey.Group()) - critNotifications := schedule.Conf.Alerts[iState.AlertKey.Name()].CritNotification.Get(schedule.Conf, iState.AlertKey.Group()) - is := IncidentSummary{ - Id: iState.Id, - Subject: iState.Subject, - Start: iState.Start.Unix(), - AlertName: iState.AlertKey.Name(), - Tags: iState.AlertKey.Group(), - TagsString: iState.AlertKey.Group().String(), - CurrentStatus: iState.CurrentStatus, - WorstStatus: iState.WorstStatus, - LastAbnormalStatus: iState.LastAbnormalStatus, - LastAbnormalTime: iState.LastAbnormalTime, - Unevaluated: iState.Unevaluated, - NeedAck: iState.NeedAck, - Silenced: suppressor(iState.AlertKey) != nil, - WarnNotificationChains: conf.GetNotificationChains(schedule.Conf, warnNotifications), - CritNotificationChains: conf.GetNotificationChains(schedule.Conf, critNotifications), - } + is := sched.MakeIncidentSummary(schedule.Conf, suppressor, iState) if parsedExpr == nil { summaries = append(summaries, is) continue From 65b7f6cd7570da1683fc5b51e17ff56a7bf7c93b Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 1 Jun 2016 16:20:37 -0400 Subject: [PATCH 07/14] fix hasTag filter --- cmd/bosun/sched/views.go | 6 +++--- vendor/github.com/kylebrandt/boolq/parse/parse.go | 3 --- vendor/vendor.json | 8 ++++---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go index ad03e6a5ba..ec3a476c9f 100644 --- a/cmd/bosun/sched/views.go +++ b/cmd/bosun/sched/views.go @@ -31,8 +31,8 @@ type IncidentSummaryView struct { } func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState) IncidentSummaryView { - warnNotifications := c.Alerts[is.AlertKey.Name()].WarnNotification.Get(c, is.AlertKey.Group()) - critNotifications := c.Alerts[is.AlertKey.Name()].CritNotification.Get(c, is.AlertKey.Group()) + warnNotifications := c.Alerts[is.AlertKey.Name()].WarnNotification.Get(c, is.AlertKey.Group()) + critNotifications := c.Alerts[is.AlertKey.Name()].CritNotification.Get(c, is.AlertKey.Group()) return IncidentSummaryView{ Id: is.Id, Subject: is.Subject, @@ -93,8 +93,8 @@ func (is IncidentSummaryView) Ask(filter string) (bool, error) { if k == sp[0] && glob.Glob(sp[1], v) { return true, nil } - return false, nil } + return false, nil } q := strings.TrimRight(value, "=") _, ok := is.Tags[q] diff --git a/vendor/github.com/kylebrandt/boolq/parse/parse.go b/vendor/github.com/kylebrandt/boolq/parse/parse.go index b57a5c37a3..f330f9cbca 100644 --- a/vendor/github.com/kylebrandt/boolq/parse/parse.go +++ b/vendor/github.com/kylebrandt/boolq/parse/parse.go @@ -10,7 +10,6 @@ package parse import ( "fmt" "runtime" - "runtime/debug" ) // Tree is the representation of a single parsed expression. @@ -98,7 +97,6 @@ func (t *Tree) expectOneOf(expected1, expected2 itemType, context string) item { // unexpected complains about the token and terminates processing. func (t *Tree) unexpected(token item, context string) { - debug.PrintStack() t.errorf("unexpected %s in %s", token, context) } @@ -204,4 +202,3 @@ func (t *Tree) v() Node { } return nil } - diff --git a/vendor/vendor.json b/vendor/vendor.json index 171b78651c..a5c8941bb2 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -404,13 +404,13 @@ }, { "path": "github.com/kylebrandt/boolq", - "revision": "9cb634993c9b0e2280559a7c45735985216e6486", - "revisionTime": "2016-05-31T17:44:58-04:00" + "revision": "3c0efef9c6cd5f400eba1871a3be6191796db71a", + "revisionTime": "2016-06-01T16:00:19-04:00" }, { "path": "github.com/kylebrandt/boolq/parse", - "revision": "9cb634993c9b0e2280559a7c45735985216e6486", - "revisionTime": "2016-05-31T17:44:58-04:00" + "revision": "3c0efef9c6cd5f400eba1871a3be6191796db71a", + "revisionTime": "2016-06-01T16:00:19-04:00" }, { "path": "github.com/kylebrandt/gohop", From cc7e262408a1bfa86d1ce51257383711a29a3144 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 3 Jun 2016 08:07:13 -0400 Subject: [PATCH 08/14] add events --- cmd/bosun/sched/views.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go index ec3a476c9f..ee509b62e4 100644 --- a/cmd/bosun/sched/views.go +++ b/cmd/bosun/sched/views.go @@ -26,6 +26,7 @@ type IncidentSummaryView struct { Unevaluated bool NeedAck bool Silenced bool + Actions []models.Action WarnNotificationChains [][]string CritNotificationChains [][]string } @@ -47,6 +48,7 @@ func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState Unevaluated: is.Unevaluated, NeedAck: is.NeedAck, Silenced: s(is.AlertKey) != nil, + Actions: is.Actions, WarnNotificationChains: conf.GetNotificationChains(c, warnNotifications), CritNotificationChains: conf.GetNotificationChains(c, critNotifications), } @@ -111,6 +113,13 @@ func (is IncidentSummaryView) Ask(filter string) (bool, error) { } case "name": return glob.Glob(value, is.AlertName), nil + case "user": + for _, action := range is.Actions { + if action.User == value { + return true, nil + } + } + return false, nil case "notify": for _, chain := range is.WarnNotificationChains { for _, wn := range chain { From 82383320cdf4c5a208876fac5b19e73fbb55ac8d Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 3 Jun 2016 13:06:37 -0400 Subject: [PATCH 09/14] include event list in incident summary --- cmd/bosun/sched/views.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go index ee509b62e4..5714c76123 100644 --- a/cmd/bosun/sched/views.go +++ b/cmd/bosun/sched/views.go @@ -12,6 +12,20 @@ import ( ) // Views + +type EventSummary struct { + Status models.Status + Time int64 +} + +// EventSummary is like a models.Event but strips the Results and Unevaluated +func MakeEventSummary(e models.Event) (EventSummary, bool) { + return EventSummary{ + Status: e.Status, + Time: e.Time.Unix(), + }, e.Unevaluated +} + type IncidentSummaryView struct { Id int64 Subject string @@ -27,6 +41,7 @@ type IncidentSummaryView struct { NeedAck bool Silenced bool Actions []models.Action + Events []EventSummary WarnNotificationChains [][]string CritNotificationChains [][]string } @@ -34,6 +49,12 @@ type IncidentSummaryView struct { func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState) IncidentSummaryView { warnNotifications := c.Alerts[is.AlertKey.Name()].WarnNotification.Get(c, is.AlertKey.Group()) critNotifications := c.Alerts[is.AlertKey.Name()].CritNotification.Get(c, is.AlertKey.Group()) + eventSummaries := []EventSummary{} + for _, event := range is.Events { + if eventSummary, unevaluated := MakeEventSummary(event); !unevaluated { + eventSummaries = append(eventSummaries, eventSummary) + } + } return IncidentSummaryView{ Id: is.Id, Subject: is.Subject, @@ -49,6 +70,7 @@ func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState NeedAck: is.NeedAck, Silenced: s(is.AlertKey) != nil, Actions: is.Actions, + Events: eventSummaries, WarnNotificationChains: conf.GetNotificationChains(c, warnNotifications), CritNotificationChains: conf.GetNotificationChains(c, critNotifications), } From d3c3f9a9140ff8c7ed0180fe8893516862653263 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Mon, 6 Jun 2016 10:13:39 -0400 Subject: [PATCH 10/14] Make events have epoch time --- cmd/bosun/sched/views.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go index 5714c76123..e571a0bdaf 100644 --- a/cmd/bosun/sched/views.go +++ b/cmd/bosun/sched/views.go @@ -26,6 +26,22 @@ func MakeEventSummary(e models.Event) (EventSummary, bool) { }, e.Unevaluated } +type EpochAction struct { + User string + Message string + Time int64 + Type models.ActionType +} + +func MakeEpochAction(a models.Action) (EpochAction) { + return EpochAction{ + User: a.User, + Message: a.Message, + Time: a.Time.UTC().Unix(), + Type: a.Type, + } +} + type IncidentSummaryView struct { Id int64 Subject string @@ -40,7 +56,7 @@ type IncidentSummaryView struct { Unevaluated bool NeedAck bool Silenced bool - Actions []models.Action + Actions []EpochAction Events []EventSummary WarnNotificationChains [][]string CritNotificationChains [][]string @@ -55,6 +71,10 @@ func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState eventSummaries = append(eventSummaries, eventSummary) } } + actions := make([]EpochAction, len(is.Actions)) + for i, action := range is.Actions { + actions[i] = MakeEpochAction(action) + } return IncidentSummaryView{ Id: is.Id, Subject: is.Subject, @@ -69,7 +89,7 @@ func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState Unevaluated: is.Unevaluated, NeedAck: is.NeedAck, Silenced: s(is.AlertKey) != nil, - Actions: is.Actions, + Actions: actions, Events: eventSummaries, WarnNotificationChains: conf.GetNotificationChains(c, warnNotifications), CritNotificationChains: conf.GetNotificationChains(c, critNotifications), From 3f3b4c3a306b3ca63339ebf60d6104414b80dcea Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Tue, 7 Jun 2016 13:26:02 -0400 Subject: [PATCH 11/14] s/silence/silenced --- cmd/bosun/sched/views.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go index e571a0bdaf..6bb8e73fcf 100644 --- a/cmd/bosun/sched/views.go +++ b/cmd/bosun/sched/views.go @@ -178,7 +178,7 @@ func (is IncidentSummaryView) Ask(filter string) (bool, error) { } } return false, nil - case "silence": + case "silenced": switch value { case "true": return is.Silenced == true, nil From b3d7c2214a19e0492e5dee5a4961da710e149186 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 8 Jun 2016 12:28:56 -0400 Subject: [PATCH 12/14] document GetNotificationChains, update boolq --- cmd/bosun/conf/conf.go | 8 ++++---- cmd/bosun/sched/sched.go | 7 +++---- cmd/bosun/web/incident.go | 7 +++---- vendor/github.com/kylebrandt/boolq/boolq.go | 15 ++++++++++++++- vendor/vendor.json | 4 ++-- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/cmd/bosun/conf/conf.go b/cmd/bosun/conf/conf.go index 4da4517def..4dc3a6b08a 100644 --- a/cmd/bosun/conf/conf.go +++ b/cmd/bosun/conf/conf.go @@ -295,6 +295,10 @@ func (ns *Notifications) Get(c *Conf, tags opentsdb.TagSet) map[string]*Notifica return nots } +// GetNotificationChains returns the warn or crit notification chains for a configured +// alert. Each chain is a list of notification names. If a notification name +// as already been seen in the chain it ends the list with the notification +// name with a of "..." which indicates that the chain will loop. func GetNotificationChains(c *Conf, n map[string]*Notification) ([][]string) { chains := [][]string{} for _, root := range n { @@ -365,10 +369,6 @@ type Notification struct { body string } -// func (n *Notification) MarshalJSON() ([]byte, error) { -// return nil, fmt.Errorf("conf: cannot json marshal notifications") -// } - type Vars map[string]string func ParseFile(fname string) (*Conf, error) { diff --git a/cmd/bosun/sched/sched.go b/cmd/bosun/sched/sched.go index 74c0b5019e..56745ed120 100644 --- a/cmd/bosun/sched/sched.go +++ b/cmd/bosun/sched/sched.go @@ -23,7 +23,6 @@ import ( "github.com/boltdb/bolt" "github.com/bradfitz/slice" "github.com/kylebrandt/boolq" - "github.com/kylebrandt/boolq/parse" "github.com/tatsushid/go-fastping" ) @@ -379,9 +378,9 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro err = err2 return } - var parsedExpr *parse.Tree + var parsedExpr *boolq.Tree if filter != "" { - parsedExpr, err2 = parse.Parse(filter) + parsedExpr, err2 = boolq.Parse(filter) if err2 != nil { err = err2 return @@ -402,7 +401,7 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro } is := MakeIncidentSummary(s.Conf, silenced, v) match := false - match, err2 = boolq.AskParsedExpr(*parsedExpr, is) + match, err2 = boolq.AskParsedExpr(parsedExpr, is) if err2 != nil { err = err2 return diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go index 86bb219359..c4d12a42e8 100644 --- a/cmd/bosun/web/incident.go +++ b/cmd/bosun/web/incident.go @@ -8,7 +8,6 @@ import ( "github.com/MiniProfiler/go/miniprofiler" "github.com/kylebrandt/boolq" - "github.com/kylebrandt/boolq/parse" ) func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Request) (interface{}, error) { @@ -23,9 +22,9 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ } summaries := []sched.IncidentSummaryView{} filterText := r.FormValue("filter") - var parsedExpr *parse.Tree + var parsedExpr *boolq.Tree if filterText != "" { - parsedExpr, err = parse.Parse(filterText) + parsedExpr, err = boolq.Parse(filterText) if err != nil { return nil, fmt.Errorf("bad filter: %v", err) } @@ -36,7 +35,7 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ summaries = append(summaries, is) continue } - match, err := boolq.AskParsedExpr(*parsedExpr, is) + match, err := boolq.AskParsedExpr(parsedExpr, is) if err != nil { return nil, err } diff --git a/vendor/github.com/kylebrandt/boolq/boolq.go b/vendor/github.com/kylebrandt/boolq/boolq.go index 8f072b7241..3571a4f3c9 100644 --- a/vendor/github.com/kylebrandt/boolq/boolq.go +++ b/vendor/github.com/kylebrandt/boolq/boolq.go @@ -30,10 +30,23 @@ func AskExpr(expr string, asker Asker) (bool, error) { // AskParsedExpr is like AskExpr but takes an expression that has already // been parsed by parse.Parse on the expression. This is useful if you are calling // the same expression multiple times. -func AskParsedExpr(q parse.Tree, asker Asker) (bool, error) { +func AskParsedExpr(q *Tree, asker Asker) (bool, error) { return walk(q.Root, asker) } +type Tree struct { + *parse.Tree +} + +// Parse parses an expression and returns the parsed expression. +// It can be used wtih AskParsedExpr +func Parse(text string) (*Tree, error) { + tree := &Tree{} + var err error + tree.Tree, err = parse.Parse(text) + return tree, err +} + func walk(node parse.Node, asker Asker) (bool, error) { switch node := node.(type) { case *parse.AskNode: diff --git a/vendor/vendor.json b/vendor/vendor.json index a5c8941bb2..81f917fa06 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -404,8 +404,8 @@ }, { "path": "github.com/kylebrandt/boolq", - "revision": "3c0efef9c6cd5f400eba1871a3be6191796db71a", - "revisionTime": "2016-06-01T16:00:19-04:00" + "revision": "fbef909db018d0e382a1ef721530456b9e76a72a", + "revisionTime": "2016-06-08T12:27:42-04:00" }, { "path": "github.com/kylebrandt/boolq/parse", From 28475423a73ae6d54691f0c39985662917c15119 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 8 Jun 2016 12:47:00 -0400 Subject: [PATCH 13/14] simplify filter logic, update boolq --- cmd/bosun/sched/sched.go | 14 ++++---------- cmd/bosun/web/incident.go | 12 +++--------- vendor/github.com/kylebrandt/boolq/boolq.go | 7 +++++++ vendor/vendor.json | 4 ++-- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/cmd/bosun/sched/sched.go b/cmd/bosun/sched/sched.go index 56745ed120..9b2b14044a 100644 --- a/cmd/bosun/sched/sched.go +++ b/cmd/bosun/sched/sched.go @@ -379,12 +379,10 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro return } var parsedExpr *boolq.Tree - if filter != "" { - parsedExpr, err2 = boolq.Parse(filter) - if err2 != nil { - err = err2 - return - } + parsedExpr, err2 = boolq.Parse(filter) + if err2 != nil { + err = err2 + return } for k, v := range status2 { a := s.Conf.Alerts[k.Name()] @@ -395,10 +393,6 @@ func (s *Schedule) MarshalGroups(T miniprofiler.Timer, filter string) (*StateGro } continue } - if parsedExpr == nil { - status[k] = v - continue - } is := MakeIncidentSummary(s.Conf, silenced, v) match := false match, err2 = boolq.AskParsedExpr(parsedExpr, is) diff --git a/cmd/bosun/web/incident.go b/cmd/bosun/web/incident.go index c4d12a42e8..d774e4ac57 100644 --- a/cmd/bosun/web/incident.go +++ b/cmd/bosun/web/incident.go @@ -23,18 +23,12 @@ func ListOpenIncidents(t miniprofiler.Timer, w http.ResponseWriter, r *http.Requ summaries := []sched.IncidentSummaryView{} filterText := r.FormValue("filter") var parsedExpr *boolq.Tree - if filterText != "" { - parsedExpr, err = boolq.Parse(filterText) - if err != nil { - return nil, fmt.Errorf("bad filter: %v", err) - } + parsedExpr, err = boolq.Parse(filterText) + if err != nil { + return nil, fmt.Errorf("bad filter: %v", err) } for _, iState := range list { is := sched.MakeIncidentSummary(schedule.Conf, suppressor, iState) - if parsedExpr == nil { - summaries = append(summaries, is) - continue - } match, err := boolq.AskParsedExpr(parsedExpr, is) if err != nil { return nil, err diff --git a/vendor/github.com/kylebrandt/boolq/boolq.go b/vendor/github.com/kylebrandt/boolq/boolq.go index 3571a4f3c9..1ebf8fafbc 100644 --- a/vendor/github.com/kylebrandt/boolq/boolq.go +++ b/vendor/github.com/kylebrandt/boolq/boolq.go @@ -31,6 +31,9 @@ func AskExpr(expr string, asker Asker) (bool, error) { // been parsed by parse.Parse on the expression. This is useful if you are calling // the same expression multiple times. func AskParsedExpr(q *Tree, asker Asker) (bool, error) { + if q.Tree.Root == nil { + return true, nil + } return walk(q.Root, asker) } @@ -42,6 +45,10 @@ type Tree struct { // It can be used wtih AskParsedExpr func Parse(text string) (*Tree, error) { tree := &Tree{} + if text == "" { + tree.Tree = &parse.Tree{} + return tree, nil + } var err error tree.Tree, err = parse.Parse(text) return tree, err diff --git a/vendor/vendor.json b/vendor/vendor.json index 81f917fa06..78b0540822 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -404,8 +404,8 @@ }, { "path": "github.com/kylebrandt/boolq", - "revision": "fbef909db018d0e382a1ef721530456b9e76a72a", - "revisionTime": "2016-06-08T12:27:42-04:00" + "revision": "f869a7265c7ec4f3e2618a39b7b3877a5629eb17", + "revisionTime": "2016-06-08T12:45:48-04:00" }, { "path": "github.com/kylebrandt/boolq/parse", From c1a4d4896abcaa46a65e9d2729ce9d450214798f Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Wed, 8 Jun 2016 13:58:11 -0400 Subject: [PATCH 14/14] fmt --- cmd/bosun/conf/conf.go | 6 +++--- cmd/bosun/sched/views.go | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cmd/bosun/conf/conf.go b/cmd/bosun/conf/conf.go index 4dc3a6b08a..f0f04c2e1c 100644 --- a/cmd/bosun/conf/conf.go +++ b/cmd/bosun/conf/conf.go @@ -299,18 +299,18 @@ func (ns *Notifications) Get(c *Conf, tags opentsdb.TagSet) map[string]*Notifica // alert. Each chain is a list of notification names. If a notification name // as already been seen in the chain it ends the list with the notification // name with a of "..." which indicates that the chain will loop. -func GetNotificationChains(c *Conf, n map[string]*Notification) ([][]string) { +func GetNotificationChains(c *Conf, n map[string]*Notification) [][]string { chains := [][]string{} for _, root := range n { chain := []string{} seen := make(map[string]bool) var walkChain func(next *Notification) walkChain = func(next *Notification) { - if (next == nil) { + if next == nil { chains = append(chains, chain) return } - if (seen[next.Name]) { + if seen[next.Name] { chain = append(chain, fmt.Sprintf("...%v", next.Name)) chains = append(chains, chain) return diff --git a/cmd/bosun/sched/views.go b/cmd/bosun/sched/views.go index 6bb8e73fcf..74ea30a566 100644 --- a/cmd/bosun/sched/views.go +++ b/cmd/bosun/sched/views.go @@ -33,13 +33,13 @@ type EpochAction struct { Type models.ActionType } -func MakeEpochAction(a models.Action) (EpochAction) { - return EpochAction{ - User: a.User, - Message: a.Message, - Time: a.Time.UTC().Unix(), - Type: a.Type, - } +func MakeEpochAction(a models.Action) EpochAction { + return EpochAction{ + User: a.User, + Message: a.Message, + Time: a.Time.UTC().Unix(), + Type: a.Type, + } } type IncidentSummaryView struct { @@ -71,10 +71,10 @@ func MakeIncidentSummary(c *conf.Conf, s SilenceTester, is *models.IncidentState eventSummaries = append(eventSummaries, eventSummary) } } - actions := make([]EpochAction, len(is.Actions)) - for i, action := range is.Actions { - actions[i] = MakeEpochAction(action) - } + actions := make([]EpochAction, len(is.Actions)) + for i, action := range is.Actions { + actions[i] = MakeEpochAction(action) + } return IncidentSummaryView{ Id: is.Id, Subject: is.Subject,