Bridging Prometheus¶
Code instrumented with client_golang joins systemd's report framework with
two lines: wrap the registry in prombridge.New and register it on a
provider. The same registry can keep serving /metrics.
p, err := provider.New("io.example.Api")
if err != nil {
return err
}
p.Register(prombridge.New(prometheus.DefaultGatherer))
go p.Serve(ctx)
prombridge.New takes any prometheus.Gatherer: a *prometheus.Registry,
prometheus.DefaultGatherer, or a prometheus.Gatherers list. Each List or
Describe call gathers the registry once, so values are always current.
A complete example¶
examples/prometheus-bridge is an HTTP service instrumented with
client_golang that publishes its registry through a provider:
// Command prometheus-bridge is an HTTP service instrumented with the
// Prometheus client library that also publishes its metrics through
// systemd's report framework, without re-instrumenting: the provider reads
// the same registry the /metrics handler serves.
//
// go run ./examples/prometheus-bridge -dir /tmp/report
// curl -s localhost:8080/hello
// go run ./cmd/sdmetrics-exporter -once -dir /tmp/report
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rossedman/sdmetrics/provider"
"github.com/rossedman/sdmetrics/provider/prombridge"
)
func main() {
dir := flag.String("dir", provider.DefaultDir, "report directory")
addr := flag.String("listen", "127.0.0.1:8080", "HTTP address")
flag.Parse()
// Existing instrumentation, unchanged.
reg := prometheus.NewRegistry()
reg.MustRegister(collectors.NewGoCollector())
requests := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total", Help: "HTTP requests served",
}, []string{"path", "code"})
latency := prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "http_request_duration_seconds", Help: "HTTP request latency", Buckets: prometheus.DefBuckets,
})
reg.MustRegister(requests, latency)
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
t := prometheus.NewTimer(latency)
defer t.ObserveDuration()
requests.WithLabelValues(r.URL.Path, "200").Inc()
fmt.Fprintln(w, "hello")
})
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
// The bridge: two lines.
p, err := provider.New("io.example.Api", provider.WithDir(*dir), provider.WithSocketMode(0o666))
if err != nil {
log.Fatal(err)
}
p.Register(prombridge.New(reg))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := p.Serve(ctx); err != nil {
log.Fatal(err)
}
}()
srv := &http.Server{Addr: *addr, Handler: mux}
go func() { <-ctx.Done(); srv.Close() }()
log.Printf("serving http://%s/hello and %s", *addr, p.Path())
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
go run ./examples/prometheus-bridge -dir /tmp/report &
curl -s localhost:8080/hello
go run ./cmd/sdmetrics-exporter -once -dir /tmp/report -include io.example.Api
How types map¶
| Prometheus | systemd families | Fields |
|---|---|---|
Counter name |
counter name |
labels |
Gauge name |
gauge name |
labels |
Untyped name |
gauge name |
labels |
Histogram name |
counter name_bucket, counter name_sum, counter name_count |
labels; le on buckets |
Gauge histogram name |
the same three, as gauges | labels; le on buckets |
Summary name |
gauge name, counter name_sum, counter name_count |
labels; quantile on name |
This is the Prometheus text format's own layout, so dashboards built on the
classic histogram series keep working. A +Inf bucket is added when the
histogram doesn't report one. Counter values that are whole numbers are
published as integers, like systemd's own counters; others keep their
fraction.
For the example above, the families are:
io.example.Api.http_requests_total counter {code, path}
io.example.Api.http_request_duration_seconds_bucket counter {le}
io.example.Api.http_request_duration_seconds_sum counter
io.example.Api.http_request_duration_seconds_count counter
io.example.Api.go_goroutines gauge
...
How names map¶
Prometheus names that are valid Varlink field names (a letter, then letters, digits and single underscores) are kept as they are. Others are sanitized:
| Rule | Example |
|---|---|
Characters other than letters and digits become _ |
http:requests → http_requests |
Runs of _ collapse |
go__gc → go_gc |
Leading and trailing _ are dropped |
_internal_ → internal |
A leading digit gets an m prefix |
5xx_total → m5xx_total |
Prometheus label names keep their spelling as fields.
With the exporter's naming: otel,
io.example.Api.http_requests_total is exported as
io.example.api.http_requests and ..._duration_seconds_sum as
io.example.api.http_request_duration_seconds_sum. The unit suffix is only
recognized as the last word of a name.
Bridge or re-instrument?
The bridge costs one Gather per scrape and keeps your code unchanged.
If you are writing new code, or need allocation-free updates on a hot
path, register metrics on the provider directly. Both can share one
provider.
Next: Bridging OpenTelemetry.