Skip to content

Commit 94784db

Browse files
authored
Merge pull request prometheus-community#74 from rustycl0ck/feature/jsonpath-lib-change
Migrate JSONPath library
2 parents 3fce0fb + fe22a83 commit 94784db

File tree

10 files changed

+461
-236
lines changed

10 files changed

+461
-236
lines changed

README.md

+22-42
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@ json_exporter
33
[![CircleCI](https://circleci.com/gh/prometheus-community/json_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/json_exporter)
44

55
A [prometheus](https://prometheus.io/) exporter which scrapes remote JSON by JSONPath.
6+
For checking the JSONPath configuration supported by this exporter please head over [here](https://kubernetes.io/docs/reference/kubectl/jsonpath/).
7+
Checkout the [examples](/examples) directory for sample exporter configuration, prometheus configuration and expected data format.
68

7-
# Build
9+
#### :warning: The configuration syntax has changed in version `0.3.x`. If you are migrating from `0.2.x`, then please use the above mentioned JSONPath guide for correct configuration syntax.
810

9-
```sh
10-
make build
11-
```
11+
## Example Usage
1212

13-
# Example Usage
14-
15-
```sh
16-
$ cat example/data.json
13+
```console
14+
$ cat examples/data.json
1715
{
1816
"counter": 1234,
1917
"values": [
@@ -43,23 +41,23 @@ $ cat examples/config.yml
4341
---
4442
metrics:
4543
- name: example_global_value
46-
path: $.counter
44+
path: "{ .counter }"
4745
help: Example of a top-level global value scrape in the json
4846
labels:
4947
environment: beta # static label
50-
location: $.location # dynamic label
48+
location: "planet-{.location}" # dynamic label
5149

5250
- name: example_value
5351
type: object
5452
help: Example of sub-level value scrapes from a json
55-
path: $.values[*]?(@.state == "ACTIVE")
53+
path: '{.values[?(@.state == "ACTIVE")]}'
5654
labels:
5755
environment: beta # static label
58-
id: $.id # dynamic label
56+
id: '{.id}' # dynamic label
5957
values:
6058
active: 1 # static value
61-
count: $.count # dynamic value
62-
boolean: $.some_boolean
59+
count: '{.count}' # dynamic value
60+
boolean: '{.some_boolean}'
6361

6462
headers:
6563
X-Dummy: my-test-header
@@ -70,7 +68,7 @@ Serving HTTP on 0.0.0.0 port 8000 ...
7068
$ ./json_exporter --config.file examples/config.yml &
7169

7270
$ curl "http://localhost:7979/probe?target=http://localhost:8000/examples/data.json" | grep ^example
73-
example_global_value{environment="beta",location="mars"} 1234
71+
example_global_value{environment="beta",location="planet-mars"} 1234
7472
example_value_active{environment="beta",id="id-A"} 1
7573
example_value_active{environment="beta",id="id-C"} 1
7674
example_value_boolean{environment="beta",id="id-A"} 1
@@ -83,40 +81,22 @@ $ docker run --rm -it -p 9090:9090 -v $PWD/examples/prometheus.yml:/etc/promethe
8381
```
8482
Then head over to http://localhost:9090/graph?g0.range_input=1h&g0.expr=example_value_active&g0.tab=1 or http://localhost:9090/targets to check the scraped metrics or the targets.
8583

86-
# Exposing metrics through HTTPS
84+
## Exposing metrics through HTTPS
8785

88-
web-config.yml
89-
```
90-
# Minimal TLS configuration example. Additionally, a certificate and a key file
91-
# are needed.
92-
tls_server_config:
93-
cert_file: server.crt
94-
key_file: server.key
95-
```
96-
Running
97-
```
98-
$ ./json_exporter --config.file examples/config.yml --web.config=web-config.yml &
86+
TLS configuration supported by this exporter can be found at [exporter-toolkit/web](https://github.com/prometheus/exporter-toolkit/blob/v0.5.1/docs/web-configuration.md)
9987

100-
$ curl -k "https://localhost:7979/probe?target=http://localhost:8000/examples/data.json" | grep ^example
101-
example_global_value{environment="beta",location="mars"} 1234
102-
example_value_active{environment="beta",id="id-A"} 1
103-
example_value_active{environment="beta",id="id-C"} 1
104-
example_value_boolean{environment="beta",id="id-A"} 1
105-
example_value_boolean{environment="beta",id="id-C"} 0
106-
example_value_count{environment="beta",id="id-A"} 1
107-
example_value_count{environment="beta",id="id-C"} 3
88+
## Build
89+
90+
```sh
91+
make build
10892
```
109-
For futher information about TLS configuration, please visit: [exporter-toolkit/https](https://github.com/prometheus/exporter-toolkit/blob/v0.1.0/https/README.md)
11093

111-
# Docker
94+
## Docker
11295

11396
```console
11497
docker run \
115-
-v $PWD/examples/config.yml:/config.yml
98+
-v $PWD/examples/config.yml:/config.yml \
11699
quay.io/prometheuscommunity/json-exporter \
117-
--config.file /config.yml
100+
--config.file=/config.yml
118101
```
119102

120-
# See Also
121-
- [kawamuray/jsonpath](https://github.com/kawamuray/jsonpath#path-syntax) : For syntax reference of JSONPath.
122-
Originally forked from nicksardo/jsonpath(now is https://github.com/NodePrime/jsonpath).

cmd/main.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"github.com/prometheus/common/promlog"
2929
"github.com/prometheus/common/promlog/flag"
3030
"github.com/prometheus/common/version"
31-
"github.com/prometheus/exporter-toolkit/https"
31+
"github.com/prometheus/exporter-toolkit/web"
3232
"gopkg.in/alecthomas/kingpin.v2"
3333
)
3434

@@ -74,7 +74,7 @@ func Run() {
7474
})
7575

7676
server := &http.Server{Addr: *listenAddress}
77-
if err := https.Listen(server, *tlsConfigFile, logger); err != nil {
77+
if err := web.ListenAndServe(server, *tlsConfigFile, logger); err != nil {
7878
level.Error(logger).Log("msg", "Failed to start the server", "err", err) //nolint:errcheck
7979
os.Exit(1)
8080
}

examples/config.yml

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
---
22
metrics:
33
- name: example_global_value
4-
path: $.counter
4+
path: "{ .counter }"
55
help: Example of a top-level global value scrape in the json
66
labels:
77
environment: beta # static label
8-
location: $.location # dynamic label
8+
location: "planet-{.location}" # dynamic label
99

1010
- name: example_value
1111
type: object
1212
help: Example of sub-level value scrapes from a json
13-
path: $.values[*]?(@.state == "ACTIVE")
13+
path: '{.values[?(@.state == "ACTIVE")]}'
1414
labels:
1515
environment: beta # static label
16-
id: $.id # dynamic label
16+
id: '{.id}' # dynamic label
1717
values:
1818
active: 1 # static value
19-
count: $.count # dynamic value
20-
boolean: $.some_boolean
19+
count: '{.count}' # dynamic value
20+
boolean: '{.some_boolean}'
2121

2222
headers:
2323
X-Dummy: my-test-header

exporter/collector.go

+67-101
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
package exporter
1515

1616
import (
17-
"errors"
18-
"strconv"
17+
"bytes"
18+
"encoding/json"
1919

2020
"github.com/go-kit/kit/log"
2121
"github.com/go-kit/kit/log/level"
22-
"github.com/kawamuray/jsonpath" // Originally: "github.com/NickSardo/jsonpath"
2322
"github.com/prometheus/client_golang/prometheus"
23+
"k8s.io/client-go/util/jsonpath"
2424
)
2525

2626
type JsonMetricCollector struct {
@@ -45,140 +45,106 @@ func (mc JsonMetricCollector) Describe(ch chan<- *prometheus.Desc) {
4545
func (mc JsonMetricCollector) Collect(ch chan<- prometheus.Metric) {
4646
for _, m := range mc.JsonMetrics {
4747
if m.ValueJsonPath == "" { // ScrapeType is 'value'
48-
floatValue, err := extractValue(mc.Logger, mc.Data, m.KeyJsonPath)
48+
value, err := extractValue(mc.Logger, mc.Data, m.KeyJsonPath, false)
4949
if err != nil {
50-
// Avoid noise and continue silently if it was a missing path error
51-
if err.Error() == "Path not found" {
52-
level.Debug(mc.Logger).Log("msg", "Failed to extract float value for metric", "path", m.KeyJsonPath, "err", err, "metric", m.Desc) //nolint:errcheck
53-
continue
54-
}
55-
level.Error(mc.Logger).Log("msg", "Failed to extract float value for metric", "path", m.KeyJsonPath, "err", err, "metric", m.Desc) //nolint:errcheck
50+
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.KeyJsonPath, "err", err, "metric", m.Desc) //nolint:errcheck
5651
continue
5752
}
5853

59-
ch <- prometheus.MustNewConstMetric(
60-
m.Desc,
61-
prometheus.UntypedValue,
62-
floatValue,
63-
extractLabels(mc.Logger, mc.Data, m.LabelsJsonPaths)...,
64-
)
65-
} else { // ScrapeType is 'object'
66-
path, err := compilePath(m.KeyJsonPath)
67-
if err != nil {
68-
level.Error(mc.Logger).Log("msg", "Failed to compile path", "path", m.KeyJsonPath, "err", err) //nolint:errcheck
54+
if floatValue, err := SanitizeValue(value); err == nil {
55+
56+
ch <- prometheus.MustNewConstMetric(
57+
m.Desc,
58+
prometheus.UntypedValue,
59+
floatValue,
60+
extractLabels(mc.Logger, mc.Data, m.LabelsJsonPaths)...,
61+
)
62+
} else {
63+
level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.KeyJsonPath, "value", value, "err", err, "metric", m.Desc) //nolint:errcheck
6964
continue
7065
}
71-
72-
eval, err := jsonpath.EvalPathsInBytes(mc.Data, []*jsonpath.Path{path})
66+
} else { // ScrapeType is 'object'
67+
values, err := extractValue(mc.Logger, mc.Data, m.KeyJsonPath, true)
7368
if err != nil {
74-
level.Error(mc.Logger).Log("msg", "Failed to create evaluator for json path", "path", m.KeyJsonPath, "err", err) //nolint:errcheck
69+
level.Error(mc.Logger).Log("msg", "Failed to extract json objects for metric", "err", err, "metric", m.Desc) //nolint:errcheck
7570
continue
7671
}
77-
for {
78-
if result, ok := eval.Next(); ok {
79-
floatValue, err := extractValue(mc.Logger, result.Value, m.ValueJsonPath)
72+
73+
var jsonData []interface{}
74+
if err := json.Unmarshal([]byte(values), &jsonData); err == nil {
75+
for _, data := range jsonData {
76+
jdata, err := json.Marshal(data)
8077
if err != nil {
81-
level.Error(mc.Logger).Log("msg", "Failed to extract value", "path", m.ValueJsonPath, "err", err) //nolint:errcheck
78+
level.Error(mc.Logger).Log("msg", "Failed to marshal data to json", "path", m.ValueJsonPath, "err", err, "metric", m.Desc, "data", data) //nolint:errcheck
79+
continue
80+
}
81+
value, err := extractValue(mc.Logger, jdata, m.ValueJsonPath, false)
82+
if err != nil {
83+
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.ValueJsonPath, "err", err, "metric", m.Desc) //nolint:errcheck
8284
continue
8385
}
8486

85-
ch <- prometheus.MustNewConstMetric(
86-
m.Desc,
87-
prometheus.UntypedValue,
88-
floatValue,
89-
extractLabels(mc.Logger, result.Value, m.LabelsJsonPaths)...,
90-
)
91-
} else {
92-
break
87+
if floatValue, err := SanitizeValue(value); err == nil {
88+
ch <- prometheus.MustNewConstMetric(
89+
m.Desc,
90+
prometheus.UntypedValue,
91+
floatValue,
92+
extractLabels(mc.Logger, jdata, m.LabelsJsonPaths)...,
93+
)
94+
} else {
95+
level.Error(mc.Logger).Log("msg", "Failed to convert extracted value to float64", "path", m.ValueJsonPath, "value", value, "err", err, "metric", m.Desc) //nolint:errcheck
96+
continue
97+
}
9398
}
99+
} else {
100+
level.Error(mc.Logger).Log("msg", "Failed to convert extracted objects to json", "err", err, "metric", m.Desc) //nolint:errcheck
101+
continue
94102
}
95103
}
96104
}
97105
}
98106

99-
func compilePath(path string) (*jsonpath.Path, error) {
100-
// All paths in this package is for extracting a value.
101-
// Complete trailing '+' sign if necessary.
102-
if path[len(path)-1] != '+' {
103-
path += "+"
104-
}
107+
// Returns the last matching value at the given json path
108+
func extractValue(logger log.Logger, data []byte, path string, enableJSONOutput bool) (string, error) {
109+
var jsonData interface{}
110+
buf := new(bytes.Buffer)
105111

106-
paths, err := jsonpath.ParsePaths(path)
107-
if err != nil {
108-
return nil, err
112+
j := jsonpath.New("jp")
113+
if enableJSONOutput {
114+
j.EnableJSONOutput(true)
109115
}
110-
return paths[0], nil
111-
}
112116

113-
// Returns the first matching float value at the given json path
114-
func extractValue(logger log.Logger, json []byte, path string) (float64, error) {
115-
var floatValue = -1.0
116-
var result *jsonpath.Result
117-
var err error
118-
119-
if len(path) < 1 || path[0] != '$' {
120-
// Static value
121-
return parseValue([]byte(path))
117+
if err := json.Unmarshal(data, &jsonData); err != nil {
118+
level.Error(logger).Log("msg", "Failed to unmarshal data to json", "err", err, "data", data) //nolint:errcheck
119+
return "", err
122120
}
123121

124-
// Dynamic value
125-
p, err := compilePath(path)
126-
if err != nil {
127-
return floatValue, err
122+
if err := j.Parse(path); err != nil {
123+
level.Error(logger).Log("msg", "Failed to parse jsonpath", "err", err, "path", path, "data", data) //nolint:errcheck
124+
return "", err
128125
}
129126

130-
eval, err := jsonpath.EvalPathsInBytes(json, []*jsonpath.Path{p})
131-
if err != nil {
132-
return floatValue, err
127+
if err := j.Execute(buf, jsonData); err != nil {
128+
level.Error(logger).Log("msg", "Failed to execute jsonpath", "err", err, "path", path, "data", data) //nolint:errcheck
129+
return "", err
133130
}
134131

135-
result, ok := eval.Next()
136-
if result == nil || !ok {
137-
if eval.Error != nil {
138-
return floatValue, eval.Error
139-
} else {
140-
level.Debug(logger).Log("msg", "Path not found", "path", path, "json", string(json)) //nolint:errcheck
141-
return floatValue, errors.New("Path not found")
142-
}
132+
// Since we are finally going to extract only float64, unquote if necessary
133+
if res, err := jsonpath.UnquoteExtend(buf.String()); err == nil {
134+
return res, nil
143135
}
144136

145-
return SanitizeValue(result)
137+
return buf.String(), nil
146138
}
147139

148140
// Returns the list of labels created from the list of provided json paths
149-
func extractLabels(logger log.Logger, json []byte, paths []string) []string {
141+
func extractLabels(logger log.Logger, data []byte, paths []string) []string {
150142
labels := make([]string, len(paths))
151143
for i, path := range paths {
152-
153-
// Dynamic value
154-
p, err := compilePath(path)
155-
if err != nil {
156-
level.Error(logger).Log("msg", "Failed to compile path for label", "path", path, "err", err) //nolint:errcheck
157-
continue
158-
}
159-
160-
eval, err := jsonpath.EvalPathsInBytes(json, []*jsonpath.Path{p})
161-
if err != nil {
162-
level.Error(logger).Log("msg", "Failed to create evaluator for json", "path", path, "err", err) //nolint:errcheck
163-
continue
164-
}
165-
166-
result, ok := eval.Next()
167-
if result == nil || !ok {
168-
if eval.Error != nil {
169-
level.Error(logger).Log("msg", "Failed to evaluate", "json", string(json), "err", eval.Error) //nolint:errcheck
170-
} else {
171-
level.Warn(logger).Log("msg", "Label path not found in json", "path", path) //nolint:errcheck
172-
level.Debug(logger).Log("msg", "Label path not found in json", "path", path, "json", string(json)) //nolint:errcheck
173-
}
174-
continue
175-
}
176-
177-
l, err := strconv.Unquote(string(result.Value))
178-
if err == nil {
179-
labels[i] = l
144+
if result, err := extractValue(logger, data, path, false); err == nil {
145+
labels[i] = result
180146
} else {
181-
labels[i] = string(result.Value)
147+
level.Error(logger).Log("msg", "Failed to extract label value", "err", err, "path", path, "data", data) //nolint:errcheck
182148
}
183149
}
184150
return labels

0 commit comments

Comments
 (0)