dogdata_reqwest_middleware/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/// Here are some convenient functions to checkout [`default_on_request_success`], [`default_on_request_failure`],
28/// and [`default_on_request_end`].
29///
30/// # Why a macro?
31///
32/// [`tracing`] requires all the properties attached to a span to be declared upfront, when the span is created.
33/// You cannot add new ones afterwards.
34/// This makes it extremely fast, but it pushes us to reach for macros when we need some level of composition.
35///
36/// # Macro syntax
37///
38/// The first argument is a [span name](https://opentelemetry.io/docs/reference/specification/trace/api/#span).
39/// The second argument passed to [`reqwest_otel_span!`](crate::reqwest_otel_span) is a reference to an [`reqwest::Request`].
40///
41/// ```rust
42/// use reqwest_middleware::Result;
43/// use http::Extensions;
44/// use reqwest::{Request, Response};
45/// use dogdata_reqwest_middleware::{
46/// default_on_request_end, reqwest_otel_span, ReqwestOtelSpanBackend
47/// };
48/// use tracing::Span;
49///
50/// pub struct CustomReqwestOtelSpanBackend;
51///
52/// impl ReqwestOtelSpanBackend for CustomReqwestOtelSpanBackend {
53/// fn on_request_start(req: &Request, _extension: &mut Extensions) -> Span {
54/// reqwest_otel_span!(name = "reqwest-http-request", req)
55/// }
56///
57/// fn on_request_end(span: &Span, outcome: &Result<Response>, _extension: &mut Extensions) {
58/// default_on_request_end(span, outcome)
59/// }
60/// }
61/// ```
62///
63/// If nothing else is specified, the span generated by `reqwest_otel_span!` is identical to the one you'd
64/// get by using [`DefaultSpanBackend`]. Note that to avoid leaking sensitive information, the
65/// macro doesn't include `url.full`, even though it's required by opentelemetry. You can add the
66/// URL attribute explicitly by using [`SpanBackendWithUrl`] instead of `DefaultSpanBackend` or
67/// adding the field on your own implementation.
68///
69/// You can define new fields following the same syntax of [`tracing::info_span!`] for fields:
70///
71/// ```rust,should_panic
72/// use dogdata_reqwest_middleware::reqwest_otel_span;
73/// # let request: &reqwest::Request = todo!();
74///
75/// // Define a `time_elapsed` field as empty. It might be populated later.
76/// // (This example is just to show how to inject data - otel already tracks durations)
77/// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty);
78///
79/// // Define a `name` field with a known value, `AppName`.
80/// reqwest_otel_span!(name = "reqwest-http-request", request, name = "AppName");
81///
82/// // Define an `app_id` field using the variable with the same name as value.
83/// let app_id = "XYZ";
84/// reqwest_otel_span!(name = "reqwest-http-request", request, app_id);
85///
86/// // All together
87/// reqwest_otel_span!(name = "reqwest-http-request", request, time_elapsed = tracing::field::Empty, name = "AppName", app_id);
88/// ```
89///
90/// You can also choose to customise the level of the generated span:
91///
92/// ```rust,should_panic
93/// use dogdata_reqwest_middleware::reqwest_otel_span;
94/// use tracing::Level;
95/// # let request: &reqwest::Request = todo!();
96///
97/// // Reduce the log level for service endpoints/probes
98/// let level = if request.method().as_str() == "POST" {
99/// Level::DEBUG
100/// } else {
101/// Level::INFO
102/// };
103///
104/// // `level =` and name MUST come before the request, in this order
105/// reqwest_otel_span!(level = level, name = "reqwest-http-request", request);
106/// ```
107///
108///
109/// [`DefaultSpanBackend`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
110/// [`SpanBackendWithUrl`]: crate::reqwest_otel_span_builder::DefaultSpanBackend
111/// [`default_on_request_success`]: crate::reqwest_otel_span_builder::default_on_request_success
112/// [`default_on_request_failure`]: crate::reqwest_otel_span_builder::default_on_request_failure
113/// [`default_on_request_end`]: crate::reqwest_otel_span_builder::default_on_request_end
114macro_rules! reqwest_otel_span {
115 // Vanilla root span at default INFO level, with no additional fields
116 (name=$name:expr, $request:ident) => {
117 reqwest_otel_span!(name=$name, $request,)
118 };
119 // Vanilla root span, with no additional fields but custom level
120 (level=$level:expr, name=$name:expr, $request:ident) => {
121 reqwest_otel_span!(level=$level, name=$name, $request,)
122 };
123 // Root span with additional fields, default INFO level
124 (name=$name:expr, $request:ident, $($field:tt)*) => {
125 reqwest_otel_span!(level=$crate::reqwest_otel_span_macro::private::Level::INFO, name=$name, $request, $($field)*)
126 };
127 // Root span with additional fields and custom level
128 (level=$level:expr, name=$name:expr, $request:ident, $($field:tt)*) => {
129 {
130 let method = $request.method();
131 let url = $request.url();
132 let _scheme = url.scheme();
133 let host = url.host_str().unwrap_or("");
134 let _host_port = url.port_or_known_default().unwrap_or(0) as i64;
135 let _otel_name = $name.to_string();
136 let header_default = &::http::HeaderValue::from_static("");
137 let _user_agent = format!("{:?}", $request.headers().get("user-agent").unwrap_or(header_default)).replace('"', "");
138
139 // The match here is necessary, because tracing expects the level to be static.
140 match $level {
141 $crate::reqwest_otel_span_macro::private::Level::TRACE => {
142 $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::TRACE, method, _scheme, host, _host_port, _user_agent, _otel_name, $($field)*)
143 },
144 $crate::reqwest_otel_span_macro::private::Level::DEBUG => {
145 $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::DEBUG, method, _scheme, host, _host_port, _user_agent, _otel_name, $($field)*)
146 },
147 $crate::reqwest_otel_span_macro::private::Level::INFO => {
148 $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::INFO, method, _scheme, host, _host_port, _user_agent, _otel_name, $($field)*)
149 },
150 $crate::reqwest_otel_span_macro::private::Level::WARN => {
151 $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::WARN, method, _scheme, host, _host_port, _user_agent, _otel_name, $($field)*)
152 },
153 $crate::reqwest_otel_span_macro::private::Level::ERROR => {
154 $crate::request_span!($crate::reqwest_otel_span_macro::private::Level::ERROR, method, _scheme, host, _host_port, _user_agent, _otel_name, $($field)*)
155 },
156 }
157 }
158 }
159}
160
161#[doc(hidden)]
162pub mod private {
163 #[doc(hidden)]
164 pub use tracing::{Level, span};
165
166 #[doc(hidden)]
167 #[macro_export]
168 macro_rules! request_span {
169 ($level:expr, $method:expr, $scheme:expr, $host:expr, $host_port:expr, $user_agent:expr, $otel_name:expr, $($field:tt)*) => {
170 $crate::reqwest_otel_span_macro::private::span!(
171 $level,
172 "HTTP request",
173 type = "http",
174 otel.type = "http",
175 component = "reqwest",
176 http.method = %$method,
177 http.status_code = tracing::field::Empty,
178 http.url = tracing::field::Empty,
179 out.host = %$host,
180 otel.kind = "client",
181 span.kind = "client",
182 $($field)*
183 )
184 }
185 }
186}