git » prometheus-expvar-exporter » main » tree

[main] / main.go

// prometheus-expvar-exporter collects expvar metrics from different sources,
// and exports them for Prometheus.
package main

import (
	"crypto/tls"
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"regexp"
	"strings"
	"unicode"

	"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{},
		}

		if config.Has(t + ".insecure") {
			c.insecure = config.Get(t + ".insecure").(bool)
		}

		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

	// Disable TLS checking for this URL.
	insecure bool

	// 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) GetURL() (*http.Response, error) {
	client := &http.Client{}
	if c.insecure {
		client.Transport = &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		}
	}
	return client.Get(c.url)
}

func (c *Collector) Collect(ch chan<- prometheus.Metric) {
	resp, err := c.GetURL()
	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 := sanitizeMetricName(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))
}

func sanitizeMetricName(n string) string {
	// Prometheus metric names must match the regex
	// `[a-zA-Z_:][a-zA-Z0-9_:]*`.
	// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
	//
	// This function replaces all non-matching ASCII characters with
	// underscores.
	//
	// In particular, it is common that expvar names contain `/` or `-`, which
	// we replace with `_` so they end up resembling more Prometheus-ideomatic
	// names.
	//
	// Non-ascii characters are not supported, and will panic as so to force
	// users to handle them explicitly.  There is no good way to handle all of
	// them automatically, as they can't be all reasonably mapped to ascii. In
	// the future, we may handle _some_ of them automatically when possible.
	// But for now, forcing the users to be explicit is the safest option, and
	// also ensures forwards compatibility.
	return strings.Map(func(r rune) rune {
		if r >= 'a' && r <= 'z' {
			return r
		}
		if r >= 'A' && r <= 'Z' {
			return r
		}
		if r >= '0' && r <= '9' {
			return r
		}
		if r == '_' || r == ':' {
			return r
		}
		if r > unicode.MaxASCII {
			panic(fmt.Sprintf(
				"non-ascii character %q is unsupported, please configure the metric %q explicitly",
				r, n))
		}
		return '_'
	}, n)
}

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))
}