use std::borrow::Cow;
use std::sync::{Arc, OnceLock};
use std::time::Instant;
use axum::extract::{MatchedPath, Request};
use axum::http::HeaderValue;
use axum::middleware::Next;
use axum::response::Response;
use tracing::{Instrument, info_span};
use uuid::Uuid;
use super::request_id::{CURRENT_REQUEST_ID, RequestId, X_REQUEST_ID};
use crate::config::Environment;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct AuthSummary {
pub(crate) client_id: Cow<'static, str>,
pub(crate) client_ip: Cow<'static, str>,
pub(crate) user_id: Cow<'static, str>,
pub(crate) auth_method: Cow<'static, str>,
pub(crate) auth_result: Cow<'static, str>,
pub(crate) is_privileged: bool,
}
impl AuthSummary {
pub fn builder() -> AuthSummaryBuilder {
AuthSummaryBuilder(Self::default())
}
#[must_use]
pub fn client_id(&self) -> &str {
&self.client_id
}
#[must_use]
pub fn client_ip(&self) -> &str {
&self.client_ip
}
#[must_use]
pub fn user_id(&self) -> &str {
&self.user_id
}
#[must_use]
pub fn auth_method(&self) -> &str {
&self.auth_method
}
#[must_use]
pub fn auth_result(&self) -> &str {
&self.auth_result
}
#[must_use]
pub const fn is_privileged(&self) -> bool {
self.is_privileged
}
}
#[derive(Debug, Default)]
#[must_use = "AuthSummaryBuilder does nothing until `.build()` is called"]
pub struct AuthSummaryBuilder(AuthSummary);
impl AuthSummaryBuilder {
pub fn client_id(mut self, v: impl Into<Cow<'static, str>>) -> Self {
self.0.client_id = v.into();
self
}
pub fn client_ip(mut self, v: impl Into<Cow<'static, str>>) -> Self {
self.0.client_ip = v.into();
self
}
pub fn user_id(mut self, v: impl Into<Cow<'static, str>>) -> Self {
self.0.user_id = v.into();
self
}
pub fn auth_method(mut self, v: impl Into<Cow<'static, str>>) -> Self {
self.0.auth_method = v.into();
self
}
pub fn auth_result(mut self, v: impl Into<Cow<'static, str>>) -> Self {
self.0.auth_result = v.into();
self
}
pub const fn privileged(mut self, v: bool) -> Self {
self.0.is_privileged = v;
self
}
#[must_use]
pub fn build(self) -> AuthSummary {
self.0
}
}
#[derive(Clone, Default)]
pub struct LoggingContext {
inner: Arc<OnceLock<AuthSummary>>,
}
impl std::fmt::Debug for LoggingContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoggingContext").finish_non_exhaustive()
}
}
impl LoggingContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn set(&self, summary: AuthSummary) {
drop(self.inner.set(summary));
}
#[must_use]
pub fn get(&self) -> Option<&AuthSummary> {
self.inner.get()
}
}
const MAX_REQUEST_ID_LEN: usize = 128;
fn is_valid_request_id(s: &str) -> bool {
!s.is_empty()
&& s.len() <= MAX_REQUEST_ID_LEN
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
}
fn get_or_generate_request_id(headers: &http::HeaderMap) -> String {
headers
.get(X_REQUEST_ID)
.and_then(|h| h.to_str().ok())
.filter(|s| is_valid_request_id(s))
.map_or_else(|| Uuid::now_v7().to_string(), ToString::to_string)
}
fn detect_env() -> &'static str {
static ENV: OnceLock<String> = OnceLock::new();
ENV.get_or_init(|| {
std::env::var("GASKET_ENV")
.or_else(|_| std::env::var("DEPLOYMENT_ENV"))
.unwrap_or_else(|_| "local".to_string())
})
}
pub async fn logging_middleware(mut request: Request, next: Next) -> Response {
let start = Instant::now();
let request_id = get_or_generate_request_id(request.headers());
let method = request.method().to_string();
let path = request.uri().path().to_string();
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map_or_else(|| "unknown".to_string(), |mp| mp.as_str().to_string());
let logging_ctx = LoggingContext::new();
request.extensions_mut().insert(logging_ctx.clone());
request
.extensions_mut()
.insert(RequestId(request_id.clone()));
let user_agent = request
.headers()
.get(http::header::USER_AGENT)
.and_then(|h| h.to_str().ok())
.unwrap_or("")
.to_string();
let span = info_span!(
"http_request",
request_id = %request_id,
method = %method,
path = %path,
matched_path = %matched_path,
client_id = tracing::field::Empty,
client_ip = tracing::field::Empty,
user_id = tracing::field::Empty,
auth_method = tracing::field::Empty,
auth_result = tracing::field::Empty,
is_privileged = tracing::field::Empty,
status = tracing::field::Empty,
duration_ms = tracing::field::Empty,
env = %detect_env(),
user_agent = %user_agent,
);
let mut response = CURRENT_REQUEST_ID
.scope(request_id.clone(), async { next.run(request).await })
.instrument(span.clone())
.await;
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
response.headers_mut().insert(X_REQUEST_ID, header_value);
}
let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX);
let status = response.status().as_u16();
if let Some(c) = logging_ctx.get() {
span.record("client_id", c.client_id.as_ref());
span.record("client_ip", c.client_ip.as_ref());
span.record("user_id", c.user_id.as_ref());
span.record("auth_method", c.auth_method.as_ref());
span.record("auth_result", c.auth_result.as_ref());
span.record("is_privileged", c.is_privileged);
}
span.record("status", status);
span.record("duration_ms", duration_ms);
tracing::info!(parent: &span, "request completed");
response
}
pub fn init_tracing_from_env() {
let env_str = std::env::var("GASKET_ENV").unwrap_or_else(|_| "local".to_string());
let env: Environment =
serde_json::from_value(serde_json::Value::String(env_str)).unwrap_or(Environment::Local);
init_tracing(env);
}
pub fn init_tracing(env: Environment) {
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let registry = tracing_subscriber::registry().with(filter);
match env {
Environment::Local => {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(false)
.pretty();
registry.with(fmt_layer).init();
}
_ => {
#[cfg(feature = "json-log")]
{
let fmt_layer = tracing_subscriber::fmt::layer().json().with_target(true);
registry.with(fmt_layer).init();
}
#[cfg(not(feature = "json-log"))]
{
let fmt_layer = tracing_subscriber::fmt::layer().with_target(true);
registry.with(fmt_layer).init();
}
}
}
}
#[cfg(feature = "otlp")]
pub fn init_tracing_with_otel(
env: Environment,
provider: &opentelemetry_sdk::trace::SdkTracerProvider,
service_name: &'static str,
) {
use opentelemetry::trace::TracerProvider;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,h2=off,hyper=off,rustls=off,tonic=off"));
let tracer = provider.tracer(service_name);
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let registry = tracing_subscriber::registry().with(filter).with(otel_layer);
match env {
Environment::Local => {
let fmt_layer = tracing_subscriber::fmt::layer().with_target(true).pretty();
registry.with(fmt_layer).init();
}
_ => {
#[cfg(feature = "json-log")]
{
let fmt_layer = tracing_subscriber::fmt::layer().json().with_target(true);
registry.with(fmt_layer).init();
}
#[cfg(not(feature = "json-log"))]
{
let fmt_layer = tracing_subscriber::fmt::layer().with_target(true);
registry.with(fmt_layer).init();
}
}
}
}