use std::sync::atomic::{AtomicBool, Ordering};
use metrics::{counter, gauge, histogram};
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
static METRICS_ENABLED: AtomicBool = AtomicBool::new(false);
pub(crate) fn set_enabled(enabled: bool) {
METRICS_ENABLED.store(enabled, Ordering::Relaxed);
}
#[inline(always)]
pub(crate) fn is_enabled() -> bool {
METRICS_ENABLED.load(Ordering::Relaxed)
}
pub fn init_metrics() -> PrometheusHandle {
init_metrics_with_instance(None)
}
const LATENCY_BUCKETS: &[f64] = &[
0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0,
];
const SIZE_BUCKETS: &[f64] = &[1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0];
pub fn init_metrics_with_instance(instance_id: Option<&str>) -> PrometheusHandle {
set_enabled(true);
let build = || {
let mut b = PrometheusBuilder::new();
b = b
.set_buckets_for_metric(
metrics_exporter_prometheus::Matcher::Suffix("_seconds".to_string()),
LATENCY_BUCKETS,
)
.expect("latency buckets are non-empty and finite");
b = b
.set_buckets_for_metric(
metrics_exporter_prometheus::Matcher::Suffix("_batch_size".to_string()),
SIZE_BUCKETS,
)
.expect("size buckets are non-empty and finite");
if let Some(id) = instance_id {
b = b.add_global_label("instance", id);
}
b
};
build().install_recorder().unwrap_or_else(|_| {
build().build_recorder().handle()
})
}
pub fn record_message(channel: &str, status: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_messages_total", "channel" => channel.to_owned(), "status" => status)
.increment(1);
}
pub fn record_error(reason: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_errors_total", "reason" => reason).increment(1);
}
pub fn record_build_info() {
if !is_enabled() {
return;
}
gauge!(
"orion_build_info",
"version" => env!("CARGO_PKG_VERSION"),
"git_hash" => env!("GIT_HASH"),
"build_timestamp" => env!("BUILD_TIMESTAMP"),
)
.set(1.0);
}
pub fn record_admin_auth_failure(reason: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_admin_auth_failures_total", "reason" => reason).increment(1);
}
pub fn record_message_duration(channel: &str, duration_secs: f64) {
if !is_enabled() {
return;
}
histogram!("orion_message_duration_seconds", "channel" => channel.to_owned())
.record(duration_secs);
}
pub fn record_circuit_breaker_trip(connector: &str, channel: &str) {
if !is_enabled() {
return;
}
counter!(
"orion_circuit_breaker_trips_total",
"connector" => connector.to_owned(),
"channel" => channel.to_owned()
)
.increment(1);
}
pub fn record_circuit_breaker_rejection(connector: &str, channel: &str) {
if !is_enabled() {
return;
}
counter!(
"orion_circuit_breaker_rejections_total",
"connector" => connector.to_owned(),
"channel" => channel.to_owned()
)
.increment(1);
}
pub fn set_active_workflows(count: f64) {
if !is_enabled() {
return;
}
gauge!("orion_active_workflows").set(count);
}
pub fn record_http_request(method: &str, path: &str, status: u16, duration_secs: f64) {
if !is_enabled() {
return;
}
let status = status.to_string();
counter!(
"orion_http_requests_total",
"method" => method.to_owned(),
"path" => path.to_owned(),
"status" => status.clone()
)
.increment(1);
histogram!(
"orion_http_request_duration_seconds",
"method" => method.to_owned(),
"path" => path.to_owned(),
"status" => status
)
.record(duration_secs);
}
fn record_db_query_duration(operation: &'static str, duration_secs: f64) {
if !is_enabled() {
return;
}
histogram!("orion_db_query_duration_seconds", "operation" => operation).record(duration_secs);
}
pub async fn timed_db_op<F, T>(operation: &'static str, f: F) -> T
where
F: std::future::Future<Output = T>,
{
let start = std::time::Instant::now();
let result = f.await;
record_db_query_duration(operation, start.elapsed().as_secs_f64());
result
}
pub fn record_engine_reload_duration(duration_secs: f64) {
if !is_enabled() {
return;
}
histogram!("orion_engine_reload_duration_seconds").record(duration_secs);
}
pub fn record_engine_reload(status: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_engine_reloads_total", "status" => status).increment(1);
}
pub fn record_rate_limit_rejected(scope: &str) {
if !is_enabled() {
return;
}
counter!("orion_rate_limit_rejections_total", "scope" => scope.to_owned()).increment(1);
}
pub fn record_cache_hit(channel: &str) {
if !is_enabled() {
return;
}
counter!("orion_response_cache_hits_total", "channel" => channel.to_owned()).increment(1);
}
pub fn record_cache_miss(channel: &str) {
if !is_enabled() {
return;
}
counter!("orion_response_cache_misses_total", "channel" => channel.to_owned()).increment(1);
}
pub fn record_job_success(job: &'static str) {
if !is_enabled() {
return;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0);
gauge!("orion_job_last_success_timestamp_seconds", "job" => job).set(now);
}
pub fn set_trace_queue_depth(depth: f64) {
if !is_enabled() {
return;
}
gauge!("orion_trace_queue_depth").set(depth);
}
pub fn set_trace_workers_active(count: f64) {
if !is_enabled() {
return;
}
gauge!("orion_trace_workers_active").set(count);
}
pub fn set_trace_workers_total(count: f64) {
if !is_enabled() {
return;
}
gauge!("orion_trace_workers_total").set(count);
}
pub fn set_trace_queue_memory_bytes(bytes: f64) {
if !is_enabled() {
return;
}
gauge!("orion_trace_queue_memory_bytes").set(bytes);
}
pub fn record_trace_queue_rejected(reason: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_trace_queue_rejected_total", "reason" => reason).increment(1);
}
pub fn set_trace_dlq_depth(depth: f64) {
if !is_enabled() {
return;
}
gauge!("orion_trace_dlq_depth").set(depth);
}
pub fn record_trace_dlq_retry(outcome: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_trace_dlq_retries_total", "outcome" => outcome).increment(1);
}
pub fn record_trace_dropped(reason: &'static str) {
if !is_enabled() {
return;
}
counter!("orion_trace_dropped_total", "reason" => reason).increment(1);
}
pub fn set_trace_persistence_queue_depth(depth: f64) {
if !is_enabled() {
return;
}
gauge!("orion_trace_persistence_queue_depth").set(depth);
}
pub fn record_trace_persistence_batch_size(size: usize) {
if !is_enabled() {
return;
}
histogram!("orion_trace_persistence_batch_size").record(size as f64);
}
pub fn record_trace_persistence_failure() {
if !is_enabled() {
return;
}
counter!("orion_trace_persistence_failures_total").increment(1);
}
pub fn record_connector_request(connector: &str, channel: &str, status: &'static str) {
if !is_enabled() {
return;
}
counter!(
"orion_connector_requests_total",
"connector" => connector.to_owned(),
"channel" => channel.to_owned(),
"status" => status
)
.increment(1);
}
pub fn record_connector_duration(connector: &str, channel: &str, duration_secs: f64) {
if !is_enabled() {
return;
}
histogram!(
"orion_connector_request_duration_seconds",
"connector" => connector.to_owned(),
"channel" => channel.to_owned()
)
.record(duration_secs);
}
pub fn record_task_duration(workflow: &str, task: &str, function: &'static str, secs: f64) {
if !is_enabled() {
return;
}
histogram!(
"orion_task_duration_seconds",
"workflow" => workflow.to_owned(),
"task" => task.to_owned(),
"function" => function
)
.record(secs);
}
pub fn set_kafka_consumer_lag(topic: &str, partition: i32, lag: f64) {
if !is_enabled() {
return;
}
gauge!(
"orion_kafka_consumer_lag_messages",
"topic" => topic.to_owned(),
"partition" => partition.to_string()
)
.set(lag);
}
pub fn set_kafka_ingest_degraded(degraded: bool) {
if !is_enabled() {
return;
}
gauge!("orion_kafka_ingest_degraded").set(if degraded { 1.0 } else { 0.0 });
}
pub fn set_db_pool_size(size: f64) {
if !is_enabled() {
return;
}
gauge!("orion_db_pool_size").set(size);
}
pub fn set_db_pool_idle(idle: f64) {
if !is_enabled() {
return;
}
gauge!("orion_db_pool_idle").set(idle);
}
pub fn record_admin_audit(action: &str, resource_type: &str) {
if !is_enabled() {
return;
}
counter!(
"orion_admin_audit_events_total",
"action" => action.to_owned(),
"resource_type" => resource_type.to_owned()
)
.increment(1);
}
pub fn record_audit_events_dropped(reason: &'static str, count: u64) {
if !is_enabled() || count == 0 {
return;
}
counter!("orion_audit_events_dropped_total", "reason" => reason).increment(count);
}
pub fn record_audit_event_dropped(reason: &'static str) {
record_audit_events_dropped(reason, 1);
}
pub fn set_audit_queue_depth(depth: f64) {
if !is_enabled() {
return;
}
gauge!("orion_audit_queue_depth").set(depth);
}
#[cfg(test)]
pub(crate) fn render_local(f: impl FnOnce()) -> String {
set_enabled(true);
let recorder = PrometheusBuilder::new().build_recorder();
let handle = recorder.handle();
::metrics::with_local_recorder(&recorder, f);
handle.render()
}
#[cfg(test)]
mod tests {
use super::*;
fn ensure_recorder() {
let _ = PrometheusBuilder::new().install_recorder();
set_enabled(true);
}
#[test]
fn test_record_message() {
ensure_recorder();
record_message("test-channel", "ok");
record_message("test-channel", "error");
}
#[test]
fn test_record_error() {
ensure_recorder();
record_error("engine");
record_error("storage");
}
#[test]
fn test_record_message_duration() {
ensure_recorder();
record_message_duration("orders", 0.123);
}
#[test]
fn test_record_circuit_breaker_trip() {
ensure_recorder();
record_circuit_breaker_trip("my-connector", "orders");
}
#[test]
fn test_record_circuit_breaker_rejection() {
ensure_recorder();
record_circuit_breaker_rejection("my-connector", "orders");
}
#[test]
fn test_set_active_workflows() {
ensure_recorder();
set_active_workflows(5.0);
set_active_workflows(0.0);
}
#[test]
fn test_record_http_request() {
ensure_recorder();
record_http_request("GET", "/health", 200, 0.005);
record_http_request("POST", "/api/v1/data/orders", 201, 0.010);
}
#[test]
fn test_record_db_query_duration() {
ensure_recorder();
record_db_query_duration("list_rules", 0.010);
}
#[tokio::test]
async fn test_timed_db_op() {
ensure_recorder();
let result = timed_db_op("test_op", async { 42 }).await;
assert_eq!(result, 42);
}
#[test]
fn test_record_engine_reload_duration() {
ensure_recorder();
record_engine_reload_duration(0.250);
}
#[test]
fn test_record_engine_reload() {
ensure_recorder();
record_engine_reload("success");
record_engine_reload("failure");
}
#[test]
fn test_record_rate_limit_rejected() {
ensure_recorder();
record_rate_limit_rejected("orders");
record_rate_limit_rejected("admin");
}
#[test]
fn test_record_trace_queue_rejected() {
let out = render_local(|| {
record_trace_queue_rejected("full");
record_trace_queue_rejected("full");
record_trace_queue_rejected("memory");
});
assert!(
out.contains(r#"trace_queue_rejected_total{reason="full"} 2"#),
"missing full-queue rejections in:\n{out}"
);
assert!(
out.contains(r#"trace_queue_rejected_total{reason="memory"} 1"#),
"missing memory rejections in:\n{out}"
);
}
#[test]
fn test_record_trace_dlq_retry() {
let out = render_local(|| {
record_trace_dlq_retry("retried");
record_trace_dlq_retry("exhausted");
record_trace_dlq_retry("failed");
record_trace_dlq_retry("exhausted");
});
assert!(
out.contains(r#"trace_dlq_retries_total{outcome="retried"} 1"#),
"{out}"
);
assert!(
out.contains(r#"trace_dlq_retries_total{outcome="exhausted"} 2"#),
"{out}"
);
assert!(
out.contains(r#"trace_dlq_retries_total{outcome="failed"} 1"#),
"{out}"
);
}
#[test]
fn test_set_trace_dlq_depth() {
let out = render_local(|| {
set_trace_dlq_depth(7.0);
set_trace_dlq_depth(4.0);
});
assert!(
out.contains("trace_dlq_depth 4"),
"gauge must hold the latest value:\n{out}"
);
}
#[test]
fn test_record_job_success_stamps_unix_time_per_job() {
let out = render_local(|| {
record_job_success("trace_cleanup");
record_job_success("dlq_retry");
});
let value: f64 = out
.lines()
.find(|l| {
l.starts_with(r#"orion_job_last_success_timestamp_seconds{job="trace_cleanup"}"#)
})
.and_then(|l| l.rsplit(' ').next())
.and_then(|v| v.parse().ok())
.unwrap_or_default();
assert!(
value > 1e9,
"expected a unix-seconds gauge for trace_cleanup, got {value}; output:\n{out}"
);
assert!(
out.contains(r#"orion_job_last_success_timestamp_seconds{job="dlq_retry"}"#),
"each job must get its own series:\n{out}"
);
}
#[test]
fn test_record_trace_persistence_failure() {
let out = render_local(|| {
record_trace_persistence_failure();
record_trace_persistence_failure();
record_trace_persistence_failure();
});
assert!(out.contains("trace_persistence_failures_total 3"), "{out}");
}
#[test]
fn errors_total_is_labelled_by_reason() {
let out = render_local(|| record_error("engine"));
assert!(
out.contains(r#"orion_errors_total{reason="engine"}"#),
"errors must be labelled by reason:\n{out}"
);
assert!(
!out.contains(r#"type="engine""#),
"the old `type` label must be gone:\n{out}"
);
}
#[test]
fn one_per_channel_invocation_counter() {
let out = render_local(|| {
record_message("orders", "ok");
record_message("orders", "error");
record_message_duration("orders", 0.01);
});
assert!(
out.contains(r#"orion_messages_total{channel="orders",status="ok"} 1"#),
"messages must carry channel + status:\n{out}"
);
assert!(
out.contains(r#"orion_messages_total{channel="orders",status="error"} 1"#),
"the error arm must land on the same family:\n{out}"
);
assert!(
!out.contains("channel_executions_total"),
"the redundant second counter must stay gone:\n{out}"
);
}
#[test]
fn kafka_lag_gauge_is_prefixed_and_carries_its_unit() {
let out = render_local(|| set_kafka_consumer_lag("orders", 3, 42.0));
assert!(
out.contains(r#"orion_kafka_consumer_lag_messages{"#),
"the lag gauge must be prefixed and unit-suffixed:\n{out}"
);
assert!(out.contains(r#"topic="orders""#), "{out}");
assert!(out.contains(r#"partition="3""#), "{out}");
assert!(
!out.contains("# TYPE kafka_consumer_lag gauge"),
"the unprefixed family must stay gone:\n{out}"
);
}
#[test]
fn test_record_audit_events_dropped() {
let out = render_local(|| {
record_audit_event_dropped("queue_full");
record_audit_event_dropped("queue_full");
record_audit_event_dropped("write_failed");
record_audit_events_dropped("drain_timeout", 7);
record_audit_events_dropped("writer_stopped", 0);
});
assert!(
out.contains(r#"orion_audit_events_dropped_total{reason="queue_full"} 2"#),
"{out}"
);
assert!(
out.contains(r#"orion_audit_events_dropped_total{reason="write_failed"} 1"#),
"{out}"
);
assert!(
out.contains(r#"orion_audit_events_dropped_total{reason="drain_timeout"} 7"#),
"the batch form must add its count, not one:\n{out}"
);
assert!(
!out.contains(r#"reason="writer_stopped""#),
"a zero-count drop must not create a series:\n{out}"
);
}
#[test]
fn test_set_audit_queue_depth() {
let out = render_local(|| {
set_audit_queue_depth(12.0);
set_audit_queue_depth(3.0);
});
assert!(
out.contains("orion_audit_queue_depth 3"),
"gauge must hold the latest value:\n{out}"
);
}
#[test]
fn test_init_metrics() {
let handle = init_metrics();
let output = handle.render();
assert!(output.is_ascii());
}
}