Skip to content

Commit 94ce67c

Browse files
johnrengelmandgnorton
authored andcommitted
Add support to parse JSON array. (influxdata#1965)
1 parent 33ed528 commit 94ce67c

File tree

6 files changed

+337
-8
lines changed

6 files changed

+337
-8
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ documentation for configuring journald. There is also a [`logfile` config option
3030
available in 1.1, which will allow users to easily configure telegraf to
3131
continue sending logs to /var/log/telegraf/telegraf.log.
3232

33+
- The JSON parser can now parse JSON data where the root object is an array.
34+
The parsing configuration is applied to each element of the array.
35+
3336
### Features
3437

3538
- [#1726](https://github.com/influxdata/telegraf/issues/1726): Processor & Aggregator plugin support.

docs/DATA_FORMATS_INPUT.md

+56
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,62 @@ Your Telegraf metrics would get tagged with "my_tag_1"
147147
exec_mycollector,my_tag_1=foo a=5,b_c=6
148148
```
149149

150+
If the JSON data is an array, then each element of the array is parsed with the configured settings.
151+
Each resulting metric will be output with the same timestamp.
152+
153+
For example, if the following configuration:
154+
155+
```toml
156+
[[inputs.exec]]
157+
## Commands array
158+
commands = ["/usr/bin/mycollector --foo=bar"]
159+
160+
## measurement name suffix (for separating different commands)
161+
name_suffix = "_mycollector"
162+
163+
## Data format to consume.
164+
## Each data format has it's own unique set of configuration options, read
165+
## more about them here:
166+
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md
167+
data_format = "json"
168+
169+
## List of tag names to extract from top-level of JSON server response
170+
tag_keys = [
171+
"my_tag_1",
172+
"my_tag_2"
173+
]
174+
```
175+
176+
with this JSON output from a command:
177+
178+
```json
179+
[
180+
{
181+
"a": 5,
182+
"b": {
183+
"c": 6
184+
},
185+
"my_tag_1": "foo",
186+
"my_tag_2": "baz"
187+
},
188+
{
189+
"a": 7,
190+
"b": {
191+
"c": 8
192+
},
193+
"my_tag_1": "bar",
194+
"my_tag_2": "baz"
195+
}
196+
]
197+
```
198+
199+
Your Telegraf metrics would get tagged with "my_tag_1" and "my_tag_2"
200+
201+
```
202+
exec_mycollector,my_tag_1=foo,my_tag_2=baz a=5,b_c=6
203+
exec_mycollector,my_tag_1=bar,my_tag_2=baz a=7,b_c=8
204+
```
205+
150206
# Value:
151207

152208
The "value" data format translates single values into Telegraf metrics. This

plugins/inputs/httpjson/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ You can also specify which keys from server response should be considered tags:
3737
]
3838
```
3939

40+
If the JSON response is an array of objects, then each object will be parsed with the same configuration.
41+
4042
You can also specify additional request parameters for the service:
4143

4244
```
@@ -150,3 +152,53 @@ httpjson_mycollector1_b_e,server='http://my.service.com/_stats' value=5
150152
httpjson_mycollector2_load,server='http://service.net/json/stats' value=100
151153
httpjson_mycollector2_users,server='http://service.net/json/stats' value=1335
152154
```
155+
156+
# Example 3, Multiple Metrics in Response:
157+
158+
The response JSON can be treated as an array of data points that are all parsed with the same configuration.
159+
160+
```
161+
[[inputs.httpjson]]
162+
name = "mycollector"
163+
servers = [
164+
"http://my.service.com/_stats"
165+
]
166+
# HTTP method to use (case-sensitive)
167+
method = "GET"
168+
tag_keys = ["service"]
169+
```
170+
171+
which responds with the following JSON:
172+
173+
```json
174+
[
175+
{
176+
"service": "service01",
177+
"a": 0.5,
178+
"b": {
179+
"c": "some text",
180+
"d": 0.1,
181+
"e": 5
182+
}
183+
},
184+
{
185+
"service": "service02",
186+
"a": 0.6,
187+
"b": {
188+
"c": "some text",
189+
"d": 0.2,
190+
"e": 6
191+
}
192+
}
193+
]
194+
```
195+
196+
The collected metrics will be:
197+
```
198+
httpjson_mycollector_a,service='service01',server='http://my.service.com/_stats' value=0.5
199+
httpjson_mycollector_b_d,service='service01',server='http://my.service.com/_stats' value=0.1
200+
httpjson_mycollector_b_e,service='service01',server='http://my.service.com/_stats' value=5
201+
httpjson_mycollector_a,service='service02',server='http://my.service.com/_stats' value=0.6
202+
httpjson_mycollector_b_d,service='service02',server='http://my.service.com/_stats' value=0.2
203+
httpjson_mycollector_b_e,service='service02',server='http://my.service.com/_stats' value=6
204+
```

plugins/inputs/httpjson/httpjson_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -511,3 +511,52 @@ func TestHttpJson200Tags(t *testing.T) {
511511
}
512512
}
513513
}
514+
515+
const validJSONArrayTags = `
516+
[
517+
{
518+
"value": 15,
519+
"role": "master",
520+
"build": "123"
521+
},
522+
{
523+
"value": 17,
524+
"role": "slave",
525+
"build": "456"
526+
}
527+
]`
528+
529+
// Test that array data is collected correctly
530+
func TestHttpJsonArray200Tags(t *testing.T) {
531+
httpjson := genMockHttpJson(validJSONArrayTags, 200)
532+
533+
for _, service := range httpjson {
534+
if service.Name == "other_webapp" {
535+
var acc testutil.Accumulator
536+
err := service.Gather(&acc)
537+
// Set responsetime
538+
for _, p := range acc.Metrics {
539+
p.Fields["response_time"] = 1.0
540+
}
541+
require.NoError(t, err)
542+
assert.Equal(t, 8, acc.NFields())
543+
assert.Equal(t, uint64(4), acc.NMetrics())
544+
545+
for _, m := range acc.Metrics {
546+
if m.Tags["role"] == "master" {
547+
assert.Equal(t, "123", m.Tags["build"])
548+
assert.Equal(t, float64(15), m.Fields["value"])
549+
assert.Equal(t, float64(1), m.Fields["response_time"])
550+
assert.Equal(t, "httpjson_"+service.Name, m.Measurement)
551+
} else if m.Tags["role"] == "slave" {
552+
assert.Equal(t, "456", m.Tags["build"])
553+
assert.Equal(t, float64(17), m.Fields["value"])
554+
assert.Equal(t, float64(1), m.Fields["response_time"])
555+
assert.Equal(t, "httpjson_"+service.Name, m.Measurement)
556+
} else {
557+
assert.FailNow(t, "unknown metric")
558+
}
559+
}
560+
}
561+
}
562+
}

plugins/parsers/json/parser.go

+37-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package json
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"strconv"
@@ -16,15 +17,22 @@ type JSONParser struct {
1617
DefaultTags map[string]string
1718
}
1819

19-
func (p *JSONParser) Parse(buf []byte) ([]telegraf.Metric, error) {
20+
func (p *JSONParser) parseArray(buf []byte) ([]telegraf.Metric, error) {
2021
metrics := make([]telegraf.Metric, 0)
2122

22-
var jsonOut map[string]interface{}
23+
var jsonOut []map[string]interface{}
2324
err := json.Unmarshal(buf, &jsonOut)
2425
if err != nil {
25-
err = fmt.Errorf("unable to parse out as JSON, %s", err)
26+
err = fmt.Errorf("unable to parse out as JSON Array, %s", err)
2627
return nil, err
2728
}
29+
for _, item := range jsonOut {
30+
metrics, err = p.parseObject(metrics, item)
31+
}
32+
return metrics, nil
33+
}
34+
35+
func (p *JSONParser) parseObject(metrics []telegraf.Metric, jsonOut map[string]interface{}) ([]telegraf.Metric, error) {
2836

2937
tags := make(map[string]string)
3038
for k, v := range p.DefaultTags {
@@ -44,7 +52,7 @@ func (p *JSONParser) Parse(buf []byte) ([]telegraf.Metric, error) {
4452
}
4553

4654
f := JSONFlattener{}
47-
err = f.FlattenJSON("", jsonOut)
55+
err := f.FlattenJSON("", jsonOut)
4856
if err != nil {
4957
return nil, err
5058
}
@@ -57,6 +65,21 @@ func (p *JSONParser) Parse(buf []byte) ([]telegraf.Metric, error) {
5765
return append(metrics, metric), nil
5866
}
5967

68+
func (p *JSONParser) Parse(buf []byte) ([]telegraf.Metric, error) {
69+
70+
if !isarray(buf) {
71+
metrics := make([]telegraf.Metric, 0)
72+
var jsonOut map[string]interface{}
73+
err := json.Unmarshal(buf, &jsonOut)
74+
if err != nil {
75+
err = fmt.Errorf("unable to parse out as JSON, %s", err)
76+
return nil, err
77+
}
78+
return p.parseObject(metrics, jsonOut)
79+
}
80+
return p.parseArray(buf)
81+
}
82+
6083
func (p *JSONParser) ParseLine(line string) (telegraf.Metric, error) {
6184
metrics, err := p.Parse([]byte(line + "\n"))
6285

@@ -115,3 +138,13 @@ func (f *JSONFlattener) FlattenJSON(
115138
}
116139
return nil
117140
}
141+
142+
func isarray(buf []byte) bool {
143+
ia := bytes.IndexByte(buf, '[')
144+
ib := bytes.IndexByte(buf, '{')
145+
if ia > -1 && ia < ib {
146+
return true
147+
} else {
148+
return false
149+
}
150+
}

0 commit comments

Comments
 (0)