Writing Prometheus Exporter Using Go

Exporter name

Think about name of the exporter, existing go exporter names can be found here

Lets create volume_exporter which basically exports metrics about the volume (Total , Free And Used)

Here is an example of how this can be done

Initialize Project

Create module

E:\practices\Go\volume_exporter>go mod init github.com/mnadeem/volume_exporter
go: creating new go.mod: module github.com/mnadeem/volume_exporter

E:\practices\Go\volume_exporter>

get packages

E:\practices\Go\volume_exporter>go get github.com/prometheus/client_golang 
go: downloading github.com/prometheus/client_golang v1.9.0
go: github.com/prometheus/client_golang upgrade => v1.9.0

Create main file main.go

The following would run the http server on port 9888 and expose metrics endpoint /metrics

package main

import (
	"flag"
	"log"
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	listenAddress = flag.String("web.listen-address", ":9888", "Address to listen on for web interface.")
	metricPath    = flag.String("web.metrics-path", "/metrics", "Path under which to expose metrics.")
)

func main() {
	log.Fatal(serverMetrics(*listenAddress, *metricPath))
}

func serverMetrics(listenAddress, metricsPath string) error {
	http.Handle(metricsPath, promhttp.Handler())
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`
			<html>
			<head><title>Volume Exporter Metrics</title></head>
			<body>
			<h1>ConfigMap Reload</h1>
			<p><a href='` + metricsPath + `'>Metrics</a></p>
			</body>
			</html>
		`))
	})
	return http.ListenAndServe(listenAddress, nil)
}

go run main.go

Identify the Options for Exporter

FlagDescription
web.listen-addressAddress to listen on for web interface and telemetry. Default is 9888
web.telemetry-pathPath under which to expose metrics. Default is /metrics
volume-dirvolumes to report, the format is volumeName:VolumeDir
For example ==> logs:/app/logs
you can use this flag multiple times to provide multiple volumes

Identify the Metrics to Export

metricsTypeDescription
volume_bytes_total{name=”someName”, path=”/some/path”}GaugeTotal size of the volume/disk
volume_bytes_free{name=”someName”, path=”/some/path”}GaugeFree size of the volume/disk
volume_bytes_used{name=”someName”, path=”/some/path”}GaugeUsed size of volume/disk

Implementation

Options

We will use flag library to parse command line flags

Exporter

There is two types to data exposed by exporters to prometheus

First one is metrics definition (name, definition and type) and the second one is Metric Value

If we analyze Prometheus Collector, this is what is expected as well, when ever prometheus calls the metrics endpoint, the following two methods would be invoked. First one basically describes the metrics while the other one collects the metrics values.

// Collector is the interface implemented by anything that can be used by
// Prometheus to collect metrics. A Collector has to be registered for
// collection. See Registerer.Register.
//
// The stock metrics provided by this package (Gauge, Counter, Summary,
// Histogram, Untyped) are also Collectors (which only ever collect one metric,
// namely itself). An implementer of Collector may, however, collect multiple
// metrics in a coordinated fashion and/or create metrics on the fly. Examples
// for collectors already implemented in this library are the metric vectors
// (i.e. collection of multiple instances of the same Metric but with different
// label values) like GaugeVec or SummaryVec, and the ExpvarCollector.
type Collector interface {
	// Describe sends the super-set of all possible descriptors of metrics
	// collected by this Collector to the provided channel and returns once
	// the last descriptor has been sent. The sent descriptors fulfill the
	// consistency and uniqueness requirements described in the Desc
	// documentation.
	//
	// It is valid if one and the same Collector sends duplicate
	// descriptors. Those duplicates are simply ignored. However, two
	// different Collectors must not send duplicate descriptors.
	//
	// 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.
	//
	// This method idempotently sends the same descriptors throughout the
	// lifetime of the Collector. It may be called concurrently and
	// therefore must be implemented in a concurrency safe way.
	//
	// If a Collector encounters an error while executing this method, it
	// must send an invalid descriptor (created with NewInvalidDesc) to
	// signal the error to the registry.
	Describe(chan<- *Desc)
	// Collect is called by the Prometheus registry when collecting
	// metrics. The implementation sends each collected metric via the
	// provided channel and returns once the last metric has been sent. The
	// descriptor of each sent metric is one of those returned by Describe
	// (unless the Collector is unchecked, see above). Returned metrics that
	// share the same descriptor must differ in their variable label
	// values.
	//
	// This method may be called concurrently and must therefore be
	// implemented in a concurrency safe way. Blocking occurs at the expense
	// of total performance of rendering all registered metrics. Ideally,
	// Collector implementations support concurrent readers.
	Collect(chan<- Metric)
}

