Skip to content

Commit

Permalink
Refactor the complete code
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
rustycl0ck committed Aug 4, 2020
1 parent 2c09f42 commit 4ea7ef2
Show file tree
Hide file tree
Showing 20 changed files with 579 additions and 806 deletions.
12 changes: 3 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
142 changes: 142 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -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 changes: 88 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions example/config.yml → examples/config.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,3 +18,6 @@
active: 1 # static value
count: $.count # dynamic value
boolean: $.some_boolean

headers:
X-Dummy: my-test-header
5 changes: 3 additions & 2 deletions example/data.json → examples/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"count": 3,
"some_boolean": false,
"state": "ACTIVE"
},
]
}
],
"location": "mars"
}
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
18 changes: 0 additions & 18 deletions harness/collector.go

This file was deleted.

Loading

0 comments on commit 4ea7ef2

Please sign in to comment.