use tracing::{Level, Span, event, span};
use uuid::Uuid;
#[cfg(feature = "prometheus")]
use std::time::Instant;
const TARGET: &str = "axum_gate::audit";
pub fn request_span(method: &str, path: &str, request_id: Option<&str>) -> Span {
match request_id {
Some(id) => span!(target: TARGET, Level::INFO, "request", %method, %path, request_id = %id),
None => span!(target: TARGET, Level::INFO, "request", %method, %path),
}
}
pub fn authorization_span(account_id: Option<&Uuid>, role: Option<&str>) -> Span {
match (account_id, role) {
(Some(id), Some(role)) => {
span!(target: TARGET, Level::INFO, "authz.check", account_id = %id, role = %role)
}
(Some(id), None) => span!(target: TARGET, Level::INFO, "authz.check", account_id = %id),
(None, Some(role)) => span!(target: TARGET, Level::INFO, "authz.check", role = %role),
(None, None) => span!(target: TARGET, Level::INFO, "authz.check"),
}
}
pub fn authorized(account_id: &Uuid, role: Option<&str>) {
match role {
Some(r) => {
event!(target: TARGET, Level::INFO, account_id = %account_id, role = %r, "authorized")
}
None => event!(target: TARGET, Level::INFO, account_id = %account_id, "authorized"),
}
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.authz_authorized.inc();
}
}
pub fn denied(account_id: Option<&Uuid>, reason_code: &str) {
match account_id {
Some(id) => {
event!(target: TARGET, Level::WARN, account_id = %id, reason = %reason_code, "denied")
}
None => event!(target: TARGET, Level::WARN, reason = %reason_code, "denied"),
}
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.authz_denied.with_label_values(&[reason_code]).inc();
}
}
pub fn jwt_invalid_issuer(expected: &str, actual: &str) {
event!(
target: TARGET,
Level::WARN,
expected_issuer = %expected,
actual_issuer = %actual,
"jwt_invalid_issuer"
);
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.jwt_invalid
.with_label_values(&[prometheus_metrics::JwtInvalidKind::Issuer.as_ref()])
.inc();
}
}
pub fn jwt_invalid_token(summary: &str) {
event!(target: TARGET, Level::WARN, error = %summary, "jwt_invalid_token");
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.jwt_invalid
.with_label_values(&[prometheus_metrics::JwtInvalidKind::Token.as_ref()])
.inc();
}
}
pub fn account_delete_start(user_id: &str, account_id: &Uuid) {
event!(
target: TARGET,
Level::INFO,
%user_id,
account_id = %account_id,
"account_delete_start"
);
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.account_delete_outcome
.with_label_values(&[
prometheus_metrics::AccountDeleteOutcome::Start.as_ref(),
prometheus_metrics::SecretRestored::None_.as_ref(),
])
.inc();
}
}
pub fn account_delete_success(user_id: &str, account_id: &Uuid) {
event!(
target: TARGET,
Level::INFO,
%user_id,
account_id = %account_id,
"account_delete_success"
);
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.account_delete_outcome
.with_label_values(&[
prometheus_metrics::AccountDeleteOutcome::Success.as_ref(),
prometheus_metrics::SecretRestored::None_.as_ref(),
])
.inc();
}
}
pub fn account_delete_failure(
user_id: &str,
account_id: &Uuid,
secret_restored: Option<bool>,
error_summary: &str,
) {
match secret_restored {
Some(true) => event!(
target: TARGET,
Level::ERROR,
%user_id,
account_id = %account_id,
error = %error_summary,
secret_restored = true,
"account_delete_failure"
),
Some(false) => event!(
target: TARGET,
Level::ERROR,
%user_id,
account_id = %account_id,
error = %error_summary,
secret_restored = false,
"account_delete_failure"
),
None => event!(
target: TARGET,
Level::ERROR,
%user_id,
account_id = %account_id,
error = %error_summary,
"account_delete_failure"
),
}
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
use prometheus_metrics::{AccountDeleteOutcome, SecretRestored};
let sr = match secret_restored {
Some(true) => SecretRestored::True,
Some(false) => SecretRestored::False,
None => SecretRestored::None_,
};
m.account_delete_outcome
.with_label_values(&[AccountDeleteOutcome::Failure.as_ref(), sr.as_ref()])
.inc();
}
}
pub fn account_created(user_id: &str, account_id: &Uuid) {
event!(
target: TARGET,
Level::INFO,
%user_id,
account_id = %account_id,
"account_created"
);
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.account_insert_outcome
.with_label_values(&[
prometheus_metrics::AccountInsertOutcome::Success.as_ref(),
"none",
])
.inc();
}
}
pub fn account_insert_failure(user_id: &str, reason_code: &str) {
event!(
target: TARGET,
Level::ERROR,
%user_id,
reason = %reason_code,
"account_insert_failure"
);
#[cfg(feature = "prometheus")]
if let Some(m) = prometheus_metrics::metrics() {
m.account_insert_outcome
.with_label_values(&[
prometheus_metrics::AccountInsertOutcome::Failure.as_ref(),
reason_code,
])
.inc();
}
}
#[cfg(feature = "prometheus")]
#[derive(Copy, Clone, Debug)]
pub enum AuthzOutcome {
Authorized,
Denied,
}
#[cfg(feature = "prometheus")]
impl AuthzOutcome {
fn as_label(&self) -> &'static str {
match self {
AuthzOutcome::Authorized => "authorized",
AuthzOutcome::Denied => "denied",
}
}
}
#[cfg(feature = "prometheus")]
pub fn observe_authz_latency(start: Instant, outcome: AuthzOutcome) {
if let Some(m) = prometheus_metrics::metrics() {
let elapsed = start.elapsed().as_secs_f64();
m.authz_decision_latency
.with_label_values(&[outcome.as_label()])
.observe(elapsed);
}
}
#[cfg(feature = "prometheus")]
pub mod prometheus_metrics {
use prometheus::{Counter, CounterVec, HistogramOpts, HistogramVec, Registry};
use std::sync::OnceLock;
use strum::AsRefStr;
#[derive(Copy, Clone, Debug, Eq, PartialEq, AsRefStr)]
pub enum JwtInvalidKind {
Issuer,
Token,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, AsRefStr)]
pub enum AccountDeleteOutcome {
Start,
Success,
Failure,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, AsRefStr)]
pub enum SecretRestored {
True,
False,
#[strum(serialize = "none")]
None_,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, AsRefStr)]
pub enum AccountInsertOutcome {
Success,
Failure,
}
#[derive(Copy, Clone, Debug)]
pub enum JwtValidationOutcome {
#[doc = "JWT was successfully validated."]
Valid,
#[doc = "JWT failed issuer validation (issuer mismatch)."]
InvalidIssuer,
#[doc = "JWT was invalid for other reasons (decode failure, signature, format, etc.)."]
InvalidToken,
}
impl JwtValidationOutcome {
#[doc = "Returns the stable label value used in `axum_gate_jwt_validation_seconds`."]
pub fn as_label(&self) -> &'static str {
match self {
JwtValidationOutcome::Valid => "valid",
JwtValidationOutcome::InvalidIssuer => "invalid_issuer",
JwtValidationOutcome::InvalidToken => "invalid_token",
}
}
}
pub struct Metrics {
pub authz_authorized: Counter,
pub authz_denied: CounterVec,
pub jwt_invalid: CounterVec,
pub account_delete_outcome: CounterVec,
pub account_insert_outcome: CounterVec,
pub authz_decision_latency: HistogramVec,
pub jwt_validation_latency: HistogramVec,
}
static METRICS: OnceLock<Metrics> = OnceLock::new();
pub fn metrics() -> Option<&'static Metrics> {
METRICS.get()
}
pub fn install_prometheus_metrics() -> Result<(), prometheus::Error> {
install_prometheus_metrics_with_registry(prometheus::default_registry())
}
pub fn install_prometheus_metrics_with_registry(
registry: &Registry,
) -> Result<(), prometheus::Error> {
if METRICS.get().is_some() {
return Ok(()); }
let authz_authorized = Counter::new(
"axum_gate_authz_authorized_total",
"Total number of successful authorization decisions",
)?;
let authz_denied = CounterVec::new(
prometheus::Opts::new(
"axum_gate_authz_denied_total",
"Total number of denied authorization attempts",
),
&["reason"],
)?;
let jwt_invalid = CounterVec::new(
prometheus::Opts::new(
"axum_gate_jwt_invalid_total",
"Total number of invalid JWT tokens",
),
&["kind"],
)?;
let account_delete_outcome = CounterVec::new(
prometheus::Opts::new(
"axum_gate_account_delete_outcome_total",
"Total number of account deletion operations",
),
&["outcome", "secret_restored"],
)?;
let account_insert_outcome = CounterVec::new(
prometheus::Opts::new(
"axum_gate_account_insert_outcome_total",
"Total number of account insertion operations",
),
&["outcome", "reason"],
)?;
let authz_decision_latency = HistogramVec::new(
HistogramOpts::new(
"axum_gate_authz_decision_seconds",
"Authorization decision latency in seconds",
)
.buckets(vec![
0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0,
]),
&["outcome"],
)?;
let jwt_validation_latency = HistogramVec::new(
HistogramOpts::new(
"axum_gate_jwt_validation_seconds",
"JWT validation latency in seconds",
)
.buckets(vec![
0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5,
]),
&["outcome"],
)?;
registry.register(Box::new(authz_authorized.clone()))?;
registry.register(Box::new(authz_denied.clone()))?;
registry.register(Box::new(jwt_invalid.clone()))?;
registry.register(Box::new(account_delete_outcome.clone()))?;
registry.register(Box::new(account_insert_outcome.clone()))?;
registry.register(Box::new(authz_decision_latency.clone()))?;
registry.register(Box::new(jwt_validation_latency.clone()))?;
let metrics = Metrics {
authz_authorized,
authz_denied,
jwt_invalid,
account_delete_outcome,
account_insert_outcome,
authz_decision_latency,
jwt_validation_latency,
};
let _ = METRICS.set(metrics);
Ok(())
}
pub fn observe_jwt_validation_latency(
start: std::time::Instant,
outcome: JwtValidationOutcome,
) {
if let Some(m) = metrics() {
let elapsed = start.elapsed().as_secs_f64();
m.jwt_validation_latency
.with_label_values(&[outcome.as_label()])
.observe(elapsed);
}
}
}