pub use crate::semantic::metrics::CONTROLLER_ACTIVE_BROKERS;
pub use crate::semantic::metrics::CONTROLLER_ACTIVE_BROKER_NUM;
pub use crate::semantic::metrics::CONTROLLER_DLEDGER_DISK_USAGE;
pub use crate::semantic::metrics::CONTROLLER_DLEDGER_OP_LATENCY;
pub use crate::semantic::metrics::CONTROLLER_DLEDGER_OP_TOTAL;
pub use crate::semantic::metrics::CONTROLLER_ELECTION_LATENCY;
pub use crate::semantic::metrics::CONTROLLER_ELECTION_TOTAL;
pub use crate::semantic::metrics::CONTROLLER_ELECTION_TOTAL_JAVA;
pub use crate::semantic::metrics::CONTROLLER_HEARTBEAT_AGE;
pub use crate::semantic::metrics::CONTROLLER_LEADER_CHANGES_TOTAL;
pub use crate::semantic::metrics::CONTROLLER_QUORUM_HEALTH;
pub use crate::semantic::metrics::CONTROLLER_REQUEST_LATENCY;
pub use crate::semantic::metrics::CONTROLLER_REQUEST_TOTAL;
pub use crate::semantic::metrics::CONTROLLER_ROLE;
pub use crate::semantic::metrics::CONTROLLER_STALE_BROKERS;
#[cfg(not(feature = "otel-metrics"))]
#[derive(Debug, Clone, Default)]
pub struct ControllerMetrics;
#[cfg(not(feature = "otel-metrics"))]
impl ControllerMetrics {
pub fn noop() -> Self {
Self
}
#[inline]
pub fn record_election_total(&self, _count: u64) {}
#[inline]
pub fn record_election_latency(&self, _latency_ms: u64) {}
#[inline]
pub fn record_leader_changes_total(&self, _count: u64) {}
#[inline]
pub fn record_active_brokers(&self, _count: u64) {}
#[inline]
pub fn record_quorum_health(&self, _healthy: u64, _attributes: &crate::MetricAttributes) {}
#[inline]
pub fn record_heartbeat_age(&self, _age_ms: u64, _attributes: &crate::MetricAttributes) {}
#[inline]
pub fn record_stale_brokers(&self, _count: u64, _attributes: &crate::MetricAttributes) {}
}
#[cfg(feature = "otel-metrics")]
#[derive(Clone)]
pub struct ControllerMetrics {
instruments: Option<ControllerMetricInstruments>,
}
#[cfg(feature = "otel-metrics")]
#[derive(Clone)]
struct ControllerMetricInstruments {
election_total: opentelemetry::metrics::Counter<u64>,
election_latency: opentelemetry::metrics::Histogram<u64>,
leader_changes_total: opentelemetry::metrics::Counter<u64>,
active_brokers: opentelemetry::metrics::Gauge<u64>,
quorum_health: opentelemetry::metrics::Gauge<u64>,
heartbeat_age: opentelemetry::metrics::Histogram<u64>,
stale_brokers: opentelemetry::metrics::Gauge<u64>,
}
#[cfg(feature = "otel-metrics")]
impl ControllerMetrics {
#[must_use]
pub const fn noop() -> Self {
Self { instruments: None }
}
#[must_use]
pub(crate) fn new(meter: &opentelemetry::metrics::Meter) -> Self {
let election_total = meter
.u64_counter(CONTROLLER_ELECTION_TOTAL)
.with_description("Total number of controller elections")
.with_unit("{election}")
.build();
let election_latency = meter
.u64_histogram(CONTROLLER_ELECTION_LATENCY)
.with_description("Controller election latency")
.with_unit("ms")
.build();
let leader_changes_total = meter
.u64_counter(CONTROLLER_LEADER_CHANGES_TOTAL)
.with_description("Total number of controller leader changes")
.with_unit("{change}")
.build();
let active_brokers = meter
.u64_gauge(CONTROLLER_ACTIVE_BROKERS)
.with_description("Number of active brokers known by controller")
.with_unit("{broker}")
.build();
let quorum_health = meter
.u64_gauge(CONTROLLER_QUORUM_HEALTH)
.with_description("Controller leader has a recent quorum acknowledgement")
.with_unit("1")
.build();
let heartbeat_age = meter
.u64_histogram(CONTROLLER_HEARTBEAT_AGE)
.with_description("Age of broker heartbeats observed during liveness scans")
.with_unit("ms")
.build();
let stale_brokers = meter
.u64_gauge(CONTROLLER_STALE_BROKERS)
.with_description("Brokers classified as stale during the latest liveness scan")
.with_unit("{broker}")
.build();
Self {
instruments: Some(ControllerMetricInstruments {
election_total,
election_latency,
leader_changes_total,
active_brokers,
quorum_health,
heartbeat_age,
stale_brokers,
}),
}
}
#[inline]
pub fn record_election_total(&self, count: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments.election_total.add(count, attributes.as_key_values());
}
}
#[inline]
pub fn record_election_latency(&self, latency_ms: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments
.election_latency
.record(latency_ms, attributes.as_key_values());
}
}
#[inline]
pub fn record_leader_changes_total(&self, count: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments.leader_changes_total.add(count, attributes.as_key_values());
}
}
#[inline]
pub fn record_active_brokers(&self, count: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments.active_brokers.record(count, attributes.as_key_values());
}
}
#[inline]
pub fn record_quorum_health(&self, healthy: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments.quorum_health.record(healthy, attributes.as_key_values());
}
}
#[inline]
pub fn record_heartbeat_age(&self, age_ms: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments.heartbeat_age.record(age_ms, attributes.as_key_values());
}
}
#[inline]
pub fn record_stale_brokers(&self, count: u64, attributes: &crate::MetricAttributes) {
if let Some(instruments) = &self.instruments {
instruments.stale_brokers.record(count, attributes.as_key_values());
}
}
}
#[cfg(all(test, feature = "otel-metrics"))]
mod tests {
use opentelemetry::metrics::MeterProvider;
use opentelemetry_sdk::metrics::SdkMeterProvider;
use super::*;
#[test]
fn controller_metrics_constructs_and_records() {
let provider = SdkMeterProvider::builder().build();
let meter = provider.meter("controller-metrics-test");
let metrics = ControllerMetrics::new(&meter);
let attrs = crate::MetricAttributes::new().with("controller_id", "controller-a");
metrics.record_election_total(1, &attrs);
metrics.record_election_latency(15, &attrs);
metrics.record_leader_changes_total(1, &attrs);
metrics.record_active_brokers(3, &attrs);
metrics.record_quorum_health(1, &attrs);
metrics.record_heartbeat_age(50, &attrs);
metrics.record_stale_brokers(0, &attrs);
}
#[test]
fn noop_controller_metrics_accept_records() {
let metrics = ControllerMetrics::noop();
metrics.record_election_total(1, &crate::MetricAttributes::new());
metrics.record_election_latency(15, &crate::MetricAttributes::new());
metrics.record_leader_changes_total(1, &crate::MetricAttributes::new());
metrics.record_active_brokers(3, &crate::MetricAttributes::new());
metrics.record_quorum_health(1, &crate::MetricAttributes::new());
metrics.record_heartbeat_age(50, &crate::MetricAttributes::new());
metrics.record_stale_brokers(0, &crate::MetricAttributes::new());
}
#[test]
fn controller_metrics_source_has_no_global_facade() {
let source = include_str!("controller.rs");
for forbidden in [
concat!("static ", "CONTROLLER_METRICS"),
concat!("fn ", "init_global"),
concat!("pub fn ", "record_election_total(count"),
concat!("pub fn ", "record_leader_changes_total(count"),
] {
assert!(
!source.contains(forbidden),
"controller metrics must be instance-scoped: {forbidden}"
);
}
}
}