kamu_logging/actix.rs
1//! Actix Web integration: tracing middleware with correlation enrichment.
2
3use actix_web::body::MessageBody;
4use actix_web::dev::{ServiceRequest, ServiceResponse};
5use tracing::Span;
6use tracing_actix_web::{DefaultRootSpanBuilder, RootSpanBuilder, TracingLogger};
7
8use crate::correlation::{DEFAULT_HEADER_CHAIN, extract_from_headers};
9
10/// A [`RootSpanBuilder`] that adds a `correlation_id` field to the root
11/// span when one of [`DEFAULT_HEADER_CHAIN`] is present on the request.
12///
13/// All other span fields match [`DefaultRootSpanBuilder`].
14pub struct EnrichedRootSpanBuilder;
15
16impl RootSpanBuilder for EnrichedRootSpanBuilder {
17 fn on_request_start(request: &ServiceRequest) -> Span {
18 let correlation_id = extract_from_headers(request.headers(), DEFAULT_HEADER_CHAIN, |h, n| {
19 h.get(n).and_then(|v| v.to_str().ok()).map(str::to_owned)
20 });
21
22 let span = tracing_actix_web::root_span!(request, correlation_id = tracing::field::Empty);
23 if let Some(id) = correlation_id {
24 span.record("correlation_id", tracing::field::display(&id));
25 }
26 span
27 }
28
29 fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, actix_web::Error>) {
30 DefaultRootSpanBuilder::on_request_end(span, outcome);
31 }
32}
33
34/// Construct a [`TracingLogger`] middleware backed by
35/// [`EnrichedRootSpanBuilder`] (the default root span enriched with a
36/// `correlation_id` field).
37///
38/// For a custom [`RootSpanBuilder`], use [`get_actix_web_logger_with`].
39///
40/// ```no_run
41/// use actix_web::{App, HttpServer};
42/// use kamu_logging::get_actix_web_logger;
43///
44/// # async fn run() -> std::io::Result<()> {
45/// HttpServer::new(|| App::new().wrap(get_actix_web_logger()))
46/// .bind("127.0.0.1:8080")?
47/// .run()
48/// .await
49/// # }
50/// ```
51#[must_use]
52pub fn get_actix_web_logger() -> TracingLogger<EnrichedRootSpanBuilder> {
53 TracingLogger::<EnrichedRootSpanBuilder>::new()
54}
55
56/// Construct a [`TracingLogger`] middleware backed by the supplied
57/// [`RootSpanBuilder`]. Use this when the enriched default does not fit
58/// (e.g., to add tenant_id, user_id, or a service-specific field).
59///
60/// ```no_run
61/// use kamu_logging::get_actix_web_logger_with;
62/// use tracing_actix_web::DefaultRootSpanBuilder;
63///
64/// let middleware = get_actix_web_logger_with::<DefaultRootSpanBuilder>();
65/// # let _ = middleware;
66/// ```
67#[must_use]
68pub fn get_actix_web_logger_with<RSB: RootSpanBuilder>() -> TracingLogger<RSB> {
69 TracingLogger::<RSB>::new()
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn constructors_build_middleware() {
78 let _enriched = get_actix_web_logger();
79 let _default = get_actix_web_logger_with::<DefaultRootSpanBuilder>();
80 let _explicit = get_actix_web_logger_with::<EnrichedRootSpanBuilder>();
81 }
82
83 // `EnrichedRootSpanBuilder::on_request_start` calls `tracing_actix_web::root_span!`,
84 // which unwraps a `RequestId` request extension that only the `TracingLogger`
85 // middleware inserts — so the enrichment path is integration-bound (needs a full
86 // `App` + test service), not unit-testable in isolation. Its correlation-id
87 // extraction half is covered directly by the `correlation` module tests.
88}