Skip to content

Commit 2729378

Browse files
MrMaxBuildsdanielnelson
authored andcommitted
Add name, time, path and string field options to JSON parser (#4351)
1 parent d6d6539 commit 2729378

File tree

15 files changed

+670
-164
lines changed

15 files changed

+670
-164
lines changed

docs/DATA_FORMATS_INPUT.md

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,31 @@ but can be overridden using the `name_override` config option.
107107

108108
#### JSON Configuration:
109109

110-
The JSON data format supports specifying "tag keys". If specified, keys
111-
will be searched for in the root-level of the JSON blob. If the key(s) exist,
112-
they will be applied as tags to the Telegraf metrics.
110+
The JSON data format supports specifying "tag_keys", "string_keys", and "json_query".
111+
If specified, keys in "tag_keys" and "string_keys" will be searched for in the root-level
112+
and any nested lists of the JSON blob. All int and float values are added to fields by default.
113+
If the key(s) exist, they will be applied as tags or fields to the Telegraf metrics.
114+
If "string_keys" is specified, the string will be added as a field.
115+
116+
The "json_query" configuration is a gjson path to an JSON object or
117+
list of JSON objects. If this path leads to an array of values or
118+
single data point an error will be thrown. If this configuration
119+
is specified, only the result of the query will be parsed and returned as metrics.
120+
121+
The "json_name_key" configuration specifies the key of the field whos value will be
122+
added as the metric name.
123+
124+
Object paths are specified using gjson path format, which is denoted by object keys
125+
concatenated with "." to go deeper in nested JSON objects.
126+
Additional information on gjson paths can be found here: https://github.com/tidwall/gjson#path-syntax
127+
128+
The JSON data format also supports extracting time values through the
129+
config "json_time_key" and "json_time_format". If "json_time_key" is set,
130+
"json_time_format" must be specified. The "json_time_key" describes the
131+
name of the field containing time information. The "json_time_format"
132+
must be a recognized Go time format.
133+
If there is no year provided, the metrics will have the current year.
134+
More info on time formats can be found here: https://golang.org/pkg/time/#Parse
113135

114136
For example, if you had this configuration:
115137

@@ -127,11 +149,28 @@ For example, if you had this configuration:
127149
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md
128150
data_format = "json"
129151

130-
## List of tag names to extract from top-level of JSON server response
152+
## List of tag names to extract from JSON server response
131153
tag_keys = [
132154
"my_tag_1",
133155
"my_tag_2"
134156
]
157+
158+
## The json path specifying where to extract the metric name from
159+
# json_name_key = ""
160+
161+
## List of field names to extract from JSON and add as string fields
162+
# json_string_fields = []
163+
164+
## gjson query path to specify a specific chunk of JSON to be parsed with
165+
## the above configuration. If not specified, the whole file will be parsed.
166+
## gjson query paths are described here: https://github.com/tidwall/gjson#path-syntax
167+
# json_query = ""
168+
169+
## holds the name of the tag of timestamp
170+
# json_time_key = ""
171+
172+
## holds the format of timestamp to be parsed
173+
# json_time_format = ""
135174
```
136175

137176
with this JSON output from a command:
@@ -152,8 +191,9 @@ Your Telegraf metrics would get tagged with "my_tag_1"
152191
exec_mycollector,my_tag_1=foo a=5,b_c=6
153192
```
154193

155-
If the JSON data is an array, then each element of the array is parsed with the configured settings.
156-
Each resulting metric will be output with the same timestamp.
194+
If the JSON data is an array, then each element of the array is
195+
parsed with the configured settings. Each resulting metric will
196+
be output with the same timestamp.
157197

158198
For example, if the following configuration:
159199

@@ -176,6 +216,19 @@ For example, if the following configuration:
176216
"my_tag_1",
177217
"my_tag_2"
178218
]
219+
220+
## List of field names to extract from JSON and add as string fields
221+
# string_fields = []
222+
223+
## gjson query path to specify a specific chunk of JSON to be parsed with
224+
## the above configuration. If not specified, the whole file will be parsed
225+
# json_query = ""
226+
227+
## holds the name of the tag of timestamp
228+
json_time_key = "b_time"
229+
230+
## holds the format of timestamp to be parsed
231+
json_time_format = "02 Jan 06 15:04 MST"
179232
```
180233

181234
with this JSON output from a command:
@@ -185,27 +238,89 @@ with this JSON output from a command:
185238
{
186239
"a": 5,
187240
"b": {
188-
"c": 6
241+
"c": 6,
242+
"time":"04 Jan 06 15:04 MST"
189243
},
190244
"my_tag_1": "foo",
191245
"my_tag_2": "baz"
192246
},
193247
{
194248
"a": 7,
195249
"b": {
196-
"c": 8
250+
"c": 8,
251+
"time":"11 Jan 07 15:04 MST"
197252
},
198253
"my_tag_1": "bar",
199254
"my_tag_2": "baz"
200255
}
201256
]
202257
```
203258

