Skip to content
wiki edited this page Sep 4, 2026 · 1 revision

rextension-metric

Prometheus / OpenMetrics instrumentation for Rex, with no external dependencies — the counter, gauge and histogram primitives are implemented in this module.

go get github.com/kryovyx/rextension-metric
import (
	"github.com/kryovyx/rex"
	metric "github.com/kryovyx/rextension-metric"
)

app := rex.New(metric.WithMetrics(nil))

That exposes /metrics on a dedicated listener at :9090, instruments every request, and samples process metrics every 5 seconds.

$ curl :9090/metrics
# HELP http_requests_total Total HTTP requests, by route pattern and response status
# TYPE http_requests_total counter
http_requests_total{router="default",method="GET",path="/users/{id}",status="200"} 1834
…

What you get for free

  • http_requests_total — by router, method, route pattern and status
  • http_request_duration_seconds — a histogram, same labels minus status
  • http_requests_in_flight — a gauge, maintained by middleware
  • http_requests_unresolved_total — 404s and 405s
  • rex_uptime_seconds, rex_memory_bytes, rex_goroutines
  • rex_metric_series_dropped_total, rex_events_dropped_total, rex_routes_total

See Exported Metrics.

Two things that make the numbers trustworthy

Labels use the route pattern, never the URL path. http_request_duration_seconds{path="/users/{id}"} is one time series no matter how many user identifiers exist. Labelling by Request.URL.Path made every distinct URL its own retained series — unbounded, and controlled by the client. See Cardinality.

The in-flight gauge is middleware, not an event subscriber. The increment and the decrement are the same function call and its defer, so they cannot be lost independently. As events they could: Emit is non-blocking, a full queue drops events, and increments happen under precisely the load that fills the queue. The gauge drifted one way and never recovered.

Both are examples of the same rule: anything that must be exact belongs in middleware, not in an event subscriber.

Adding your own

var reg metric.Registry
_ = app.Container().Resolve(&reg)

orders := metric.NewCounterVec([]string{"status"})
orders.SetHelp("Orders processed, by outcome")
if err := reg.Register("orders_processed_total", orders); err != nil {
	return err // a duplicate name is refused, not appended
}

orders.With(map[string]string{"status": "settled"}).Inc()

See Custom Metrics.

Keep the endpoint internal

/metrics is on a dedicated router precisely so it can be firewalled. It exposes request rates, error counts, latency distributions and every route pattern in the application — a map of your API to anyone who can reach it.

metric.WithMetricsRouter(rx.RouterConfig{Addr: "127.0.0.1:9090"})

WithAtDefaultAddr(true) puts it on the public listener. It is a deliberate choice, not a default.

Clone this wiki locally