Files
zitadel/backend/v3/instrumentation/metrics/metric.go
T
Tim MöhlmannandGitHub 73de633c71 feat(database): publish pgxpool pool stats as metrics (#12737)
# Which Problems Are Solved

ZITADEL opens its Postgres connection pool via `pgxpool.NewWithConfig`
(`internal/database/postgres/pg.go`) but never reads
`pgxpool.Pool.Stat()`,
so operators have no way to observe pool saturation (acquired/idle/total
conns vs. `MaxConns`) or connection churn (acquire waits, canceled/empty
acquires, idle/lifetime destroys, new connections) through ZITADEL's
existing metrics pipeline.

# How the Problems Are Solved

- Adds `RegisterCounterObserver` (backed by an OTel
`Int64ObservableCounter`)
  to the `Metrics` interface, its implementation, and the test mock
  (`backend/v3/instrumentation/metric.go`,
  `backend/v3/instrumentation/metrics/metric.go`,
  `backend/v3/instrumentation/metrics/mock.go`), mirroring the existing
`RegisterValueObserver`/`RegisterUpDownSumObserver` — there was
previously
  no observable instrument for cumulative counters.
- Adds `database.RegisterPoolMetrics(ctx, pool)`
  (`internal/database/metrics.go`), which registers 5 observable gauges
  (`acquired_conns`, `constructing_conns`, `idle_conns`, `max_conns`,
  `total_conns`) and 7 observable counters (`acquire_count`,
  `acquire_duration_milliseconds`, `canceled_acquire_count`,
  `empty_acquire_count`, `max_idle_destroy_count`,
  `max_lifetime_destroy_count`, `new_conns_count`) sourced directly from
`pgxpool.Pool.Stat()`, published under `zitadel.database.pool.*` metric
  names.
- Wires the registration call into `cmd/start/start.go` right after the
  pool is created, so the running server process exposes these series on
  `/metrics`.

# Additional Changes

- Adds unit tests covering the new `RegisterCounterObserver` method
  (`backend/v3/instrumentation/metric_test.go`) and the pool metrics
  registration (`internal/database/metrics_test.go`).

# Additional Context

- Scope is intentionally limited to `cmd/start` (the long-running
process
  that serves `/metrics`); one-shot commands (`setup`, `mirror`,
`initialise`, `key`) are left untouched since an exporter running for a
  single command invocation wouldn't produce useful series.
- https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Stat
2026-09-15 12:13:30 +02:00

77 lines
2.7 KiB
Go

package metrics
import (
"context"
"net/http"
"sync"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"github.com/zitadel/zitadel/backend/v3/instrumentation"
"github.com/zitadel/zitadel/cmd/build"
)
const (
ActiveSessionCounter = "zitadel.active_session_counter"
ActiveSessionCounterDescription = "Active session counter"
SpoolerDivCounter = "zitadel.spooler_div_milliseconds"
SpoolerDivCounterDescription = "Spooler div from last successful run to now in milliseconds"
Database = "database"
ViewName = "view_name"
)
type Metrics interface {
GetExporter() http.Handler
RegisterCounter(name, description string) error
AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error
AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error
RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error
RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error
RegisterCounterObserver(name, description string, callbackFunc metric.Int64Callback) error
RegisterHistogram(name, description, unit string, buckets []float64) error
}
var globalMeter = sync.OnceValue(func() Metrics {
return instrumentation.NewMeter(
instrumentation.Name,
metric.WithInstrumentationVersion(build.Version()),
)
})
func GlobalMeter() Metrics {
return globalMeter()
}
func GetExporter() http.Handler {
return globalMeter().GetExporter()
}
func RegisterCounter(name, description string) error {
return globalMeter().RegisterCounter(name, description)
}
func AddCount(ctx context.Context, name string, value int64, labels map[string]attribute.Value) error {
return globalMeter().AddCount(ctx, name, value, labels)
}
func AddHistogramMeasurement(ctx context.Context, name string, value float64, labels map[string]attribute.Value) error {
return globalMeter().AddHistogramMeasurement(ctx, name, value, labels)
}
func RegisterHistogram(name, description, unit string, buckets []float64) error {
return globalMeter().RegisterHistogram(name, description, unit, buckets)
}
func RegisterUpDownSumObserver(name, description string, callbackFunc metric.Int64Callback) error {
return globalMeter().RegisterUpDownSumObserver(name, description, callbackFunc)
}
func RegisterValueObserver(name, description string, callbackFunc metric.Int64Callback) error {
return globalMeter().RegisterValueObserver(name, description, callbackFunc)
}
func RegisterCounterObserver(name, description string, callbackFunc metric.Int64Callback) error {
return globalMeter().RegisterCounterObserver(name, description, callbackFunc)
}