tracing_opentelemetry_instrumentation_sdk/http/
tools.rs1use 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#[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]
31pub 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]
42fn 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]
55fn 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 .find_map(|directive| directive.trim().strip_prefix("for="))
64 .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("/", "", "")]
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")] #[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}