Lets first create exporter package and volume_exporter.go file

If we have more than one metrics to expose, it is always better to group them by Structure.

Lets define our volumeCollector Struct having descriptors

//Define a struct for you collector that contains pointers
//to prometheus descriptors for each metric you wish to expose.
//Note you can also include fields of other types if they provide utility
//but we just won't be exposing them as metrics.
type volumeCollector struct {
	volumeBytesTotal *prometheus.Desc
	volumeBytesFree  *prometheus.Desc
	volumeBytesUsed  *prometheus.Desc
}

Lets define the factory method that returns the structure

//You must create a constructor for you collector that
//initializes every descriptor and returns a pointer to the collector
func newVolumeCollector() *volumeCollector {
	return &volumeCollector{
		volumeBytesTotal: prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "bytes_total"),
			"Total size of the volume/disk",
			[]string{"name", "path"}, nil,
		),
		volumeBytesFree: prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "bytes_free"),
			"Free size of the volume/disk",
			[]string{"name", "path"}, nil,
		),
		volumeBytesUsed: prometheus.NewDesc(prometheus.BuildFQName(namespace, "", "bytes_used"),
			"Used size of volume/disk",
			[]string{"name", "path"}, nil,
		),
	}
}

Implement Describe method on Exporter

//Each and every collector must implement the Describe function.
//It essentially writes all descriptors to the prometheus desc channel.
func (collector *volumeCollector) Describe(ch chan<- *prometheus.Desc) {

	//Update this section with the each metric you create for a given collector
	ch <- collector.volumeBytesTotal
	ch <- collector.volumeBytesFree
	ch <- collector.volumeBytesUsed
}

Implement Collect method on Exporter

//Collect implements required collect function for all promehteus collectors
func (collector *volumeCollector) Collect(ch chan<- prometheus.Metric) {

	//Implement logic here to determine proper metric value to return to prometheus
	//for each descriptor or call other functions that do so.
	var metricValue float64
	if 1 == 1 {
		metricValue = 1
	}

	//Write latest value for each metric in the prometheus metric channel.
	//Note that you can pass CounterValue, GaugeValue, or UntypedValue types here.
	ch <- prometheus.MustNewConstMetric(collector.volumeBytesTotal, prometheus.GaugeValue, metricValue, "log", "path")
	ch <- prometheus.MustNewConstMetric(collector.volumeBytesFree, prometheus.GaugeValue, metricValue, "log", "path")
	ch <- prometheus.MustNewConstMetric(collector.volumeBytesUsed, prometheus.GaugeValue, metricValue, "log", "path")
}

Lets define a method so that other packages can talk to exporter

func Register() {
	collector := newVolumeCollector()
	prometheus.MustRegister(collector)
}

Now if you run

go run main.go -volume-dir=abc:/abc -volume-dir=pqr:/pqr

Continue implementing the logic in Collect method to populate the value dynamically.

And finally

go mod tidy

Here is the fully working exporter

Up And Running

Locally

go run main.go --volume-dir=practices:E:\practices
docker run --rm -p 9889:9888 -it mnadeem/volume_exporter --volume-dir=bin:/bin

Add as a side container to existing deployments

        - name: volume-exporter
          image:  mnadeem/volume_exporter
          imagePullPolicy: "Always"
          args:
            - --volume-dir=prometheus:/prometheus
          ports:
          - name: metrics-volume
            containerPort: 9888
          volumeMounts:
          - mountPath: /prometheus
            name: prometheus-data
            readOnly: true

Next

Enable CI/CD for the project

References

One thought on “Writing Prometheus Exporter Using Go

Leave a comment