204-
Your Telegraf metrics would get tagged with "my_tag_1" and "my_tag_2"
259+
Your Telegraf metrics would get tagged with "my_tag_1" and "my_tag_2" and fielded with "b_c"
260+
The metric's time will be a time.Time object, as specified by "b_time"
261+
262+
```
263+
exec_mycollector,my_tag_1=foo,my_tag_2=baz b_c=6 1136387040000000000
264+
exec_mycollector,my_tag_1=bar,my_tag_2=baz b_c=8 1168527840000000000
265+
```
266+
267+
If you want to only use a specific portion of your JSON, use the "json_query"
268+
configuration to specify a path to a JSON object.
269+
270+
For example, with the following config:
271+
```toml
272+
[[inputs.exec]]
273+
## Commands array
274+
commands = ["/usr/bin/mycollector --foo=bar"]
275+
276+
## measurement name suffix (for separating different commands)
277+
name_suffix = "_mycollector"
278+
279+
## Data format to consume.
280+
## Each data format has its own unique set of configuration options, read
281+
## more about them here:
282+
## https://github.com/influxdata/telegraf/blob/master/docs/DATA_FORMATS_INPUT.md
283+
data_format = "json"
284+
285+
## List of tag names to extract from top-level of JSON server response
286+
tag_keys = ["first"]
287+
288+
## List of field names to extract from JSON and add as string fields
289+
string_fields = ["last"]
290+
291+
## gjson query path to specify a specific chunk of JSON to be parsed with
292+
## the above configuration. If not specified, the whole file will be parsed
293+
json_query = "obj.friends"
294+
295+
## holds the name of the tag of timestamp
296+
# json_time_key = ""
297+
298+
## holds the format of timestamp to be parsed
299+
# json_time_format = ""
300+
```
301+
302+
with this JSON as input:
303+
```json
304+
{
305+
"obj": {
306+
"name": {"first": "Tom", "last": "Anderson"},
307+
"age":37,
308+
"children": ["Sara","Alex","Jack"],
309+
"fav.movie": "Deer Hunter",
310+
"friends": [
311+
{"first": "Dale", "last": "Murphy", "age": 44},
312+
{"first": "Roger", "last": "Craig", "age": 68},
313+
{"first": "Jane", "last": "Murphy", "age": 47}
314+
]
315+
}
316+
}
317+
```
318+
You would recieve 3 metrics tagged with "first", and fielded with "last" and "age"
205319

206320
```
207-
exec_mycollector,my_tag_1=foo,my_tag_2=baz a=5,b_c=6
208-
exec_mycollector,my_tag_1=bar,my_tag_2=baz a=7,b_c=8
321+
exec_mycollector, "first":"Dale" "last":"Murphy","age":44
322+
exec_mycollector, "first":"Roger" "last":"Craig","age":68
323+
exec_mycollector, "first":"Jane" "last":"Murphy","age":47
209324
```
210325

211326
# Value:

internal/config/config.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,50 @@ func buildParser(name string, tbl *ast.Table) (parsers.Parser, error) {
12611261
}
12621262
}
12631263

