diff --git a/alert.go b/alert.go index d635304de..bd4c9ffa5 100644 --- a/alert.go +++ b/alert.go @@ -17,6 +17,7 @@ import ( "github.com/influxdata/kapacitor/models" "github.com/influxdata/kapacitor/pipeline" alertservice "github.com/influxdata/kapacitor/services/alert" + "github.com/influxdata/kapacitor/services/bigpanda" "github.com/influxdata/kapacitor/services/discord" "github.com/influxdata/kapacitor/services/hipchat" "github.com/influxdata/kapacitor/services/httppost" @@ -486,6 +487,18 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, d NodeDiagnostic) (a } an.handlers = append(an.handlers, h) } + + for _, s := range n.BigPandaHandlers { + c := bigpanda.HandlerConfig{ + AppKey: s.AppKey, + } + h, err := et.tm.BigPandaService.Handler(c, ctx...) + if err != nil { + return nil, errors.Wrap(err, "failed to create BigPanda handler") + } + an.handlers = append(an.handlers, h) + } + for _, t := range n.TeamsHandlers { c := teams.HandlerConfig{ ChannelURL: t.ChannelURL, @@ -544,6 +557,20 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, d NodeDiagnostic) (a n.IsStateChangesOnly = true } + if len(n.BigPandaHandlers) == 0 && (et.tm.BigPandaService != nil && et.tm.BigPandaService.Global()) { + h, err := et.tm.BigPandaService.Handler(bigpanda.HandlerConfig{}, ctx...) + if err != nil { + return nil, errors.Wrap(err, "failed to create BigPanda handler") + } + an.handlers = append(an.handlers, h) + } + // If BigPanda has been configured with state changes only set it. + if et.tm.BigPandaService != nil && + et.tm.BigPandaService.Global() && + et.tm.BigPandaService.StateChangesOnly() { + n.IsStateChangesOnly = true + } + // Parse level expressions an.levels = make([]stateful.Expression, alert.Critical+1) an.scopePools = make([]stateful.ScopePool, alert.Critical+1) diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go index 1df4a4652..7948b8613 100644 --- a/integrations/streamer_test.go +++ b/integrations/streamer_test.go @@ -41,6 +41,8 @@ import ( "github.com/influxdata/kapacitor/services/alert/alerttest" "github.com/influxdata/kapacitor/services/alerta" "github.com/influxdata/kapacitor/services/alerta/alertatest" + "github.com/influxdata/kapacitor/services/bigpanda" + "github.com/influxdata/kapacitor/services/bigpanda/bigpandatest" "github.com/influxdata/kapacitor/services/diagnostic" "github.com/influxdata/kapacitor/services/hipchat" "github.com/influxdata/kapacitor/services/hipchat/hipchattest" @@ -9229,6 +9231,105 @@ stream } } +func TestStream_AlertBigPanda(t *testing.T) { + ts := bigpandatest.NewServer() + defer ts.Close() + + var script = ` +stream + |from() + .measurement('cpu') + .where(lambda: "host" == 'serverA') + .groupBy('host') + |window() + .period(10s) + .every(10s) + |count('value') + |alert() + .id('kapacitor/{{ .Name }}/{{ index .Tags "host" }}') + .message('kapacitor/{{ .Name }}/{{ index .Tags "host" }} is {{ .Level }} @{{.Time}}') + .details('https://example.org/link') + .info(lambda: "count" > 6.0) + .warn(lambda: "count" > 7.0) + .crit(lambda: "count" > 8.0) + .bigPanda() + .AppKey('111111') + .bigPanda() + .AppKey('222222') + .bigPanda() +` + tmInit := func(tm *kapacitor.TaskMaster) { + + c := bigpanda.NewConfig() + c.Enabled = true + c.AppKey = "XXXXXXX" + c.Token = "testtoken1231234" + c.URL = ts.URL + "/test/bigpanda/url" + + d := diagService.NewBigPandaHandler().WithContext(keyvalue.KV("test", "111")) + sl, err := bigpanda.NewService(c, d) + if err != nil { + t.Error(err) + } + + tm.BigPandaService = sl + } + + testStreamerNoOutput(t, "TestStream_Alert", script, 13*time.Second, tmInit) + + exp := []interface{}{ + bigpandatest.Request{ + URL: "/test/bigpanda/url", + PostData: bigpandatest.PostData{ + Check: "kapacitor/cpu/serverA", + Description: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC", + AppKey: "111111", + Status: "critical", + Host: "serverA", + Timestamp: 31536010, + Task: "TestStream_Alert:cpu", + Details: "https://example.org/link", + }, + }, + bigpandatest.Request{ + URL: "/test/bigpanda/url", + PostData: bigpandatest.PostData{ + Check: "kapacitor/cpu/serverA", + Description: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC", + AppKey: "222222", + Status: "critical", + Host: "serverA", + Timestamp: 31536010, + Task: "TestStream_Alert:cpu", + Details: "https://example.org/link", + }, + }, + bigpandatest.Request{ + URL: "/test/bigpanda/url", + PostData: bigpandatest.PostData{ + Check: "kapacitor/cpu/serverA", + Description: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC", + AppKey: "XXXXXXX", + Status: "critical", + Host: "serverA", + Timestamp: 31536010, + Task: "TestStream_Alert:cpu", + Details: "https://example.org/link", + }, + }, + } + + ts.Close() + var got []interface{} + for _, g := range ts.Requests() { + got = append(got, g) + } + + if err := compareListIgnoreOrder(got, exp, nil); err != nil { + t.Error(err) + } +} + func TestStream_AlertPushover(t *testing.T) { ts := pushovertest.NewServer() defer ts.Close() diff --git a/pipeline/alert.go b/pipeline/alert.go index 6174f6fba..1eb1d51d0 100644 --- a/pipeline/alert.go +++ b/pipeline/alert.go @@ -357,6 +357,10 @@ type AlertNodeData struct { // tick:ignore DiscordHandlers []*DiscordHandler `tick:"Discord" json:"discord"` + // Send alert to BigPanda + // tick:ignore + BigPandaHandlers []*BigPandaHandler `tick:"BigPanda" json:"bigPanda"` + // Send alert to Telegram. // tick:ignore TelegramHandlers []*TelegramHandler `tick:"Telegram" json:"telegram"` @@ -1615,6 +1619,71 @@ type DiscordHandler struct { EmbedTitle string `json:"embedTitle"` } +// To allow Kapacitor to post to BigPanda, +// follow this guide https://docs.bigpanda.io/docs/api-key-management +// and create a new api key +// in the 'bigpanda' configuration section. +// +// Example: +// [bigpanda] +// enabled = true +// app-key = "my-app-key" +// token = "your-api-key" +// +// In order to not post a message every alert interval +// use AlertNode.StateChangesOnly so that only events +// where the alert changed state are posted to the channel. +// +// Example: +// stream +// |alert() +// .bigPanda() +// +// Send alerts with default app key +// +// Example: +// stream +// |alert() +// .bigPanda() +// .appKey('my-application') +// +// send alerts with custom appKey +// +// If the 'bigpanda' section in the configuration has the option: global = true +// then all alerts are sent to BigpPanda without the need to explicitly state it +// in the TICKscript. +// +// Example: +// [bigpanda] +// enabled = true +// default = true +// app-key = examplecorp +// global = true +// state-changes-only = true +// +// Example: +// stream +// |alert() +// +// Send alert to BigPanda. +// tick:property + +func (n *AlertNodeData) BigPanda() *BigPandaHandler { + bigPanda := &BigPandaHandler{ + AlertNodeData: n, + } + n.BigPandaHandlers = append(n.BigPandaHandlers, bigPanda) + return bigPanda +} + +// tick:embedded:AlertNode.BigPanda +type BigPandaHandler struct { + *AlertNodeData `json:"-"` + // Application id + // If empty uses the default config + AppKey string `json:"app-key"` +} + // Send the alert to Telegram. // For step-by-step instructions on setting up Kapacitor with Telegram, see the [Event Handler Setup Guide](https://docs.influxdata.com//kapacitor/latest/guides/event-handler-setup/#telegram-setup). // To allow Kapacitor to post to Telegram, diff --git a/pipeline/alert_test.go b/pipeline/alert_test.go index ea9d350e0..fba170cab 100644 --- a/pipeline/alert_test.go +++ b/pipeline/alert_test.go @@ -73,6 +73,7 @@ func TestAlertNode_MarshalJSON(t *testing.T) { "sensu": null, "slack": null, "discord": null, + "bigPanda": null, "telegram": null, "hipChat": null, "alerta": null, diff --git a/pipeline/json_test.go b/pipeline/json_test.go index f2ef9c302..27c8de655 100644 --- a/pipeline/json_test.go +++ b/pipeline/json_test.go @@ -252,6 +252,7 @@ func TestPipeline_MarshalJSON(t *testing.T) { "sensu": null, "slack": null, "discord": null, + "bigPanda": null, "telegram": null, "hipChat": null, "alerta": null, diff --git a/server/config.go b/server/config.go index a3b093a63..336bbb3ca 100644 --- a/server/config.go +++ b/server/config.go @@ -15,6 +15,7 @@ import ( "github.com/influxdata/kapacitor/services/alert" "github.com/influxdata/kapacitor/services/alerta" "github.com/influxdata/kapacitor/services/azure" + "github.com/influxdata/kapacitor/services/bigpanda" "github.com/influxdata/kapacitor/services/config" "github.com/influxdata/kapacitor/services/consul" "github.com/influxdata/kapacitor/services/deadman" @@ -89,6 +90,7 @@ type Config struct { // Alert handlers Alerta alerta.Config `toml:"alerta" override:"alerta"` + BigPanda bigpanda.Config `toml:"bigpanda" override:"bigpanda"` Discord discord.Configs `toml:"discord" override:"discord,element-key=workspace"` HipChat hipchat.Config `toml:"hipchat" override:"hipchat"` Kafka kafka.Configs `toml:"kafka" override:"kafka,element-key=id"` @@ -161,6 +163,7 @@ func NewConfig() *Config { c.OpenTSDB = opentsdb.NewConfig() c.Alerta = alerta.NewConfig() + c.BigPanda = bigpanda.NewConfig() c.Discord = discord.Configs{discord.NewDefaultConfig()} c.HipChat = hipchat.NewConfig() c.Kafka = kafka.Configs{kafka.NewConfig()} @@ -281,6 +284,9 @@ func (c *Config) Validate() error { if err := c.Alerta.Validate(); err != nil { return errors.Wrap(err, "alerta") } + if err := c.BigPanda.Validate(); err != nil { + return errors.Wrap(err, "bigpanda") + } if err := c.Discord.Validate(); err != nil { return err } diff --git a/server/server.go b/server/server.go index cb334d879..82194039b 100644 --- a/server/server.go +++ b/server/server.go @@ -25,6 +25,7 @@ import ( "github.com/influxdata/kapacitor/services/alert" "github.com/influxdata/kapacitor/services/alerta" "github.com/influxdata/kapacitor/services/azure" + "github.com/influxdata/kapacitor/services/bigpanda" "github.com/influxdata/kapacitor/services/config" "github.com/influxdata/kapacitor/services/consul" "github.com/influxdata/kapacitor/services/deadman" @@ -243,6 +244,9 @@ func New(c *Config, buildInfo BuildInfo, diagService *diagnostic.Service) (*Serv // Append Alert integration services s.appendAlertaService() + if err := s.appendBigPandaService(); err != nil { + return nil, errors.Wrap(err, "bigpanda service") + } if err := s.appendDiscordService(); err != nil { return nil, errors.Wrap(err, "discord service") } @@ -776,6 +780,22 @@ func (s *Server) appendAlertaService() { s.AppendService("alerta", srv) } +func (s *Server) appendBigPandaService() error { + c := s.config.BigPanda + d := s.DiagService.NewBigPandaHandler() + srv, err := bigpanda.NewService(c, d) + if err != nil { + return err + } + + s.TaskMaster.BigPandaService = srv + s.AlertService.BigPandaService = srv + + s.SetDynamicService("bigpanda", srv) + s.AppendService("bigpanda", srv) + return nil +} + func (s *Server) appendDiscordService() error { c := s.config.Discord d := s.DiagService.NewDiscordHandler() diff --git a/server/server_test.go b/server/server_test.go index f008dd70e..3fb5a090c 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -22,8 +22,6 @@ import ( "testing" "time" - "github.com/influxdata/kapacitor/services/discord/discordtest" - "github.com/davecgh/go-spew/spew" "github.com/dgrijalva/jwt-go" "github.com/google/go-cmp/cmp" @@ -39,6 +37,8 @@ import ( "github.com/influxdata/kapacitor/server" "github.com/influxdata/kapacitor/services/alert/alerttest" "github.com/influxdata/kapacitor/services/alerta/alertatest" + "github.com/influxdata/kapacitor/services/bigpanda/bigpandatest" + "github.com/influxdata/kapacitor/services/discord/discordtest" "github.com/influxdata/kapacitor/services/hipchat/hipchattest" "github.com/influxdata/kapacitor/services/httppost" "github.com/influxdata/kapacitor/services/httppost/httpposttest" @@ -8850,6 +8850,28 @@ func TestServer_ListServiceTests(t *testing.T) { "id": "", }, }, + { + Link: client.Link{Relation: client.Self, Href: "/kapacitor/v1/service-tests/bigpanda"}, + Name: "bigpanda", + Options: client.ServiceTestOptions{ + "app_key": "my-app-key-123456", + "level": "CRITICAL", + "message": "test bigpanda message", + "timestamp": "1970-01-01T00:00:01Z", + "event_data": map[string]interface{}{ + "Fields": map[string]interface{}{}, + "Result": map[string]interface{}{ + "series": interface{}(nil), + }, + "Name": "testBigPanda", + "TaskName": "", + "Group": "", + "Tags": map[string]interface{}{}, + "Recoverable": false, + "Category": "", + }, + }, + }, { Link: client.Link{Relation: "self", Href: "/kapacitor/v1/service-tests/consul"}, Name: "consul", @@ -9795,6 +9817,45 @@ func TestServer_AlertHandlers(t *testing.T) { return nil }, }, + { + handler: client.TopicHandler{ + Kind: "bigpanda", + Options: map[string]interface{}{ + "app-key": "my-app-key-123456", + }, + }, + setup: func(c *server.Config, ha *client.TopicHandler) (context.Context, error) { + ts := bigpandatest.NewServer() + ctxt := context.WithValue(nil, "server", ts) + + c.BigPanda.Enabled = true + c.BigPanda.Token = "my-token-123" + c.BigPanda.AppKey = "my-app-key" + c.BigPanda.URL = ts.URL + "/test/bigpanda/alert" + return ctxt, nil + }, + result: func(ctxt context.Context) error { + ts := ctxt.Value("server").(*bigpandatest.Server) + ts.Close() + got := ts.Requests() + exp := []bigpandatest.Request{{ + URL: "/test/bigpanda/alert", + PostData: bigpandatest.PostData{ + AppKey: "my-app-key-123456", + Check: "id", + Status: "critical", + Timestamp: 0, + Task: "testAlertHandlers:alert", + Description: "message", + Details: "details", + }, + }} + if !reflect.DeepEqual(exp, got) { + return fmt.Errorf("unexpected bigpanda request:\nexp\n%+v\ngot\n%+v\n", exp, got) + } + return nil + }, + }, { handler: client.TopicHandler{ Kind: "discord", diff --git a/services/alert/service.go b/services/alert/service.go index 6c50cc043..a9458e355 100644 --- a/services/alert/service.go +++ b/services/alert/service.go @@ -14,6 +14,7 @@ import ( "github.com/influxdata/kapacitor/keyvalue" "github.com/influxdata/kapacitor/models" "github.com/influxdata/kapacitor/services/alerta" + "github.com/influxdata/kapacitor/services/bigpanda" "github.com/influxdata/kapacitor/services/discord" "github.com/influxdata/kapacitor/services/hipchat" "github.com/influxdata/kapacitor/services/httpd" @@ -87,6 +88,9 @@ type Service struct { DefaultHandlerConfig() alerta.HandlerConfig Handler(alerta.HandlerConfig, ...keyvalue.T) (alert.Handler, error) } + BigPandaService interface { + Handler(bigpanda.HandlerConfig, ...keyvalue.T) (alert.Handler, error) + } HipChatService interface { Handler(hipchat.HandlerConfig, ...keyvalue.T) alert.Handler } @@ -789,6 +793,17 @@ func (s *Service) createHandlerFromSpec(spec HandlerSpec) (handler, error) { return handler{}, err } h = newExternalHandler(h) + case "bigpanda": + c := bigpanda.HandlerConfig{} + err = decodeOptions(spec.Options, &c) + if err != nil { + return handler{}, err + } + h, err = s.BigPandaService.Handler(c, ctx...) + if err != nil { + return handler{}, err + } + h = newExternalHandler(h) case "discord": c := discord.HandlerConfig{} err = decodeOptions(spec.Options, &c) diff --git a/services/bigpanda/bigpandatest/bigpandatest.go b/services/bigpanda/bigpandatest/bigpandatest.go new file mode 100644 index 000000000..1ce50358d --- /dev/null +++ b/services/bigpanda/bigpandatest/bigpandatest.go @@ -0,0 +1,67 @@ +package bigpandatest + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "sync" +) + +type Server struct { + mu sync.Mutex + ts *httptest.Server + URL string + requests []Request + closed bool +} + +func NewServer() *Server { + s := new(Server) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + pr := Request{ + URL: r.URL.String(), + } + dec := json.NewDecoder(r.Body) + dec.Decode(&pr.PostData) + s.mu.Lock() + s.requests = append(s.requests, pr) + s.mu.Unlock() + w.WriteHeader(http.StatusCreated) + + })) + s.ts = ts + s.URL = ts.URL + return s +} + +func (s *Server) Requests() []Request { + s.mu.Lock() + defer s.mu.Unlock() + return s.requests +} + +func (s *Server) Close() { + if s.closed { + return + } + s.closed = true + s.ts.Close() +} + +type Request struct { + URL string + PostData PostData +} + +// PostData is the default struct to send an element through to BigPanda +type PostData struct { + AppKey string `json:"app_key"` + Status string `json:"status"` + Host string `json:"host"` + Timestamp int64 `json:"timestamp"` + Check string `json:"check"` + Description string `json:"description"` + Cluster string `json:"cluster"` + Task string `json:"task"` + Details string `json:"details"` +} diff --git a/services/bigpanda/config.go b/services/bigpanda/config.go new file mode 100644 index 000000000..92f31e2fc --- /dev/null +++ b/services/bigpanda/config.go @@ -0,0 +1,54 @@ +package bigpanda + +import ( + "github.com/pkg/errors" + "net/url" +) + +const ( + defaultBigPandaAlertApi = "https://api.bigpanda.io/data/v2/alerts" +) + +type Config struct { + // Whether BigPanda integration is enabled. + Enabled bool `toml:"enabled" override:"enabled"` + + // Whether all alerts should automatically post to Teams. + Global bool `toml:"global" override:"global"` + + //Each integration must have an App Key in BigPanda to identify it as a unique source. + AppKey string `toml:"app-key" override:"app-key"` + + //Each integration must have an App Key in BigPanda to identify it as a unique source. + Token string `toml:"token" override:"token,redact"` + + // Whether all alerts should automatically use stateChangesOnly mode. + // Only applies if global is also set. + StateChangesOnly bool `toml:"state-changes-only" override:"state-changes-only"` + + // Whether to skip the tls verification of the alerta host + InsecureSkipVerify bool `toml:"insecure-skip-verify" override:"insecure-skip-verify"` + + //Optional alert api URL, if not specified https://api.bigpanda.io/data/v2/alerts is used + URL string `toml:"url" override:"url"` +} + +func NewConfig() Config { + return Config{ + URL: defaultBigPandaAlertApi, + } +} + +func (c Config) Validate() error { + if c.Enabled && c.AppKey == "" { + return errors.New("must specify BigPanda app-key") + } + if _, err := url.Parse(c.URL); err != nil { + return errors.Wrapf(err, "invalid url %q", c.URL) + } + if c.Enabled && c.Token == "" { + return errors.New("must specify BigPanda token") + } + + return nil +} diff --git a/services/bigpanda/service.go b/services/bigpanda/service.go new file mode 100644 index 000000000..4c3923b48 --- /dev/null +++ b/services/bigpanda/service.go @@ -0,0 +1,280 @@ +package bigpanda + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + khttp "github.com/influxdata/kapacitor/http" + "github.com/influxdata/kapacitor/models" + "html" + "io/ioutil" + "net/http" + "net/url" + "strings" + "sync/atomic" + "time" + + "github.com/influxdata/kapacitor/alert" + "github.com/influxdata/kapacitor/keyvalue" + "github.com/pkg/errors" +) + +const ( + defaultTokenPrefix = "Bearer" +) + +type Diagnostic interface { + WithContext(ctx ...keyvalue.T) Diagnostic + Error(msg string, err error) +} + +type Service struct { + configValue atomic.Value + clientValue atomic.Value + diag Diagnostic +} + +func NewService(c Config, d Diagnostic) (*Service, error) { + s := &Service{ + diag: d, + } + s.configValue.Store(c) + s.clientValue.Store(&http.Client{ + Transport: khttp.NewDefaultTransportWithTLS(&tls.Config{InsecureSkipVerify: c.InsecureSkipVerify}), + }) + + return s, nil +} + +func (s *Service) Open() error { + return nil +} + +func (s *Service) Close() error { + return nil +} + +func (s *Service) config() Config { + return s.configValue.Load().(Config) +} + +func (s *Service) Update(newConfig []interface{}) error { + if l := len(newConfig); l != 1 { + return fmt.Errorf("expected only one new config object, got %d", l) + } + if c, ok := newConfig[0].(Config); !ok { + return fmt.Errorf("expected config object to be of type %T, got %T", c, newConfig[0]) + } else { + s.configValue.Store(c) + s.clientValue.Store(&http.Client{ + Transport: khttp.NewDefaultTransportWithTLS(&tls.Config{InsecureSkipVerify: c.InsecureSkipVerify}), + }) + } + return nil +} + +func (s *Service) Global() bool { + return s.config().Global +} + +func (s *Service) StateChangesOnly() bool { + return s.config().StateChangesOnly +} + +type testOptions struct { + AppKey string `json:"app_key"` + Message string `json:"message"` + Level alert.Level `json:"level"` + Data alert.EventData `json:"event_data"` + Timestamp time.Time `json:"timestamp"` +} + +func (s *Service) TestOptions() interface{} { + t, _ := time.Parse(time.RFC3339, "1970-01-01T00:00:01Z") + + return &testOptions{ + AppKey: "my-app-key-123456", + Message: "test bigpanda message", + Level: alert.Critical, + Data: alert.EventData{ + Name: "testBigPanda", + Tags: make(map[string]string), + Fields: make(map[string]interface{}), + Result: models.Result{}, + }, + Timestamp: t, + } + +} + +func (s *Service) Test(options interface{}) error { + o, ok := options.(*testOptions) + if !ok { + return fmt.Errorf("unexpected options type %T", options) + } + return s.Alert(o.AppKey, "", o.Message, "", o.Level, o.Timestamp, o.Data) +} + +func (s *Service) Alert(appKey, id string, message string, details string, level alert.Level, timestamp time.Time, data alert.EventData) error { + req, err := s.preparePost(appKey, id, message, details, level, timestamp, data) + + if err != nil { + return err + } + + client := s.clientValue.Load().(*http.Client) + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + type response struct { + Error string `json:"error"` + } + r := &response{Error: fmt.Sprintf("failed to understand BigPanda response. code: %d content: %s", resp.StatusCode, string(body))} + b := bytes.NewReader(body) + dec := json.NewDecoder(b) + dec.Decode(r) + return errors.New(r.Error) + } + + return nil +} + +// BigPanda alert +// See https://docs.bigpanda.io/reference#alerts +/* + +curl -X POST -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + https://api.bigpanda.io/data/v2/alerts \ + -d '{ "app_key": "", "status": "critical", "host": "production-database-1", "check": "CPU overloaded" }' + +{ + "app_key": "123", + "status": "critical", + "host": "production-database-1", + "timestamp": 1402302570, + "check": "CPU overloaded", + "description": "CPU is above upper limit (70%)", + "cluster": "production-databases", + "my_unique_attribute": "my_unique_value" +} + + statuses: ok, critical, warning, acknowledged + + "primary_property": "application", + "secondary_property": "host" +*/ +func (s *Service) preparePost(appKey, id string, message string, details string, level alert.Level, timestamp time.Time, data alert.EventData) (*http.Request, error) { + c := s.config() + if !c.Enabled { + return nil, errors.New("service is not enabled") + } + + var status string + switch level { + case alert.OK: + status = "ok" + case alert.Warning: + status = "warning" + case alert.Critical: + status = "critical" + case alert.Info: + status = "ok" + default: + status = "critical" + } + + bpData := make(map[string]interface{}) + + if message != "" { + bpData["description"] = message + } + + //ignore default details containing full json event + if details != "" { + unescapeString := html.UnescapeString(details) + if !strings.HasPrefix(unescapeString, "{") { + bpData["details"] = unescapeString + } + } + + if id != "" { + bpData["check"] = id + } + + bpData["task"] = fmt.Sprintf("%s:%s", data.TaskName, data.Name) + bpData["timestamp"] = timestamp.Unix() + bpData["status"] = status + + if appKey == "" { + appKey = c.AppKey + } + bpData["app_key"] = appKey + + if len(data.Tags) > 0 { + for k, v := range data.Tags { + bpData[k] = v + } + } + + var post bytes.Buffer + enc := json.NewEncoder(&post) + if err := enc.Encode(bpData); err != nil { + return nil, err + } + + alertUrl, err := url.Parse(c.URL) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", alertUrl.String(), &post) + req.Header.Add("Authorization", defaultTokenPrefix+" "+c.Token) + req.Header.Add("Content-Type", "application/json") + if err != nil { + return nil, err + } + return req, nil +} + +// HandlerConfig defines the high-level struct required to connect to BigPanda +type HandlerConfig struct { + AppKey string `mapstructure:"app-key"` +} + +type handler struct { + s *Service + c HandlerConfig + diag Diagnostic +} + +func (s *Service) Handler(c HandlerConfig, ctx ...keyvalue.T) (alert.Handler, error) { + return &handler{ + s: s, + c: c, + diag: s.diag.WithContext(ctx...), + }, nil +} + +func (h *handler) Handle(event alert.Event) { + if err := h.s.Alert( + h.c.AppKey, + event.State.ID, + event.State.Message, + event.State.Details, + event.State.Level, + event.State.Time, + event.Data, + ); err != nil { + h.diag.Error("failed to send event to BigPanda", err) + } +} diff --git a/services/diagnostic/handlers.go b/services/diagnostic/handlers.go index da39965e9..f9209dd3f 100644 --- a/services/diagnostic/handlers.go +++ b/services/diagnostic/handlers.go @@ -17,6 +17,7 @@ import ( "github.com/influxdata/kapacitor/models" alertservice "github.com/influxdata/kapacitor/services/alert" "github.com/influxdata/kapacitor/services/alerta" + "github.com/influxdata/kapacitor/services/bigpanda" "github.com/influxdata/kapacitor/services/discord" "github.com/influxdata/kapacitor/services/ec2" "github.com/influxdata/kapacitor/services/hipchat" @@ -627,6 +628,32 @@ func (h *DiscordHandler) WithContext(ctx ...keyvalue.T) discord.Diagnostic { } } +// BigPanda Handler + +type BigPandaHandler struct { + l Logger +} + +func (h *BigPandaHandler) InsecureSkipVerify() { + h.l.Info("service is configured to skip ssl verification") +} + +func (h *BigPandaHandler) Error(msg string, err error) { + h.l.Error(msg, Error(err)) +} + +func (h *BigPandaHandler) TemplateError(err error, kv keyvalue.T) { + h.l.Error("failed to evaluate BigPanda template", Error(err), String(kv.Key, kv.Value)) +} + +func (h *BigPandaHandler) WithContext(ctx ...keyvalue.T) bigpanda.Diagnostic { + fields := logFieldsFromContext(ctx) + + return &BigPandaHandler{ + l: h.l.With(fields...), + } +} + // Storage Handler type StorageHandler struct { diff --git a/services/diagnostic/service.go b/services/diagnostic/service.go index 318e7a9a7..e009fc338 100644 --- a/services/diagnostic/service.go +++ b/services/diagnostic/service.go @@ -154,6 +154,12 @@ func (s *Service) NewDiscordHandler() *DiscordHandler { } } +func (s *Service) NewBigPandaHandler() *BigPandaHandler { + return &BigPandaHandler{ + l: s.Logger.With(String("service", "bigpanda")), + } +} + func (s *Service) NewTaskStoreHandler() *TaskStoreHandler { return &TaskStoreHandler{ l: s.Logger.With(String("service", "task_store")), diff --git a/task_master.go b/task_master.go index 5d723eeea..58039888f 100644 --- a/task_master.go +++ b/task_master.go @@ -19,6 +19,7 @@ import ( "github.com/influxdata/kapacitor/server/vars" alertservice "github.com/influxdata/kapacitor/services/alert" "github.com/influxdata/kapacitor/services/alerta" + "github.com/influxdata/kapacitor/services/bigpanda" "github.com/influxdata/kapacitor/services/discord" ec2 "github.com/influxdata/kapacitor/services/ec2/client" "github.com/influxdata/kapacitor/services/hipchat" @@ -157,6 +158,11 @@ type TaskMaster struct { StateChangesOnly() bool Handler(discord.HandlerConfig, ...keyvalue.T) (alert.Handler, error) } + BigPandaService interface { + Global() bool + StateChangesOnly() bool + Handler(bigpanda.HandlerConfig, ...keyvalue.T) (alert.Handler, error) + } SlackService interface { Global() bool StateChangesOnly() bool @@ -299,6 +305,7 @@ func (tm *TaskMaster) New(id string) *TaskMaster { n.PushoverService = tm.PushoverService n.HTTPPostService = tm.HTTPPostService n.DiscordService = tm.DiscordService + n.BigPandaService = tm.BigPandaService n.SlackService = tm.SlackService n.TelegramService = tm.TelegramService n.SNMPTrapService = tm.SNMPTrapService