entertainarr-adapter-http 0.1.0

HTTP adapter for entertainarr
Documentation
use tower_http::classify::{ServerErrorsAsFailures, SharedClassifier};
use tower_http::trace::{
    DefaultOnBodyChunk, DefaultOnEos, MakeSpan, OnEos, OnFailure, OnRequest, OnResponse, TraceLayer,
};

pub fn layer() -> TraceLayer<
    SharedClassifier<ServerErrorsAsFailures>,
    SpanCreator,
    EventBuilder,
    EventBuilder,
    DefaultOnBodyChunk,
    DefaultOnEos,
    EventBuilder,
> {
    TraceLayer::new_for_http()
        .make_span_with(SpanCreator)
        .on_request(EventBuilder)
        .on_response(EventBuilder)
        .on_failure(EventBuilder)
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct SpanCreator;

impl<B> MakeSpan<B> for SpanCreator {
    fn make_span(&mut self, req: &axum::http::Request<B>) -> tracing::Span {
        let uri = req.uri();
        let span_name = format!("{} {uri}", req.method());
        let span = tracing::info_span!(
            parent: None,
            "http.server.request",
            "error.type" = tracing::field::Empty,
            "exception.message" = tracing::field::Empty,
            "exception.stacktrace" = tracing::field::Empty,
            "http.request.header.content-length" = tracing::field::Empty,
            "http.request.header.content-type" = tracing::field::Empty,
            "http.request.header.user-agent" = tracing::field::Empty,
            "http.request.method" = %req.method(),
            "http.response.status_code" = tracing::field::Empty,
            "network.protocol.version" = ?req.version(),
            "otel.kind" = "server",
            "otel.name" = span_name,
            "otel.status_code" = tracing::field::Empty,
            "otel.status_description" = tracing::field::Empty,
            "span.type" = "web",
            "url.path" = uri.path(),
            "url.query" = tracing::field::Empty,
            "url.scheme" = tracing::field::Empty,
        );
        let headers = req.headers();
        if let Some(value) = headers
            .get("Content-Length")
            .and_then(|value| value.to_str().ok())
        {
            span.record("http.request.header.content-length", value);
        }
        if let Some(value) = headers
            .get("Content-Type")
            .and_then(|value| value.to_str().ok())
        {
            span.record("http.request.header.content-type", value);
        }
        if let Some(value) = headers
            .get("User-Agent")
            .and_then(|value| value.to_str().ok())
        {
            span.record("http.request.header.user-agent", value);
        }
        if let Some(query) = uri.query() {
            span.record("url.query", query);
        }
        if let Some(scheme) = uri.scheme_str() {
            span.record("url.scheme", scheme);
        }
        span
    }
}

#[derive(Clone, Copy, Debug)]
pub(crate) struct EventBuilder;

impl<B> OnRequest<B> for EventBuilder {
    fn on_request(&mut self, _req: &axum::extract::Request<B>, _span: &tracing::Span) {
        tracing::info!("request started");
    }
}

impl<B> OnResponse<B> for EventBuilder {
    fn on_response(
        self,
        res: &axum::http::Response<B>,
        latency: std::time::Duration,
        span: &tracing::Span,
    ) {
        span.record("http.response.status_code", res.status().as_str());

        if res.status().is_server_error() {
            tracing::error!(latency_ns = latency.as_nanos(), "request processed");
        } else if res.status().is_client_error() {
            tracing::warn!(latency_ns = latency.as_nanos(), "request processed");
        } else {
            tracing::info!(latency_ns = latency.as_nanos(), "request processed");
        }
    }
}

impl<F> OnFailure<F> for EventBuilder
where
    F: std::fmt::Display,
{
    fn on_failure(
        &mut self,
        failure_classification: F,
        latency: std::time::Duration,
        span: &tracing::Span,
    ) {
        span.record("error.type", "server");
        span.record("exception.message", failure_classification.to_string());
        span.record("otel.status_code", "error");
        span.record(
            "otel.status_description",
            failure_classification.to_string(),
        );

        tracing::error!(
            error = %failure_classification,
            latency_ns = latency.as_nanos(),
            "response failed",
        );
    }
}

impl OnEos for EventBuilder {
    fn on_eos(
        self,
        _trailers: Option<&axum::http::HeaderMap>,
        stream_duration: std::time::Duration,
        _span: &tracing::Span,
    ) {
        tracing::debug!(
            stream_duration_ns = stream_duration.as_nanos(),
            "end of stream",
        );
    }
}