// prometheus-expvar-exporter collects expvar metrics from different sources,
// and exports them for Prometheus.
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"regexp"
"strings"
"github.com/pelletier/go-toml"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
configPath = flag.String("config", "config.toml",
"configuration file")
)
func main() {
flag.Parse()
config, err := toml.LoadFile(*configPath)
if err != nil {
log.Fatalf("error loading config file %q: %v", *configPath, err)
}
for _, t := range config.Keys() {
if !config.Has(t + ".url") {
continue
}
c := &Collector{
url: config.Get(t + ".url").(string),
names: map[string]string{},
helps: map[string]string{},
labelName: map[string]string{},
}
mnames := config.GetDefault(t+".m", &toml.Tree{}).(*toml.Tree).Keys()
for _, name := range mnames {
info := config.Get(t + ".m." + name).(*toml.Tree)
expvar := info.Get("expvar").(string)
c.names[expvar] = name
if info.Has("help") {
c.helps[expvar] = info.Get("help").(string)
}
if info.Has("label_name") {
c.labelName[expvar] = info.Get("label_name").(string)
}
}
log.Printf("Collecting %q\n", c.url)
prometheus.MustRegister(c)
}
http.HandleFunc("/", indexHandler)
http.Handle("/metrics", promhttp.Handler())
if !config.Has("listen_addr") {
log.Fatal("Configuration has no listen_addr")
}
addr := config.Get("listen_addr").(string)
log.Printf("Listening on %q", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
type Collector struct {
// URL to collect from.
url string
// expvar -> prometheus name
names map[string]string
// expvar -> prometheus help
helps map[string]string
// expvar -> prometheus label name
labelName map[string]string
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
// Not returning anything is explicitly allowed, and seems to fit our use
// case.
// From the documentation:
// Sending no descriptor at all marks the Collector as “unchecked”, i.e.
// no checks will be performed at registration time, and the Collector
// may yield any Metric it sees fit in its Collect method/
return
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
resp, err := http.Get(c.url)
if err != nil {
log.Printf("Error scraping %q: %v", c.url, err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading body of %q: %v", c.url, err)
return
}
// Replace "\xNN" with "?" because the default parser doesn't handle them
// well.
re := regexp.MustCompile(`\\x..`)
body = re.ReplaceAllFunc(body, func(s []byte) []byte {
return []byte("?")
})
var vs map[string]interface{}
err = json.Unmarshal(body, &vs)
if err != nil {
log.Printf("Error unmarshalling json from %q: %v", c.url, err)
return
}
for k, v := range vs {
name := strings.ReplaceAll(k, "/", "_")
if n, ok := c.names[k]; ok {
name = n
}
help := fmt.Sprintf("expvar %q", k)
if h, ok := c.helps[k]; ok {
help = h
}
lnames := []string{}
if ln, ok := c.labelName[k]; ok {
lnames = append(lnames, ln)
}
desc := prometheus.NewDesc(name, help, lnames, nil)
switch v := v.(type) {
case float64:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, v)
case bool:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(v))
case map[string]interface{}:
// We only support explicitly written label names.
if len(lnames) != 1 {
continue
}
for lk, lv := range v {
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(lv), lk)
}
case string:
// Not supported by Prometheus.
continue
case []interface{}:
// Not supported by Prometheus.
continue
default:
// TODO: support nested labels / richer structures?
//fmt.Printf("Not supported: %q %#v\n", name, v)
continue
}
}
}
func valToFloat(v interface{}) float64 {
switch v := v.(type) {
case float64:
return v
case bool:
if v {
return 1.0
}
return 0.0
}
panic(fmt.Sprintf("unexpected value type: %#v", v))
}
const indexHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>expvar exporter</title>
</head>
<body>
<h1>Prometheus expvar exporter</h1>
This is a <a href="https://prometheus.io">Prometheus</a>
<a href="https://prometheus.io/docs/instrumenting/exporters/">exporter</a>,
takes <a href="https://golang.org/pkg/expvar/">expvars</a> and converts
them to Prometheus metrics.<p>
Go to <tt><a href="/metrics">/metrics</a></tt> to see the exported metrics.
</body>
</html
`
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(indexHTML))
}