mirror of
https://github.com/prometheus-community/json_exporter
synced 2025-04-11 03:32:19 +00:00
Refactor the complete code
* Make the working of this exporter similar to that of the blackbox_exporter to allow probing multiple targets. * Add functionality to add headers to the request * Update the example config to use `headers` as well as the `metrics` keys in alignment with the new code * Add default header 'Accept: application/json' Signed-off-by: rustyclock <rustyclock@protonmail.com>
This commit is contained in:
parent
95c468e892
commit
c869516e98
12
README.md
12
README.md
@ -58,16 +58,10 @@ $ cat example/config.yml
|
||||
$ python -m SimpleHTTPServer 8000 &
|
||||
Serving HTTP on 0.0.0.0 port 8000 ...
|
||||
|
||||
$ ./json_exporter http://localhost:8000/example/data.json example/config.yml &
|
||||
INFO[2016-02-08T22:44:38+09:00] metric registered;name:<example_global_value>
|
||||
INFO[2016-02-08T22:44:38+09:00] metric registered;name:<example_value_boolean>
|
||||
INFO[2016-02-08T22:44:38+09:00] metric registered;name:<example_value_active>
|
||||
INFO[2016-02-08T22:44:38+09:00] metric registered;name:<example_value_count>
|
||||
127.0.0.1 - - [08/Feb/2016 22:44:38] "GET /example/data.json HTTP/1.1" 200 -
|
||||
$ ./json_exporter examples/config.yml &
|
||||
|
||||
|
||||
$ curl http://localhost:7979/metrics | grep ^example
|
||||
example_global_value{environment="beta"} 1234
|
||||
$ curl "http://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
|
||||
|
142
cmd/main.go
Normal file
142
cmd/main.go
Normal file
@ -0,0 +1,142 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/json_exporter/config"
|
||||
"github.com/prometheus-community/json_exporter/internal"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultOpts = []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "The port number used to expose metrics via http",
|
||||
Value: 7979,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Usage: "Set Logging level",
|
||||
Value: "info",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func MakeApp() *cli.App {
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = "json_exporter"
|
||||
app.Version = internal.Version
|
||||
app.Usage = "A prometheus exporter for scraping metrics from JSON REST API endpoints"
|
||||
app.UsageText = "[OPTIONS] CONFIG_PATH"
|
||||
app.Action = main
|
||||
app.Flags = defaultOpts
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func main(c *cli.Context) {
|
||||
setupLogging(c.String("log-level"))
|
||||
|
||||
internal.Init(c)
|
||||
|
||||
config, err := config.LoadConfig(c.Args()[0])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
configJson, err := json.MarshalIndent(config, "", "\t")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to marshal loaded config to JSON. ERROR: '%s'", err)
|
||||
}
|
||||
log.Infof("Config:\n%s", string(configJson))
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.HandleFunc("/probe", func(w http.ResponseWriter, req *http.Request) {
|
||||
probeHandler(w, req, config)
|
||||
})
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request, config config.Config) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Global.TimeoutSeconds*float64(time.Second)))
|
||||
defer cancel()
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
registry := prometheus.NewPedanticRegistry()
|
||||
|
||||
metrics, err := internal.CreateMetricsList(registry, config)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create metrics from config. Error: %s", err)
|
||||
}
|
||||
|
||||
probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "probe_success",
|
||||
Help: "Displays whether or not the probe was a success",
|
||||
})
|
||||
probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "probe_duration_seconds",
|
||||
Help: "Returns how long the probe took to complete in seconds",
|
||||
})
|
||||
|
||||
target := r.URL.Query().Get("target")
|
||||
if target == "" {
|
||||
http.Error(w, "Target parameter is missing", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
registry.MustRegister(probeSuccessGauge)
|
||||
registry.MustRegister(probeDurationGauge)
|
||||
|
||||
data, err := internal.FetchJson(ctx, target, config.Headers)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
duration := time.Since(start).Seconds()
|
||||
log.Errorf("Probe failed. duration_seconds: %f", duration)
|
||||
} else {
|
||||
internal.Scrape(metrics, data)
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
probeDurationGauge.Set(duration)
|
||||
probeSuccessGauge.Set(1)
|
||||
}
|
||||
|
||||
h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
|
||||
h.ServeHTTP(w, r)
|
||||
|
||||
}
|
||||
|
||||
func setupLogging(level string) {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
})
|
||||
logLevel, err := log.ParseLevel(level)
|
||||
if err != nil {
|
||||
log.Fatalf("could not set log level to '%s';err:<%s>", level, err)
|
||||
}
|
||||
log.SetLevel(logLevel)
|
||||
}
|
88
config/config.go
Normal file
88
config/config.go
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Metric contains values that define a metric
|
||||
type Metric struct {
|
||||
Name string
|
||||
Path string
|
||||
Labels map[string]string
|
||||
Type MetricType
|
||||
Help string
|
||||
Values map[string]string
|
||||
}
|
||||
|
||||
type MetricType string
|
||||
|
||||
const (
|
||||
ValueScrape MetricType = "value" // default
|
||||
ObjectScrape MetricType = "object"
|
||||
)
|
||||
|
||||
// Config contains metrics and headers defining a configuration
|
||||
type Config struct {
|
||||
Headers map[string]string
|
||||
Metrics []Metric
|
||||
Global GlobalConfig
|
||||
}
|
||||
|
||||
type GlobalConfig struct {
|
||||
TimeoutSeconds float64
|
||||
}
|
||||
|
||||
func (metric *Metric) LabelNames() []string {
|
||||
var labelNames []string
|
||||
for name := range metric.Labels {
|
||||
labelNames = append(labelNames, name)
|
||||
}
|
||||
return labelNames
|
||||
}
|
||||
|
||||
func LoadConfig(configPath string) (Config, error) {
|
||||
var config Config
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to load config: %s, Error: %s", configPath, err)
|
||||
return config, err
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
log.Errorf("Failed to parse YAML: %s", err)
|
||||
return config, err
|
||||
}
|
||||
|
||||
// Complete Defaults
|
||||
for i := 0; i < len(config.Metrics); i++ {
|
||||
if config.Metrics[i].Type == "" {
|
||||
config.Metrics[i].Type = ValueScrape
|
||||
//config.Metrics[i].Type = "DefaultScrapeType"
|
||||
}
|
||||
if config.Metrics[i].Help == "" {
|
||||
config.Metrics[i].Help = config.Metrics[i].Name
|
||||
}
|
||||
}
|
||||
if config.Global.TimeoutSeconds == 0 {
|
||||
config.Global.TimeoutSeconds = 10
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
---
|
||||
metrics:
|
||||
- name: example_global_value
|
||||
path: $.counter
|
||||
help: Example of a top-level global value scrape in the json
|
||||
labels:
|
||||
environment: beta # static label
|
||||
location: $.location # dynamic label
|
||||
|
||||
- name: example_value
|
||||
type: object
|
||||
help: Example of sub-level value scrapes from a json
|
||||
path: $.values[*]?(@.state == "ACTIVE")
|
||||
labels:
|
||||
environment: beta # static label
|
||||
@ -13,3 +18,6 @@
|
||||
active: 1 # static value
|
||||
count: $.count # dynamic value
|
||||
boolean: $.some_boolean
|
||||
|
||||
headers:
|
||||
X-Dummy: my-test-header
|
@ -18,6 +18,7 @@
|
||||
"count": 3,
|
||||
"some_boolean": false,
|
||||
"state": "ACTIVE"
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
"location": "mars"
|
||||
}
|
2
go.sum
2
go.sum
@ -28,7 +28,6 @@ github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
@ -114,7 +113,6 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
|
@ -1,18 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package harness
|
||||
|
||||
type Collector interface {
|
||||
Collect(*MetricRegistry)
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package harness
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const DefaultMetricsPath = "/metrics"
|
||||
|
||||
type ExporterOpts struct {
|
||||
// The representative name of exporter
|
||||
Name string
|
||||
// The version of exporter
|
||||
Version string
|
||||
// The HTTP endpoint path which used to provide metrics
|
||||
MetricsPath string
|
||||
// Whether to call Collect() of collector periodically
|
||||
Tick bool
|
||||
// Whether to reset all metrics per tick
|
||||
ResetOnTick bool
|
||||
// Command line usage
|
||||
Usage string
|
||||
// Additional command line flags which can be accepted
|
||||
Flags []cli.Flag
|
||||
// Function to instantiate collector
|
||||
Init func(*cli.Context, *MetricRegistry) (Collector, error)
|
||||
}
|
||||
|
||||
func NewExporterOpts(name string, version string) *ExporterOpts {
|
||||
return &ExporterOpts{
|
||||
Name: name,
|
||||
Version: version,
|
||||
MetricsPath: DefaultMetricsPath,
|
||||
Tick: true,
|
||||
ResetOnTick: true,
|
||||
Usage: "",
|
||||
}
|
||||
}
|
||||
|
||||
type exporter struct {
|
||||
*ExporterOpts
|
||||
}
|
||||
|
||||
func setupLogging(c *cli.Context) {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
})
|
||||
levelString := c.String("log-level")
|
||||
level, err := log.ParseLevel(levelString)
|
||||
if err != nil {
|
||||
log.Fatalf("could not set log level to '%s';err:<%s>", levelString, err)
|
||||
}
|
||||
log.SetLevel(level)
|
||||
}
|
||||
|
||||
func (exp *exporter) main(c *cli.Context) {
|
||||
setupLogging(c)
|
||||
|
||||
registry := newRegistry()
|
||||
|
||||
collector, err := exp.Init(c, registry)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if exp.Tick {
|
||||
collector.Collect(registry)
|
||||
interval := c.Int("interval")
|
||||
go func() {
|
||||
for range time.Tick(time.Duration(interval) * time.Second) {
|
||||
if exp.ResetOnTick {
|
||||
registry.Reset()
|
||||
}
|
||||
collector.Collect(registry)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Add("Location", exp.MetricsPath)
|
||||
w.WriteHeader(http.StatusFound)
|
||||
})
|
||||
http.Handle(exp.MetricsPath, promhttp.Handler())
|
||||
if err := http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package harness
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultOpts = []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "port",
|
||||
Usage: "The port number used to expose metrics via http",
|
||||
Value: 7979,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Usage: "Set Logging level",
|
||||
Value: "info",
|
||||
},
|
||||
}
|
||||
|
||||
defaultTickOpt = cli.IntFlag{
|
||||
Name: "interval",
|
||||
Usage: "Interval to fetch metrics from the endpoint in second",
|
||||
Value: 60,
|
||||
}
|
||||
)
|
||||
|
||||
func MakeApp(opts *ExporterOpts) *cli.App {
|
||||
exp := &exporter{opts}
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = opts.Name
|
||||
app.Version = opts.Version
|
||||
app.Usage = "A prometheus " + opts.Name
|
||||
app.UsageText = opts.Usage
|
||||
app.Action = exp.main
|
||||
app.Flags = buildOptsWithDefault(opts.Flags, defaultOpts)
|
||||
|
||||
if opts.Tick && !contains(app.Flags, defaultTickOpt) {
|
||||
app.Flags = append(app.Flags, defaultTickOpt)
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func buildOptsWithDefault(opts []cli.Flag, defaultOpts []cli.Flag) []cli.Flag {
|
||||
for _, opt := range defaultOpts {
|
||||
if !contains(opts, opt) {
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func contains(opts []cli.Flag, opt cli.Flag) bool {
|
||||
for _, o := range opts {
|
||||
if o.GetName() == opt.GetName() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Main(opts *ExporterOpts) {
|
||||
err := MakeApp(opts).Run(os.Args)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package harness
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MetricRegistry struct {
|
||||
metrics map[string]prometheus.Collector
|
||||
}
|
||||
|
||||
func newRegistry() *MetricRegistry {
|
||||
return &MetricRegistry{
|
||||
metrics: make(map[string]prometheus.Collector),
|
||||
}
|
||||
}
|
||||
|
||||
func (reg *MetricRegistry) Register(name string, metric prometheus.Collector) {
|
||||
log.Infof("metric registered;name:<%s>", name)
|
||||
reg.metrics[name] = metric
|
||||
prometheus.MustRegister(metric)
|
||||
}
|
||||
|
||||
func (reg *MetricRegistry) Unregister(name string) {
|
||||
if metric := reg.metrics[name]; metric != nil {
|
||||
log.Infof("metric unregistered;name:<%s>", name)
|
||||
prometheus.Unregister(metric)
|
||||
delete(reg.metrics, name)
|
||||
}
|
||||
}
|
||||
|
||||
func (reg *MetricRegistry) Get(name string) prometheus.Collector {
|
||||
return reg.metrics[name]
|
||||
}
|
||||
|
||||
// Since prometheus.MetricVec is a struct but not interface,
|
||||
// need to intrduce an interface to check if we can call Reset() on a metric.
|
||||
type resettable interface {
|
||||
Reset()
|
||||
}
|
||||
|
||||
func (reg *MetricRegistry) Reset() {
|
||||
for name, metric := range reg.metrics {
|
||||
if vec, ok := metric.(resettable); ok {
|
||||
log.Debugf("resetting metric;name:<%s>", name)
|
||||
vec.Reset()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package harness
|
||||
|
||||
const Version = "0.0.1"
|
201
internal/collector.go
Normal file
201
internal/collector.go
Normal file
@ -0,0 +1,201 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/kawamuray/jsonpath" // Originally: "github.com/NickSardo/jsonpath"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type JsonGaugeCollector struct {
|
||||
*prometheus.GaugeVec
|
||||
KeyJsonPath string
|
||||
ValueJsonPath string
|
||||
LabelsJsonPath map[string]string
|
||||
}
|
||||
|
||||
func Scrape(collectors []JsonGaugeCollector, json []byte) {
|
||||
|
||||
for _, collector := range collectors {
|
||||
if collector.ValueJsonPath == "" { // ScrapeType is 'value'
|
||||
|
||||
// Since this is a 'value' type metric, there should be exactly one element in results
|
||||
// If there are more, just return the first one
|
||||
// TODO: Better handling/logging for this scenario
|
||||
floatValue, err := extractValue(json, collector.KeyJsonPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
collector.With(extractLabels(json, collector.LabelsJsonPath)).Set(floatValue)
|
||||
} else { // ScrapeType is 'object'
|
||||
path, err := compilePath(collector.KeyJsonPath)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to compile path: '%s', ERROR: '%s'", collector.KeyJsonPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
eval, err := jsonpath.EvalPathsInBytes(json, []*jsonpath.Path{path})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create evaluator for JSON Path: %s, ERROR: '%s'", collector.KeyJsonPath, err)
|
||||
continue
|
||||
}
|
||||
for {
|
||||
if result, ok := eval.Next(); ok {
|
||||
floatValue, err := extractValue(result.Value, collector.ValueJsonPath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
collector.With(extractLabels(result.Value, collector.LabelsJsonPath)).Set(floatValue)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 += "+"
|
||||
}
|
||||
|
||||
paths, err := jsonpath.ParsePaths(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return paths[0], nil
|
||||
}
|
||||
|
||||
// Returns the first matching float value at the given json path
|
||||
func extractValue(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))
|
||||
}
|
||||
|
||||
// Dynamic value
|
||||
p, err := compilePath(path)
|
||||
if err != nil {
|
||||
return floatValue, fmt.Errorf("Failed to compile path: '%s', ERROR: '%s'", path, err)
|
||||
}
|
||||
|
||||
eval, err := jsonpath.EvalPathsInBytes(json, []*jsonpath.Path{p})
|
||||
if err != nil {
|
||||
return floatValue, fmt.Errorf("Failed to create evaluator for JSON Path: %s, ERROR: '%s'", path, err)
|
||||
}
|
||||
|
||||
result, ok := eval.Next()
|
||||
if result == nil || !ok {
|
||||
if eval.Error != nil {
|
||||
return floatValue, fmt.Errorf("Failed to evaluate json. ERROR: '%s', PATH: '%s', JSON: '%s'", eval.Error, path, string(json))
|
||||
} else {
|
||||
log.Debugf("Could not find path. PATH: '%s', JSON: '%s'", path, string(json))
|
||||
return floatValue, fmt.Errorf("Could not find path. PATH: '%s'", path)
|
||||
}
|
||||
}
|
||||
|
||||
return SanitizeValue(result)
|
||||
}
|
||||
|
||||
func extractLabels(json []byte, l map[string]string) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
for label, path := range l {
|
||||
|
||||
if len(path) < 1 || path[0] != '$' {
|
||||
// Static value
|
||||
labels[label] = path
|
||||
continue
|
||||
}
|
||||
|
||||
// Dynamic value
|
||||
p, err := compilePath(path)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to compile path for label: '%s', PATH: '%s', ERROR: '%s'", label, path, err)
|
||||
labels[label] = ""
|
||||
continue
|
||||
}
|
||||
|
||||
eval, err := jsonpath.EvalPathsInBytes(json, []*jsonpath.Path{p})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create evaluator for JSON Path: %s, ERROR: '%s'", path, err)
|
||||
labels[label] = ""
|
||||
continue
|
||||
}
|
||||
|
||||
result, ok := eval.Next()
|
||||
if result == nil || !ok {
|
||||
if eval.Error != nil {
|
||||
log.Errorf("Failed to evaluate json for label: '%s', ERROR: '%s', PATH: '%s', JSON: '%s'", label, eval.Error, path, string(json))
|
||||
} else {
|
||||
log.Debugf("Could not find path in json for label: '%s', PATH: '%s', JSON: '%s'", label, path, string(json))
|
||||
log.Warnf("Could not find path in json for label: '%s', PATH: '%s'", label, path)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
l, err := strconv.Unquote(string(result.Value))
|
||||
if err == nil {
|
||||
labels[label] = l
|
||||
} else {
|
||||
labels[label] = string(result.Value)
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func FetchJson(ctx context.Context, endpoint string, headers map[string]string) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", endpoint, nil)
|
||||
req = req.WithContext(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating request. ERROR: '%s'", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
if req.Header.Get("Accept") == "" {
|
||||
req.Header.Add("Accept", "application/json")
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch json from endpoint;endpoint:<%s>,err:<%s>", endpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body;err:<%s>", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
@ -11,16 +11,30 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/prometheus-community/json_exporter/harness"
|
||||
"github.com/prometheus-community/json_exporter/jsonexporter"
|
||||
"log"
|
||||
|
||||
"github.com/prometheus-community/json_exporter/config"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
opts := harness.NewExporterOpts("json_exporter", jsonexporter.Version)
|
||||
opts.Usage = "[OPTIONS] HTTP_ENDPOINT CONFIG_PATH"
|
||||
opts.Init = jsonexporter.Init
|
||||
harness.Main(opts)
|
||||
func Init(c *cli.Context) {
|
||||
args := c.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
cli.ShowAppHelp(c) //nolint:errcheck
|
||||
log.Fatalf("Not enought arguments")
|
||||
}
|
||||
|
||||
var (
|
||||
configPath = args[0]
|
||||
)
|
||||
|
||||
_, err := config.LoadConfig(configPath)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load config")
|
||||
}
|
||||
}
|
102
internal/util.go
Normal file
102
internal/util.go
Normal file
@ -0,0 +1,102 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kawamuray/jsonpath"
|
||||
"github.com/prometheus-community/json_exporter/config"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func MakeMetricName(parts ...string) string {
|
||||
return strings.Join(parts, "_")
|
||||
}
|
||||
|
||||
func SanitizeValue(v *jsonpath.Result) (float64, error) {
|
||||
var value float64
|
||||
var boolValue bool
|
||||
var err error
|
||||
switch v.Type {
|
||||
case jsonpath.JsonNumber:
|
||||
value, err = parseValue(v.Value)
|
||||
case jsonpath.JsonString:
|
||||
// If it is a string, lets pull off the quotes and attempt to parse it as a number
|
||||
value, err = parseValue(v.Value[1 : len(v.Value)-1])
|
||||
case jsonpath.JsonNull:
|
||||
value = math.NaN()
|
||||
case jsonpath.JsonBool:
|
||||
if boolValue, err = strconv.ParseBool(string(v.Value)); boolValue {
|
||||
value = 1.0
|
||||
} else {
|
||||
value = 0.0
|
||||
}
|
||||
default:
|
||||
value, err = parseValue(v.Value)
|
||||
}
|
||||
if err != nil {
|
||||
// Should never happen.
|
||||
return -1.0, err
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
|
||||
func parseValue(bytes []byte) (float64, error) {
|
||||
value, err := strconv.ParseFloat(string(bytes), 64)
|
||||
if err != nil {
|
||||
return -1.0, fmt.Errorf("failed to parse value as float;value:<%s>", bytes)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func CreateMetricsList(r *prometheus.Registry, c config.Config) ([]JsonGaugeCollector, error) {
|
||||
var metrics []JsonGaugeCollector
|
||||
for _, metric := range c.Metrics {
|
||||
switch metric.Type {
|
||||
case config.ValueScrape:
|
||||
jsonCollector := JsonGaugeCollector{
|
||||
GaugeVec: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: metric.Name,
|
||||
Help: metric.Help,
|
||||
}, metric.LabelNames()),
|
||||
KeyJsonPath: metric.Path,
|
||||
LabelsJsonPath: metric.Labels,
|
||||
}
|
||||
r.MustRegister(jsonCollector)
|
||||
metrics = append(metrics, jsonCollector)
|
||||
case config.ObjectScrape:
|
||||
for subName, valuePath := range metric.Values {
|
||||
name := MakeMetricName(metric.Name, subName)
|
||||
jsonCollector := JsonGaugeCollector{
|
||||
GaugeVec: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: name,
|
||||
Help: name,
|
||||
}, metric.LabelNames()),
|
||||
KeyJsonPath: metric.Path,
|
||||
ValueJsonPath: valuePath,
|
||||
LabelsJsonPath: metric.Labels,
|
||||
}
|
||||
r.MustRegister(jsonCollector)
|
||||
metrics = append(metrics, jsonCollector)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown metric type: '%s', for metric: '%s'", metric.Type, metric.Name)
|
||||
}
|
||||
}
|
||||
return metrics, nil
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package jsonexporter
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/prometheus/common/version"
|
@ -1,96 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package jsonexporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/kawamuray/jsonpath" // Originally: "github.com/NickSardo/jsonpath"
|
||||
"github.com/prometheus-community/json_exporter/harness"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Collector struct {
|
||||
Endpoint string
|
||||
scrapers []JsonScraper
|
||||
}
|
||||
|
||||
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 += "+"
|
||||
}
|
||||
|
||||
paths, err := jsonpath.ParsePaths(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return paths[0], nil
|
||||
}
|
||||
|
||||
func compilePaths(paths map[string]string) (map[string]*jsonpath.Path, error) {
|
||||
compiledPaths := make(map[string]*jsonpath.Path)
|
||||
for name, value := range paths {
|
||||
if len(value) < 1 || value[0] != '$' {
|
||||
// Static value
|
||||
continue
|
||||
}
|
||||
compiledPath, err := compilePath(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse path;path:<%s>,err:<%s>", value, err)
|
||||
}
|
||||
compiledPaths[name] = compiledPath
|
||||
}
|
||||
return compiledPaths, nil
|
||||
}
|
||||
|
||||
func NewCollector(endpoint string, scrapers []JsonScraper) *Collector {
|
||||
return &Collector{
|
||||
Endpoint: endpoint,
|
||||
scrapers: scrapers,
|
||||
}
|
||||
}
|
||||
|
||||
func (col *Collector) fetchJson() ([]byte, error) {
|
||||
resp, err := http.Get(col.Endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch json from endpoint;endpoint:<%s>,err:<%s>", col.Endpoint, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body;err:<%s>", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (col *Collector) Collect(reg *harness.MetricRegistry) {
|
||||
json, err := col.fetchJson()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, scraper := range col.scrapers {
|
||||
if err := scraper.Scrape(json, reg); err != nil {
|
||||
log.Errorf("error while scraping json;err:<%s>", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package jsonexporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name"`
|
||||
Path string `yaml:"path"`
|
||||
Labels map[string]string `yaml:"labels"`
|
||||
Type string `yaml:"type"`
|
||||
Help string `yaml:"help"`
|
||||
Values map[string]string `yaml:"values"`
|
||||
}
|
||||
|
||||
func (config *Config) labelNames() []string {
|
||||
labelNames := make([]string, 0, len(config.Labels))
|
||||
for name := range config.Labels {
|
||||
labelNames = append(labelNames, name)
|
||||
}
|
||||
return labelNames
|
||||
}
|
||||
|
||||
func loadConfig(configPath string) ([]*Config, error) {
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load config;path:<%s>,err:<%s>", configPath, err)
|
||||
}
|
||||
|
||||
var configs []*Config
|
||||
if err := yaml.Unmarshal(data, &configs); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse yaml;err:<%s>", err)
|
||||
}
|
||||
// Complete defaults
|
||||
for _, config := range configs {
|
||||
if config.Type == "" {
|
||||
config.Type = DefaultScrapeType
|
||||
}
|
||||
if config.Help == "" {
|
||||
config.Help = config.Name
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package jsonexporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus-community/json_exporter/harness"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type ScrapeType struct {
|
||||
Configure func(*Config, *harness.MetricRegistry)
|
||||
NewScraper func(*Config) (JsonScraper, error)
|
||||
}
|
||||
|
||||
var ScrapeTypes = map[string]*ScrapeType{
|
||||
"object": {
|
||||
Configure: func(config *Config, reg *harness.MetricRegistry) {
|
||||
for subName := range config.Values {
|
||||
name := harness.MakeMetricName(config.Name, subName)
|
||||
reg.Register(
|
||||
name,
|
||||
prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: name,
|
||||
Help: config.Help + " - " + subName,
|
||||
}, config.labelNames()),
|
||||
)
|
||||
}
|
||||
},
|
||||
NewScraper: NewObjectScraper,
|
||||
},
|
||||
"value": {
|
||||
Configure: func(config *Config, reg *harness.MetricRegistry) {
|
||||
reg.Register(
|
||||
config.Name,
|
||||
prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: config.Name,
|
||||
Help: config.Help,
|
||||
}, config.labelNames()),
|
||||
)
|
||||
},
|
||||
NewScraper: NewValueScraper,
|
||||
},
|
||||
}
|
||||
|
||||
var DefaultScrapeType = "value"
|
||||
|
||||
func Init(c *cli.Context, reg *harness.MetricRegistry) (harness.Collector, error) {
|
||||
args := c.Args()
|
||||
|
||||
if len(args) < 2 {
|
||||
cli.ShowAppHelp(c) //nolint:errcheck
|
||||
return nil, fmt.Errorf("not enough arguments")
|
||||
}
|
||||
|
||||
var (
|
||||
endpoint = args[0]
|
||||
configPath = args[1]
|
||||
)
|
||||
|
||||
configs, err := loadConfig(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scrapers := make([]JsonScraper, len(configs))
|
||||
for i, config := range configs {
|
||||
tpe := ScrapeTypes[config.Type]
|
||||
if tpe == nil {
|
||||
return nil, fmt.Errorf("unknown scrape type;type:<%s>", config.Type)
|
||||
}
|
||||
tpe.Configure(config, reg)
|
||||
scraper, err := tpe.NewScraper(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create scraper;name:<%s>,err:<%s>", config.Name, err)
|
||||
}
|
||||
scrapers[i] = scraper
|
||||
}
|
||||
|
||||
return NewCollector(endpoint, scrapers), nil
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
// Copyright 2020 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package jsonexporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/kawamuray/jsonpath" // Originally: "github.com/NickSardo/jsonpath"
|
||||
"github.com/prometheus-community/json_exporter/harness"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type JsonScraper interface {
|
||||
Scrape(data []byte, reg *harness.MetricRegistry) error
|
||||
}
|
||||
|
||||
type ValueScraper struct {
|
||||
*Config
|
||||
valueJsonPath *jsonpath.Path
|
||||
}
|
||||
|
||||
func NewValueScraper(config *Config) (JsonScraper, error) {
|
||||
valuepath, err := compilePath(config.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse path;path:<%s>,err:<%s>", config.Path, err)
|
||||
}
|
||||
|
||||
scraper := &ValueScraper{
|
||||
Config: config,
|
||||
valueJsonPath: valuepath,
|
||||
}
|
||||
return scraper, nil
|
||||
}
|
||||
|
||||
func (vs *ValueScraper) parseValue(bytes []byte) (float64, error) {
|
||||
value, err := strconv.ParseFloat(string(bytes), 64)
|
||||
if err != nil {
|
||||
return -1.0, fmt.Errorf("failed to parse value as float;value:<%s>", bytes)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (vs *ValueScraper) forTargetValue(data []byte, handle func(*jsonpath.Result)) error {
|
||||
eval, err := jsonpath.EvalPathsInBytes(data, []*jsonpath.Path{vs.valueJsonPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to eval jsonpath;path:<%v>,json:<%s>", vs.valueJsonPath, data)
|
||||
}
|
||||
|
||||
for {
|
||||
result, ok := eval.Next()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
handle(result)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vs *ValueScraper) Scrape(data []byte, reg *harness.MetricRegistry) error {
|
||||
isFirst := true
|
||||
return vs.forTargetValue(data, func(result *jsonpath.Result) {
|
||||
if !isFirst {
|
||||
log.Infof("ignoring non-first value;path:<%v>", vs.valueJsonPath)
|
||||
return
|
||||
}
|
||||
isFirst = false
|
||||
|
||||
var value float64
|
||||
var boolValue bool
|
||||
var err error
|
||||
switch result.Type {
|
||||
case jsonpath.JsonNumber:
|
||||
value, err = vs.parseValue(result.Value)
|
||||
case jsonpath.JsonString:
|
||||
// If it is a string, lets pull off the quotes and attempt to parse it as a number
|
||||
value, err = vs.parseValue(result.Value[1 : len(result.Value)-1])
|
||||
case jsonpath.JsonNull:
|
||||
value = math.NaN()
|
||||
case jsonpath.JsonBool:
|
||||
if boolValue, err = strconv.ParseBool(string(result.Value)); boolValue {
|
||||
value = 1
|
||||
} else {
|
||||
value = 0
|
||||
}
|
||||
default:
|
||||
log.Warnf("skipping not numerical result;path:<%v>,value:<%s>",
|
||||
vs.valueJsonPath, result.Value)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// Should never happen.
|
||||
log.Errorf("could not parse numerical value as float;path:<%v>,value:<%s>",
|
||||
vs.valueJsonPath, result.Value)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("metric updated;name:<%s>,labels:<%s>,value:<%.2f>", vs.Name, vs.Labels, value)
|
||||
reg.Get(vs.Name).(*prometheus.GaugeVec).With(vs.Labels).Set(value)
|
||||
})
|
||||
}
|
||||
|
||||
type ObjectScraper struct {
|
||||
*ValueScraper
|
||||
labelJsonPaths map[string]*jsonpath.Path
|
||||
valueJsonPaths map[string]*jsonpath.Path
|
||||
}
|
||||
|
||||
func NewObjectScraper(config *Config) (JsonScraper, error) {
|
||||
valueScraper, err := NewValueScraper(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
labelPaths, err := compilePaths(config.Labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valuePaths, err := compilePaths(config.Values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scraper := &ObjectScraper{
|
||||
ValueScraper: valueScraper.(*ValueScraper),
|
||||
labelJsonPaths: labelPaths,
|
||||
valueJsonPaths: valuePaths,
|
||||
}
|
||||
return scraper, nil
|
||||
}
|
||||
|
||||
func (obsc *ObjectScraper) newLabels() map[string]string {
|
||||
labels := make(map[string]string)
|
||||
for name, value := range obsc.Labels {
|
||||
if _, ok := obsc.labelJsonPaths[name]; !ok {
|
||||
// Static label value.
|
||||
labels[name] = value
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func (obsc *ObjectScraper) extractFirstValue(data []byte, path *jsonpath.Path) (*jsonpath.Result, error) {
|
||||
eval, err := jsonpath.EvalPathsInBytes(data, []*jsonpath.Path{path})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to eval jsonpath;err:<%s>", err)
|
||||
}
|
||||
|
||||
result, ok := eval.Next()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no value found for path")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (obsc *ObjectScraper) Scrape(data []byte, reg *harness.MetricRegistry) error {
|
||||
return obsc.forTargetValue(data, func(result *jsonpath.Result) {
|
||||
if result.Type != jsonpath.JsonObject && result.Type != jsonpath.JsonArray {
|
||||
log.Warnf("skipping not structual result;path:<%v>,value:<%s>",
|
||||
obsc.valueJsonPath, result.Value)
|
||||
return
|
||||
}
|
||||
|
||||
labels := obsc.newLabels()
|
||||
for name, path := range obsc.labelJsonPaths {
|
||||
firstResult, err := obsc.extractFirstValue(result.Value, path)
|
||||
if err != nil {
|
||||
log.Warnf("could not find value for label path;path:<%v>,json:<%s>,err:<%s>", path, result.Value, err)
|
||||
continue
|
||||
}
|
||||
value := firstResult.Value
|
||||
if firstResult.Type == jsonpath.JsonString {
|
||||
// Strip quotes
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
labels[name] = string(value)
|
||||
}
|
||||
|
||||
for name, configValue := range obsc.Values {
|
||||
var metricValue float64
|
||||
path := obsc.valueJsonPaths[name]
|
||||
|
||||
if path == nil {
|
||||
// Static value
|
||||
value, err := obsc.parseValue([]byte(configValue))
|
||||
if err != nil {
|
||||
log.Errorf("could not use configured value as float number;err:<%s>", err)
|
||||
continue
|
||||
}
|
||||
metricValue = value
|
||||
} else {
|
||||
// Dynamic value
|
||||
firstResult, err := obsc.extractFirstValue(result.Value, path)
|
||||
if err != nil {
|
||||
log.Warnf("could not find value for value path;path:<%v>,json:<%s>,err:<%s>", path, result.Value, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var value float64
|
||||
var boolValue bool
|
||||
switch firstResult.Type {
|
||||
case jsonpath.JsonNumber:
|
||||
value, err = obsc.parseValue(firstResult.Value)
|
||||
case jsonpath.JsonString:
|
||||
// If it is a string, lets pull off the quotes and attempt to parse it as a number
|
||||
value, err = obsc.parseValue(firstResult.Value[1 : len(firstResult.Value)-1])
|
||||
case jsonpath.JsonNull:
|
||||
value = math.NaN()
|
||||
case jsonpath.JsonBool:
|
||||
if boolValue, err = strconv.ParseBool(string(firstResult.Value)); boolValue {
|
||||
value = 1.0
|
||||
} else {
|
||||
value = 0.0
|
||||
}
|
||||
default:
|
||||
log.Warnf("skipping not numerical result;path:<%v>,value:<%s>",
|
||||
obsc.valueJsonPath, result.Value)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
// Should never happen.
|
||||
log.Errorf("could not parse numerical value as float;path:<%v>,value:<%s>",
|
||||
obsc.valueJsonPath, firstResult.Value)
|
||||
continue
|
||||
}
|
||||
metricValue = value
|
||||
}
|
||||
|
||||
fqn := harness.MakeMetricName(obsc.Name, name)
|
||||
log.Debugf("metric updated;name:<%s>,labels:<%s>,value:<%.2f>", fqn, labels, metricValue)
|
||||
reg.Get(fqn).(*prometheus.GaugeVec).With(labels).Set(metricValue)
|
||||
}
|
||||
})
|
||||
}
|
@ -11,12 +11,17 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package harness
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"os"
|
||||
|
||||
"github.com/prometheus-community/json_exporter/cmd"
|
||||
)
|
||||
|
||||
func MakeMetricName(parts ...string) string {
|
||||
return strings.Join(parts, "_")
|
||||
func main() {
|
||||
err := cmd.MakeApp().Run(os.Args)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user