opentelemetry_lambda_tower/extractors/
http.rs1use crate::extractor::TraceContextExtractor;
9use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayV2httpRequest};
10use http::HeaderMap;
11use lambda_runtime::Context as LambdaContext;
12use opentelemetry::Context;
13use opentelemetry::propagation::Extractor;
14use opentelemetry::trace::TraceContextExt;
15use opentelemetry_semantic_conventions::attribute::{
16 CLIENT_ADDRESS, HTTP_REQUEST_METHOD, HTTP_ROUTE, NETWORK_PROTOCOL_VERSION, SERVER_ADDRESS,
17 URL_PATH, URL_QUERY, URL_SCHEME, USER_AGENT_ORIGINAL,
18};
19use tracing::Span;
20
21pub type HttpEventExtractor = ApiGatewayV2Extractor;
23
24#[derive(Clone, Debug, Default)]
40pub struct ApiGatewayV2Extractor;
41
42impl ApiGatewayV2Extractor {
43 pub fn new() -> Self {
47 Self
48 }
49}
50
51impl TraceContextExtractor<ApiGatewayV2httpRequest> for ApiGatewayV2Extractor {
52 fn extract_context(&self, event: &ApiGatewayV2httpRequest) -> Context {
53 let extractor = HeaderMapExtractor(&event.headers);
54 let ctx = opentelemetry::global::get_text_map_propagator(|propagator| {
55 propagator.extract(&extractor)
56 });
57
58 if ctx.span().span_context().is_valid() {
59 return ctx;
60 }
61
62 if let Ok(xray_header) = std::env::var("_X_AMZN_TRACE_ID") {
63 let env_extractor = XRayEnvExtractor::new(&xray_header);
64 let xray_ctx = opentelemetry::global::get_text_map_propagator(|propagator| {
65 propagator.extract(&env_extractor)
66 });
67 if xray_ctx.span().span_context().is_valid() {
68 return xray_ctx;
69 }
70 }
71
72 Context::current()
73 }
74
75 fn trigger_type(&self) -> &'static str {
76 "http"
77 }
78
79 fn span_name(&self, event: &ApiGatewayV2httpRequest, lambda_ctx: &LambdaContext) -> String {
80 let method = event.request_context.http.method.as_str();
82
83 let route = event
86 .route_key
87 .as_deref()
88 .and_then(|rk| rk.split_once(' ').map(|(_, route)| route))
89 .or(event.raw_path.as_deref())
90 .unwrap_or(&lambda_ctx.env_config.function_name);
91
92 format!("{} {}", method, route)
93 }
94
95 fn record_attributes(&self, event: &ApiGatewayV2httpRequest, span: &Span) {
96 span.record(
97 HTTP_REQUEST_METHOD,
98 event.request_context.http.method.as_str(),
99 );
100
101 if let Some(ref path) = event.raw_path {
102 span.record(URL_PATH, path.as_str());
103 }
104
105 if let Some(ref route_key) = event.route_key {
106 if let Some((_, route)) = route_key.split_once(' ') {
107 span.record(HTTP_ROUTE, route);
108 } else {
109 span.record(HTTP_ROUTE, route_key.as_str());
110 }
111 }
112
113 span.record(URL_SCHEME, "https");
114
115 if let Some(ref qs) = event.raw_query_string
116 && !qs.is_empty()
117 {
118 span.record(URL_QUERY, qs.as_str());
119 }
120
121 if let Some(ua) = event.headers.get("user-agent")
122 && let Ok(ua_str) = ua.to_str()
123 {
124 span.record(USER_AGENT_ORIGINAL, ua_str);
125 }
126
127 if let Some(ref ip) = event.request_context.http.source_ip {
128 span.record(CLIENT_ADDRESS, ip.as_str());
129 }
130
131 if let Some(host) = event.headers.get("host")
132 && let Ok(host_str) = host.to_str()
133 {
134 span.record(SERVER_ADDRESS, host_str);
135 }
136
137 if let Some(ref protocol) = event.request_context.http.protocol {
138 let version = extract_http_version(protocol);
139 span.record(NETWORK_PROTOCOL_VERSION, version);
140 }
141 }
142}
143
144#[derive(Clone, Debug, Default)]
149pub struct ApiGatewayV1Extractor;
150
151impl ApiGatewayV1Extractor {
152 pub fn new() -> Self {
156 Self
157 }
158}
159
160impl TraceContextExtractor<ApiGatewayProxyRequest> for ApiGatewayV1Extractor {
161 fn extract_context(&self, event: &ApiGatewayProxyRequest) -> Context {
162 let extractor = HeaderMapExtractor(&event.headers);
163 let ctx = opentelemetry::global::get_text_map_propagator(|propagator| {
164 propagator.extract(&extractor)
165 });
166
167 if ctx.span().span_context().is_valid() {
168 return ctx;
169 }
170
171 if let Ok(xray_header) = std::env::var("_X_AMZN_TRACE_ID") {
172 let env_extractor = XRayEnvExtractor::new(&xray_header);
173 let xray_ctx = opentelemetry::global::get_text_map_propagator(|propagator| {
174 propagator.extract(&env_extractor)
175 });
176 if xray_ctx.span().span_context().is_valid() {
177 return xray_ctx;
178 }
179 }
180
181 Context::current()
182 }
183
184 fn trigger_type(&self) -> &'static str {
185 "http"
186 }
187
188 fn span_name(&self, event: &ApiGatewayProxyRequest, lambda_ctx: &LambdaContext) -> String {
189 let method = event.http_method.as_str();
190
191 let route = event
193 .resource
194 .as_deref()
195 .or(event.path.as_deref())
196 .unwrap_or(&lambda_ctx.env_config.function_name);
197
198 format!("{} {}", method, route)
199 }
200
201 fn record_attributes(&self, event: &ApiGatewayProxyRequest, span: &Span) {
202 span.record(HTTP_REQUEST_METHOD, event.http_method.as_str());
203
204 if let Some(ref path) = event.path {
205 span.record(URL_PATH, path.as_str());
206 }
207
208 if let Some(ref resource) = event.resource {
209 span.record(HTTP_ROUTE, resource.as_str());
210 }
211
212 span.record(URL_SCHEME, "https");
213
214 if let Some(ua) = event.headers.get("user-agent")
215 && let Ok(ua_str) = ua.to_str()
216 {
217 span.record(USER_AGENT_ORIGINAL, ua_str);
218 }
219
220 if let Some(ref ip) = event.request_context.identity.source_ip {
221 span.record(CLIENT_ADDRESS, ip.as_str());
222 }
223
224 if let Some(host) = event.headers.get("host")
225 && let Ok(host_str) = host.to_str()
226 {
227 span.record(SERVER_ADDRESS, host_str);
228 }
229
230 if let Some(ref protocol) = event.request_context.protocol {
231 let version = extract_http_version(protocol);
232 span.record(NETWORK_PROTOCOL_VERSION, version);
233 }
234 }
235}
236
237fn extract_http_version(protocol: &str) -> &str {
242 protocol
243 .strip_prefix("HTTP/")
244 .map(|v| v.trim_end_matches(".0"))
245 .unwrap_or(protocol)
246}
247
248struct HeaderMapExtractor<'a>(&'a HeaderMap);
250
251impl Extractor for HeaderMapExtractor<'_> {
252 fn get(&self, key: &str) -> Option<&str> {
253 self.0.get(key).and_then(|v| v.to_str().ok())
254 }
255
256 fn keys(&self) -> Vec<&str> {
257 self.0.keys().map(|k| k.as_str()).collect()
258 }
259}
260
261struct XRayEnvExtractor {
269 traceparent: Option<String>,
270}
271
272impl XRayEnvExtractor {
273 fn new(xray: &str) -> Self {
274 Self {
275 traceparent: convert_xray_to_traceparent(xray),
276 }
277 }
278}
279
280impl Extractor for XRayEnvExtractor {
281 fn get(&self, key: &str) -> Option<&str> {
282 if key.eq_ignore_ascii_case("traceparent") {
283 self.traceparent.as_deref()
284 } else {
285 None
286 }
287 }
288
289 fn keys(&self) -> Vec<&str> {
290 if self.traceparent.is_some() {
291 vec!["traceparent"]
292 } else {
293 vec![]
294 }
295 }
296}
297
298pub fn convert_xray_to_traceparent(xray: &str) -> Option<String> {
303 let mut trace_id = None;
304 let mut parent_id = None;
305 let mut sampled = false;
306
307 for part in xray.split(';') {
308 if let Some(root) = part.strip_prefix("Root=") {
309 trace_id = parse_xray_trace_id(root);
310 } else if let Some(parent) = part.strip_prefix("Parent=") {
311 parent_id = Some(parent.to_string());
312 } else if part == "Sampled=1" {
313 sampled = true;
314 }
315 }
316
317 let trace = trace_id?;
318 let parent = parent_id?;
319
320 if parent.len() != 16 {
321 return None;
322 }
323
324 let flags = if sampled { "01" } else { "00" };
325 Some(format!("00-{}-{}-{}", trace, parent, flags))
326}
327
328pub fn parse_xray_trace_id(root: &str) -> Option<String> {
333 let parts: Vec<&str> = root.split('-').collect();
334 if parts.len() == 3 && parts[0] == "1" {
335 let trace_id = format!("{}{}", parts[1], parts[2]);
336 if trace_id.len() == 32 {
337 return Some(trace_id);
338 }
339 }
340 None
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use aws_lambda_events::apigw::{
347 ApiGatewayV2httpRequestContext, ApiGatewayV2httpRequestContextHttpDescription,
348 };
349 use http::HeaderValue;
350 use opentelemetry_sdk::propagation::TraceContextPropagator;
351 use serial_test::serial;
352
353 fn create_test_v2_event() -> ApiGatewayV2httpRequest {
354 let mut headers = HeaderMap::new();
355 headers.insert("content-type", HeaderValue::from_static("application/json"));
356
357 let mut http_desc = ApiGatewayV2httpRequestContextHttpDescription::default();
358 http_desc.method = http::Method::GET;
359 http_desc.source_ip = Some("192.168.1.1".to_string());
360
361 let mut request_context = ApiGatewayV2httpRequestContext::default();
362 request_context.http = http_desc;
363
364 let mut event = ApiGatewayV2httpRequest::default();
365 event.headers = headers;
366 event.raw_path = Some("/users/123".to_string());
367 event.route_key = Some("GET /users/{id}".to_string());
368 event.raw_query_string = Some("foo=bar".to_string());
369 event.request_context = request_context;
370 event
371 }
372
373 fn create_test_lambda_context() -> LambdaContext {
374 LambdaContext::default()
375 }
376
377 #[test]
378 fn test_trigger_type() {
379 let extractor = ApiGatewayV2Extractor::new();
380 assert_eq!(extractor.trigger_type(), "http");
381 }
382
383 #[test]
384 fn test_span_name_from_route_v2() {
385 let extractor = ApiGatewayV2Extractor::new();
386 let event = create_test_v2_event();
387 let ctx = create_test_lambda_context();
388
389 let name = extractor.span_name(&event, &ctx);
390 assert_eq!(name, "GET /users/{id}");
391 }
392
393 #[test]
394 fn test_span_name_fallback_to_path() {
395 let extractor = ApiGatewayV2Extractor::new();
396 let mut event = create_test_v2_event();
397 event.route_key = None;
398 let ctx = create_test_lambda_context();
399
400 let name = extractor.span_name(&event, &ctx);
401 assert_eq!(name, "GET /users/123");
402 }
403
404 #[test]
405 fn test_extract_no_trace_context() {
406 let extractor = ApiGatewayV2Extractor::new();
407 let event = create_test_v2_event();
408
409 let ctx = extractor.extract_context(&event);
410
411 assert!(!ctx.span().span_context().is_valid());
414 }
415
416 #[test]
417 #[serial]
418 fn test_extract_traceparent_header() {
419 opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
420
421 let extractor = ApiGatewayV2Extractor::new();
422 let mut event = create_test_v2_event();
423
424 event.headers.insert(
425 "traceparent",
426 HeaderValue::from_static("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
427 );
428
429 let ctx = extractor.extract_context(&event);
430
431 assert!(ctx.span().span_context().is_valid());
432 assert_eq!(
433 ctx.span().span_context().trace_id().to_string(),
434 "4bf92f3577b34da6a3ce929d0e0e4736"
435 );
436 }
437
438 #[test]
439 #[serial]
440 fn test_extract_traceparent_case_insensitive() {
441 opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
442
443 let extractor = ApiGatewayV2Extractor::new();
444 let mut event = create_test_v2_event();
445
446 event.headers.insert(
447 "Traceparent",
448 HeaderValue::from_static("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
449 );
450
451 let ctx = extractor.extract_context(&event);
452 assert!(ctx.span().span_context().is_valid());
453 }
454
455 #[test]
456 fn test_extract_invalid_traceparent() {
457 let extractor = ApiGatewayV2Extractor::new();
458 let mut event = create_test_v2_event();
459
460 event
461 .headers
462 .insert("traceparent", HeaderValue::from_static("invalid"));
463
464 let ctx = extractor.extract_context(&event);
465
466 assert!(!ctx.span().span_context().is_valid());
467 }
468
469 #[test]
470 fn test_parse_xray_trace_id() {
471 let result = parse_xray_trace_id("1-5759e988-bd862e3fe1be46a994272793");
473 assert!(result.is_some());
474 assert_eq!(result.unwrap(), "5759e988bd862e3fe1be46a994272793");
475 }
476
477 #[test]
478 fn test_parse_xray_trace_id_invalid() {
479 assert!(parse_xray_trace_id("invalid").is_none());
480 assert!(parse_xray_trace_id("1-abc").is_none());
481 }
482
483 #[test]
484 fn test_convert_xray_to_traceparent_sampled() {
485 let xray = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
486 let result = convert_xray_to_traceparent(xray);
487 assert!(result.is_some());
488 assert_eq!(
489 result.unwrap(),
490 "00-5759e988bd862e3fe1be46a994272793-53995c3f42cd8ad8-01"
491 );
492 }
493
494 #[test]
495 fn test_convert_xray_to_traceparent_unsampled() {
496 let xray = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=0";
497 let result = convert_xray_to_traceparent(xray);
498 assert!(result.is_some());
499 assert_eq!(
500 result.unwrap(),
501 "00-5759e988bd862e3fe1be46a994272793-53995c3f42cd8ad8-00"
502 );
503 }
504
505 #[test]
506 fn test_convert_xray_to_traceparent_missing_parent() {
507 let xray = "Root=1-5759e988-bd862e3fe1be46a994272793;Sampled=1";
508 assert!(convert_xray_to_traceparent(xray).is_none());
509 }
510
511 #[test]
512 fn test_convert_xray_to_traceparent_invalid_parent() {
513 let xray = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=tooshort;Sampled=1";
514 assert!(convert_xray_to_traceparent(xray).is_none());
515 }
516
517 #[test]
518 fn test_xray_env_extractor_valid() {
519 let xray = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
520 let extractor = XRayEnvExtractor::new(xray);
521 let traceparent = extractor.get("traceparent");
522 assert!(traceparent.is_some());
523 assert_eq!(
524 traceparent.unwrap(),
525 "00-5759e988bd862e3fe1be46a994272793-53995c3f42cd8ad8-01"
526 );
527 }
528
529 #[test]
530 fn test_xray_env_extractor_case_insensitive() {
531 let xray = "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1";
532 let extractor = XRayEnvExtractor::new(xray);
533 assert!(extractor.get("Traceparent").is_some());
534 assert!(extractor.get("TRACEPARENT").is_some());
535 }
536
537 #[test]
538 fn test_extract_http_version_1_1() {
539 assert_eq!(extract_http_version("HTTP/1.1"), "1.1");
540 }
541
542 #[test]
543 fn test_extract_http_version_2_0() {
544 assert_eq!(extract_http_version("HTTP/2.0"), "2");
545 }
546
547 #[test]
548 fn test_extract_http_version_2() {
549 assert_eq!(extract_http_version("HTTP/2"), "2");
550 }
551
552 #[test]
553 fn test_extract_http_version_fallback() {
554 assert_eq!(extract_http_version("unknown"), "unknown");
555 }
556}