From 72c3642c405c59458647d46f43f803038c1a080d Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Wed, 11 Nov 2020 19:53:36 +0100 Subject: [PATCH 1/2] fix(servicenow): switch to event API --- etc/kapacitor/kapacitor.conf | 4 +- integrations/streamer_test.go | 42 ++++++++++-------- pipeline/alert.go | 4 +- server/server_test.go | 30 +++++++------ services/servicenow/config.go | 8 ++-- services/servicenow/service.go | 43 +++++++++++-------- .../servicenowtest/servicenowtest.go | 7 ++- 7 files changed, 79 insertions(+), 59 deletions(-) diff --git a/etc/kapacitor/kapacitor.conf b/etc/kapacitor/kapacitor.conf index cae3c752e..d4ba480e9 100644 --- a/etc/kapacitor/kapacitor.conf +++ b/etc/kapacitor/kapacitor.conf @@ -512,8 +512,8 @@ default-retention-policy = "" [servicenow] # Configure ServiceNow. enabled = false - # The ServiceNow URL for target table. Replace instance with actual hostname. - url = "https://instance.service-now.com/api/now/v1/table/em_alert" + # The ServiceNow events web service URL. Replace instance with actual hostname. + url = "https://instance.service-now.com/api/global/em/jsonv2" # Default source identification. source = "Kapacitor" # Username for HTTP BASIC authentication diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go index eb25a3d93..fb3c7e424 100644 --- a/integrations/streamer_test.go +++ b/integrations/streamer_test.go @@ -10296,7 +10296,7 @@ stream c := servicenow.NewConfig() c.Enabled = true - c.URL = ts.URL + c.URL = ts.URL + "/api/global/em/jsonv2" c.Source = "Kapacitor" sl := servicenow.NewService(c, diagService.NewServiceNowHandler()) tm.ServiceNowService = sl @@ -10306,25 +10306,33 @@ stream exp := []interface{}{ servicenowtest.Request{ - URL: "/", - Alert: servicenow.Alert{ - Source: "Kapacitor", - Node: "serverA", - Type: "CPU", // literal since there is no tag for this in the testdata - Resource: "CPU-Total", // literal since there is no tag for this in the testdata - MetricName: "idle", - MessageKey: "Alert: kapacitor/cpu/serverA", - Severity: "1", - Description: "kapacitor/cpu/serverA is CRITICAL", + URL: "/api/global/em/jsonv2", + Alerts: servicenow.Events{ + Records: []servicenow.Event{ + { + Source: "Kapacitor", + Node: "serverA", + Type: "CPU", // literal since there is no tag for this in the testdata + Resource: "CPU-Total", // literal since there is no tag for this in the testdata + MetricName: "idle", + MessageKey: "Alert: kapacitor/cpu/serverA", + Severity: "1", + Description: "kapacitor/cpu/serverA is CRITICAL", + }, + }, }, }, servicenowtest.Request{ - URL: "/", - Alert: servicenow.Alert{ - Source: "Kapacitor", - MessageKey: "kapacitor/cpu/serverA", - Severity: "1", - Description: "kapacitor/cpu/serverA is CRITICAL", + URL: "/api/global/em/jsonv2", + Alerts: servicenow.Events{ + Records: []servicenow.Event{ + { + Source: "Kapacitor", + MessageKey: "kapacitor/cpu/serverA", + Severity: "1", + Description: "kapacitor/cpu/serverA is CRITICAL", + }, + }, }, }, } diff --git a/pipeline/alert.go b/pipeline/alert.go index ce50dbeb8..251d159bd 100644 --- a/pipeline/alert.go +++ b/pipeline/alert.go @@ -2130,7 +2130,7 @@ type TeamsHandler struct { // Example: // [serviceNow] // enabled = true -// url = "https://instance.service-now.com/api/now/v1/table/em_alert" +// url = "https://instance.service-now.com/api/global/em/jsonv2" // // In order to not post a message every alert interval // use AlertNode.StateChangesOnly so that only events @@ -2148,7 +2148,7 @@ type TeamsHandler struct { // Example: // [serviceNow] // enabled = true -// url = "https://instance.service-now.com/api/now/v1/table/em_alert" +// url = "https://instance.service-now.com/api/global/em/jsonv2" // global = true // state-changes-only = true // diff --git a/server/server_test.go b/server/server_test.go index d6fa4cf93..f008dd70e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -7737,7 +7737,7 @@ func TestServer_UpdateConfig(t *testing.T) { { section: "servicenow", setDefaults: func(c *server.Config) { - c.ServiceNow.URL = "https://instance.service-now.com/api/now/v1/table/em_alert" + c.ServiceNow.URL = "https://instance.service-now.com/api/global/em/jsonv2" c.ServiceNow.Source = "Kapacitor" c.ServiceNow.Username = "" c.ServiceNow.Password = "" @@ -7750,7 +7750,7 @@ func TestServer_UpdateConfig(t *testing.T) { "enabled": false, "global": false, "state-changes-only": false, - "url": "https://instance.service-now.com/api/now/v1/table/em_alert", + "url": "https://instance.service-now.com/api/global/em/jsonv2", "source": "Kapacitor", "username": "", "password": false, @@ -7766,7 +7766,7 @@ func TestServer_UpdateConfig(t *testing.T) { "enabled": false, "global": false, "state-changes-only": false, - "url": "https://instance.service-now.com/api/now/v1/table/em_alert", + "url": "https://instance.service-now.com/api/global/em/jsonv2", "source": "Kapacitor", "username": "", "password": false, @@ -7780,7 +7780,7 @@ func TestServer_UpdateConfig(t *testing.T) { updateAction: client.ConfigUpdateAction{ Set: map[string]interface{}{ "enabled": true, - "url": "https://dev12345.service-now.com/api/now/v1/table/em_alert", + "url": "https://dev12345.service-now.com/api/global/em/jsonv2", "username": "dev", "password": "12345", }, @@ -7793,7 +7793,7 @@ func TestServer_UpdateConfig(t *testing.T) { "enabled": true, "global": false, "state-changes-only": false, - "url": "https://dev12345.service-now.com/api/now/v1/table/em_alert", + "url": "https://dev12345.service-now.com/api/global/em/jsonv2", "source": "Kapacitor", "username": "dev", "password": true, @@ -7809,7 +7809,7 @@ func TestServer_UpdateConfig(t *testing.T) { "enabled": true, "global": false, "state-changes-only": false, - "url": "https://dev12345.service-now.com/api/now/v1/table/em_alert", + "url": "https://dev12345.service-now.com/api/global/em/jsonv2", "source": "Kapacitor", "username": "dev", "password": true, @@ -10449,7 +10449,7 @@ func TestServer_AlertHandlers(t *testing.T) { ctxt := context.WithValue(context.Background(), "server", ts) c.ServiceNow.Enabled = true - c.ServiceNow.URL = ts.URL + c.ServiceNow.URL = ts.URL + "/api/global/em/jsonv2" c.ServiceNow.Source = "Kapacitor" return ctxt, nil }, @@ -10457,12 +10457,16 @@ func TestServer_AlertHandlers(t *testing.T) { ts := ctxt.Value("server").(*servicenowtest.Server) ts.Close() exp := []servicenowtest.Request{{ - URL: "/", - Alert: servicenow.Alert{ - Source: "Kapacitor", - Severity: "1", - Description: "message", - MessageKey: "id", + URL: "/api/global/em/jsonv2", + Alerts: servicenow.Events{ + Records: []servicenow.Event{ + { + Source: "Kapacitor", + Severity: "1", + Description: "message", + MessageKey: "id", + }, + }, }, }} got := ts.Requests() diff --git a/services/servicenow/config.go b/services/servicenow/config.go index 50161bb72..56ffed867 100644 --- a/services/servicenow/config.go +++ b/services/servicenow/config.go @@ -9,7 +9,7 @@ import ( type Config struct { // Whether ServiceNow integration is enabled. Enabled bool `toml:"enabled" override:"enabled"` - // ServiceNow alerts API URL. + // ServiceNow events API URL. URL string `toml:"url" override:"url"` // Event source. Source string `toml:"source" override:"source"` @@ -25,12 +25,14 @@ type Config struct { } func NewConfig() Config { - return Config{} + return Config{ + URL: "https://instance.service-now.com/api/global/em/jsonv2", // dummy default + } } func (c Config) Validate() error { if c.Enabled && c.URL == "" { - return errors.New("must specify Alerts URL") + return errors.New("must specify events URL") } if _, err := url.Parse(c.URL); err != nil { return errors.Wrapf(err, "invalid url %q", c.URL) diff --git a/services/servicenow/service.go b/services/servicenow/service.go index 76cd4c11e..3691eff85 100644 --- a/services/servicenow/service.go +++ b/services/servicenow/service.go @@ -128,7 +128,7 @@ func (s *Service) Alert(url, alertID string, message string, level alert.Level, } defer resp.Body.Close() - if resp.StatusCode != http.StatusCreated { + if resp.StatusCode != http.StatusOK { body, err := ioutil.ReadAll(resp.Body) if err != nil { return err @@ -148,11 +148,8 @@ func (s *Service) Alert(url, alertID string, message string, level alert.Level, return nil } -// Alert is a structure representing ServiceNow alert. It can also represent an Event. -// See: -// https://docs.servicenow.com/bundle/paris-it-operations-management/page/product/event-management/task/t_EMViewAlert.html -// https://docs.servicenow.com/bundle/paris-it-operations-management/page/product/event-management/task/t_EMManageEvent.html -type Alert struct { +// Event is a structure representing ServiceNow event. It can also represent an alert. +type Event struct { Source string `json:"source"` Node string `json:"node"` Type string `json:"type"` @@ -163,6 +160,12 @@ type Alert struct { Description string `json:"description"` } +// ServiceNow provides web service API which is recommended for publishing events. Multiple events can be published in a single call. +// https://docs.servicenow.com/bundle/paris-it-operations-management/page/product/event-management/task/send-events-via-web-service.html +type Events struct { + Records []Event `json:"records"` +} + func (s *Service) preparePost(url, alertID, message string, level alert.Level, data *alert.EventData, hc *HandlerConfig) (string, io.Reader, error) { c := s.config() if !c.Enabled { @@ -248,18 +251,22 @@ func (s *Service) preparePost(url, alertID, message string, level alert.Level, d severity = 1 } - instance := &Alert{ - Source: cutoff(source, usualCutoff), - Node: cutoff(node, usualCutoff), - Type: cutoff(metricType, usualCutoff), - Resource: cutoff(resource, usualCutoff), - MetricName: cutoff(metricName, usualCutoff), - MessageKey: cutoff(messageKey, 1024), - Severity: strconv.Itoa(severity), - Description: cutoff(message, 4000), + payload := &Events{ + Records: []Event{ + { + Source: cutoff(source, usualCutoff), + Node: cutoff(node, usualCutoff), + Type: cutoff(metricType, usualCutoff), + Resource: cutoff(resource, usualCutoff), + MetricName: cutoff(metricName, usualCutoff), + MessageKey: cutoff(messageKey, 1024), + Severity: strconv.Itoa(severity), + Description: cutoff(message, 4000), + }, + }, } - postBytes, err := json.Marshal(instance) + postBytes, err := json.Marshal(payload) if err != nil { return "", nil, errors.Wrap(err, "error marshaling alert struct") } @@ -276,8 +283,8 @@ type dataInfo struct { } type HandlerConfig struct { - // Alerts API URL used to post messages. - // If empty uses the alerts API URL from the configuration. + // web service URL used to post messages. + // If empty uses the service URL from the configuration. URL string `mapstructure:"url"` // Username for BASIC authentication. diff --git a/services/servicenow/servicenowtest/servicenowtest.go b/services/servicenow/servicenowtest/servicenowtest.go index 87de0f463..e5001e651 100644 --- a/services/servicenow/servicenowtest/servicenowtest.go +++ b/services/servicenow/servicenowtest/servicenowtest.go @@ -24,11 +24,10 @@ func NewServer() *Server { URL: r.URL.String(), } dec := json.NewDecoder(r.Body) - dec.Decode(&pr.Alert) + dec.Decode(&pr.Alerts) s.mu.Lock() s.requests = append(s.requests, pr) s.mu.Unlock() - w.WriteHeader(http.StatusCreated) })) s.ts = ts s.URL = ts.URL @@ -50,6 +49,6 @@ func (s *Server) Close() { } type Request struct { - URL string - Alert servicenow.Alert + URL string + Alerts servicenow.Events } From 957751dfbdec566a228ae80ff041931d2d2f5763 Mon Sep 17 00:00:00 2001 From: Ales Pour Date: Thu, 12 Nov 2020 15:03:01 +0100 Subject: [PATCH 2/2] feat(servicenow): support additional_info element --- alert.go | 15 ++++---- integrations/streamer_test.go | 19 +++++----- pipeline/alert.go | 16 ++++++++- pipeline/tick/alert.go | 10 ++++++ services/servicenow/service.go | 65 +++++++++++++++++++++++----------- 5 files changed, 89 insertions(+), 36 deletions(-) diff --git a/alert.go b/alert.go index 8b60b4f12..d635304de 100644 --- a/alert.go +++ b/alert.go @@ -521,13 +521,14 @@ func newAlertNode(et *ExecutingTask, n *pipeline.AlertNode, d NodeDiagnostic) (a for _, s := range n.ServiceNowHandlers { c := servicenow.HandlerConfig{ - URL: s.URL, - Source: s.Source, - Node: s.Node, - Type: s.Type, - Resource: s.Resource, - MetricName: s.MetricName, - MessageKey: s.MessageKey, + URL: s.URL, + Source: s.Source, + Node: s.Node, + Type: s.Type, + Resource: s.Resource, + MetricName: s.MetricName, + MessageKey: s.MessageKey, + AdditionalInfo: s.AdditionalInfoMap, } h := et.tm.ServiceNowService.Handler(c, ctx...) an.handlers = append(an.handlers, h) diff --git a/integrations/streamer_test.go b/integrations/streamer_test.go index fb3c7e424..1df4a4652 100644 --- a/integrations/streamer_test.go +++ b/integrations/streamer_test.go @@ -10291,6 +10291,8 @@ stream .resource('CPU-Total') .metricName('{{ index .Tags "type" }}') .messageKey('Alert: {{ .ID }}') + .additionalInfo('link', 'http://push/alert?id={{ .ID }}') + .additionalInfo('ticks', 666) ` tmInit := func(tm *kapacitor.TaskMaster) { @@ -10310,14 +10312,15 @@ stream Alerts: servicenow.Events{ Records: []servicenow.Event{ { - Source: "Kapacitor", - Node: "serverA", - Type: "CPU", // literal since there is no tag for this in the testdata - Resource: "CPU-Total", // literal since there is no tag for this in the testdata - MetricName: "idle", - MessageKey: "Alert: kapacitor/cpu/serverA", - Severity: "1", - Description: "kapacitor/cpu/serverA is CRITICAL", + Source: "Kapacitor", + Node: "serverA", + Type: "CPU", // literal since there is no tag for this in the testdata + Resource: "CPU-Total", // literal since there is no tag for this in the testdata + MetricName: "idle", + MessageKey: "Alert: kapacitor/cpu/serverA", + Severity: "1", + Description: "kapacitor/cpu/serverA is CRITICAL", + AdditionalInfo: "{\"link\":\"http://push/alert?id=kapacitor/cpu/serverA\",\"ticks\":\"666\"}", }, }, }, diff --git a/pipeline/alert.go b/pipeline/alert.go index 251d159bd..6174f6fba 100644 --- a/pipeline/alert.go +++ b/pipeline/alert.go @@ -2199,5 +2199,19 @@ type ServiceNowHandler struct { MetricName string `json:"metric_name"` // Message key. - MessageKey string `json:"messageKey"` + MessageKey string `json:"message_key"` + + // Addition info. + // tick:ignore + AdditionalInfoMap map[string]interface{} `tick:"AdditionalInfo" json:"additional_info"` +} + +// AdditionalInfo adds key values pairs to the request. +// tick:property +func (s *ServiceNowHandler) AdditionalInfo(key string, value interface{}) *ServiceNowHandler { + if s.AdditionalInfoMap == nil { + s.AdditionalInfoMap = make(map[string]interface{}) + } + s.AdditionalInfoMap[key] = value + return s } diff --git a/pipeline/tick/alert.go b/pipeline/tick/alert.go index e464b5a10..57f459492 100644 --- a/pipeline/tick/alert.go +++ b/pipeline/tick/alert.go @@ -165,6 +165,16 @@ func (n *AlertNode) Build(a *pipeline.AlertNode) (ast.Node, error) { Dot("resource", h.Resource). Dot("metricName", h.MetricName). Dot("messageKey", h.MessageKey) + + // Use stable key order + keys := make([]string, 0, len(h.AdditionalInfoMap)) + for k := range h.AdditionalInfoMap { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + n.Dot("additionalInfo", k, h.AdditionalInfoMap[k]) + } } for _, h := range a.SlackHandlers { diff --git a/services/servicenow/service.go b/services/servicenow/service.go index 3691eff85..eabb9e599 100644 --- a/services/servicenow/service.go +++ b/services/servicenow/service.go @@ -150,14 +150,15 @@ func (s *Service) Alert(url, alertID string, message string, level alert.Level, // Event is a structure representing ServiceNow event. It can also represent an alert. type Event struct { - Source string `json:"source"` - Node string `json:"node"` - Type string `json:"type"` - Resource string `json:"resource"` - MetricName string `json:"metric_name"` - MessageKey string `json:"message_key"` - Severity string `json:"severity"` - Description string `json:"description"` + Source string `json:"source"` + Node string `json:"node,omitempty"` + Type string `json:"type,omitempty"` + Resource string `json:"resource,omitempty"` + MetricName string `json:"metric_name,omitempty"` + MessageKey string `json:"message_key,omitempty"` + Severity string `json:"severity,omitempty"` + Description string `json:"description,omitempty"` + AdditionalInfo string `json:"additional_info,omitempty"` } // ServiceNow provides web service API which is recommended for publishing events. Multiple events can be published in a single call. @@ -234,6 +235,26 @@ func (s *Service) preparePost(url, alertID, message string, level alert.Level, d if err != nil { return "", nil, err } + aiBytes := []byte("") + if len(hc.AdditionalInfo) > 0 { + additionalInfo := make(map[string]string, 0) + for key, value := range hc.AdditionalInfo { + switch v := value.(type) { + case string: + rv, err := render("additional_info."+key, v) + if err != nil { + return "", nil, err + } + additionalInfo[key] = rv + default: + additionalInfo[key] = fmt.Sprintf("%v", v) + } + } + aiBytes, err = json.Marshal(additionalInfo) + if err != nil { + return "", nil, errors.Wrap(err, "error marshaling additional_info map") + } + } if messageKey == "" { // fallback to alert ID if empty messageKey = alertID } @@ -254,21 +275,22 @@ func (s *Service) preparePost(url, alertID, message string, level alert.Level, d payload := &Events{ Records: []Event{ { - Source: cutoff(source, usualCutoff), - Node: cutoff(node, usualCutoff), - Type: cutoff(metricType, usualCutoff), - Resource: cutoff(resource, usualCutoff), - MetricName: cutoff(metricName, usualCutoff), - MessageKey: cutoff(messageKey, 1024), - Severity: strconv.Itoa(severity), - Description: cutoff(message, 4000), + Source: cutoff(source, usualCutoff), + Node: cutoff(node, usualCutoff), + Type: cutoff(metricType, usualCutoff), + Resource: cutoff(resource, usualCutoff), + MetricName: cutoff(metricName, usualCutoff), + MessageKey: cutoff(messageKey, 1024), + Severity: strconv.Itoa(severity), + Description: cutoff(message, 4000), + AdditionalInfo: string(aiBytes), }, }, } postBytes, err := json.Marshal(payload) if err != nil { - return "", nil, errors.Wrap(err, "error marshaling alert struct") + return "", nil, errors.Wrap(err, "error marshaling event struct") } return u.String(), bytes.NewBuffer(postBytes), nil @@ -308,11 +330,14 @@ type HandlerConfig struct { // Node resource relevant to the event. Resource string `json:"resource"` - // Metric name for which event has been created.. + // Metric name for which event has been created. MetricName string `json:"metric_name"` - // Message key that identifies related event.. - MessageKey string `json:"messageKey"` + // Message key that identifies related event. + MessageKey string `json:"message_key"` + + // Addition info is a map of key value pairs. + AdditionalInfo map[string]interface{} `mapstructure:"additional_info"` } type handler struct {