From 93bf2becce400fa088863682ff0ba37367f405bd Mon Sep 17 00:00:00 2001 From: Joshua Powers Date: Tue, 8 Aug 2023 14:19:31 -0600 Subject: [PATCH] feat(serializers.nowmetric): Add option for JSONv2 format (#13722) --- plugins/serializers/nowmetric/README.md | 67 ++++++++++++++++--- plugins/serializers/nowmetric/nowmetric.go | 65 +++++++++--------- .../serializers/nowmetric/nowmetric_test.go | 49 ++++++++++++++ 3 files changed, 142 insertions(+), 39 deletions(-) diff --git a/plugins/serializers/nowmetric/README.md b/plugins/serializers/nowmetric/README.md index f782f25da..b5e5621cd 100644 --- a/plugins/serializers/nowmetric/README.md +++ b/plugins/serializers/nowmetric/README.md @@ -1,16 +1,22 @@ # ServiceNow Metrics serializer -The ServiceNow Metrics serializer outputs metrics in the [ServiceNow Operational Intelligence format][ServiceNow-format]. +The ServiceNow Metrics serializer outputs metrics in the +[ServiceNow Operational Intelligence format][ServiceNow-format] or optionally +with the [ServiceNow JSONv2 format][ServiceNow-jsonv2] -It can be used to write to a file using the file output, or for sending metrics to a MID Server with Enable REST endpoint activated using the standard telegraf HTTP output. -If you're using the HTTP output, this serializer knows how to batch the metrics so you don't end up with an HTTP POST per metric. +It can be used to write to a file using the file output, or for sending metrics +to a MID Server with Enable REST endpoint activated using the standard telegraf +HTTP output. If you are using the HTTP output, this serializer knows how to +batch the metrics so you do not end up with an HTTP POST per metric. [ServiceNow-format]: https://docs.servicenow.com/bundle/london-it-operations-management/page/product/event-management/reference/mid-POST-metrics.html +[ServiceNow-jsonv2]: https://docs.servicenow.com/bundle/tokyo-application-development/page/integrate/inbound-other-web-services/concept/c_JSONv2WebService.html -An example event looks like: +An example Operational Intelligence format event looks like: -```javascript -[{ +```json +[ + { "metric_type": "Disk C: % Free Space", "resource": "C:\\", "node": "lnux100", @@ -19,8 +25,29 @@ An example event looks like: "ci2metric_id": { "node": "lnux100" }, - "source": “Telegraf” -}] + "source": "Telegraf" + } +] +``` + +An example of the JSONv2 format even looks like: + +```json +{ + "records": [ + { + "metric_type": "Disk C: % Free Space", + "resource": "C:\\", + "node": "lnux100", + "value": 50, + "timestamp": 1473183012000, + "ci2metric_id": { + "node": "lnux100" + }, + "source": "Telegraf" + } + ] +} ``` ## Using with the HTTP output @@ -55,6 +82,13 @@ To send this data to a ServiceNow MID Server with Web Server extension activated ## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_OUTPUT.md data_format = "nowmetric" + ## Format Type + ## By default, the serializer returns an array of metrics matching the + ## Now Metric Operational Intelligence format or with the option set to 'oi'. + ## Optionally, if set to 'jsonv2' the output format will involve the newer + ## JSON object based format. + # nowmetric_format = "oi" + ## Additional HTTP headers [outputs.http.headers] # # Should be set manually to "application/json" for json data_format @@ -66,6 +100,23 @@ Starting with the [London release](https://docs.servicenow.com/bundle/london-it- ), you also need to explicitly create event rule to allow binding of metric events to host CIs. +## Metric Format + +The following describes the two options of the `nowmetric_format` option: + +The Operational Intelligence format is used along with the +`/api/mid/sa/metrics` API endpoint. The payload is requires a JSON array full +of metrics. This is the default settings and used when set to `oi`. See the +[ServiceNow KB0853084][KB0853084] for more details on this format. + +Another option is the use of the [JSONv2 web service][jsonv2]. This service +requires a different format that is [JSON object based][jsonv2_format]. This +option is used when set to `jsonv2`. + +[KB0853084]: https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0853084 +[jsonv2]: https://docs.servicenow.com/bundle/tokyo-application-development/page/integrate/inbound-other-web-services/concept/c_JSONv2WebService.html +[jsonv2_format]: https://docs.servicenow.com/bundle/tokyo-application-development/page/integrate/inbound-other-web-services/concept/c_JSONObjectFormat.html + ## Using with the File output You can use the file output to output the payload in a file. diff --git a/plugins/serializers/nowmetric/nowmetric.go b/plugins/serializers/nowmetric/nowmetric.go index 119edb2de..0c5857c88 100644 --- a/plugins/serializers/nowmetric/nowmetric.go +++ b/plugins/serializers/nowmetric/nowmetric.go @@ -1,7 +1,6 @@ package nowmetric import ( - "bytes" "encoding/json" "fmt" "time" @@ -10,20 +9,9 @@ import ( "github.com/influxdata/telegraf/plugins/serializers" ) -type Serializer struct{} - -/* -Example for the JSON generated and pushed to the MID -{ - "metric_type":"cpu_usage_system", - "resource":"", - "node":"ASGARD", - "value": 0.89, - "timestamp":1487365430, - "ci2metric_id":{"node":"ASGARD"}, - "source":"Telegraf" +type Serializer struct { + Format string `toml:"nowmetric_format"` } -*/ type OIMetric struct { Metric string `json:"metric_type"` @@ -36,30 +24,47 @@ type OIMetric struct { } type OIMetrics []OIMetric +type OIMetricsObj struct { + Records []OIMetric `json:"records"` +} + +func (s *Serializer) Init() error { + switch s.Format { + case "": + s.Format = "oi" + case "oi", "jsonv2": + default: + return fmt.Errorf("invalid format %q", s.Format) + } + + return nil +} func (s *Serializer) Serialize(metric telegraf.Metric) (out []byte, err error) { - serialized, err := s.createObject(metric) - if err != nil { - return []byte{}, err + m := s.createObject(metric) + + if s.Format == "jsonv2" { + obj := OIMetricsObj{Records: m} + return json.Marshal(obj) } - return serialized, nil + return json.Marshal(m) } func (s *Serializer) SerializeBatch(metrics []telegraf.Metric) (out []byte, err error) { - objects := make([]byte, 0) + objects := make([]OIMetric, 0) for _, metric := range metrics { - m, err := s.createObject(metric) - if err != nil { - return nil, fmt.Errorf("dropping invalid metric: %s", metric.Name()) - } else if m != nil { - objects = append(objects, m...) - } + objects = append(objects, s.createObject(metric)...) } - replaced := bytes.Replace(objects, []byte("]["), []byte(","), -1) - return replaced, nil + + if s.Format == "jsonv2" { + obj := OIMetricsObj{Records: objects} + return json.Marshal(obj) + } + + return json.Marshal(objects) } -func (s *Serializer) createObject(metric telegraf.Metric) ([]byte, error) { +func (s *Serializer) createObject(metric telegraf.Metric) OIMetrics { /* ServiceNow Operational Intelligence supports an array of JSON objects. ** Following elements accepted in the request body: ** metric_type: The name of the metric @@ -117,9 +122,7 @@ func (s *Serializer) createObject(metric telegraf.Metric) ([]byte, error) { allmetrics = append(allmetrics, oimetric) } - metricsJSON, err := json.Marshal(allmetrics) - - return metricsJSON, err + return allmetrics } func verifyValue(v interface{}) bool { diff --git a/plugins/serializers/nowmetric/nowmetric_test.go b/plugins/serializers/nowmetric/nowmetric_test.go index 3ea03d8a0..8eb6956ec 100644 --- a/plugins/serializers/nowmetric/nowmetric_test.go +++ b/plugins/serializers/nowmetric/nowmetric_test.go @@ -199,3 +199,52 @@ func TestSerializeBatch(t *testing.T) { buf, ) } + +func TestSerializeJSONv2Format(t *testing.T) { + m := metric.New( + "cpu", + map[string]string{}, + map[string]interface{}{ + "value": 42.0, + }, + time.Unix(0, 0), + ) + s := &Serializer{Format: "jsonv2"} + buf, err := s.Serialize(m) + require.NoError(t, err) + require.Equal( + t, + []byte(`{"records":[{"metric_type":"value","resource":"","node":"","value":42,"timestamp":0,"ci2metric_id":null,"source":"Telegraf"}]}`), + buf, + ) +} + +func TestSerializeJSONv2FormatBatch(t *testing.T) { + m := metric.New( + "cpu", + map[string]string{}, + map[string]interface{}{ + "value": 42.0, + }, + time.Unix(0, 0), + ) + s := &Serializer{Format: "jsonv2"} + metrics := []telegraf.Metric{m, m} + buf, err := s.SerializeBatch(metrics) + require.NoError(t, err) + require.Equal( + t, + []byte( + `{"records":[`+ + `{"metric_type":"value","resource":"","node":"","value":42,"timestamp":0,"ci2metric_id":null,"source":"Telegraf"},`+ + `{"metric_type":"value","resource":"","node":"","value":42,"timestamp":0,"ci2metric_id":null,"source":"Telegraf"}`+ + `]}`, + ), + buf, + ) +} + +func TestSerializeInvalidFormat(t *testing.T) { + s := &Serializer{Format: "foo"} + require.Error(t, s.Init()) +}