tracing_opentelemetry_instrumentation_sdk/http/
tools.rs

1use std::borrow::Cow;
2
3use http::{HeaderMap, Uri, Version};
4use opentelemetry::Context;
5
6use super::opentelemetry_http::{HeaderExtractor, HeaderInjector};
7
8pub fn inject_context(context: &Context, headers: &mut http::HeaderMap) {
9    let mut injector = HeaderInjector(headers);
10    opentelemetry::global::get_text_map_propagator(|propagator| {
11        propagator.inject_context(context, &mut injector);
12    });
13}
14
15// If remote request has no span data the propagator defaults to an unsampled context
16#[must_use]
17pub fn extract_context(headers: &http::HeaderMap) -> Context {
18    let extractor = HeaderExtractor(headers);
19    opentelemetry::global::get_text_map_propagator(|propagator| propagator.extract(&extractor))
20}
21
22pub fn extract_service_method(uri: &Uri) -> (&str, &str) {
23    let path = uri.path();
24    let mut parts = path.split('/').filter(|x| !x.is_empty());
25    let service = parts.next().unwrap_or_default();
26    let method = parts.next().unwrap_or_default();
27    (service, method)
28}
29
30#[must_use]
31// From [X-Forwarded-For - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
32// > If a request goes through multiple proxies, the IP addresses of each successive proxy is listed.
33// > This means that, given well-behaved client and proxies,
34// > the rightmost IP address is the IP address of the most recent proxy and
35// > the leftmost IP address is the IP address of the originating client.
36pub fn extract_client_ip_from_headers(headers: &HeaderMap) -> Option<&str> {
37    extract_client_ip_from_forwarded(headers)
38        .or_else(|| extract_client_ip_from_x_forwarded_for(headers))
39}
40
41#[must_use]
42// From [X-Forwarded-For - HTTP | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
43// > If a request goes through multiple proxies, the IP addresses of each successive proxy is listed.
44// > This means that, given well-behaved client and proxies,
45// > the rightmost IP address is the IP address of the most recent proxy and
46// > the leftmost IP address is the IP address of the originating client.
47fn extract_client_ip_from_x_forwarded_for(headers: &HeaderMap) -> Option<&str> {
48    let value = headers.get("x-forwarded-for")?;
49    let value = value.to_str().ok()?;
50    let mut ips = value.split(',');
51    Some(ips.next()?.trim())
52}
53
54#[must_use]
55// see [Forwarded header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Forwarded)
56fn extract_client_ip_from_forwarded(headers: &HeaderMap) -> Option<&str> {
57    let value = headers.get("forwarded")?;
58    let value = value.to_str().ok()?;
59    value
60        .split(';')
61        .flat_map(|directive| directive.split(','))
62        // select the left/first "for" key
63        .find_map(|directive| directive.trim().strip_prefix("for="))
64        // ipv6 are enclosed into `["..."]`
65        // string are enclosed into `"..."`
66        .map(|directive| {
67            directive
68                .trim_start_matches('[')
69                .trim_end_matches(']')
70                .trim_matches('"')
71                .trim()
72        })
73}
74
75#[inline]
76pub fn http_target(uri: &Uri) -> &str {
77    uri.path_and_query()
78        .map_or("", http::uri::PathAndQuery::as_str)
79}
80
81#[inline]
82#[must_use]
83pub fn http_flavor(version: Version) -> Cow<'static, str> {
84    match version {
85        Version::HTTP_09 => "0.9".into(),
86        Version::HTTP_10 => "1.0".into(),
87        Version::HTTP_11 => "1.1".into(),
88        Version::HTTP_2 => "2.0".into(),
89        Version::HTTP_3 => "3.0".into(),
90        other => format!("{other:?}").into(),
91    }
92}
93
94#[inline]
95pub fn url_scheme(uri: &Uri) -> &str {
96    uri.scheme_str().unwrap_or_default()
97}
98
99#[inline]
100pub fn user_agent<B>(req: &http::Request<B>) -> &str {
101    req.headers()
102        .get(http::header::USER_AGENT)
103        .map_or("", |h| h.to_str().unwrap_or(""))
104}
105
106#[inline]
107pub fn http_host<B>(req: &http::Request<B>) -> &str {
108    req.headers()
109        .get(http::header::HOST)
110        .map_or(req.uri().host(), |h| h.to_str().ok())
111        .unwrap_or("")
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use assert2::assert;
118    use rstest::rstest;
119
120    #[rstest]
121    // #[case("", "", "")]
122    #[case("/", "", "")]
123    #[case("//", "", "")]
124    #[case("/grpc.health.v1.Health/Check", "grpc.health.v1.Health", "Check")]
125    fn test_extract_service_method(
126        #[case] path: &str,
127        #[case] service: &str,
128        #[case] method: &str,
129    ) {
130        assert!(extract_service_method(&path.parse::<Uri>().unwrap()) == (service, method));
131    }
132
133    #[rstest]
134    #[case("http://example.org/hello/world", "http")] // Devskim: ignore DS137138
135    #[case("https://example.org/hello/world", "https")]
136    #[case("foo://example.org/hello/world", "foo")]
137    fn test_extract_url_scheme(#[case] input: &str, #[case] expected: &str) {
138        let uri: Uri = input.parse().unwrap();
139        assert!(url_scheme(&uri) == expected);
140    }
141
142    #[rstest]
143    #[case("", "")]
144    #[case(
145        "2001:db8:85a3:8d3:1319:8a2e:370:7348",
146        "2001:db8:85a3:8d3:1319:8a2e:370:7348"
147    )]
148    #[case("203.0.113.195", "203.0.113.195")]
149    #[case("203.0.113.195,10.10.10.10", "203.0.113.195")]
150    #[case("203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348", "203.0.113.195")]
151    fn test_extract_client_ip_from_x_forwarded_for(#[case] input: &str, #[case] expected: &str) {
152        let mut headers = HeaderMap::new();
153        if !input.is_empty() {
154            headers.insert("X-Forwarded-For", input.parse().unwrap());
155        }
156
157        let expected = if expected.is_empty() {
158            None
159        } else {
160            Some(expected)
161        };
162        assert!(extract_client_ip_from_x_forwarded_for(&headers) == expected);
163    }
164
165    #[rstest]
166    #[case("", "")]
167    #[case(
168        "for=[\"2001:db8:85a3:8d3:1319:8a2e:370:7348\"]",
169        "2001:db8:85a3:8d3:1319:8a2e:370:7348"
170    )]
171    #[case("for=203.0.113.195", "203.0.113.195")]
172    #[case("for=203.0.113.195, for=10.10.10.10", "203.0.113.195")]
173    #[case(
174        "for=203.0.113.195, for=[\"2001:db8:85a3:8d3:1319:8a2e:370:7348\"]",
175        "203.0.113.195"
176    )]
177    #[case("for=\"_mdn\"", "_mdn")]
178    #[case("for=\"secret\"", "secret")]
179    #[case("for=203.0.113.195;proto=http;by=203.0.113.43", "203.0.113.195")]
180    #[case("proto=http;by=203.0.113.43", "")]
181    fn test_extract_client_ip_from_forwarded(#[case] input: &str, #[case] expected: &str) {
182        let mut headers = HeaderMap::new();
183        if !input.is_empty() {
184            headers.insert("Forwarded", input.parse().unwrap());
185        }
186
187        let expected = if expected.is_empty() {
188            None
189        } else {
190            Some(expected)
191        };
192        assert!(extract_client_ip_from_forwarded(&headers) == expected);
193    }
194}