tracing_actix_web/root_span_macro.rs
1#[macro_export]
2/// `root_span!` creates a new [`tracing::Span`].
3/// It empowers you to add custom properties to the root span on top of the HTTP properties tracked
4/// by [`DefaultRootSpanBuilder`].
5///
6/// # Why a macro?
7///
8/// `tracing` requires all the properties attached to a span to be declared upfront, when the span is created.
9/// You cannot add new ones afterwards.
10/// This makes it extremely fast, but it pushes us to reach for macros when we need some level of composition.
11///
12/// # Macro syntax
13///
14/// The first argument passed to `root_span!` must be a reference to an [`actix_web::dev::ServiceRequest`].
15///
16/// ```rust
17/// use actix_web::body::MessageBody;
18/// use actix_web::dev::{ServiceResponse, ServiceRequest};
19/// use actix_web::Error;
20/// use tracing_actix_web::{TracingLogger, DefaultRootSpanBuilder, RootSpanBuilder, root_span};
21/// use tracing::Span;
22///
23/// pub struct CustomRootSpanBuilder;
24///
25/// impl RootSpanBuilder for CustomRootSpanBuilder {
26/// fn on_request_start(request: &ServiceRequest) -> Span {
27/// root_span!(request)
28/// }
29///
30/// fn on_request_end<B: MessageBody>(span: Span, outcome: &Result<ServiceResponse<B>, Error>) {
31/// DefaultRootSpanBuilder::on_request_end(span, outcome);
32/// }
33/// }
34/// ```
35///
36/// If nothing else is specified, the span generated by `root_span!` is identical to the one you'd
37/// get by using `DefaultRootSpanBuilder`.
38///
39/// You can define new fields following the same syntax of `tracing::info_span!` for fields:
40///
41/// ```rust,should_panic
42/// # let request: &actix_web::dev::ServiceRequest = todo!();
43/// use tracing_actix_web::Level;
44///
45/// // Define a `client_id` field as empty. It might be populated later.
46/// tracing_actix_web::root_span!(request, client_id = tracing::field::Empty);
47///
48/// // Define a `name` field with a known value, `AppName`.
49/// tracing_actix_web::root_span!(request, name = "AppName");
50///
51/// // Define an `app_id` field using the variable with the same name as value.
52/// let app_id = "XYZ";
53/// tracing_actix_web::root_span!(request, app_id);
54///
55/// // Use a custom level, `DEBUG`, instead of the default (`INFO`).
56/// tracing_actix_web::root_span!(level = Level::DEBUG, request);
57///
58/// // All together
59/// tracing_actix_web::root_span!(request, client_id = tracing::field::Empty, name = "AppName", app_id);
60/// ```
61///
62/// [`DefaultRootSpanBuilder`]: crate::DefaultRootSpanBuilder
63macro_rules! root_span {
64 // Vanilla root span, with no additional fields
65 ($request:ident) => {
66 $crate::root_span!($request,)
67 };
68 // Vanilla root span, with a level but no additional fields
69 (level = $lvl:expr, $request:ident) => {
70 $crate::root_span!(level = $lvl, $request,)
71 };
72 // One or more additional fields, comma separated, without a level
73 ($request:ident, $($field:tt)*) => {
74 $crate::root_span!(level = $crate::Level::INFO, $request, $($field)*)
75 };
76 // One or more additional fields, comma separated
77 (level = $lvl:expr, $request:ident, $($field:tt)*) => {
78 {
79 let user_agent = $request
80 .headers()
81 .get("User-Agent")
82 .map(|h| h.to_str().unwrap_or(""))
83 .unwrap_or("");
84 let http_route: std::borrow::Cow<'static, str> = $request
85 .match_pattern()
86 .map(Into::into)
87 .unwrap_or_else(|| "default".into());
88 let http_method = $crate::root_span_macro::private::http_method_str($request.method());
89 let connection_info = $request.connection_info();
90 let request_id = $crate::root_span_macro::private::get_request_id($request);
91
92 macro_rules! inner_span {
93 ($level:expr) => {
94 $crate::root_span_macro::private::tracing::span!(
95 $level,
96 "HTTP request",
97 http.method = %http_method,
98 http.route = %http_route,
99 http.flavor = %$crate::root_span_macro::private::http_flavor($request.version()),
100 http.scheme = %$crate::root_span_macro::private::http_scheme(connection_info.scheme()),
101 http.host = %connection_info.host(),
102 http.client_ip = %$request.connection_info().realip_remote_addr().unwrap_or(""),
103 http.user_agent = %user_agent,
104 http.target = %$request.uri().path_and_query().map(|p| p.as_str()).unwrap_or(""),
105 http.status_code = $crate::root_span_macro::private::tracing::field::Empty,
106 otel.name = %format!("{} {}", http_method, http_route),
107 otel.kind = "server",
108 otel.status_code = $crate::root_span_macro::private::tracing::field::Empty,
109 trace_id = $crate::root_span_macro::private::tracing::field::Empty,
110 request_id = %request_id,
111 exception.message = $crate::root_span_macro::private::tracing::field::Empty,
112 // Not proper OpenTelemetry, but their terminology is fairly exception-centric
113 exception.details = $crate::root_span_macro::private::tracing::field::Empty,
114 $($field)*
115 )
116 };
117 }
118 let span = match $lvl {
119 $crate::Level::TRACE => inner_span!($crate::Level::TRACE),
120 $crate::Level::DEBUG => inner_span!($crate::Level::DEBUG),
121 $crate::Level::INFO => inner_span!($crate::Level::INFO),
122 $crate::Level::WARN => inner_span!($crate::Level::WARN),
123 $crate::Level::ERROR => inner_span!($crate::Level::ERROR),
124 };
125 std::mem::drop(connection_info);
126
127 // Previously, this line was instrumented with an opentelemetry-specific feature
128 // flag check. However, this resulted in the feature flags being resolved in the crate
129 // which called `root_span!` as opposed to being resolved by this crate as expected.
130 // Therefore, this function simply wraps an internal function with the feature flags
131 // to ensure that the flags are resolved against this crate.
132 $crate::root_span_macro::private::set_otel_parent(&$request, &span);
133
134 span
135 }
136 };
137}
138
139#[doc(hidden)]
140pub mod private {
141 //! This module exposes and re-exports various functions and traits as public in order to leverage them
142 //! in the code generated by the `root_span` macro.
143 //! Items in this module are not part of the public interface of `tracing-actix-web` - they are considered
144 //! implementation details and will change without notice in patch, minor and major releases.
145 use crate::RequestId;
146 use actix_web::dev::ServiceRequest;
147 use actix_web::http::{Method, Version};
148 use std::borrow::Cow;
149
150 pub use tracing;
151
152 #[doc(hidden)]
153 // We need to allow unused variables because the function
154 // body is empty if the user of the library chose not to activate
155 // any OTEL feature.
156 #[allow(unused_variables)]
157 pub fn set_otel_parent(req: &ServiceRequest, span: &tracing::Span) {
158 #[cfg(any(
159 feature = "opentelemetry_0_13",
160 feature = "opentelemetry_0_14",
161 feature = "opentelemetry_0_15",
162 feature = "opentelemetry_0_16",
163 feature = "opentelemetry_0_17",
164 feature = "opentelemetry_0_18",
165 feature = "opentelemetry_0_19",
166 feature = "opentelemetry_0_20",
167 feature = "opentelemetry_0_21",
168 feature = "opentelemetry_0_22",
169 feature = "opentelemetry_0_23",
170 feature = "opentelemetry_0_24",
171 feature = "opentelemetry_0_25",
172 feature = "opentelemetry_0_26",
173 feature = "opentelemetry_0_27",
174 ))]
175 crate::otel::set_otel_parent(req, span);
176 }
177
178 #[doc(hidden)]
179 #[inline]
180 pub fn http_method_str(method: &Method) -> Cow<'static, str> {
181 match method {
182 &Method::OPTIONS => "OPTIONS".into(),
183 &Method::GET => "GET".into(),
184 &Method::POST => "POST".into(),
185 &Method::PUT => "PUT".into(),
186 &Method::DELETE => "DELETE".into(),
187 &Method::HEAD => "HEAD".into(),
188 &Method::TRACE => "TRACE".into(),
189 &Method::CONNECT => "CONNECT".into(),
190 &Method::PATCH => "PATCH".into(),
191 other => other.to_string().into(),
192 }
193 }
194
195 #[doc(hidden)]
196 #[inline]
197 pub fn http_flavor(version: Version) -> Cow<'static, str> {
198 match version {
199 Version::HTTP_09 => "0.9".into(),
200 Version::HTTP_10 => "1.0".into(),
201 Version::HTTP_11 => "1.1".into(),
202 Version::HTTP_2 => "2.0".into(),
203 Version::HTTP_3 => "3.0".into(),
204 other => format!("{other:?}").into(),
205 }
206 }
207
208 #[doc(hidden)]
209 #[inline]
210 pub fn http_scheme(scheme: &str) -> Cow<'static, str> {
211 match scheme {
212 "http" => "http".into(),
213 "https" => "https".into(),
214 other => other.to_string().into(),
215 }
216 }
217
218 #[doc(hidden)]
219 pub fn generate_request_id() -> RequestId {
220 RequestId::generate()
221 }
222
223 #[doc(hidden)]
224 pub fn get_request_id(request: &ServiceRequest) -> RequestId {
225 use actix_web::HttpMessage;
226
227 request.extensions().get::<RequestId>().cloned().unwrap()
228 }
229}