use std::sync::OnceLock;
use prometheus::{HistogramOpts, HistogramVec, IntCounterVec, Opts, Registry, TextEncoder};
const EVENT_INGEST_BUCKETS: &[f64] = &[
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
];
fn registry() -> &'static Registry {
static REG: OnceLock<Registry> = OnceLock::new();
REG.get_or_init(Registry::new)
}
pub fn events_ingested_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_events_ingested_total",
"Total events accepted by POST /api/events (incremented once per handler call, whether the body persisted or errored).",
),
&["event_type", "status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn event_ingest_duration_seconds() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let hist = HistogramVec::new(
HistogramOpts::new(
"noetl_event_ingest_duration_seconds",
"Wall-clock time spent inside POST /api/events.",
)
.buckets(EVENT_INGEST_BUCKETS.to_vec()),
&["event_type"],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(hist.clone()))
.expect("histogram registration must succeed");
hist
})
}
pub fn record_event_ingest(event_type: &str, status: &str, duration_seconds: f64) {
events_ingested_total()
.with_label_values(&[event_type, status])
.inc();
event_ingest_duration_seconds()
.with_label_values(&[event_type])
.observe(duration_seconds);
}
pub mod endpoint {
pub const CATALOG_REGISTER: &str = "catalog_register";
pub const CREDENTIALS_UPSERT: &str = "credentials_upsert";
pub const KEYCHAIN_SET: &str = "keychain_set";
pub const RUNTIME_REGISTER: &str = "runtime_register";
pub const RUNTIME_HEARTBEAT: &str = "runtime_heartbeat";
}
pub fn write_requests_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_write_requests_total",
"Total POST requests to write endpoints other than /api/events (counted once per handler call, Ok or Err).",
),
&["endpoint", "status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn write_request_duration_seconds() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let hist = HistogramVec::new(
HistogramOpts::new(
"noetl_write_request_duration_seconds",
"Wall-clock time spent inside POST write endpoints (other than /api/events).",
)
.buckets(EVENT_INGEST_BUCKETS.to_vec()),
&["endpoint"],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(hist.clone()))
.expect("histogram registration must succeed");
hist
})
}
pub fn record_write_request(endpoint: &str, status: &str, duration_seconds: f64) {
write_requests_total()
.with_label_values(&[endpoint, status])
.inc();
write_request_duration_seconds()
.with_label_values(&[endpoint])
.observe(duration_seconds);
}
pub fn credentials_sealed_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_credentials_sealed_total",
"GET /api/credentials/{id}/sealed calls by outcome status.",
),
&["status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_credential_seal(status: &str) {
credentials_sealed_total()
.with_label_values(&[status])
.inc();
}
pub fn secret_resolve_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_resolve_total",
"Keychain-entry resolutions against external secret providers, by \
provider + region + outcome.",
),
&["provider", "region", "status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_secret_resolve(provider: &str, region: &str, status: &str) {
let region_label = if region.is_empty() { "-" } else { region };
secret_resolve_total()
.with_label_values(&[provider, region_label, status])
.inc();
}
pub fn secret_provider_build_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_provider_build_total",
"ProviderRegistry get_or_build outcomes per (provider, region).",
),
&["provider", "region", "status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_secret_provider_build(provider: &str, region: &str, status: &str) {
let region_label = if region.is_empty() { "-" } else { region };
secret_provider_build_total()
.with_label_values(&[provider, region_label, status])
.inc();
}
pub fn secret_resolve_duration_seconds() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let h = HistogramVec::new(
HistogramOpts::new(
"noetl_secret_resolve_duration_seconds",
"Wall-clock seconds spent resolving one keychain entry against \
its provider.",
)
.buckets(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0,
]),
&["provider", "region"],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(h.clone()))
.expect("histogram registration must succeed");
h
})
}
pub fn record_secret_resolve_duration(provider: &str, region: &str, seconds: f64) {
let region_label = if region.is_empty() { "-" } else { region };
secret_resolve_duration_seconds()
.with_label_values(&[provider, region_label])
.observe(seconds);
}
pub fn secret_residency_check_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_residency_check_total",
"Residency-policy gate outcomes per keychain-entry \
resolution (Secrets Wallet Phase 6c).",
),
&["policy", "decision"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_secret_residency_check(policy: &str, decision: &str) {
secret_residency_check_total()
.with_label_values(&[policy, decision])
.inc();
}
pub fn secret_dynamic_ttl_seconds() -> &'static prometheus::Histogram {
static M: OnceLock<prometheus::Histogram> = OnceLock::new();
M.get_or_init(|| {
let h = prometheus::Histogram::with_opts(
HistogramOpts::new(
"noetl_secret_dynamic_ttl_seconds",
"Issuer-reported time-to-expiry of resolved dynamic secrets (Phase 6d).",
)
.buckets(vec![60.0, 300.0, 900.0, 3600.0, 14400.0, 43200.0]),
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(h.clone()))
.expect("histogram registration must succeed");
h
})
}
pub fn record_secret_dynamic_ttl(seconds: f64) {
secret_dynamic_ttl_seconds().observe(seconds);
}
pub fn secret_cache_skip_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_cache_skip_total",
"Keychain-cache writes skipped by reason (Phase 6d).",
),
&["reason"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_secret_cache_skip(reason: &str) {
secret_cache_skip_total().with_label_values(&[reason]).inc();
}
pub fn cross_region_broker_call_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_broker_call_total",
"Cross-region broker call outcomes per broker_region (Phase 6e).",
),
&["broker_region", "outcome"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_cross_region_broker_call(broker_region: &str, outcome: &str) {
let region_label = if broker_region.is_empty() {
"-"
} else {
broker_region
};
cross_region_broker_call_total()
.with_label_values(&[region_label, outcome])
.inc();
}
pub fn cross_region_broker_call_duration_seconds() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let h = HistogramVec::new(
HistogramOpts::new(
"noetl_secret_broker_call_duration_seconds",
"Wall-clock seconds spent in a cross-region broker call.",
)
.buckets(vec![0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]),
&["broker_region"],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(h.clone()))
.expect("histogram registration must succeed");
h
})
}
pub fn record_cross_region_broker_call_duration(broker_region: &str, seconds: f64) {
let region_label = if broker_region.is_empty() {
"-"
} else {
broker_region
};
cross_region_broker_call_duration_seconds()
.with_label_values(&[region_label])
.observe(seconds);
}
pub fn wallet_rotate_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_wallet_rotate_total",
"Wallet KEK-rotation pass outcomes per table (Phase 7a).",
),
&["table", "status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_wallet_rotate(table: &str, status: &str) {
wallet_rotate_total()
.with_label_values(&[table, status])
.inc();
}
pub fn secret_audit_writes_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_audit_writes_total",
"Secret-resolution audit-write outcomes (Phase 7b).",
),
&["operation", "outcome", "status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_secret_audit_write(operation: &str, outcome: &str, status: &str) {
secret_audit_writes_total()
.with_label_values(&[operation, outcome, status])
.inc();
}
pub fn secret_refresh_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_secret_refresh_total",
"Token auto-renewal outcomes (Phase 7c). Aliases are NOT \
labeled (cardinality); per-alias detail lives on the \
secret.refresh tracing span.",
),
&["outcome"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_secret_refresh(outcome: &str) {
secret_refresh_total().with_label_values(&[outcome]).inc();
}
pub fn result_store_put_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_result_store_put_total",
"PUT /api/result/{execution_id} calls by outcome status.",
),
&["status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn result_store_put_duration_seconds() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let h = HistogramVec::new(
HistogramOpts::new(
"noetl_result_store_put_duration_seconds",
"Wall-clock seconds for PUT /api/result/{execution_id}.",
)
.buckets(EVENT_INGEST_BUCKETS.to_vec()),
&["status"],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(h.clone()))
.expect("histogram registration must succeed");
h
})
}
pub fn record_result_store_put(duration_seconds: f64, bytes: usize, status: &str) {
result_store_put_total()
.with_label_values(&[status])
.inc();
result_store_put_duration_seconds()
.with_label_values(&[status])
.observe(duration_seconds);
let _ = bytes; }
pub fn result_store_resolve_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_result_store_resolve_total",
"GET /api/result/resolve calls by outcome status.",
),
&["status"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn result_store_resolve_duration_seconds() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let h = HistogramVec::new(
HistogramOpts::new(
"noetl_result_store_resolve_duration_seconds",
"Wall-clock seconds for GET /api/result/resolve.",
)
.buckets(EVENT_INGEST_BUCKETS.to_vec()),
&["status"],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(h.clone()))
.expect("histogram registration must succeed");
h
})
}
pub fn record_result_store_resolve(duration_seconds: f64, status: &str) {
result_store_resolve_total()
.with_label_values(&[status])
.inc();
result_store_resolve_duration_seconds()
.with_label_values(&[status])
.observe(duration_seconds);
}
pub fn secret_refresh_duration_seconds() -> &'static prometheus::Histogram {
static M: OnceLock<prometheus::Histogram> = OnceLock::new();
M.get_or_init(|| {
let h = prometheus::Histogram::with_opts(
HistogramOpts::new(
"noetl_secret_refresh_duration_seconds",
"Wall-clock seconds spent in one token auto-renewal (Phase 7c).",
)
.buckets(vec![0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]),
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(h.clone()))
.expect("histogram registration must succeed");
h
})
}
pub fn record_secret_refresh_duration(seconds: f64) {
secret_refresh_duration_seconds().observe(seconds);
}
pub fn execute_outcomes_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_execute_outcomes_total",
"Executions handled by /api/execute(/batch), bucketed by entry path and dedup outcome (noetl/ai-meta#90 Phase 7).",
),
&["entry", "outcome"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn execute_batch_size() -> &'static HistogramVec {
static M: OnceLock<HistogramVec> = OnceLock::new();
M.get_or_init(|| {
let hist = HistogramVec::new(
HistogramOpts::new(
"noetl_execute_batch_size",
"Number of executions submitted in one POST /api/execute/batch call (noetl/ai-meta#90 Phase 7).",
)
.buckets(vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0]),
&[],
)
.expect("static histogram spec must be valid");
registry()
.register(Box::new(hist.clone()))
.expect("histogram registration must succeed");
hist
})
}
pub fn record_execute_outcome(entry: &str, outcome: &str) {
execute_outcomes_total()
.with_label_values(&[entry, outcome])
.inc();
}
pub fn record_execute_batch_size(n: usize) {
execute_batch_size().with_label_values(&[]).observe(n as f64);
}
pub fn gather_text() -> Result<String, prometheus::Error> {
let encoder = TextEncoder::new();
let metric_families = registry().gather();
encoder.encode_to_string(&metric_families)
}
pub fn container_callback_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_container_callback_total",
"Container-tool callback receives that matched an in-flight \
execution and emitted a call.done event (Container Tool \
Callback umbrella, noetl/ai-meta#43).",
),
&["state"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_container_callback(state: &str) {
container_callback_total()
.with_label_values(&[state])
.inc();
}
pub fn container_callback_stale_total() -> &'static IntCounterVec {
static M: OnceLock<IntCounterVec> = OnceLock::new();
M.get_or_init(|| {
let counter = IntCounterVec::new(
Opts::new(
"noetl_container_callback_stale_total",
"Container-tool callback receives that did NOT match any \
in-flight execution (stale — execution gc'd, watcher \
mis-namespaced, or Job created out-of-band).",
),
&["state"],
)
.expect("static counter spec must be valid");
registry()
.register(Box::new(counter.clone()))
.expect("counter registration must succeed");
counter
})
}
pub fn record_container_callback_stale(state: &str) {
container_callback_stale_total()
.with_label_values(&[state])
.inc();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_initializes_once() {
let a = registry() as *const Registry;
let b = registry() as *const Registry;
assert_eq!(a, b, "registry() must return the same instance");
}
#[test]
fn counter_increments_by_label_set() {
events_ingested_total()
.with_label_values(&["test.counter_increments", "ok"])
.inc();
events_ingested_total()
.with_label_values(&["test.counter_increments", "ok"])
.inc();
let value = events_ingested_total()
.with_label_values(&["test.counter_increments", "ok"])
.get();
assert!(value >= 2, "expected at least 2 increments, got {value}");
}
#[test]
fn histogram_observes_duration() {
event_ingest_duration_seconds()
.with_label_values(&["test.histogram_observes"])
.observe(0.123);
let text = gather_text().expect("gather_text must succeed");
assert!(
text.contains("test.histogram_observes"),
"expected histogram label in text:\n{text}"
);
}
#[test]
fn gather_text_contains_metric_names() {
record_event_ingest("test.gather_text", "ok", 0.05);
let text = gather_text().expect("gather_text must succeed");
assert!(
text.contains("noetl_events_ingested_total"),
"expected counter name in text:\n{text}"
);
assert!(
text.contains("noetl_event_ingest_duration_seconds"),
"expected histogram name in text:\n{text}"
);
}
#[test]
fn record_event_ingest_handles_both_statuses() {
record_event_ingest("test.both_statuses", "ok", 0.01);
record_event_ingest("test.both_statuses", "error", 0.02);
let text = gather_text().expect("gather_text must succeed");
assert!(text.contains("test.both_statuses"));
assert!(
text.contains("status=\"ok\""),
"expected status=ok label in text:\n{text}"
);
assert!(
text.contains("status=\"error\""),
"expected status=error label in text:\n{text}"
);
}
#[test]
fn write_request_counter_increments_by_label_set() {
record_write_request("test.write.counter", "ok", 0.01);
record_write_request("test.write.counter", "ok", 0.02);
let value = write_requests_total()
.with_label_values(&["test.write.counter", "ok"])
.get();
assert!(value >= 2, "expected at least 2 increments, got {value}");
}
#[test]
fn write_request_metric_names_appear_in_text() {
record_write_request("test.write.text", "ok", 0.05);
let text = gather_text().expect("gather_text must succeed");
assert!(
text.contains("noetl_write_requests_total"),
"expected counter name in text:\n{text}"
);
assert!(
text.contains("noetl_write_request_duration_seconds"),
"expected histogram name in text:\n{text}"
);
assert!(text.contains("endpoint=\"test.write.text\""));
}
#[test]
fn endpoint_constants_are_used_consistently() {
let names = [
endpoint::CATALOG_REGISTER,
endpoint::CREDENTIALS_UPSERT,
endpoint::KEYCHAIN_SET,
endpoint::RUNTIME_REGISTER,
endpoint::RUNTIME_HEARTBEAT,
];
assert_eq!(
names.iter().collect::<std::collections::HashSet<_>>().len(),
names.len()
);
assert!(names.iter().all(|n| !n.is_empty()));
}
}