use axum::{
body::Body,
extract::Request,
http::{HeaderValue, Response},
middleware::Next,
};
use std::time::Instant;
use tracing::{info, info_span, warn, Instrument};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LicenseEvent {
Created,
Bound,
Released,
Validated,
ValidationFailed,
Activated,
Deactivated,
Revoked,
Reinstated,
Suspended,
Extended,
Blacklisted,
Heartbeat,
UsageUpdated,
}
impl std::fmt::Display for LicenseEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
LicenseEvent::Created => "created",
LicenseEvent::Bound => "bound",
LicenseEvent::Released => "released",
LicenseEvent::Validated => "validated",
LicenseEvent::ValidationFailed => "validation_failed",
LicenseEvent::Activated => "activated",
LicenseEvent::Deactivated => "deactivated",
LicenseEvent::Revoked => "revoked",
LicenseEvent::Reinstated => "reinstated",
LicenseEvent::Suspended => "suspended",
LicenseEvent::Extended => "extended",
LicenseEvent::Blacklisted => "blacklisted",
LicenseEvent::Heartbeat => "heartbeat",
LicenseEvent::UsageUpdated => "usage_updated",
};
write!(f, "{}", s)
}
}
pub fn log_license_event(event: LicenseEvent, license_id: &str, details: Option<&str>) {
let span = info_span!(
"license_event",
event = %event,
license_id = %license_id,
);
let _enter = span.enter();
match event {
LicenseEvent::ValidationFailed => {
if let Some(d) = details {
warn!(reason = %d, "License event occurred");
} else {
warn!("License event occurred");
}
}
_ => {
if let Some(d) = details {
info!(details = %d, "License event occurred");
} else {
info!("License event occurred");
}
}
}
}
pub fn log_license_binding_event(
event: LicenseEvent,
license_id: &str,
hardware_id: &str,
device_name: Option<&str>,
) {
let span = info_span!(
"license_binding",
event = %event,
license_id = %license_id,
hardware_id = %hardware_id,
);
let _enter = span.enter();
if let Some(name) = device_name {
info!(device_name = %name, "License binding event occurred");
} else {
info!("License binding event occurred");
}
}
pub const REQUEST_ID_HEADER: &str = "X-Request-Id";
pub fn generate_request_id() -> String {
Uuid::new_v4().to_string()
}
pub async fn request_logging_middleware(request: Request, next: Next) -> Response<Body> {
let request_id = generate_request_id();
let method = request.method().clone();
let uri = request.uri().clone();
let path = uri.path().to_string();
let span = info_span!(
"request",
request_id = %request_id,
method = %method,
path = %path,
);
let start = Instant::now();
let response = async move {
info!("Started processing request");
let response = next.run(request).await;
response
}
.instrument(span.clone())
.await;
let duration = start.elapsed();
let status = response.status();
let _enter = span.enter();
info!(
status = %status.as_u16(),
duration_ms = %duration.as_millis(),
"Request completed"
);
let (mut parts, body) = response.into_parts();
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
parts.headers.insert(REQUEST_ID_HEADER, header_value);
}
Response::from_parts(parts, body)
}
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HealthResponse {
pub status: String,
pub service: String,
pub version: String,
pub database: DatabaseHealth,
}
#[derive(Debug, Clone, serde::Serialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct DatabaseHealth {
pub connected: bool,
pub db_type: String,
}
impl HealthResponse {
pub fn healthy(db_connected: bool, db_type: &str) -> Self {
Self {
status: if db_connected { "healthy" } else { "degraded" }.to_string(),
service: "talos".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
database: DatabaseHealth {
connected: db_connected,
db_type: db_type.to_string(),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_id_is_valid_uuid() {
let id = generate_request_id();
assert!(Uuid::parse_str(&id).is_ok());
}
#[test]
fn health_response_healthy() {
let health = HealthResponse::healthy(true, "sqlite");
assert_eq!(health.status, "healthy");
assert_eq!(health.service, "talos");
assert!(health.database.connected);
}
#[test]
fn health_response_degraded() {
let health = HealthResponse::healthy(false, "postgres");
assert_eq!(health.status, "degraded");
assert!(!health.database.connected);
}
}