From 8c9ed5d67d9577e7bd2efefce447ec8f59cdbe24 Mon Sep 17 00:00:00 2001 From: alespour <42931850+alespour@users.noreply.github.com> Date: Fri, 19 Nov 2021 21:25:45 +0100 Subject: [PATCH] feat: add BigPanda handler options (#2643) * feat: add BigPanda attribute option * style: apply go fmt * docs: add BigPanda entry * chore: add BigPanda configuration * chore: extend BigPanda entry * feat: use default attributes for backward compatibility * style: apply go fmt * docs: remove duplicate entry --- CHANGELOG.md | 1 + alert.go | 2 + etc/kapacitor/kapacitor.conf | 12 ++ integrations/streamer_test.go | 59 +++++--- pipeline/alert.go | 19 ++- pipeline/tick/alert.go | 11 ++ pipeline/tick/alert_test.go | 2 + server/server_test.go | 17 ++- .../bigpanda/bigpandatest/bigpandatest.go | 90 +++++++++-- services/bigpanda/config.go | 14 +- services/bigpanda/service.go | 141 ++++++++++++++---- services/bigpanda/service_test.go | 129 +++++++++++++++- 12 files changed, 421 insertions(+), 76 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d33f5b1c3..8b3471934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - [#2575](https://github.com/influxdata/kapacitor/pull/2575): Support the "attributes" attribute in Alerta node - [#2630](https://github.com/influxdata/kapacitor/pull/2630): Upgrade to the new `google.golang` Protobuf library +- [#2643](https://github.com/influxdata/kapacitor/pull/2643): Add "host" and "attribute" options to BigPanda node and "auto-attributes" configuration option ## v1.6.2 [2021-09-24] diff --git a/alert.go b/alert.go index c8632dcd5..868b8f889 100644 --- a/alert.go +++ b/alert.go @@ -521,8 +521,10 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, d NodeDiagnostic) (a for _, s := range n.BigPandaHandlers { c := bigpanda.HandlerConfig{ AppKey: s.AppKey, + Host: s.Host, PrimaryProperty: s.PrimaryProperty, SecondaryProperty: s.SecondaryProperty, + Attributes: s.Attributes, } h, err := et.tm.BigPandaService.Handler(c, ctx...) if err != nil { diff --git a/etc/kapacitor/kapacitor.conf b/etc/kapacitor/kapacitor.conf index b1388050a..f8839787d 100644 --- a/etc/kapacitor/kapacitor.conf +++ b/etc/kapacitor/kapacitor.conf @@ -576,6 +576,18 @@ default-retention-policy = "" # Default origin. origin = "kapacitor" +[bigpanda] + # Configure BigPanda. + enabled = false + # BigPanda Alerts URL. + url = "https://api.bigpanda.io/data/v2/alerts" + # Application key + # app-key = "" + # Authentication token + # token = "" + # Default BigPanda alert additional attributes. + # auto-attributes = "tags,fields" + [servicenow] # Configure ServiceNow. enabled = false diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go index 178beea76..ec79dfc52 100644 --- a/integrations/streamer_test.go +++ b/integrations/streamer_test.go @@ -9379,18 +9379,27 @@ stream .warn(lambda: "count" > 7.0) .crit(lambda: "count" > 8.0) .bigPanda() - .AppKey('111111') + .appKey('111111') + .host('{{.Tags.host}}') + .primaryProperty('host') .bigPanda() - .AppKey('222222') + .appKey('222222') + .host('serverA-1') + .secondaryProperty('application') .bigPanda() + .attribute('x_host', '{{.Tags.host}}') + .attribute('x_duration', '{{.Duration}}') + .attribute('x_detail', '{{.Details}}') + .attribute('x_value', '{{.Fields.count}}') ` tmInit := func(tm *kapacitor.TaskMaster) { c := bigpanda.NewConfig() c.Enabled = true - c.AppKey = "XXXXXXX" + c.AppKey = "012345" c.Token = "testtoken1231234" c.URL = ts.URL + "/test/bigpanda/url" + c.AutoAttributes = "" d := diagService.NewBigPandaHandler().WithContext(keyvalue.KV("test", "111")) sl, err := bigpanda.NewService(c, d) @@ -9407,27 +9416,29 @@ stream 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", + 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", + PrimaryProperty: "host", }, }, 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", + Check: "kapacitor/cpu/serverA", + Description: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC", + AppKey: "222222", + Status: "critical", + Host: "serverA-1", + Timestamp: 31536010, + Task: "TestStream_Alert:cpu", + Details: "https://example.org/link", + SecondaryProperty: "application", }, }, bigpandatest.Request{ @@ -9435,12 +9446,18 @@ stream PostData: bigpandatest.PostData{ Check: "kapacitor/cpu/serverA", Description: "kapacitor/cpu/serverA is CRITICAL @1971-01-01 00:00:10 +0000 UTC", - AppKey: "XXXXXXX", + AppKey: "012345", Status: "critical", - Host: "serverA", + Host: "", Timestamp: 31536010, Task: "TestStream_Alert:cpu", Details: "https://example.org/link", + Attributes: map[string]string{ + "x_detail": "https://example.org/link", + "x_duration": "0s", + "x_host": "serverA", + "x_value": "10", + }, }, }, } diff --git a/pipeline/alert.go b/pipeline/alert.go index 1a71ac7fb..9f5daf04e 100644 --- a/pipeline/alert.go +++ b/pipeline/alert.go @@ -1705,15 +1705,32 @@ func (n *AlertNodeData) BigPanda() *BigPandaHandler { // tick:embedded:AlertNode.BigPanda type BigPandaHandler struct { *AlertNodeData `json:"-"` - // Application id + // Application key // If empty uses the default config AppKey string `json:"app-key"` + // Object that caused the alert + Host string `json:"host"` + // Custom primary BigPanda property PrimaryProperty string `json:"primary-property"` // Custom secondary BigPanda property SecondaryProperty string `json:"secondary-property"` + + // Additional attributes + // tick:ignore + Attributes map[string]interface{} `tick:"Attribute" json:"attributes"` +} + +// Attribute adds additional attributes to the request. +// tick:property +func (bp *BigPandaHandler) Attribute(key string, value interface{}) *BigPandaHandler { + if bp.Attributes == nil { + bp.Attributes = make(map[string]interface{}) + } + bp.Attributes[key] = value + return bp } // Send the alert to Telegram. diff --git a/pipeline/tick/alert.go b/pipeline/tick/alert.go index 670d66a9c..ffc2d11f4 100644 --- a/pipeline/tick/alert.go +++ b/pipeline/tick/alert.go @@ -180,8 +180,19 @@ func (n *AlertNode) Build(a *pipeline.AlertNode) (ast.Node, error) { for _, h := range a.BigPandaHandlers { n.Dot("bigPanda"). Dot("appKey", h.AppKey). + Dot("host", h.Host). Dot("primaryProperty", h.PrimaryProperty). Dot("secondaryProperty", h.SecondaryProperty) + + // Use stable key order + keys := make([]string, 0, len(h.Attributes)) + for k := range h.Attributes { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + n.Dot("attribute", k, h.Attributes[k]) + } } for _, h := range a.SlackHandlers { diff --git a/pipeline/tick/alert_test.go b/pipeline/tick/alert_test.go index a10790a0d..228b7cdfc 100644 --- a/pipeline/tick/alert_test.go +++ b/pipeline/tick/alert_test.go @@ -122,6 +122,7 @@ func TestAlertBigPanda(t *testing.T) { pipe, _, from := StreamFrom() handler := from.Alert().BigPanda() handler.AppKey = "A" + handler.Host = "H" handler.PrimaryProperty = "B" handler.SecondaryProperty = "C" @@ -134,6 +135,7 @@ func TestAlertBigPanda(t *testing.T) { .history(21) .bigPanda() .appKey('A') + .host('H') .primaryProperty('B') .secondaryProperty('C') ` diff --git a/server/server_test.go b/server/server_test.go index 33e19c4f9..72ec6fe3b 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -7889,6 +7889,7 @@ func TestServer_UpdateConfig(t *testing.T) { "insecure-skip-verify": false, "token": false, "app-key": "", + "auto-attributes": "tags,fields", }, Redacted: []string{ "token", @@ -7905,6 +7906,7 @@ func TestServer_UpdateConfig(t *testing.T) { "insecure-skip-verify": false, "token": false, "app-key": "", + "auto-attributes": "tags,fields", }, Redacted: []string{ "token", @@ -7914,10 +7916,11 @@ func TestServer_UpdateConfig(t *testing.T) { { updateAction: client.ConfigUpdateAction{ Set: map[string]interface{}{ - "enabled": true, - "url": "https://dev123456.bigpanda.io/data/v2/alerts", - "app-key": "appkey-123", - "token": "token-123", + "enabled": true, + "url": "https://dev123456.bigpanda.io/data/v2/alerts", + "app-key": "appkey-123", + "token": "token-123", + "auto-attributes": "", }, }, expSection: client.ConfigSection{ @@ -7931,6 +7934,7 @@ func TestServer_UpdateConfig(t *testing.T) { "url": "https://dev123456.bigpanda.io/data/v2/alerts", "token": true, "app-key": "appkey-123", + "auto-attributes": "", "insecure-skip-verify": false, }, Redacted: []string{ @@ -7946,6 +7950,7 @@ func TestServer_UpdateConfig(t *testing.T) { "state-changes-only": false, "url": "https://dev123456.bigpanda.io/data/v2/alerts", "app-key": "appkey-123", + "auto-attributes": "", "token": true, "insecure-skip-verify": false, }, @@ -9146,7 +9151,8 @@ func TestServer_ListServiceTests(t *testing.T) { Link: client.Link{Relation: client.Self, Href: "/kapacitor/v1/service-tests/bigpanda"}, Name: "bigpanda", Options: client.ServiceTestOptions{ - "app_key": "", + "app_key": "012345", + "host": "serverA", "level": "CRITICAL", "message": "test bigpanda message", "timestamp": "1970-01-01T00:00:01Z", @@ -10528,6 +10534,7 @@ func TestServer_AlertHandlers(t *testing.T) { c.BigPanda.Token = "my-token-123" c.BigPanda.AppKey = "my-app-key" c.BigPanda.URL = ts.URL + "/test/bigpanda/alert" + c.BigPanda.AutoAttributes = "" return ctxt, nil }, result: func(ctxt context.Context) error { diff --git a/services/bigpanda/bigpandatest/bigpandatest.go b/services/bigpanda/bigpandatest/bigpandatest.go index bab29af62..e90897925 100644 --- a/services/bigpanda/bigpandatest/bigpandatest.go +++ b/services/bigpanda/bigpandatest/bigpandatest.go @@ -55,15 +55,83 @@ type Request struct { // 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"` - PrimaryProperty string `json:"primary_property"` - SecondaryProperty string `json:"secondary_property"` + 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"` + PrimaryProperty string `json:"primary_property"` + SecondaryProperty string `json:"secondary_property"` + Attributes map[string]string `json:"-,omitempty"` +} + +func (pd *PostData) UnmarshalJSON(data []byte) error { + var x map[string]interface{} + if err := json.Unmarshal(data, &x); err != nil { + return nil + } + if appKey, ok := x["app_key"]; ok { + pd.AppKey = appKey.(string) + delete(x, "app_key") + } + if status, ok := x["status"]; ok { + pd.Status = status.(string) + delete(x, "status") + } + if host, ok := x["host"]; ok { + pd.Host = host.(string) + delete(x, "host") + } + if timestamp, ok := x["timestamp"]; ok { + pd.Timestamp = int64(timestamp.(float64)) + delete(x, "timestamp") + } + if check, ok := x["check"]; ok { + pd.Check = check.(string) + delete(x, "check") + } + if description, ok := x["description"]; ok { + pd.Description = description.(string) + delete(x, "description") + } + if cluster, ok := x["cluster"]; ok { + pd.Cluster = cluster.(string) + delete(x, "cluster") + } + if task, ok := x["task"]; ok { + pd.Task = task.(string) + delete(x, "task") + } + if details, ok := x["details"]; ok { + pd.Details = details.(string) + delete(x, "details") + } + if primary, ok := x["primary_property"]; ok { + pd.PrimaryProperty = primary.(string) + delete(x, "primary_property") + } + if secondary, ok := x["secondary_property"]; ok { + pd.SecondaryProperty = secondary.(string) + delete(x, "secondary_property") + } + if len(x) > 0 { + pd.Attributes = make(map[string]string, len(x)) + for k, v := range x { + switch value := v.(type) { + case string: + pd.Attributes[k] = value + default: + b, err := json.Marshal(value) + if err != nil { + return err + } + pd.Attributes[k] = string(b) + } + } + } + return nil } diff --git a/services/bigpanda/config.go b/services/bigpanda/config.go index 94e2fe307..bfa273d38 100644 --- a/services/bigpanda/config.go +++ b/services/bigpanda/config.go @@ -8,6 +8,7 @@ import ( const ( defaultBigPandaAlertApi = "https://api.bigpanda.io/data/v2/alerts" + defaultAutoAttributes = "tags,fields" ) type Config struct { @@ -17,10 +18,10 @@ type Config struct { // Whether all alerts should automatically post to BigPanda. Global bool `toml:"global" override:"global"` - //Each integration must have an App Key in BigPanda to identify it as a unique source. + // 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. + // 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. @@ -30,13 +31,17 @@ type Config struct { // Whether to skip the tls verification InsecureSkipVerify bool `toml:"insecure-skip-verify" override:"insecure-skip-verify"` - //BigPanda Alert api URL, if not specified https://api.bigpanda.io/data/v2/alerts is used + // BigPanda Alert API URL, if not specified https://api.bigpanda.io/data/v2/alerts is used. URL string `toml:"url" override:"url"` + + // Option to control tags and fields serialization into payload (for backward compatibility). + AutoAttributes string `toml:"auto-attributes" override:"auto-attributes"` } func NewConfig() Config { return Config{ - URL: defaultBigPandaAlertApi, + URL: defaultBigPandaAlertApi, + AutoAttributes: defaultAutoAttributes, } } @@ -44,7 +49,6 @@ func (c Config) Validate() error { if c.Enabled && c.URL == "" { return errors.New("must specify the BigPanda webhook URL") } - if c.Enabled && c.AppKey == "" { return errors.New("must specify BigPanda app-key") } diff --git a/services/bigpanda/service.go b/services/bigpanda/service.go index 7a6419388..2d2938196 100644 --- a/services/bigpanda/service.go +++ b/services/bigpanda/service.go @@ -11,6 +11,7 @@ import ( "net/url" "strings" "sync/atomic" + text "text/template" "time" "github.com/influxdata/kapacitor/alert" @@ -26,6 +27,7 @@ const ( type Diagnostic interface { WithContext(ctx ...keyvalue.T) Diagnostic + TemplateError(err error, kv keyvalue.T) Error(msg string, err error) } @@ -84,13 +86,14 @@ type testOptions struct { Level alert.Level `json:"level"` Data alert.EventData `json:"event_data"` Timestamp time.Time `json:"timestamp"` + Host string `json:"host"` PrimaryProperty string `json:"primary_property"` SecondaryProperty string `json:"secondary_property"` } func (s *Service) TestOptions() interface{} { return &testOptions{ - AppKey: s.config().AppKey, + AppKey: "012345", Message: "test bigpanda message", Level: alert.Critical, Data: alert.EventData{ @@ -100,6 +103,7 @@ func (s *Service) TestOptions() interface{} { Result: models.Result{}, }, Timestamp: time.Now(), + Host: "serverA", } } @@ -110,14 +114,16 @@ func (s *Service) Test(options interface{}) error { } hc := &HandlerConfig{ AppKey: o.AppKey, + Host: o.Host, PrimaryProperty: o.PrimaryProperty, SecondaryProperty: o.SecondaryProperty, } - return s.Alert("", o.Message, "", o.Level, o.Timestamp, o.Data, hc) + attrs := make(map[string]string, 0) + return s.Alert("", o.Message, "", o.Level, o.Timestamp, o.Data, hc, attrs) } -func (s *Service) Alert(id string, message string, details string, level alert.Level, timestamp time.Time, data alert.EventData, hc *HandlerConfig) error { - req, err := s.preparePost(id, message, details, level, timestamp, data, hc) +func (s *Service) Alert(id string, message string, details string, level alert.Level, timestamp time.Time, data alert.EventData, hc *HandlerConfig, attrs map[string]string) error { + req, err := s.preparePost(id, message, details, level, timestamp, data, hc, attrs) if err != nil { return err @@ -173,12 +179,22 @@ curl -X POST -H "Content-Type: application/json" \ "primary_property": "application", "secondary_property": "host" */ -func (s *Service) preparePost(id string, message string, details string, level alert.Level, timestamp time.Time, data alert.EventData, hc *HandlerConfig) (*http.Request, error) { +func (s *Service) preparePost(id string, message string, details string, level alert.Level, timestamp time.Time, data alert.EventData, hc *HandlerConfig, attrs map[string]string) (*http.Request, error) { c := s.config() if !c.Enabled { return nil, errors.New("service is not enabled") } + bpUrl := hc.URL + if bpUrl == "" { + bpUrl = c.URL + } + + alertUrl, err := url.Parse(bpUrl) + if err != nil { + return nil, err + } + var status string switch level { case alert.OK: @@ -199,7 +215,7 @@ func (s *Service) preparePost(id string, message string, details string, level a bpData["description"] = message } - //ignore default details containing full json event + // ignore default details containing full json event if details != "" { unescapeString := html.UnescapeString(details) if !strings.HasPrefix(unescapeString, "{") { @@ -223,29 +239,48 @@ func (s *Service) preparePost(id string, message string, details string, level a bpData["secondary_property"] = hc.SecondaryProperty } + // app key if hc.AppKey != "" { bpData["app_key"] = hc.AppKey } else { bpData["app_key"] = c.AppKey } - for k, v := range data.Tags { - bpData[k] = v + // host is included in additional attributes + + // auto option evaluation + auto := func(key string) bool { + return strings.Contains(strings.ToLower(c.AutoAttributes), key) } - for k, v := range data.Fields { - switch value := v.(type) { - case string: - bpData[k] = value - default: - b, err := json.Marshal(value) - if err != nil { - return nil, err + // add tags as additional attributes + if auto("tags") { + for k, v := range data.Tags { + bpData[k] = v + } + } + + // fields tags as additional attributes + if auto("fields") { + for k, v := range data.Fields { + switch value := v.(type) { + case string: + bpData[k] = value + default: + b, err := json.Marshal(value) + if err != nil { + return nil, err + } + bpData[k] = string(b) } - bpData[k] = string(b) } } + // add additional attributes (includes "host" attribute) + for k, v := range attrs { + bpData[k] = v + } + var postTemp bytes.Buffer enc := json.NewEncoder(&postTemp) if err := enc.Encode(bpData); err != nil { @@ -256,22 +291,13 @@ func (s *Service) preparePost(id string, message string, details string, level a return nil, err } - bpUrl := hc.URL - if bpUrl == "" { - bpUrl = c.URL - } - - alertUrl, err := url.Parse(bpUrl) - 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 } @@ -284,10 +310,17 @@ type HandlerConfig struct { // If empty uses the service URL from the configuration. URL string `mapstructure:"url"` + // object that caused the alert + Host string `mapstructure:"host"` + // custom primary BigPanda property PrimaryProperty string `mapstructure:"primary-property"` + // custom secondary BigPanda property SecondaryProperty string `mapstructure:"secondary-property"` + + // additional attributes + Attributes map[string]interface{} `mapstructure:"attributes"` } type handler struct { @@ -305,6 +338,13 @@ func (s *Service) Handler(c HandlerConfig, ctx ...keyvalue.T) (alert.Handler, er } func (h *handler) Handle(event alert.Event) { + td := event.TemplateData() + attrs, err := h.renderAttributes(&td) + if err != nil { + // error already reported + return + } + if err := h.s.Alert( event.State.ID, event.State.Message, @@ -313,7 +353,54 @@ func (h *handler) Handle(event alert.Event) { event.State.Time, event.Data, &h.c, + attrs, ); err != nil { h.diag.Error("failed to send event to BigPanda", err) } } + +func (h *handler) renderAttributes(td *alert.TemplateData) (map[string]string, error) { + var buf bytes.Buffer + render := func(name, template string) (string, error) { + if template != "" { + buf.Reset() + templateImpl, err := text.New(name).Parse(template) + if err != nil { + return "", err + } + templateImpl.Execute(&buf, td) + if err != nil { + return "", err + } + return buf.String(), nil + } + return "", nil + } + rendered := make(map[string]string) + rHost, err := render("host", h.c.Host) + if err != nil { + h.diag.TemplateError(err, keyvalue.KV("host", h.c.Host)) + return nil, err + } + rendered["host"] = rHost + for k, v := range h.c.Attributes { + switch value := v.(type) { + case string: + rValue, err := render(k, value) + if err != nil { + h.diag.TemplateError(err, keyvalue.KV(k, value)) + return nil, err + } + rendered[k] = rValue + default: + b, err := json.Marshal(value) + if err != nil { + h.diag.WithContext(keyvalue.KV("key", k)).Error("failed to encode tag value", err) + return nil, err + } + rendered[k] = string(b) + } + } + + return rendered, nil +} diff --git a/services/bigpanda/service_test.go b/services/bigpanda/service_test.go index b7a888729..03c343afa 100644 --- a/services/bigpanda/service_test.go +++ b/services/bigpanda/service_test.go @@ -2,7 +2,6 @@ package bigpanda import ( "bytes" - "fmt" "testing" "time" @@ -10,45 +9,74 @@ import ( "github.com/influxdata/kapacitor/models" ) +func (c Config) enable() Config { + c.Enabled = true + return c +} + +func (c Config) appKey(appKey string) Config { + c.AppKey = appKey + return c +} + +func (c Config) autoAttrs(auto string) Config { + c.AutoAttributes = auto + return c +} + func TestService_SerializeEventData(t *testing.T) { - config := Config{Enabled: true, AppKey: "key"} + config := Config{Enabled: true, AppKey: "key", AutoAttributes: "tags,fields"} s, err := NewService(config, nil) if err != nil { t.Fatal(err) } testCases := []struct { + name string + tags map[string]string fields map[string]interface{} + attrs map[string]string expBody string }{ { + name: "int field", fields: map[string]interface{}{"primitive_type": 10}, expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"primitive_type\":\"10\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", }, { + name: "string field", fields: map[string]interface{}{"primitive_type": "string"}, expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"primitive_type\":\"string\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", }, { + name: "boolean field", fields: map[string]interface{}{"primitive_type": true}, expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"primitive_type\":\"true\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", }, { + name: "float field", fields: map[string]interface{}{"primitive_type": 123.45}, expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"primitive_type\":\"123.45\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", }, { + name: "html field", fields: map[string]interface{}{"escape": "\n"}, expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"escape\":\"\\n\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", }, { + name: "array field", fields: map[string]interface{}{"array": []interface{}{10, true, "string value"}}, expBody: "{\"app_key\":\"key\",\"array\":\"[10,true,\\\"string value\\\"]\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", }, + { + name: "tags", + tags: map[string]string{"host": "localhost", "link": "http://localhost/bp"}, + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"host\":\"localhost\",\"link\":\"http://localhost/bp\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, } - for i, tc := range testCases { - t.Run(fmt.Sprint(i), func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { r, err := s.preparePost( "id", "message", @@ -57,11 +85,100 @@ func TestService_SerializeEventData(t *testing.T) { time.Date(1971, 1, 1, 0, 0, 38, 0, time.UTC), alert.EventData{ Name: "test", - Tags: make(map[string]string), + Tags: tc.tags, Fields: tc.fields, Result: models.Result{}, }, - &HandlerConfig{}) + &HandlerConfig{}, + tc.attrs) + + if err != nil { + t.Fatal(err) + } + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(r.Body) + if err != nil { + t.Fatal(err) + } + newStr := buf.String() + + if tc.expBody != newStr { + t.Fatalf("unexpected content: got '%s' exp '%s'", newStr, tc.expBody) + } + }) + } +} + +func TestService_Payload(t *testing.T) { + tags := map[string]string{"host": "localhost", "link": "http://localhost/bp"} + fields := map[string]interface{}{"count": 10, "load": 0.5} + + testCases := []struct { + name string + config Config + attrs map[string]string + expBody string + }{ + { + name: "default config", + config: NewConfig().enable(), + expBody: "{\"app_key\":\"\",\"check\":\"id\",\"count\":\"10\",\"description\":\"message\",\"details\":\"details\",\"host\":\"localhost\",\"link\":\"http://localhost/bp\",\"load\":\"0.5\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + { + name: "no auto no attributes", + config: NewConfig().enable().appKey("key").autoAttrs(""), + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + { + name: "no auto with attributes", + config: NewConfig().enable().appKey("key").autoAttrs(""), + attrs: map[string]string{"host": "example.com", "link": "http://example.com/bp"}, + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"host\":\"example.com\",\"link\":\"http://example.com/bp\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + { + name: "auto tags", + config: NewConfig().enable().appKey("key").autoAttrs("tags"), + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"description\":\"message\",\"details\":\"details\",\"host\":\"localhost\",\"link\":\"http://localhost/bp\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + { + name: "auto fields", + config: NewConfig().enable().appKey("key").autoAttrs("fields"), + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"count\":\"10\",\"description\":\"message\",\"details\":\"details\",\"load\":\"0.5\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + { + name: "auto tags and fields explicit", + config: NewConfig().enable().appKey("key").autoAttrs("tags,fields"), + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"count\":\"10\",\"description\":\"message\",\"details\":\"details\",\"host\":\"localhost\",\"link\":\"http://localhost/bp\",\"load\":\"0.5\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + { + name: "auto tags and fields with attributes override", + config: NewConfig().enable().appKey("key").autoAttrs("tags,fields"), + attrs: map[string]string{"host": "example.com", "link": "http://example.com/bp"}, + expBody: "{\"app_key\":\"key\",\"check\":\"id\",\"count\":\"10\",\"description\":\"message\",\"details\":\"details\",\"host\":\"example.com\",\"link\":\"http://example.com/bp\",\"load\":\"0.5\",\"status\":\"ok\",\"task\":\":test\",\"timestamp\":31536038}", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s, err := NewService(tc.config, nil) + if err != nil { + t.Fatal(err) + } + r, err := s.preparePost( + "id", + "message", + "details", + alert.OK, + time.Date(1971, 1, 1, 0, 0, 38, 0, time.UTC), + alert.EventData{ + Name: "test", + Tags: tags, + Fields: fields, + Result: models.Result{}, + }, + &HandlerConfig{}, + tc.attrs) if err != nil { t.Fatal(err)