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