use super::manifest::{MetricDescriptor, MetricType};
pub struct ServiceMetrics {
_private: (),
}
#[deprecated(since = "2.9.0", note = "renamed to ServiceMetrics; removed before GA")]
pub type DfeMetrics = ServiceMetrics;
impl ServiceMetrics {
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn register(manager: &super::MetricsManager) -> Self {
let reg = manager.registry();
metrics::describe_counter!(
"transport_sent_total",
"Messages successfully sent to transport"
);
metrics::describe_counter!(
"transport_send_errors_total",
"Messages that failed to send"
);
metrics::describe_counter!(
"transport_backpressured_total",
"Messages delayed due to backpressure"
);
metrics::describe_counter!(
"transport_refused_total",
"Messages refused by transport (circuit open, capacity)"
);
metrics::describe_gauge!(
"transport_healthy",
"Transport health (1=healthy, 0=unhealthy)"
);
metrics::describe_gauge!(
"transport_queue_size",
"Current number of messages in transport queue"
);
metrics::describe_gauge!(
"transport_queue_capacity",
"Maximum transport queue capacity"
);
metrics::describe_gauge!(
"transport_inflight",
"Messages currently in-flight (sent but not acked)"
);
metrics::describe_histogram!(
"transport_send_duration_seconds",
metrics::Unit::Seconds,
"Time to send a batch to transport"
);
metrics::describe_counter!(
"transport_sent_bytes_total",
metrics::Unit::Bytes,
"Raw bytes written to transport (egress)"
);
metrics::describe_counter!(
"transport_received_bytes_total",
metrics::Unit::Bytes,
"Raw bytes read from transport (ingress)"
);
metrics::describe_counter!(
"transport_received_events_total",
"Events received off the transport (ingress count)"
);
for (name, desc, mt) in [
(
"transport_sent_total",
"Messages successfully sent to transport",
MetricType::Counter,
),
(
"transport_send_errors_total",
"Messages that failed to send",
MetricType::Counter,
),
(
"transport_backpressured_total",
"Messages delayed due to backpressure",
MetricType::Counter,
),
(
"transport_refused_total",
"Messages refused by transport (circuit open, capacity)",
MetricType::Counter,
),
(
"transport_healthy",
"Transport health (1=healthy, 0=unhealthy)",
MetricType::Gauge,
),
(
"transport_queue_size",
"Current number of messages in transport queue",
MetricType::Gauge,
),
(
"transport_queue_capacity",
"Maximum transport queue capacity",
MetricType::Gauge,
),
(
"transport_inflight",
"Messages currently in-flight (sent but not acked)",
MetricType::Gauge,
),
] {
reg.push(MetricDescriptor {
name: name.into(),
metric_type: mt,
description: desc.into(),
unit: String::new(),
labels: vec!["transport".into()],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
}
reg.push(MetricDescriptor {
name: "transport_send_duration_seconds".into(),
metric_type: MetricType::Histogram,
description: "Time to send a batch to transport".into(),
unit: "seconds".into(),
labels: vec!["transport".into()],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
for (name, desc, unit) in [
(
"transport_sent_bytes_total",
"Raw bytes written to transport (egress)",
"bytes",
),
(
"transport_received_bytes_total",
"Raw bytes read from transport (ingress)",
"bytes",
),
(
"transport_received_events_total",
"Events received off the transport (ingress count)",
"",
),
] {
reg.push(MetricDescriptor {
name: name.into(),
metric_type: MetricType::Counter,
description: desc.into(),
unit: unit.into(),
labels: vec!["transport".into()],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
}
metrics::describe_gauge!(
"pipeline_ready",
"Pipeline readiness (1=ready, 0=not ready)"
);
metrics::describe_counter!(
"pipeline_stall_seconds_total",
"Cumulative seconds the pipeline was stalled"
);
reg.push(MetricDescriptor {
name: "pipeline_ready".into(),
metric_type: MetricType::Gauge,
description: "Pipeline readiness (1=ready, 0=not ready)".into(),
unit: String::new(),
labels: vec![],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
reg.push(MetricDescriptor {
name: "pipeline_stall_seconds_total".into(),
metric_type: MetricType::Counter,
description: "Cumulative seconds the pipeline was stalled".into(),
unit: "seconds".into(),
labels: vec![],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
metrics::describe_counter!(
"records_received_total",
"Records received from all sources"
);
metrics::describe_counter!(
"records_delivered_total",
"Records successfully delivered to sink"
);
metrics::describe_counter!(
"records_filtered_total",
"Records dropped by filter/routing rules"
);
metrics::describe_counter!("records_dlq_total", "Records sent to dead letter queue");
for (name, desc) in [
(
"records_received_total",
"Records received from all sources",
),
(
"records_delivered_total",
"Records successfully delivered to sink",
),
(
"records_filtered_total",
"Records dropped by filter/routing rules",
),
("records_dlq_total", "Records sent to dead letter queue"),
] {
reg.push(MetricDescriptor {
name: name.into(),
metric_type: MetricType::Counter,
description: desc.into(),
unit: String::new(),
labels: vec![],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
}
metrics::describe_gauge!("scaling_pressure", "Normalised scaling pressure (0-100)");
metrics::describe_gauge!(
"scaling_circuit_open",
"Circuit breaker state (1=open, 0=closed)"
);
metrics::describe_gauge!("scaling_memory_pressure", "Memory pressure ratio (0.0-1.0)");
for (name, desc) in [
("scaling_pressure", "Normalised scaling pressure (0-100)"),
(
"scaling_circuit_open",
"Circuit breaker state (1=open, 0=closed)",
),
("scaling_memory_pressure", "Memory pressure ratio (0.0-1.0)"),
] {
reg.push(MetricDescriptor {
name: name.into(),
metric_type: MetricType::Gauge,
description: desc.into(),
unit: String::new(),
labels: vec![],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
}
metrics::describe_gauge!("spool_bytes", "Current spool size in bytes");
metrics::describe_gauge!("spool_messages", "Current spool message count");
metrics::describe_gauge!(
"spool_disk_available",
"Available disk space for spool in bytes"
);
for (name, desc) in [
("spool_bytes", "Current spool size in bytes"),
("spool_messages", "Current spool message count"),
(
"spool_disk_available",
"Available disk space for spool in bytes",
),
] {
reg.push(MetricDescriptor {
name: name.into(),
metric_type: MetricType::Gauge,
description: desc.into(),
unit: String::new(),
labels: vec![],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
}
metrics::describe_counter!("auth_failures_total", "Authentication failures by reason");
metrics::describe_counter!("validation_failures_total", "Validation failures by reason");
reg.push(MetricDescriptor {
name: "auth_failures_total".into(),
metric_type: MetricType::Counter,
description: "Authentication failures by reason".into(),
unit: String::new(),
labels: vec!["reason".into()],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
reg.push(MetricDescriptor {
name: "validation_failures_total".into(),
metric_type: MetricType::Counter,
description: "Validation failures by reason".into(),
unit: String::new(),
labels: vec!["reason".into()],
group: "platform".into(),
buckets: None,
use_cases: vec![],
dashboard_hint: None,
});
Self { _private: () }
}
#[inline]
pub fn transport_sent(&self, transport: super::TransportKind, count: u64) {
metrics::counter!("transport_sent_total", "transport" => transport.as_label())
.increment(count);
}
#[inline]
pub fn transport_send_errors(&self, transport: super::TransportKind, count: u64) {
metrics::counter!("transport_send_errors_total", "transport" => transport.as_label())
.increment(count);
}
#[inline]
pub fn transport_backpressured(&self, transport: &str, count: u64) {
metrics::counter!("transport_backpressured_total", "transport" => transport.to_string())
.increment(count);
}
#[inline]
pub fn transport_refused(&self, transport: &str, count: u64) {
metrics::counter!("transport_refused_total", "transport" => transport.to_string())
.increment(count);
}
#[inline]
pub fn transport_healthy(&self, transport: &str, healthy: bool) {
metrics::gauge!("transport_healthy", "transport" => transport.to_string())
.set(if healthy { 1.0 } else { 0.0 });
}
#[inline]
pub fn transport_queue_size(&self, transport: &str, size: f64) {
metrics::gauge!("transport_queue_size", "transport" => transport.to_string()).set(size);
}
#[inline]
pub fn transport_queue_capacity(&self, transport: &str, capacity: f64) {
metrics::gauge!("transport_queue_capacity", "transport" => transport.to_string())
.set(capacity);
}
#[inline]
pub fn transport_inflight(&self, transport: &str, count: f64) {
metrics::gauge!("transport_inflight", "transport" => transport.to_string()).set(count);
}
#[inline]
pub fn transport_send_duration(&self, transport: &str, seconds: f64) {
metrics::histogram!(
"transport_send_duration_seconds",
"transport" => transport.to_string()
)
.record(seconds);
}
#[inline]
pub fn transport_sent_bytes(&self, transport: super::TransportKind, bytes: u64) {
metrics::counter!("transport_sent_bytes_total", "transport" => transport.as_label())
.increment(bytes);
}
#[inline]
pub fn transport_received_bytes(&self, transport: super::TransportKind, bytes: u64) {
metrics::counter!("transport_received_bytes_total", "transport" => transport.as_label())
.increment(bytes);
}
#[inline]
pub fn transport_received_events(&self, transport: super::TransportKind, count: u64) {
metrics::counter!("transport_received_events_total", "transport" => transport.as_label())
.increment(count);
}
#[inline]
pub fn pipeline_ready(&self, ready: bool) {
metrics::gauge!("pipeline_ready").set(if ready { 1.0 } else { 0.0 });
}
#[inline]
pub fn pipeline_stall(&self, seconds: u64) {
metrics::counter!("pipeline_stall_seconds_total").increment(seconds);
}
#[inline]
pub fn records_received(&self, count: u64) {
metrics::counter!("records_received_total").increment(count);
}
#[inline]
pub fn records_delivered(&self, count: u64) {
metrics::counter!("records_delivered_total").increment(count);
}
#[inline]
pub fn records_filtered(&self, count: u64) {
metrics::counter!("records_filtered_total").increment(count);
}
#[inline]
pub fn records_dlq(&self, count: u64) {
metrics::counter!("records_dlq_total").increment(count);
}
#[inline]
pub fn scaling_pressure(&self, pressure: f64) {
metrics::gauge!("scaling_pressure").set(pressure);
}
#[inline]
pub fn scaling_circuit_open(&self, open: bool) {
metrics::gauge!("scaling_circuit_open").set(if open { 1.0 } else { 0.0 });
}
#[inline]
pub fn scaling_memory_pressure(&self, ratio: f64) {
metrics::gauge!("scaling_memory_pressure").set(ratio);
}
#[inline]
pub fn spool_bytes(&self, bytes: f64) {
metrics::gauge!("spool_bytes").set(bytes);
}
#[inline]
pub fn spool_messages(&self, count: f64) {
metrics::gauge!("spool_messages").set(count);
}
#[inline]
pub fn spool_disk_available(&self, bytes: f64) {
metrics::gauge!("spool_disk_available").set(bytes);
}
#[inline]
pub fn auth_failure(&self, reason: super::AuthFailureReason) {
metrics::counter!("auth_failures_total", "reason" => reason.as_label()).increment(1);
}
#[inline]
pub fn validation_failure(&self, reason: super::ValidationFailureReason) {
metrics::counter!("validation_failures_total", "reason" => reason.as_label()).increment(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_register_does_not_panic() {
let mgr = super::super::MetricsManager::new_for_test("test_app");
let _dfe = ServiceMetrics::register(&mgr);
}
#[tokio::test]
async fn test_register_populates_registry() {
let mgr = super::super::MetricsManager::new_for_test("test_app");
let _dfe = ServiceMetrics::register(&mgr);
let manifest = mgr.registry().manifest();
let names: Vec<&str> = manifest.metrics.iter().map(|m| m.name.as_str()).collect();
assert!(names.contains(&"test_app_transport_sent_total"));
assert!(names.contains(&"test_app_transport_sent_bytes_total"));
assert!(names.contains(&"test_app_transport_received_bytes_total"));
assert!(names.contains(&"test_app_transport_received_events_total"));
assert!(names.contains(&"test_app_pipeline_ready"));
assert!(names.contains(&"test_app_records_received_total"));
assert!(names.contains(&"test_app_scaling_pressure"));
assert!(names.contains(&"test_app_spool_bytes"));
assert!(names.contains(&"test_app_auth_failures_total"));
for m in &manifest.metrics {
assert_eq!(m.group, "platform");
}
let sent = manifest
.metrics
.iter()
.find(|m| m.name == "test_app_transport_sent_total")
.unwrap();
assert_eq!(sent.labels, vec!["transport"]);
let auth = manifest
.metrics
.iter()
.find(|m| m.name == "test_app_auth_failures_total")
.unwrap();
assert_eq!(auth.labels, vec!["reason"]);
}
#[tokio::test]
async fn test_register_bare_namespace_keeps_names_bare() {
let mgr = super::super::MetricsManager::new_for_test("");
let _dfe = ServiceMetrics::register(&mgr);
let manifest = mgr.registry().manifest();
let names: Vec<&str> = manifest.metrics.iter().map(|m| m.name.as_str()).collect();
assert!(names.contains(&"transport_sent_total"));
assert!(names.contains(&"pipeline_ready"));
}
#[tokio::test]
async fn test_methods_callable_without_recorder() {
let mgr = super::super::MetricsManager::new("test_app");
let svc = ServiceMetrics::register(&mgr);
svc.transport_sent(super::super::TransportKind::Kafka, 1);
svc.transport_send_errors(super::super::TransportKind::Kafka, 1);
svc.transport_backpressured("kafka", 1);
svc.transport_refused("kafka", 1);
svc.transport_healthy("kafka", true);
svc.transport_queue_size("kafka", 100.0);
svc.transport_queue_capacity("kafka", 1000.0);
svc.transport_inflight("kafka", 50.0);
svc.transport_send_duration("kafka", 0.042);
svc.transport_sent_bytes(super::super::TransportKind::Kafka, 4096);
svc.transport_received_bytes(super::super::TransportKind::Grpc, 8192);
svc.transport_received_events(super::super::TransportKind::Grpc, 64);
svc.pipeline_ready(true);
svc.pipeline_stall(1);
svc.records_received(100);
svc.records_delivered(99);
svc.records_filtered(1);
svc.records_dlq(0);
svc.scaling_pressure(42.0);
svc.scaling_circuit_open(false);
svc.scaling_memory_pressure(0.65);
svc.spool_bytes(1024.0);
svc.spool_messages(10.0);
svc.spool_disk_available(1_000_000.0);
svc.auth_failure(super::super::AuthFailureReason::MalformedToken);
svc.validation_failure(super::super::ValidationFailureReason::FieldMissing);
}
}