mod access_log;
pub mod init;
pub mod json_log;
pub mod metrics;
#[cfg(feature = "otel")]
pub mod otel;
pub mod redact;
pub mod trace_context;
use std::fmt;
use std::str::FromStr;
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
pub use access_log::{AccessLogLayer, AccessLogService};
pub use init::{FILTER_ENV, install_logging};
pub use json_log::{CaptureSink, JsonLog, LogSink, StderrSink};
pub use metrics::{Metrics, MetricsLayer, MetricsService};
#[cfg(feature = "otel")]
pub use otel::{Telemetry, TelemetryBuilder};
pub use redact::{REDACTED, is_sensitive};
pub use trace_context::{
TRACEPARENT, TRACESTATE, TraceContext, TraceContextLayer, TraceContextService, TraceParent,
TraceState,
};
pub use tracing;
pub const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
pub const MAX_REQUEST_ID_BYTES: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RequestId(String);
impl RequestId {
#[must_use]
pub fn generate() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
pub fn parse_str(s: &str) -> Result<Self, RequestIdError> {
s.parse()
}
#[must_use]
pub fn from_header(headers: &HeaderMap) -> Self {
if let Some(value) = headers.get(&REQUEST_ID_HEADER)
&& let Ok(s) = value.to_str()
&& let Ok(id) = Self::parse_str(s)
{
return id;
}
Self::generate()
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_response_header(&self) -> Result<HeaderValue, ObserveError> {
HeaderValue::from_str(&self.0).map_err(|_| ObserveError::RequestHeader {
reason: "request id is not valid header bytes",
})
}
}
impl fmt::Display for RequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for RequestId {
type Err = RequestIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(RequestIdError::Empty);
}
if s.len() > MAX_REQUEST_ID_BYTES {
return Err(RequestIdError::TooLarge {
size: s.len(),
limit: MAX_REQUEST_ID_BYTES,
});
}
if !s.bytes().all(is_allowed_request_id_byte) {
return Err(RequestIdError::InvalidChar);
}
Ok(Self(s.to_string()))
}
}
fn is_allowed_request_id_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b':' | b'@' | b'+' | b'/' | b'=')
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestIdError {
Empty,
TooLarge { size: usize, limit: usize },
InvalidChar,
}
impl fmt::Display for RequestIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("request id must not be empty"),
Self::TooLarge { size, limit } => {
write!(f, "request id is {size} bytes, exceeds {limit}-byte limit")
}
Self::InvalidChar => f.write_str("request id contains a disallowed character"),
}
}
}
impl std::error::Error for RequestIdError {}
#[derive(Debug)]
pub enum ObserveError {
RequestHeader { reason: &'static str },
Logging { reason: &'static str },
#[cfg(feature = "otel")]
Telemetry { reason: &'static str },
}
impl fmt::Display for ObserveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RequestHeader { reason } => write!(f, "observe header error: {reason}"),
Self::Logging { reason } => write!(f, "observe logging error: {reason}"),
#[cfg(feature = "otel")]
Self::Telemetry { reason } => write!(f, "observe telemetry error: {reason}"),
}
}
}
impl std::error::Error for ObserveError {}
pub const REQUEST: &str = "arcature.request";
pub const DB_QUERY: &str = "arcature.db.query";
pub const CACHE_GET: &str = "arcature.cache.get";
pub const JOB_HANDLE: &str = "arcature.job.handle";
pub const PAGE_RENDER: &str = "arcature.page.render";
pub const EVENT_LISTENER: &str = "arcature.event.listener";
pub const SCHEDULE_TICK: &str = "arcature.schedule.tick";
pub const ALL: &[&str] = &[
REQUEST,
DB_QUERY,
CACHE_GET,
JOB_HANDLE,
PAGE_RENDER,
EVENT_LISTENER,
SCHEDULE_TICK,
];
#[must_use]
pub fn is_stable(name: &str) -> bool {
ALL.contains(&name)
}
use axum::extract::Request;
use axum::response::Response;
use std::convert::Infallible;
use tower::{Layer, Service};
#[derive(Debug, Clone, Copy, Default)]
pub struct RequestIdLayer;
impl<S> Layer<S> for RequestIdLayer {
type Service = RequestIdService<S>;
fn layer(&self, inner: S) -> Self::Service {
RequestIdService { inner }
}
}
#[derive(Debug, Clone)]
pub struct RequestIdService<S> {
inner: S,
}
impl<S> Service<Request> for RequestIdService<S>
where
S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send + 'static,
{
type Response = Response;
type Error = Infallible;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
>;
fn poll_ready(
&mut self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request) -> Self::Future {
let id = RequestId::from_header(req.headers());
req.extensions_mut().insert(id.clone());
let inner = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, inner);
Box::pin(async move {
let mut response = inner.call(req).await?;
if let Ok(value) = id.to_response_header() {
response.headers_mut().insert(REQUEST_ID_HEADER, value);
}
Ok(response)
})
}
}
#[must_use]
pub fn admission_status(_e: &ObserveError) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}