reqwest_datadog_tracing/
reqwest_otel_span_macro.rs

1// MIT License
2//
3// Copyright (c) 2021 TrueLayer
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23#[macro_export]
24/// [`reqwest_otel_span!`](crate::reqwest_otel_span) creates a new [`tracing::Span`].
25/// It empowers you to add custom properties to the span on top of the default properties provided by the macro
26///
27/// Default Fields:
28/// - http.request.method
29/// - url.scheme
30/// - server.address
31/// - server.port
32/// - otel.kind
33/// - otel.name
34/// - otel.status_code
35/// - user_agent.original
36/// - http.response.status_code
37/// - error.message
38/// - error.cause_chain
39///
40/// Here are some convenient functions to checkout [`default_on_request_success`], [`default_on_request_failure`],
41/// and [`default_on_request_end`].
42///
43/// # Why a macro?
44///
45/// [`tracing`] requires all the properties attached to a span to be declared upfront, when the span is created.
46/// You cannot add new ones afterwards.
47/// This makes it extremely fast, but it pushes us to reach for macros when we need some level of composition.
48///
49/// # Macro syntax
50///
51/// The first argument is a [span name](https://opentelemetry.io/docs/reference/specification/trace/api/#span).
52/// The second argument passed to [`reqwest_otel_span!`](crate::reqwest_otel_span) is a reference to an [`reqwest::Request`].
53///
54/// ```rust
55/// use reqwest_middleware::Result;
56/// use http::Extensions;
57/// use reqwest::{Request, Response};
58/// use reqwest_datadog_tracing::{
59///     default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend
60/// };
61/// use tracing::Span;
62///
63/// pub struct CustomReqwestOtelSpanBackend;
64///
65/// impl ReqwestOtelSpanBackend for CustomReqwestOtelSpanBackend {
66///     fn on_request_start(req: &Request, _extension: &mut Extensions) -> Span {
67///         reqwest_otel_span!(name = "reqwest-http-request", req)
68///     }
69///
70///     fn on_request_end(span: &Span, outcome: &Result<Response>, _extension: &mut Extensions) {
71///         default_on_request_end(span, outcome)
72///     }
73/// }
74/// ```
75///
76/// If nothing else is specified, the span generated by `reqwest_otel_span!` is identical to the one you'd
77/// get by using [`DefaultSpanBackend`]. Note that to avoid leaking sensitive information, the
78/// macro doesn't include `url.full`, even though it's required by opentelemetry. You can add the
79/// URL attribute explicitly by using [`SpanBackendWithUrl`] instead of `DefaultSpanBackend` or
80/// adding the field on your own implementation.
81///
82/// You can define new fields following the same syntax of [`tracing::info_span!`] for fields:
83///
84/// ```rust,should_panic
85/// use reqwest_datadog_tracing::reqwest_otel_span;
86/// # let request: &reqwest::Request = todo!();
87///
88/// // Define a `time_elapsed` field as empty. It might be populated later.
89/// // (This example is just to show how to inject data - otel already tracks durations)
90/// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty);
91///
92/// // Define a `name` field with a known value, `AppName`.
93/// reqwest_otel_span!(name = "reqwest-http-request", request, name = "AppName");
94///
95/// // Define an `app_id` field using the variable with the same name as value.
96/// let app_id = "XYZ";
97/// reqwest_otel_span!(name = "reqwest-http-request", request, app_id);
98///
99/// // All together
100/// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty, name = "AppName", app_id);
101/// ```
102///
103/// You can also choose to customise the level of the generated span:
104///
105/// ```rust,should_panic
106/// use reqwest_datadog_tracing::reqwest_otel_span;
107/// use tracing::Level;
108/// # let request: &reqwest::Request = todo!();
109///
110/// // Reduce the log level for service endpoints/probes
111/// let level = if request.method().as_str() == "POST" {
112///     Level::DEBUG
113/// } else {
114///     Level::INFO
115/// };
116///
117/// // `level =` and name MUST come before the request, in this order
118/// reqwest_otel_span!(level = level, name = "reqwest-http-request", request);
119/// ```
120///
121///
122/// [`DefaultSpanBackend`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
123/// [`SpanBackendWithUrl`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
124/// [`default_on_request_success`]: crate::reqwest_otel_span_builder::default_on_request_success
125/// [`default_on_request_failure`]: crate::reqwest_otel_span_builder::default_on_request_failure
126/// [`default_on_request_end`]: crate::reqwest_otel_span_builder::default_on_request_end
127macro_rules! reqwest_otel_span {
128    // Vanilla root span at default INFO level, with no additional fields
129    (name=$name:expr, $request:ident) => {
130        reqwest_otel_span!(name=$name, $request,)
131    };
132    // Vanilla root span, with no additional fields but custom level
133    (level=$level:expr, name=$name:expr, $request:ident) => {
134        reqwest_otel_span!(level=$level, name=$name, $request,)
135    };
136    // Root span with additional fields, default INFO level
137    (name=$name:expr, $request:ident, $($field:tt)*) => {
138        reqwest_otel_span!(level=$crate::reqwest_otel_span_macro::private::Level::INFO, name=$name, $request, $($field)*)
139    };
140    // Root span with additional fields and custom level
141    (level=$level:expr, name=$name:expr, $request:ident, $($field:tt)*) => {
142        {
143            let method = $request.method();
144            let url = $request.url();
145            let scheme = url.scheme();
146            let host = url.host_str().unwrap_or("");
147            let host_port = url.port_or_known_default().unwrap_or(0) as i64;
148            let otel_name = $name.to_string();
149            let header_default = &::http::HeaderValue::from_static("");
150            let user_agent = format!("{:?}", $request.headers().get("user-agent").unwrap_or(header_default)).replace('"', "");
151
152            // The match here is necessary, because tracing expects the level to be static.
153            match $level {
154                $crate::reqwest_otel_span_macro::private::Level::TRACE => {
155                    $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::TRACE, method, scheme, host, host_port, user_agent, otel_name, $($field)*)
156                },
157                $crate::reqwest_otel_span_macro::private::Level::DEBUG => {
158                    $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::DEBUG, method, scheme, host, host_port, user_agent, otel_name, $($field)*)
159                },
160                $crate::reqwest_otel_span_macro::private::Level::INFO => {
161                    $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::INFO, method, scheme, host, host_port, user_agent, otel_name, $($field)*)
162                },
163                $crate::reqwest_otel_span_macro::private::Level::WARN => {
164                    $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::WARN, method, scheme, host, host_port, user_agent, otel_name, $($field)*)
165                },
166                $crate::reqwest_otel_span_macro::private::Level::ERROR => {
167                    $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::ERROR, method, scheme, host, host_port, user_agent, otel_name, $($field)*)
168                },
169            }
170        }
171    }
172}
173
174#[doc(hidden)]
175pub mod private {
176    #[doc(hidden)]
177    pub use tracing::{Level, span};
178
179    #[cfg(not(feature = "deprecated_attributes"))]
180    #[doc(hidden)]
181    #[macro_export]
182    macro_rules! request_span {
183        ($level:expr, $method:expr, $scheme:expr, $host:expr, $host_port:expr, $user_agent:expr, $otel_name:expr, $($field:tt)*) => {
184            $crate::reqwest_otel_span_macro::private::span!(
185                $level,
186                "HTTP request",
187                http.request.method = %$method,
188                url.scheme = %$scheme,
189                server.address = %$host,
190                server.port = %$host_port,
191                user_agent.original = %$user_agent,
192                otel.kind = "client",
193                otel.name = %$otel_name,
194                otel.status_code = tracing::field::Empty,
195                http.response.status_code = tracing::field::Empty,
196                error.message = tracing::field::Empty,
197                error.cause_chain = tracing::field::Empty,
198                $($field)*
199            )
200        }
201    }
202
203    // With the deprecated attributes flag enabled, we publish both the old and new attributes.
204    #[cfg(feature = "deprecated_attributes")]
205    #[doc(hidden)]
206    #[macro_export]
207    macro_rules! request_span {
208        ($level:expr, $method:expr, $scheme:expr, $host:expr, $host_port:expr, $user_agent:expr, $otel_name:expr, $($field:tt)*) => {
209            $crate::reqwest_otel_span_macro::private::span!(
210                $level,
211                "HTTP request",
212                http.request.method = %$method,
213                url.scheme = %$scheme,
214                server.address = %$host,
215                server.port = %$host_port,
216                user_agent.original = %$user_agent,
217                otel.kind = "client",
218                otel.name = %$otel_name,
219                otel.status_code = tracing::field::Empty,
220                http.response.status_code = tracing::field::Empty,
221                error.message = tracing::field::Empty,
222                error.cause_chain = tracing::field::Empty,
223                // old attributes
224                http.method = %$method,
225                http.scheme = %$scheme,
226                http.host = %$host,
227                net.host.port = %$host_port,
228                http.user_agent = tracing::field::Empty,
229                http.status_code = tracing::field::Empty,
230                $($field)*
231            )
232        }
233    }
234}