use crate::{ConnectorError, ConnectorResult};
use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
use metrics_exporter_prometheus::PrometheusBuilder;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::OnceLock;
use std::time::Duration;
static METRICS_EXPORTER_PORT: OnceLock<u16> = OnceLock::new();
static METRICS_EXPORTER_INIT: OnceLock<Result<(), String>> = OnceLock::new();
#[derive(Debug, Clone)]
pub struct ConnectorMetrics {
connector_name: String,
topic: String,
}
impl ConnectorMetrics {
pub fn initialize_exporter(port: u16) -> ConnectorResult<()> {
let existing_port = METRICS_EXPORTER_PORT.get_or_init(|| port);
if *existing_port != port {
return Err(ConnectorError::config(format!(
"metrics exporter already initialized on port {}, cannot reinitialize on port {}",
existing_port, port
)));
}
let init_result = METRICS_EXPORTER_INIT.get_or_init(|| {
let listen_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port);
PrometheusBuilder::new()
.with_http_listener(listen_addr)
.install()
.map_err(|e| {
format!(
"failed to install Prometheus metrics exporter on {}: {}",
listen_addr, e
)
})
});
match init_result {
Ok(()) => Ok(()),
Err(message) => Err(ConnectorError::fatal(message.clone())),
}
}
pub fn new(connector_name: impl Into<String>, topic: impl Into<String>) -> Self {
let connector_name = connector_name.into();
let topic = topic.into();
Self::register_metrics();
Self {
connector_name,
topic,
}
}
fn register_metrics() {
describe_counter!(
"danube_connector_messages_received_total",
"Total number of messages received by the connector"
);
describe_counter!(
"danube_connector_messages_processed_total",
"Total number of messages successfully processed"
);
describe_counter!(
"danube_connector_messages_failed_total",
"Total number of messages that failed processing"
);
describe_counter!(
"danube_connector_messages_retried_total",
"Total number of message processing retries"
);
describe_histogram!(
"danube_connector_processing_duration_seconds",
"Time spent processing each message"
);
describe_histogram!(
"danube_connector_batch_size",
"Number of messages in each batch"
);
describe_gauge!(
"danube_connector_inflight_messages",
"Current number of messages being processed"
);
describe_gauge!(
"danube_connector_health",
"Connector health status (1 = healthy, 0 = unhealthy)"
);
}
pub fn record_received(&self) {
counter!(
"danube_connector_messages_received_total",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.increment(1);
}
pub fn record_success(&self) {
counter!(
"danube_connector_messages_processed_total",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.increment(1);
}
pub fn record_error(&self, error_type: &str) {
counter!(
"danube_connector_messages_failed_total",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
"error_type" => error_type.to_string(),
)
.increment(1);
}
pub fn record_retry(&self) {
counter!(
"danube_connector_messages_retried_total",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.increment(1);
}
pub fn record_processing_time(&self, duration: Duration) {
histogram!(
"danube_connector_processing_duration_seconds",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.record(duration.as_secs_f64());
}
pub fn record_batch_size(&self, size: usize) {
histogram!(
"danube_connector_batch_size",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.record(size as f64);
}
pub fn set_inflight(&self, count: usize) {
gauge!(
"danube_connector_inflight_messages",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.set(count as f64);
}
pub fn increment_inflight(&self) {
gauge!(
"danube_connector_inflight_messages",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.increment(1.0);
}
pub fn decrement_inflight(&self) {
gauge!(
"danube_connector_inflight_messages",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.decrement(1.0);
}
pub fn set_health(&self, healthy: bool) {
gauge!(
"danube_connector_health",
"connector" => self.connector_name.clone(),
"topic" => self.topic.clone(),
)
.set(if healthy { 1.0 } else { 0.0 });
}
}
#[allow(dead_code)]
pub struct ProcessingTimer {
start: std::time::Instant,
metrics: ConnectorMetrics,
}
#[allow(dead_code)]
impl ProcessingTimer {
pub fn new(metrics: ConnectorMetrics) -> Self {
metrics.increment_inflight();
Self {
start: std::time::Instant::now(),
metrics,
}
}
pub fn stop(self) {
let duration = self.start.elapsed();
self.metrics.record_processing_time(duration);
self.metrics.decrement_inflight();
}
}
impl Drop for ProcessingTimer {
fn drop(&mut self) {
self.metrics.decrement_inflight();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_creation() {
let metrics = ConnectorMetrics::new("test-connector", "/default/test");
assert_eq!(metrics.connector_name, "test-connector");
assert_eq!(metrics.topic, "/default/test");
}
#[test]
fn test_timer() {
let metrics = ConnectorMetrics::new("test-connector", "/default/test");
let timer = ProcessingTimer::new(metrics.clone());
std::thread::sleep(Duration::from_millis(10));
timer.stop();
}
}