1264+
if node, ok := tbl.Fields["json_string_fields"]; ok {
1265+
if kv, ok := node.(*ast.KeyValue); ok {
1266+
if ary, ok := kv.Value.(*ast.Array); ok {
1267+
for _, elem := range ary.Value {
1268+
if str, ok := elem.(*ast.String); ok {
1269+
c.JSONStringFields = append(c.JSONStringFields, str.Value)
1270+
}
1271+
}
1272+
}
1273+
}
1274+
}
1275+
1276+
if node, ok := tbl.Fields["json_name_key"]; ok {
1277+
if kv, ok := node.(*ast.KeyValue); ok {
1278+
if str, ok := kv.Value.(*ast.String); ok {
1279+
c.JSONNameKey = str.Value
1280+
}
1281+
}
1282+
}
1283+
1284+
if node, ok := tbl.Fields["json_query"]; ok {
1285+
if kv, ok := node.(*ast.KeyValue); ok {
1286+
if str, ok := kv.Value.(*ast.String); ok {
1287+
c.JSONQuery = str.Value
1288+
}
1289+
}
1290+
}
1291+
1292+
if node, ok := tbl.Fields["json_time_key"]; ok {
1293+
if kv, ok := node.(*ast.KeyValue); ok {
1294+
if str, ok := kv.Value.(*ast.String); ok {
1295+
c.JSONTimeKey = str.Value
1296+
}
1297+
}
1298+
}
1299+
1300+
if node, ok := tbl.Fields["json_time_format"]; ok {
1301+
if kv, ok := node.(*ast.KeyValue); ok {
1302+
if str, ok := kv.Value.(*ast.String); ok {
1303+
c.JSONTimeFormat = str.Value
1304+
}
1305+
}
1306+
}
1307+
12641308
if node, ok := tbl.Fields["data_type"]; ok {
12651309
if kv, ok := node.(*ast.KeyValue); ok {
12661310
if str, ok := kv.Value.(*ast.String); ok {
@@ -1405,6 +1449,11 @@ func buildParser(name string, tbl *ast.Table) (parsers.Parser, error) {
14051449
delete(tbl.Fields, "separator")
14061450
delete(tbl.Fields, "templates")
14071451
delete(tbl.Fields, "tag_keys")
1452+
delete(tbl.Fields, "string_fields")
1453+
delete(tbl.Fields, "json_query")
1454+
delete(tbl.Fields, "json_name_key")
1455+
delete(tbl.Fields, "json_time_key")
1456+
delete(tbl.Fields, "json_time_format")
14081457
delete(tbl.Fields, "data_type")
14091458
delete(tbl.Fields, "collectd_auth_file")
14101459
delete(tbl.Fields, "collectd_security_level")

internal/config/config_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ func TestConfig_LoadDirectory(t *testing.T) {
143143
"Testdata did not produce correct memcached metadata.")
144144

145145
ex := inputs.Inputs["exec"]().(*exec.Exec)
146-
p, err := parsers.NewJSONParser("exec", nil, nil)
146+
p, err := parsers.NewParser(&parsers.Config{
147+
MetricName: "exec",
148+
DataFormat: "json",
149+
})
147150
assert.NoError(t, err)
148151
ex.SetParser(p)
149152
ex.Command = "/usr/bin/myothercollector --foo=bar"

plugins/inputs/exec/exec_test.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ func (r runnerMock) Run(e *Exec, command string, acc telegraf.Accumulator) ([]by
9393
}
9494

9595
func TestExec(t *testing.T) {
96-
parser, _ := parsers.NewJSONParser("exec", []string{}, nil)
96+
parser, _ := parsers.NewParser(&parsers.Config{
97+
DataFormat: "json",
98+
MetricName: "exec",
99+
})
97100
e := &Exec{
98101
runner: newRunnerMock([]byte(validJson), nil),
99102
Commands: []string{"testcommand arg1"},
@@ -119,7 +122,10 @@ func TestExec(t *testing.T) {
119122
}
120123

121124
func TestExecMalformed(t *testing.T) {
122-
parser, _ := parsers.NewJSONParser("exec", []string{}, nil)
125+
parser, _ := parsers.NewParser(&parsers.Config{
126+
DataFormat: "json",
127+
MetricName: "exec",
128+
})
123129
e := &Exec{
124130
runner: newRunnerMock([]byte(malformedJson), nil),
125131
Commands: []string{"badcommand arg1"},
@@ -132,7 +138,10 @@ func TestExecMalformed(t *testing.T) {
132138
}
133139

134140
func TestCommandError(t *testing.T) {
135-
parser, _ := parsers.NewJSONParser("exec", []string{}, nil)
141+
parser, _ := parsers.NewParser(&parsers.Config{
142+
DataFormat: "json",
143+
MetricName: "exec",
144+
})
136145
e := &Exec{
137146
runner: newRunnerMock(nil, fmt.Errorf("exit status code 1")),
138147
Commands: []string{"badcommand"},

plugins/inputs/http/http_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ func TestHTTPwithJSONFormat(t *testing.T) {
2626
URLs: []string{url},
2727
}
2828
metricName := "metricName"
29-
p, _ := parsers.NewJSONParser(metricName, nil, nil)
29+
30+
p, _ := parsers.NewParser(&parsers.Config{
31+
DataFormat: "json",
32+
MetricName: "metricName",
33+
})
3034
plugin.SetParser(p)
3135

3236
var acc testutil.Accumulator
@@ -63,8 +67,11 @@ func TestHTTPHeaders(t *testing.T) {
6367
URLs: []string{url},
6468
Headers: map[string]string{header: headerValue},
6569
}
66-
metricName := "metricName"
67-
p, _ := parsers.NewJSONParser(metricName, nil, nil)
70+
71+
p, _ := parsers.NewParser(&parsers.Config{
72+
DataFormat: "json",
73+
MetricName: "metricName",
74+
})
6875
plugin.SetParser(p)
6976

7077
var acc testutil.Accumulator
@@ -83,7 +90,10 @@ func TestInvalidStatusCode(t *testing.T) {
8390
}
8491

8592
metricName := "metricName"
86-
p, _ := parsers.NewJSONParser(metricName, nil, nil)
93+
p, _ := parsers.NewParser(&parsers.Config{
94+
DataFormat: "json",
95+
MetricName: metricName,
96+
})
8797
plugin.SetParser(p)
8898

8999
var acc testutil.Accumulator
@@ -105,8 +115,10 @@ func TestMethod(t *testing.T) {
105115
Method: "POST",
106116
}
107117

108-
metricName := "metricName"
109-
p, _ := parsers.NewJSONParser(metricName, nil, nil)
118+
p, _ := parsers.NewParser(&parsers.Config{
119+
DataFormat: "json",
120+
MetricName: "metricName",
121+
})
110122
plugin.SetParser(p)
111123

112124
var acc testutil.Accumulator

plugins/inputs/httpjson/httpjson.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,12 @@ func (h *HttpJson) gatherServer(
181181
"server": serverURL,
182182
}
183183

184-
parser, err := parsers.NewJSONParser(msrmnt_name, h.TagKeys, tags)
184+
parser, err := parsers.NewParser(&parsers.Config{
185+
DataFormat: "json",
186+
MetricName: msrmnt_name,
187+
TagKeys: h.TagKeys,
188+
DefaultTags: tags,
189+
})
185190
if err != nil {
186191
return err
187192
}

plugins/inputs/kafka_consumer/kafka_consumer_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ func TestRunParserAndGatherJSON(t *testing.T) {
125125
k.acc = &acc
126126
defer close(k.done)
127127

128-
k.parser, _ = parsers.NewJSONParser("kafka_json_test", []string{}, nil)
128+
k.parser, _ = parsers.NewParser(&parsers.Config{
129+
DataFormat: "json",
130+
MetricName: "kafka_json_test",
131+
})
129132
go k.receiver()
130133
in <- saramaMsg(testMsgJSON)
131134
acc.Wait(1)

plugins/inputs/kafka_consumer_legacy/kafka_consumer_legacy_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ func TestRunParserAndGatherJSON(t *testing.T) {
125125
k.acc = &acc
126126
defer close(k.done)
127127

128-
k.parser, _ = parsers.NewJSONParser("kafka_json_test", []string{}, nil)
128+
k.parser, _ = parsers.NewParser(&parsers.Config{
129+
DataFormat: "json",
130+
MetricName: "kafka_json_test",
131+
})
129132
go k.receiver()
130133
in <- saramaMsg(testMsgJSON)
131134
acc.Wait(1)

0 commit comments

Comments
 (0)