Prometheus custom exporters are the secret sauce for getting your application’s internal metrics into Prometheus when the standard exporters don’t cut it.
Let’s build one. We’ll make a Go exporter that exposes the current number of goroutines running in our application. This is a surprisingly useful metric for understanding application health and resource utilization.
Here’s the core of it:
package main
import (
"fmt"
"net/http"
"runtime"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
goroutineCount = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "myapp_goroutines_current",
Help: "Current number of goroutines running in the application.",
})
)
func init() {
// Register the metric
prometheus.MustRegister(goroutineCount)
}
func updateGoroutineCount() {
for {
// Get the current goroutine count
count := float64(runtime.NumGoroutine())
goroutineCount.Set(count)
// Update every 15 seconds
time.Sleep(15 * time.Second)
}
}
func main() {
// Start a goroutine to continuously update the metric
go updateGoroutineCount()
// Expose the metrics endpoint
http.Handle("/metrics", promhttp.Handler())
fmt.Println("Starting exporter on :9091")
// Listen on port 9091
err := http.ListenAndServe(":9091", nil)
if err != nil {
fmt.Printf("Error starting exporter: %v\n", err)
}
}
This code does a few key things:
- Defines a Metric: We create a
prometheus.Gaugenamedmyapp_goroutines_current. Gauges are perfect for values that can go up or down, like a count. - Registers the Metric:
prometheus.MustRegistertells Prometheus about our metric. - Updates the Metric: The
updateGoroutineCountfunction runs in its own goroutine. It repeatedly callsruntime.NumGoroutine()to get the current count and updates our gauge. - Exposes Metrics: The
mainfunction sets up an HTTP server on port 9091. It maps the/metricsendpoint topromhttp.Handler(), which is the standard way to serve Prometheus metrics.
To run this, save it as main.go, make sure you have the Prometheus Go client library:
go mod init myapp_exporter
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp
Then, build and run:
go build -o myapp_exporter
./myapp_exporter
Now, if you visit http://localhost:9091/metrics in your browser, you’ll see output like this:
# HELP myapp_goroutines_current Current number of goroutines running in the application.
# TYPE myapp_goroutines_current gauge
myapp_goroutines_current 7
The number 7 will change as your application’s goroutine count fluctuates.
To get Prometheus to scrape this, you’d add a job to your prometheus.yml:
scrape_configs:
- job_name: 'myapp'
static_configs:
- targets: ['localhost:9091']
And then reload Prometheus. You’ll see your myapp_goroutines_current metric appear in the Prometheus UI.
The real power comes when you start exposing more complex application-specific metrics. For instance, instead of just a count, you might want to expose metrics about the latency of specific internal functions, the number of items in a particular queue, or the status of connections to external services. You’d use prometheus.Counter, prometheus.Summary, or prometheus.Histogram for those, depending on the data’s nature.
The promhttp.Handler() is clever. It doesn’t just serve raw data; it formats it according to the Prometheus exposition format. This means you don’t have to worry about line endings, comments, or the # HELP and # TYPE directives. The client library handles all of that for you, translating your Go metric objects into the text-based format Prometheus expects. When you call Set() on a gauge or Observe() on a histogram, the library updates the internal state of that metric, ready to be served.
When you’re designing your exporter, think about the lifecycle of your application. If your exporter relies on other services or internal components, you need to ensure those dependencies are healthy before your exporter can report accurate metrics. For example, if your exporter queries a database for a count, what happens if the database is down? Your exporter should ideally report a zero count or a specific error metric for that situation, rather than crashing or reporting stale data. The runtime.NumGoroutine() example is simple because it’s a self-contained metric, but real-world exporters often involve more complex data gathering.