Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate JSONPath library #74

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 22 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ json_exporter
[![CircleCI](https://circleci.com/gh/prometheus-community/json_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/json_exporter)

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

# Build
#### :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.

```sh
make build
```
## Example Usage

# Example Usage

```sh
$ cat example/data.json
```console
$ cat examples/data.json
{
"counter": 1234,
"values": [
Expand Down Expand Up @@ -43,23 +41,23 @@ $ cat examples/config.yml
---
metrics:
- name: example_global_value
path: $.counter
path: "{ .counter }"
help: Example of a top-level global value scrape in the json
labels:
environment: beta # static label
location: $.location # dynamic label
location: "planet-{.location}" # dynamic label

- name: example_value
type: object
help: Example of sub-level value scrapes from a json
path: $.values[*]?(@.state == "ACTIVE")
path: '{.values[?(@.state == "ACTIVE")]}'
labels:
environment: beta # static label
id: $.id # dynamic label
id: '{.id}' # dynamic label
values:
active: 1 # static value
count: $.count # dynamic value
boolean: $.some_boolean
count: '{.count}' # dynamic value
boolean: '{.some_boolean}'

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

$ curl "http://localhost:7979/probe?target=http://localhost:8000/examples/data.json" | grep ^example
example_global_value{environment="beta",location="mars"} 1234
example_global_value{environment="beta",location="planet-mars"} 1234
example_value_active{environment="beta",id="id-A"} 1
example_value_active{environment="beta",id="id-C"} 1
example_value_boolean{environment="beta",id="id-A"} 1
Expand All @@ -83,40 +81,22 @@ $ docker run --rm -it -p 9090:9090 -v $PWD/examples/prometheus.yml:/etc/promethe
```
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.

# Exposing metrics through HTTPS
## Exposing metrics through HTTPS

web-config.yml
```
# Minimal TLS configuration example. Additionally, a certificate and a key file
# are needed.
tls_server_config:
cert_file: server.crt
key_file: server.key
```
Running
```
$ ./json_exporter --config.file examples/config.yml --web.config=web-config.yml &
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)

$ curl -k "https://localhost:7979/probe?target=http://localhost:8000/examples/data.json" | grep ^example
example_global_value{environment="beta",location="mars"} 1234
example_value_active{environment="beta",id="id-A"} 1
example_value_active{environment="beta",id="id-C"} 1
example_value_boolean{environment="beta",id="id-A"} 1
example_value_boolean{environment="beta",id="id-C"} 0
example_value_count{environment="beta",id="id-A"} 1
example_value_count{environment="beta",id="id-C"} 3
## Build

```sh
make build
```
For futher information about TLS configuration, please visit: [exporter-toolkit/https](https://github.com/prometheus/exporter-toolkit/blob/v0.1.0/https/README.md)

# Docker
## Docker

```console
docker run \
-v $PWD/examples/config.yml:/config.yml
-v $PWD/examples/config.yml:/config.yml \
quay.io/prometheuscommunity/json-exporter \
--config.file /config.yml
--config.file=/config.yml
```

# See Also
- [kawamuray/jsonpath](https://github.com/kawamuray/jsonpath#path-syntax) : For syntax reference of JSONPath.
Originally forked from nicksardo/jsonpath(now is https://github.com/NodePrime/jsonpath).
4 changes: 2 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/https"
"github.com/prometheus/exporter-toolkit/web"
"gopkg.in/alecthomas/kingpin.v2"
)

Expand Down Expand Up @@ -74,7 +74,7 @@ func Run() {
})

server := &http.Server{Addr: *listenAddress}
if err := https.Listen(server, *tlsConfigFile, logger); err != nil {
if err := web.ListenAndServe(server, *tlsConfigFile, logger); err != nil {
level.Error(logger).Log("msg", "Failed to start the server", "err", err) //nolint:errcheck
os.Exit(1)
}
Expand Down
12 changes: 6 additions & 6 deletions examples/config.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
---
metrics:
- name: example_global_value
path: $.counter
path: "{ .counter }"
help: Example of a top-level global value scrape in the json
labels:
environment: beta # static label
location: $.location # dynamic label
location: "planet-{.location}" # dynamic label

- name: example_value
type: object
help: Example of sub-level value scrapes from a json
path: $.values[*]?(@.state == "ACTIVE")
path: '{.values[?(@.state == "ACTIVE")]}'
labels:
environment: beta # static label
id: $.id # dynamic label
id: '{.id}' # dynamic label
values:
active: 1 # static value
count: $.count # dynamic value
boolean: $.some_boolean
count: '{.count}' # dynamic value
boolean: '{.some_boolean}'

headers:
X-Dummy: my-test-header
Expand Down
168 changes: 67 additions & 101 deletions exporter/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
package exporter

import (
"errors"
"strconv"
"bytes"
"encoding/json"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kawamuray/jsonpath" // Originally: "github.com/NickSardo/jsonpath"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/client-go/util/jsonpath"
)

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

ch <- prometheus.MustNewConstMetric(
m.Desc,
prometheus.UntypedValue,
floatValue,
extractLabels(mc.Logger, mc.Data, m.LabelsJsonPaths)...,
)
} else { // ScrapeType is 'object'
path, err := compilePath(m.KeyJsonPath)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to compile path", "path", m.KeyJsonPath, "err", err) //nolint:errcheck
if floatValue, err := SanitizeValue(value); err == nil {

ch <- prometheus.MustNewConstMetric(
m.Desc,
prometheus.UntypedValue,
floatValue,
extractLabels(mc.Logger, mc.Data, m.LabelsJsonPaths)...,
)
} else {
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
continue
}

eval, err := jsonpath.EvalPathsInBytes(mc.Data, []*jsonpath.Path{path})
} else { // ScrapeType is 'object'
values, err := extractValue(mc.Logger, mc.Data, m.KeyJsonPath, true)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to create evaluator for json path", "path", m.KeyJsonPath, "err", err) //nolint:errcheck
level.Error(mc.Logger).Log("msg", "Failed to extract json objects for metric", "err", err, "metric", m.Desc) //nolint:errcheck
continue
}
for {
if result, ok := eval.Next(); ok {
floatValue, err := extractValue(mc.Logger, result.Value, m.ValueJsonPath)

var jsonData []interface{}
if err := json.Unmarshal([]byte(values), &jsonData); err == nil {
for _, data := range jsonData {
jdata, err := json.Marshal(data)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to extract value", "path", m.ValueJsonPath, "err", err) //nolint:errcheck
level.Error(mc.Logger).Log("msg", "Failed to marshal data to json", "path", m.ValueJsonPath, "err", err, "metric", m.Desc, "data", data) //nolint:errcheck
continue
}
value, err := extractValue(mc.Logger, jdata, m.ValueJsonPath, false)
if err != nil {
level.Error(mc.Logger).Log("msg", "Failed to extract value for metric", "path", m.ValueJsonPath, "err", err, "metric", m.Desc) //nolint:errcheck
continue
}

ch <- prometheus.MustNewConstMetric(
m.Desc,
prometheus.UntypedValue,
floatValue,
extractLabels(mc.Logger, result.Value, m.LabelsJsonPaths)...,
)
} else {
break
if floatValue, err := SanitizeValue(value); err == nil {
ch <- prometheus.MustNewConstMetric(
m.Desc,
prometheus.UntypedValue,
floatValue,
extractLabels(mc.Logger, jdata, m.LabelsJsonPaths)...,
)
} else {
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
continue
}
}
} else {
level.Error(mc.Logger).Log("msg", "Failed to convert extracted objects to json", "err", err, "metric", m.Desc) //nolint:errcheck
continue
}
}
}
}

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

paths, err := jsonpath.ParsePaths(path)
if err != nil {
return nil, err
j := jsonpath.New("jp")
if enableJSONOutput {
j.EnableJSONOutput(true)
}
return paths[0], nil
}

// Returns the first matching float value at the given json path
func extractValue(logger log.Logger, json []byte, path string) (float64, error) {
var floatValue = -1.0
var result *jsonpath.Result
var err error

if len(path) < 1 || path[0] != '$' {
// Static value
return parseValue([]byte(path))
if err := json.Unmarshal(data, &jsonData); err != nil {
level.Error(logger).Log("msg", "Failed to unmarshal data to json", "err", err, "data", data) //nolint:errcheck
return "", err
}

// Dynamic value
p, err := compilePath(path)
if err != nil {
return floatValue, err
if err := j.Parse(path); err != nil {
level.Error(logger).Log("msg", "Failed to parse jsonpath", "err", err, "path", path, "data", data) //nolint:errcheck
return "", err
}

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

result, ok := eval.Next()
if result == nil || !ok {
if eval.Error != nil {
return floatValue, eval.Error
} else {
level.Debug(logger).Log("msg", "Path not found", "path", path, "json", string(json)) //nolint:errcheck
return floatValue, errors.New("Path not found")
}
// Since we are finally going to extract only float64, unquote if necessary
if res, err := jsonpath.UnquoteExtend(buf.String()); err == nil {
return res, nil
}

return SanitizeValue(result)
return buf.String(), nil
}

// Returns the list of labels created from the list of provided json paths
func extractLabels(logger log.Logger, json []byte, paths []string) []string {
func extractLabels(logger log.Logger, data []byte, paths []string) []string {
labels := make([]string, len(paths))
for i, path := range paths {

// Dynamic value
p, err := compilePath(path)
if err != nil {
level.Error(logger).Log("msg", "Failed to compile path for label", "path", path, "err", err) //nolint:errcheck
continue
}

eval, err := jsonpath.EvalPathsInBytes(json, []*jsonpath.Path{p})
if err != nil {
level.Error(logger).Log("msg", "Failed to create evaluator for json", "path", path, "err", err) //nolint:errcheck
continue
}

result, ok := eval.Next()
if result == nil || !ok {
if eval.Error != nil {
level.Error(logger).Log("msg", "Failed to evaluate", "json", string(json), "err", eval.Error) //nolint:errcheck
} else {
level.Warn(logger).Log("msg", "Label path not found in json", "path", path) //nolint:errcheck
level.Debug(logger).Log("msg", "Label path not found in json", "path", path, "json", string(json)) //nolint:errcheck
}
continue
}

l, err := strconv.Unquote(string(result.Value))
if err == nil {
labels[i] = l
if result, err := extractValue(logger, data, path, false); err == nil {
labels[i] = result
} else {
labels[i] = string(result.Value)
level.Error(logger).Log("msg", "Failed to extract label value", "err", err, "path", path, "data", data) //nolint:errcheck
}
}
return labels
Expand Down
Loading