snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
400
internal/metrics/metrics.go
Normal file
400
internal/metrics/metrics.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const defaultMetricsNamespace = "centrifugo"
|
||||
|
||||
// Config contains metrics configuration.
|
||||
type Config struct {
|
||||
// Namespace is the prometheus namespace for all metrics. If empty, defaults to "centrifugo".
|
||||
Namespace string
|
||||
// ConstLabels are labels that will be added to all metrics as constant labels.
|
||||
// These are useful for adding environment, region, or other deployment-specific labels.
|
||||
ConstLabels map[string]string
|
||||
// Registerer is the prometheus registerer to use. If nil, prometheus.DefaultRegisterer is used.
|
||||
Registerer prometheus.Registerer
|
||||
// NativeHistograms switches Histogram instruments to Prometheus native
|
||||
// (sparse, exponential) schema with no explicit buckets exposed. Designed
|
||||
// for OpenTelemetry export via the client_golang Prometheus bridge.
|
||||
// Text-format scrapes lose _bucket series; use protobuf scrape format.
|
||||
NativeHistograms bool
|
||||
}
|
||||
|
||||
// nativeHistogramOpts returns opts unchanged when native is false. When true,
|
||||
// it enables Prometheus native histogram schema with no explicit buckets —
|
||||
// the metric exposes only _count, _sum, and the native histogram chunk.
|
||||
func nativeHistogramOpts(opts prometheus.HistogramOpts, native bool) prometheus.HistogramOpts {
|
||||
if !native {
|
||||
return opts
|
||||
}
|
||||
opts.Buckets = nil
|
||||
opts.NativeHistogramBucketFactor = 1.1
|
||||
opts.NativeHistogramMaxBucketNumber = 200
|
||||
opts.NativeHistogramMinResetDuration = time.Hour
|
||||
return opts
|
||||
}
|
||||
|
||||
// noopObserverVec implements prometheus.ObserverVec with all no-op methods.
|
||||
// Assigned to Summary accessors when NativeHistograms is true so that
|
||||
// callers (proxy handlers, helpers) can still construct cached observers
|
||||
// and call .Observe() without nil checks — the calls become no-ops and no
|
||||
// metric data is recorded for the disabled Summary instrument.
|
||||
type noopObserverVec struct{}
|
||||
|
||||
func (noopObserverVec) Describe(chan<- *prometheus.Desc) {}
|
||||
func (noopObserverVec) Collect(chan<- prometheus.Metric) {}
|
||||
func (noopObserverVec) WithLabelValues(...string) prometheus.Observer { return noopObserver{} }
|
||||
func (noopObserverVec) With(prometheus.Labels) prometheus.Observer { return noopObserver{} }
|
||||
func (noopObserverVec) GetMetricWith(prometheus.Labels) (prometheus.Observer, error) {
|
||||
return noopObserver{}, nil
|
||||
}
|
||||
func (noopObserverVec) GetMetricWithLabelValues(...string) (prometheus.Observer, error) {
|
||||
return noopObserver{}, nil
|
||||
}
|
||||
func (noopObserverVec) CurryWith(prometheus.Labels) (prometheus.ObserverVec, error) {
|
||||
return noopObserverVec{}, nil
|
||||
}
|
||||
func (noopObserverVec) MustCurryWith(prometheus.Labels) prometheus.ObserverVec {
|
||||
return noopObserverVec{}
|
||||
}
|
||||
|
||||
type noopObserver struct{}
|
||||
|
||||
func (noopObserver) Observe(float64) {}
|
||||
|
||||
// Registry holds all Centrifugo metrics.
|
||||
type Registry struct {
|
||||
config Config
|
||||
|
||||
// Proxy metrics
|
||||
proxyCallDurationSummary prometheus.ObserverVec
|
||||
proxyCallDurationHistogram *prometheus.HistogramVec
|
||||
proxyCallErrorCount *prometheus.CounterVec
|
||||
proxyCallInflightRequests *prometheus.GaugeVec
|
||||
|
||||
// API metrics
|
||||
apiCommandErrorsTotal *prometheus.CounterVec
|
||||
apiCommandDurationSummary prometheus.ObserverVec
|
||||
apiCommandDurationHistogram *prometheus.HistogramVec
|
||||
// rpcDurationSummary is the legacy Summary by default; no-op when
|
||||
// NativeHistograms is true. rpcDurationHistogram is the new companion
|
||||
// Histogram exposed as "rpc_duration_seconds_histogram" — unconditional,
|
||||
// with native schema when NativeHistograms is true.
|
||||
rpcDurationSummary prometheus.ObserverVec
|
||||
rpcDurationHistogram *prometheus.HistogramVec
|
||||
|
||||
// Consumer metrics
|
||||
consumerProcessedTotal *prometheus.CounterVec
|
||||
consumerErrorsTotal *prometheus.CounterVec
|
||||
|
||||
// Shared poll proxy metrics
|
||||
sharedPollProxyRequestItems *prometheus.HistogramVec
|
||||
sharedPollProxyResponseItems *prometheus.HistogramVec
|
||||
|
||||
// Middleware metrics
|
||||
connLimitReached prometheus.Counter
|
||||
httpRequestsTotal *prometheus.CounterVec
|
||||
|
||||
// PostgreSQL-specific broker metrics. Cleanup is Postgres-specific here (only
|
||||
// the PG stream broker prunes rows, with PG table names in the "pass" label),
|
||||
// so it carries the postgres_ prefix like the rest. Metrics emitted by both
|
||||
// PG brokers are split per kind so stream and map never share a series.
|
||||
brokerPostgresCleanupRemovedTotal *prometheus.CounterVec
|
||||
brokerPostgresOutboxCursorLagSeconds *prometheus.GaugeVec
|
||||
mapBrokerPostgresOutboxCursorLagSeconds *prometheus.GaugeVec
|
||||
brokerPostgresPartitions *prometheus.GaugeVec
|
||||
mapBrokerPostgresPartitions *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// Init initializes the metrics registry with the provided configuration.
|
||||
// It creates all metrics and registers them with the provided registerer.
|
||||
// If registerer is nil, prometheus.DefaultRegisterer is used.
|
||||
// Returns an error if metric registration fails.
|
||||
func Init(cfg Config) error {
|
||||
reg, err := newRegistry(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Populate exported variables for backward compatibility
|
||||
ProxyCallDurationSummary = reg.proxyCallDurationSummary
|
||||
ProxyCallDurationHistogram = reg.proxyCallDurationHistogram
|
||||
ProxyCallErrorCount = reg.proxyCallErrorCount
|
||||
ProxyCallInflightRequests = reg.proxyCallInflightRequests
|
||||
|
||||
APICommandErrorsTotal = reg.apiCommandErrorsTotal
|
||||
APICommandDurationSummary = reg.apiCommandDurationSummary
|
||||
APICommandDurationHistogram = reg.apiCommandDurationHistogram
|
||||
RPCDurationSummary = reg.rpcDurationSummary
|
||||
RPCDurationHistogram = reg.rpcDurationHistogram
|
||||
|
||||
ConsumerProcessedTotal = reg.consumerProcessedTotal
|
||||
ConsumerErrorsTotal = reg.consumerErrorsTotal
|
||||
|
||||
SharedPollProxyRequestItems = reg.sharedPollProxyRequestItems
|
||||
SharedPollProxyResponseItems = reg.sharedPollProxyResponseItems
|
||||
|
||||
ConnLimitReached = reg.connLimitReached
|
||||
HTTPRequestsTotal = reg.httpRequestsTotal
|
||||
|
||||
BrokerPostgresCleanupRemovedTotal = reg.brokerPostgresCleanupRemovedTotal
|
||||
BrokerPostgresOutboxCursorLagSeconds = reg.brokerPostgresOutboxCursorLagSeconds
|
||||
MapBrokerPostgresOutboxCursorLagSeconds = reg.mapBrokerPostgresOutboxCursorLagSeconds
|
||||
BrokerPostgresPartitions = reg.brokerPostgresPartitions
|
||||
MapBrokerPostgresPartitions = reg.mapBrokerPostgresPartitions
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newRegistry(cfg Config) (*Registry, error) {
|
||||
registerer := cfg.Registerer
|
||||
if registerer == nil {
|
||||
registerer = prometheus.DefaultRegisterer
|
||||
}
|
||||
|
||||
metricsNamespace := cfg.Namespace
|
||||
if metricsNamespace == "" {
|
||||
metricsNamespace = defaultMetricsNamespace
|
||||
}
|
||||
|
||||
constLabels := prometheus.Labels(cfg.ConstLabels)
|
||||
|
||||
m := &Registry{
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
// Proxy metrics
|
||||
if cfg.NativeHistograms {
|
||||
m.proxyCallDurationSummary = noopObserverVec{}
|
||||
} else {
|
||||
m.proxyCallDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "proxy",
|
||||
Name: "duration_seconds",
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
|
||||
Help: "DEPRECATED — use centrifugo_proxy_duration_seconds_histogram. Will be removed in Centrifugo v7. Duration of proxy call.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"protocol", "type", "name"})
|
||||
}
|
||||
|
||||
m.proxyCallDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "proxy",
|
||||
Name: "duration_seconds_histogram",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
Help: "Histogram of duration of proxy call.",
|
||||
ConstLabels: constLabels,
|
||||
}, cfg.NativeHistograms), []string{"protocol", "type", "name"})
|
||||
|
||||
m.proxyCallErrorCount = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "proxy",
|
||||
Name: "errors",
|
||||
Help: "Proxy call error count.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"protocol", "type", "name"})
|
||||
|
||||
m.proxyCallInflightRequests = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "proxy",
|
||||
Name: "inflight_requests",
|
||||
Help: "Number of inflight proxy requests.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"protocol", "type", "name"})
|
||||
|
||||
// API metrics
|
||||
m.apiCommandErrorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "api",
|
||||
Name: "command_errors_total",
|
||||
Help: "Total errors in API commands.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"protocol", "method", "error"})
|
||||
|
||||
if cfg.NativeHistograms {
|
||||
m.apiCommandDurationSummary = noopObserverVec{}
|
||||
} else {
|
||||
m.apiCommandDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "api",
|
||||
Name: "command_duration_seconds",
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
|
||||
Help: "DEPRECATED — use centrifugo_api_command_duration_seconds_histogram. Will be removed in Centrifugo v7. Duration of API per command.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"protocol", "method"})
|
||||
}
|
||||
|
||||
m.apiCommandDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "api",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
Name: "command_duration_seconds_histogram",
|
||||
Help: "Histogram of duration of API per command.",
|
||||
ConstLabels: constLabels,
|
||||
}, cfg.NativeHistograms), []string{"protocol", "method"})
|
||||
|
||||
if cfg.NativeHistograms {
|
||||
m.rpcDurationSummary = noopObserverVec{}
|
||||
} else {
|
||||
m.rpcDurationSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "api",
|
||||
Name: "rpc_duration_seconds",
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.99: 0.001, 0.999: 0.0001},
|
||||
Help: "DEPRECATED — use centrifugo_api_rpc_duration_seconds_histogram. Will be removed in Centrifugo v7. Duration of API per command.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"protocol", "method"})
|
||||
}
|
||||
|
||||
m.rpcDurationHistogram = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "api",
|
||||
Name: "rpc_duration_seconds_histogram",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
Help: "Histogram of duration of API per command.",
|
||||
ConstLabels: constLabels,
|
||||
}, cfg.NativeHistograms), []string{"protocol", "method"})
|
||||
|
||||
// Consumer metrics
|
||||
m.consumerProcessedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "consumers",
|
||||
Name: "messages_processed_total",
|
||||
Help: "Total number of processed messages in consumer",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"consumer_name"})
|
||||
|
||||
m.consumerErrorsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "consumers",
|
||||
Name: "errors_total",
|
||||
Help: "Total number of errors in consumer",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"consumer_name"})
|
||||
|
||||
// Shared poll proxy metrics
|
||||
m.sharedPollProxyRequestItems = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "shared_poll_proxy",
|
||||
Name: "request_items",
|
||||
Help: "Number of items per shared poll proxy request.",
|
||||
Buckets: []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
|
||||
ConstLabels: constLabels,
|
||||
}, cfg.NativeHistograms), []string{"name"})
|
||||
|
||||
m.sharedPollProxyResponseItems = prometheus.NewHistogramVec(nativeHistogramOpts(prometheus.HistogramOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "shared_poll_proxy",
|
||||
Name: "response_items",
|
||||
Help: "Number of items per shared poll proxy response.",
|
||||
Buckets: []float64{1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
|
||||
ConstLabels: constLabels,
|
||||
}, cfg.NativeHistograms), []string{"name"})
|
||||
|
||||
// Middleware metrics
|
||||
m.connLimitReached = prometheus.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "node",
|
||||
Name: "client_connection_limit",
|
||||
Help: "Number of refused requests due to node client connection limit.",
|
||||
ConstLabels: constLabels,
|
||||
})
|
||||
|
||||
m.httpRequestsTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "node",
|
||||
Name: "incoming_http_requests_total",
|
||||
Help: "Number of incoming HTTP requests",
|
||||
ConstLabels: constLabels,
|
||||
},
|
||||
[]string{"path", "method", "status"},
|
||||
)
|
||||
|
||||
// Postgres-specific broker metrics carry a postgres_ name prefix. Cleanup is
|
||||
// Postgres-specific here: only the PG stream broker prunes rows (the "pass"
|
||||
// label is the PG table being cleaned), so it is not the generic cleanup
|
||||
// metric. Metrics emitted by both PG brokers are split per kind below.
|
||||
m.brokerPostgresCleanupRemovedTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "broker",
|
||||
Name: "postgres_cleanup_removed_total",
|
||||
Help: "Total rows removed by each Postgres cleanup pass.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"broker_name", "pass"})
|
||||
|
||||
m.brokerPostgresOutboxCursorLagSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "broker",
|
||||
Name: "postgres_outbox_cursor_lag_seconds",
|
||||
Help: "Time between the outbox cursor's row created_at and now, per shard.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"broker_name", "shard"})
|
||||
|
||||
m.mapBrokerPostgresOutboxCursorLagSeconds = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "map_broker",
|
||||
Name: "postgres_outbox_cursor_lag_seconds",
|
||||
Help: "Time between the outbox cursor's row created_at and now, per shard.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"broker_name", "shard"})
|
||||
|
||||
m.brokerPostgresPartitions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "broker",
|
||||
Name: "postgres_partitions",
|
||||
Help: "Count of stream/history table partitions.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"broker_name"})
|
||||
|
||||
m.mapBrokerPostgresPartitions = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: metricsNamespace,
|
||||
Subsystem: "map_broker",
|
||||
Name: "postgres_partitions",
|
||||
Help: "Count of stream/history table partitions.",
|
||||
ConstLabels: constLabels,
|
||||
}, []string{"broker_name"})
|
||||
|
||||
// Register all metrics
|
||||
var alreadyRegistered prometheus.AlreadyRegisteredError
|
||||
|
||||
collectors := []prometheus.Collector{
|
||||
m.proxyCallDurationSummary,
|
||||
m.proxyCallDurationHistogram,
|
||||
m.proxyCallErrorCount,
|
||||
m.proxyCallInflightRequests,
|
||||
m.apiCommandErrorsTotal,
|
||||
m.apiCommandDurationSummary,
|
||||
m.apiCommandDurationHistogram,
|
||||
m.rpcDurationSummary,
|
||||
m.rpcDurationHistogram,
|
||||
m.consumerProcessedTotal,
|
||||
m.consumerErrorsTotal,
|
||||
m.sharedPollProxyRequestItems,
|
||||
m.sharedPollProxyResponseItems,
|
||||
m.connLimitReached,
|
||||
m.httpRequestsTotal,
|
||||
m.brokerPostgresCleanupRemovedTotal,
|
||||
m.brokerPostgresOutboxCursorLagSeconds,
|
||||
m.mapBrokerPostgresOutboxCursorLagSeconds,
|
||||
m.brokerPostgresPartitions,
|
||||
m.mapBrokerPostgresPartitions,
|
||||
}
|
||||
|
||||
for _, collector := range collectors {
|
||||
err := registerer.Register(collector)
|
||||
if err != nil {
|
||||
// Ignore if already registered (allows re-initialization in tests)
|
||||
if !errors.As(err, &alreadyRegistered) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
Reference in New Issue
Block a user