lambda_otel_lite/extractors.rs
1//! Attribute extraction for OpenTelemetry spans in AWS Lambda functions.
2//!
3//! This module provides functionality for extracting OpenTelemetry span attributes from AWS Lambda
4//! events. It includes:
5//! - Built-in support for common AWS event types (API Gateway, ALB)
6//! - Extensible trait system for custom event types
7//! - Automatic W3C Trace Context and AWS X-Ray propagation
8//! - Support for span links and custom attributes
9//!
10//! # Architecture
11//!
12//! The module uses a trait-based approach for attribute extraction:
13//!
14//! 1. **Event Processing**: Each supported event type implements the `SpanAttributesExtractor` trait
15//! 2. **Attribute Collection**: Standard attributes are collected based on event type
16//! 3. **Context Propagation**: W3C Trace Context and AWS X-Ray headers are automatically extracted
17//! 4. **Custom Attributes**: Additional attributes can be added through custom implementations
18//!
19//! # Automatic Attributes
20//!
21//! The module automatically extracts and sets several types of attributes:
22//!
23//! ## Resource Attributes
24//! - `cloud.provider`: Set to "aws"
25//! - `cloud.region`: From AWS_REGION
26//! - `faas.name`: From AWS_LAMBDA_FUNCTION_NAME
27//! - `faas.version`: From AWS_LAMBDA_FUNCTION_VERSION
28//! - `faas.instance`: From AWS_LAMBDA_LOG_STREAM_NAME
29//! - `faas.max_memory`: From AWS_LAMBDA_FUNCTION_MEMORY_SIZE
30//! - `service.name`: From OTEL_SERVICE_NAME or function name
31//!
32//! ## Span Attributes
33//! - `faas.coldstart`: True only on first invocation
34//! - `faas.invocation_id`: From Lambda request ID
35//! - `cloud.account.id`: From function ARN
36//! - `cloud.resource_id`: Complete function ARN
37//! - `otel.kind`: "SERVER" by default
38//! - `otel.status_code`/`message`: From response processing
39//!
40//! ## HTTP Attributes (for supported event types)
41//! - `faas.trigger`: Set to "http" for API/ALB events
42//! - `http.status_code`: From response
43//! - `http.route`: Route key or resource path
44//! - `http.method`: HTTP method
45//! - `url.path`: Request path
46//! - `url.query`: Query parameters if present
47//! - `url.scheme`: Protocol (https)
48//! - `network.protocol.version`: HTTP version
49//! - `client.address`: Client IP address
50//! - `user_agent.original`: User agent string
51//! - `server.address`: Server hostname
52//!
53//! # Built-in Support
54//!
55//! The following AWS event types are supported out of the box:
56//! - API Gateway v1/v2 (HTTP API and REST API)
57//! - Application Load Balancer
58//!
59//! Each implementation follows OpenTelemetry semantic conventions for HTTP spans:
60//! - `http.request.method`: The HTTP method (e.g., "GET", "POST")
61//! - `url.path`: The request path
62//! - `url.query`: The query string (if present)
63//! - `url.scheme`: The protocol scheme ("https" for API Gateway, configurable for ALB)
64//! - `network.protocol.version`: The HTTP protocol version
65//! - `http.route`: The route pattern or resource path
66//! - `client.address`: The client's IP address
67//! - `user_agent.original`: The user agent string
68//! - `server.address`: The server's domain name or host
69//!
70//! # Performance Considerations
71//!
72//! - Attribute extraction is done lazily when spans are created
73//! - String allocations are minimized where possible
74//! - Header extraction filters invalid UTF-8 values
75//!
76use aws_lambda_events::event::alb::AlbTargetGroupRequest;
77use aws_lambda_events::event::apigw::{ApiGatewayProxyRequest, ApiGatewayV2httpRequest};
78use bon::Builder;
79use lambda_runtime::Context;
80use opentelemetry::trace::{Link, Status};
81use opentelemetry::Value;
82use serde_json::Value as JsonValue;
83use std::collections::HashMap;
84use std::fmt::{self, Display};
85use tracing::Span;
86use tracing_opentelemetry::OpenTelemetrySpanExt;
87use urlencoding;
88
89/// Common trigger types for Lambda functions.
90///
91/// These variants follow OpenTelemetry semantic conventions:
92/// - `Datasource`: Database triggers
93/// - `Http`: HTTP/API triggers
94/// - `PubSub`: Message/event triggers
95/// - `Timer`: Schedule/cron triggers
96/// - `Other`: Fallback for unknown types
97///
98/// Custom trigger types can be used for more specific cases by using
99/// the string value directly in SpanAttributes.
100#[derive(Debug, Clone, PartialEq)]
101pub enum TriggerType {
102 /// Database trigger
103 Datasource,
104 /// HTTP/API trigger
105 Http,
106 /// Message/event trigger
107 PubSub,
108 /// Schedule/cron trigger
109 Timer,
110 /// Other/unknown trigger
111 Other,
112}
113
114impl Display for TriggerType {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 TriggerType::Datasource => write!(f, "datasource"),
118 TriggerType::Http => write!(f, "http"),
119 TriggerType::PubSub => write!(f, "pubsub"),
120 TriggerType::Timer => write!(f, "timer"),
121 TriggerType::Other => write!(f, "other"),
122 }
123 }
124}
125
126impl Default for TriggerType {
127 fn default() -> Self {
128 Self::Other
129 }
130}
131
132/// Data extracted from a Lambda event for span creation.
133///
134/// This struct contains all the information needed to create and configure an OpenTelemetry span,
135/// including custom attributes, span kind, links, and context propagation headers.
136///
137/// # Span Kind
138///
139/// The `kind` field accepts standard OpenTelemetry span kinds:
140/// - "SERVER" (default): Inbound request handling
141/// - "CLIENT": Outbound calls
142/// - "PRODUCER": Message/event production
143/// - "CONSUMER": Message/event consumption
144/// - "INTERNAL": Internal operations
145///
146/// # Span Name
147///
148/// For HTTP spans, the `span_name` is automatically generated from the HTTP method and route:
149/// - API Gateway V2: "GET /users/{id}" (with $default mapped to "/")
150/// - API Gateway V1: "POST /orders"
151/// - ALB: "PUT /items/123"
152///
153/// # Attributes
154///
155/// Standard HTTP attributes following OpenTelemetry semantic conventions:
156/// - `http.request.method`: The HTTP method
157/// - `url.path`: The request path
158/// - `url.query`: The query string (if present)
159/// - `url.scheme`: The protocol scheme
160/// - `network.protocol.version`: The HTTP protocol version
161/// - `http.route`: The route pattern or resource path
162/// - `client.address`: The client's IP address
163/// - `user_agent.original`: The user agent string
164/// - `server.address`: The server's domain name or host
165///
166/// # Context Propagation
167///
168/// The `carrier` field supports both W3C Trace Context and AWS X-Ray headers:
169/// - `traceparent`: W3C format containing trace ID, span ID, and trace flags
170/// - `tracestate`: W3C vendor-specific trace information
171/// - `x-amzn-trace-id`: AWS X-Ray trace header format
172///
173/// # Examples
174///
175/// Basic usage with custom attributes:
176///
177/// ```no_run
178/// use lambda_otel_lite::SpanAttributes;
179/// use std::collections::HashMap;
180/// use opentelemetry::Value;
181///
182/// let mut attributes = HashMap::new();
183/// attributes.insert("custom.field".to_string(), Value::String("value".into()));
184///
185/// let span_attrs = SpanAttributes::builder()
186/// .attributes(attributes)
187/// .build();
188/// ```
189///
190#[derive(Builder)]
191pub struct SpanAttributes {
192 /// Optional span kind (defaults to SERVER if not provided)
193 /// Valid values: "SERVER", "CLIENT", "PRODUCER", "CONSUMER", "INTERNAL"
194 pub kind: Option<String>,
195
196 /// Optional span name.
197 /// For HTTP spans, this should be "{http.method} {http.route}"
198 /// Example: "GET /users/:id"
199 pub span_name: Option<String>,
200
201 /// Custom attributes to add to the span.
202 /// Follow OpenTelemetry semantic conventions for naming:
203 /// <https://opentelemetry.io/docs/specs/semconv/>
204 #[builder(default)]
205 pub attributes: HashMap<String, Value>,
206
207 /// Optional span links for connecting related traces.
208 /// Useful for batch processing or joining multiple workflows.
209 #[builder(default)]
210 pub links: Vec<Link>,
211
212 /// Optional carrier headers for context propagation.
213 /// Supports both W3C Trace Context and AWS X-Ray formats:
214 /// - `traceparent`: W3C format containing trace ID, span ID, and trace flags
215 /// - `tracestate`: W3C vendor-specific trace information
216 /// - `x-amzn-trace-id`: AWS X-Ray trace header format
217 pub carrier: Option<HashMap<String, String>>,
218
219 /// The type of trigger for this Lambda invocation.
220 /// Common values: "datasource", "http", "pubsub", "timer", "other"
221 /// Custom values can be used for more specific triggers.
222 #[builder(default = TriggerType::Other.to_string())]
223 pub trigger: String,
224}
225
226impl Default for SpanAttributes {
227 fn default() -> Self {
228 Self {
229 kind: None,
230 span_name: None,
231 attributes: HashMap::new(),
232 links: Vec::new(),
233 carrier: None,
234 trigger: TriggerType::Other.to_string(),
235 }
236 }
237}
238
239/// Extract status code from response if it's an HTTP response.
240///
241/// This function attempts to extract an HTTP status code from a response Value.
242/// It looks for a top-level "statusCode" field and returns its value if present
243/// and valid.
244pub fn get_status_code(response: &JsonValue) -> Option<i64> {
245 response
246 .as_object()
247 .and_then(|obj| obj.get("statusCode"))
248 .and_then(|v| v.as_i64())
249}
250
251/// Set response attributes on the span based on the response value.
252///
253/// This function extracts and sets response-related attributes on the span,
254/// including status code and error status for HTTP responses.
255pub fn set_response_attributes(span: &Span, response: &JsonValue) {
256 if let Some(status_code) = get_status_code(response) {
257 span.set_attribute("http.status_code", status_code.to_string());
258
259 // Set span status based on status code
260 if status_code >= 500 {
261 span.set_status(Status::error(format!("HTTP {} response", status_code)));
262 } else {
263 span.set_status(Status::Ok);
264 }
265 span.set_attribute("http.response.status_code", status_code.to_string());
266 }
267}
268
269/// Set common attributes on the span based on the Lambda context.
270///
271/// This function sets standard Lambda-related attributes on the span using
272/// information from the Lambda context.
273pub fn set_common_attributes(span: &Span, context: &Context, is_cold_start: bool) {
274 // Set basic attributes
275 span.set_attribute("faas.invocation_id", context.request_id.to_string());
276 span.set_attribute(
277 "cloud.resource_id",
278 context.invoked_function_arn.to_string(),
279 );
280 if is_cold_start {
281 span.set_attribute("faas.coldstart", true);
282 }
283
284 // Extract and set AWS account ID
285 if let Some(account_id) = context.invoked_function_arn.split(':').nth(4) {
286 span.set_attribute("cloud.account.id", account_id.to_string());
287 }
288
289 // Set AWS region if available
290 if let Some(region) = context.invoked_function_arn.split(':').nth(3) {
291 span.set_attribute("cloud.region", region.to_string());
292 }
293
294 // Set function name and version
295 // TODO: these are already set in the resource, we can remove them
296 span.set_attribute(
297 "faas.name",
298 std::env::var("AWS_LAMBDA_FUNCTION_NAME").unwrap_or_default(),
299 );
300 span.set_attribute(
301 "faas.version",
302 std::env::var("AWS_LAMBDA_FUNCTION_VERSION").unwrap_or_default(),
303 );
304}
305
306/// Trait for types that can provide span attributes.
307///
308/// This trait enables automatic extraction of OpenTelemetry span attributes from event types.
309/// The tracing layer automatically detects and uses implementations of this trait when
310/// processing Lambda events.
311///
312/// # Implementation Guidelines
313///
314/// When implementing this trait, follow these best practices:
315///
316/// 1. **Attribute Naming**:
317/// - Follow [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/)
318/// - Use lowercase with dots for namespacing (e.g., "http.method")
319/// - Keep names concise but descriptive
320///
321/// 2. **Span Kind**:
322/// - Set appropriate kind based on event type:
323/// - "SERVER" for inbound requests (default)
324/// - "CONSUMER" for event/message processing
325/// - "CLIENT" for outbound calls
326///
327/// 3. **Context Propagation**:
328/// - Extract both W3C Trace Context and AWS X-Ray headers
329/// - Handle `traceparent`, `tracestate`, and `x-amzn-trace-id` headers
330/// - Validate header values when possible
331///
332/// 4. **Performance**:
333/// - Minimize string allocations
334/// - Avoid unnecessary cloning
335/// - Filter out invalid or unnecessary headers
336///
337/// # Examples
338///
339/// Basic implementation for a custom event:
340///
341/// ```no_run
342/// use lambda_otel_lite::{SpanAttributes, SpanAttributesExtractor};
343/// use std::collections::HashMap;
344/// use opentelemetry::Value;
345///
346/// struct CustomEvent {
347/// operation: String,
348/// trace_parent: Option<String>,
349/// }
350///
351/// impl SpanAttributesExtractor for CustomEvent {
352/// fn extract_span_attributes(&self) -> SpanAttributes {
353/// let mut attributes = HashMap::new();
354/// attributes.insert("operation".to_string(), Value::String(self.operation.clone().into()));
355///
356/// // Add trace context if available
357/// let mut carrier = HashMap::new();
358/// if let Some(header) = &self.trace_parent {
359/// carrier.insert("traceparent".to_string(), header.clone());
360/// }
361/// // You can also handle X-Ray headers
362/// // if let Some(xray_header) = &self.xray_trace_id {
363/// // carrier.insert("x-amzn-trace-id".to_string(), xray_header.clone());
364/// // }
365///
366/// SpanAttributes::builder()
367/// .attributes(attributes)
368/// .carrier(carrier)
369/// .build()
370/// }
371/// }
372/// ```
373pub trait SpanAttributesExtractor {
374 /// Extract span attributes from this type.
375 ///
376 /// This method should extract any relevant information from the implementing type
377 /// that should be included in the OpenTelemetry span. This includes:
378 /// - Custom attributes describing the event
379 /// - Span kind if different from SERVER
380 /// - Headers for context propagation
381 /// - Links to related traces
382 ///
383 /// # Returns
384 ///
385 /// Returns a `SpanAttributes` instance containing all extracted information.
386 /// If extraction fails in any way, it should return a default instance rather
387 /// than failing.
388 fn extract_span_attributes(&self) -> SpanAttributes;
389}
390
391/// Implementation for API Gateway V2 HTTP API events.
392///
393/// Extracts standard HTTP attributes following OpenTelemetry semantic conventions:
394/// - `http.request.method`: The HTTP method
395/// - `url.path`: The request path (from raw_path)
396/// - `url.query`: The query string if present (from raw_query_string)
397/// - `url.scheme`: The protocol scheme (always "https" for API Gateway)
398/// - `network.protocol.version`: The HTTP protocol version
399/// - `http.route`: The API Gateway route key (e.g. "$default" or "GET /users/{id}")
400/// - `client.address`: The client's IP address (from source_ip)
401/// - `user_agent.original`: The user agent header
402/// - `server.address`: The domain name
403///
404/// Also extracts W3C Trace Context headers and AWS X-Ray headers for distributed tracing.
405impl SpanAttributesExtractor for ApiGatewayV2httpRequest {
406 fn extract_span_attributes(&self) -> SpanAttributes {
407 let mut attributes = HashMap::new();
408 let method = self.request_context.http.method.to_string();
409 let path = self.raw_path.as_deref().unwrap_or("/");
410
411 // Add HTTP attributes following OTel semantic conventions
412 attributes.insert(
413 "http.request.method".to_string(),
414 Value::String(method.clone().into()),
415 );
416
417 // Use raw_path directly for url.path
418 if let Some(raw_path) = &self.raw_path {
419 attributes.insert(
420 "url.path".to_string(),
421 Value::String(raw_path.to_string().into()),
422 );
423 }
424
425 // Use raw_query_string directly for url.query
426 if let Some(query) = &self.raw_query_string {
427 if !query.is_empty() {
428 attributes.insert(
429 "url.query".to_string(),
430 Value::String(query.to_string().into()),
431 );
432 }
433 }
434
435 if let Some(protocol) = &self.request_context.http.protocol {
436 let protocol_lower = protocol.to_lowercase();
437 if protocol_lower.starts_with("http/") {
438 attributes.insert(
439 "network.protocol.version".to_string(),
440 Value::String(
441 protocol_lower
442 .trim_start_matches("http/")
443 .to_string()
444 .into(),
445 ),
446 );
447 }
448 attributes.insert(
449 "url.scheme".to_string(),
450 Value::String("https".to_string().into()),
451 ); // API Gateway is always HTTPS
452 }
453
454 // Add route key as http.route
455 if let Some(route_key) = &self.route_key {
456 attributes.insert(
457 "http.route".to_string(),
458 Value::String(route_key.to_string().into()),
459 );
460 }
461
462 // Extract headers for context propagation
463 let carrier = self
464 .headers
465 .iter()
466 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
467 .collect();
468
469 // Add source IP and user agent
470 if let Some(source_ip) = &self.request_context.http.source_ip {
471 attributes.insert(
472 "client.address".to_string(),
473 Value::String(source_ip.to_string().into()),
474 );
475 }
476 if let Some(user_agent) = self.headers.get("user-agent").and_then(|h| h.to_str().ok()) {
477 attributes.insert(
478 "user_agent.original".to_string(),
479 Value::String(user_agent.to_string().into()),
480 );
481 }
482
483 // Add domain name if available
484 if let Some(domain_name) = &self.request_context.domain_name {
485 attributes.insert(
486 "server.address".to_string(),
487 Value::String(domain_name.to_string().into()),
488 );
489 }
490
491 SpanAttributes::builder()
492 .attributes(attributes)
493 .carrier(carrier)
494 .span_name(format!("{} {}", method, path))
495 .trigger(TriggerType::Http.to_string())
496 .build()
497 }
498}
499
500/// Implementation for API Gateway V1 REST API events.
501///
502/// Extracts standard HTTP attributes following OpenTelemetry semantic conventions:
503/// - `http.request.method`: The HTTP method
504/// - `url.path`: The request path
505/// - `url.query`: The query string (constructed from multi_value_query_string_parameters)
506/// - `url.scheme`: The protocol scheme (always "https" for API Gateway)
507/// - `network.protocol.version`: The HTTP protocol version
508/// - `http.route`: The API Gateway resource path
509/// - `client.address`: The client's IP address (from identity.source_ip)
510/// - `user_agent.original`: The user agent header
511/// - `server.address`: The domain name
512///
513/// Also extracts W3C Trace Context headers and AWS X-Ray headers for distributed tracing.
514impl SpanAttributesExtractor for ApiGatewayProxyRequest {
515 fn extract_span_attributes(&self) -> SpanAttributes {
516 let mut attributes = HashMap::new();
517 let method = self.http_method.to_string();
518 let route = self.resource.as_deref().unwrap_or("/");
519
520 // Add HTTP attributes following OTel semantic conventions
521 attributes.insert(
522 "http.request.method".to_string(),
523 Value::String(method.clone().into()),
524 );
525
526 // Use path directly
527 if let Some(path) = &self.path {
528 attributes.insert(
529 "url.path".to_string(),
530 Value::String(path.to_string().into()),
531 );
532 }
533
534 // Use multi_value_query_string_parameters and format query string manually
535 if !self.multi_value_query_string_parameters.is_empty() {
536 let mut query_parts = Vec::new();
537 for key in self
538 .multi_value_query_string_parameters
539 .iter()
540 .map(|(k, _)| k)
541 {
542 if let Some(values) = self.multi_value_query_string_parameters.all(key) {
543 for value in values {
544 query_parts.push(format!(
545 "{}={}",
546 urlencoding::encode(key),
547 urlencoding::encode(value)
548 ));
549 }
550 }
551 }
552 if !query_parts.is_empty() {
553 let query = query_parts.join("&");
554 attributes.insert("url.query".to_string(), Value::String(query.into()));
555 }
556 }
557
558 if let Some(protocol) = &self.request_context.protocol {
559 let protocol_lower = protocol.to_lowercase();
560 if protocol_lower.starts_with("http/") {
561 attributes.insert(
562 "network.protocol.version".to_string(),
563 Value::String(
564 protocol_lower
565 .trim_start_matches("http/")
566 .to_string()
567 .into(),
568 ),
569 );
570 }
571 attributes.insert(
572 "url.scheme".to_string(),
573 Value::String("https".to_string().into()),
574 ); // API Gateway is always HTTPS
575 }
576
577 // Add route
578 attributes.insert(
579 "http.route".to_string(),
580 Value::String(route.to_string().into()),
581 );
582
583 // Extract headers for context propagation
584 let carrier = self
585 .headers
586 .iter()
587 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
588 .collect();
589
590 // Add source IP and user agent
591 if let Some(source_ip) = &self.request_context.identity.source_ip {
592 attributes.insert(
593 "client.address".to_string(),
594 Value::String(source_ip.to_string().into()),
595 );
596 }
597 if let Some(user_agent) = self.headers.get("user-agent").and_then(|h| h.to_str().ok()) {
598 attributes.insert(
599 "user_agent.original".to_string(),
600 Value::String(user_agent.to_string().into()),
601 );
602 }
603
604 // Add domain name if available
605 if let Some(domain_name) = &self.request_context.domain_name {
606 attributes.insert(
607 "server.address".to_string(),
608 Value::String(domain_name.to_string().into()),
609 );
610 }
611
612 SpanAttributes::builder()
613 .attributes(attributes)
614 .carrier(carrier)
615 .span_name(format!("{} {}", method, route))
616 .trigger(TriggerType::Http.to_string())
617 .build()
618 }
619}
620
621/// Implementation for Application Load Balancer target group events.
622///
623/// Extracts standard HTTP attributes following OpenTelemetry semantic conventions:
624/// - `http.request.method`: The HTTP method
625/// - `url.path`: The request path
626/// - `url.query`: The query string (constructed from multi_value_query_string_parameters)
627/// - `url.scheme`: The protocol scheme (defaults to "http")
628/// - `network.protocol.version`: The HTTP protocol version (always "1.1" for ALB)
629/// - `http.route`: The request path
630/// - `client.address`: The client's IP address (from x-forwarded-for header)
631/// - `user_agent.original`: The user agent header
632/// - `server.address`: The host header
633/// - `alb.target_group_arn`: The ARN of the target group
634///
635/// Also extracts W3C Trace Context headers and AWS X-Ray headers for distributed tracing.
636impl SpanAttributesExtractor for AlbTargetGroupRequest {
637 fn extract_span_attributes(&self) -> SpanAttributes {
638 let mut attributes = HashMap::new();
639 let method = self.http_method.to_string();
640 let route = self.path.as_deref().unwrap_or("/");
641
642 // Add HTTP attributes following OTel semantic conventions
643 attributes.insert(
644 "http.request.method".to_string(),
645 Value::String(method.clone().into()),
646 );
647
648 // Use path directly
649 if let Some(path) = &self.path {
650 attributes.insert(
651 "url.path".to_string(),
652 Value::String(path.to_string().into()),
653 );
654 }
655
656 // Use multi_value_query_string_parameters and format query string manually
657 if !self.multi_value_query_string_parameters.is_empty() {
658 let mut query_parts = Vec::new();
659 for key in self
660 .multi_value_query_string_parameters
661 .iter()
662 .map(|(k, _)| k)
663 {
664 if let Some(values) = self.multi_value_query_string_parameters.all(key) {
665 for value in values {
666 query_parts.push(format!(
667 "{}={}",
668 urlencoding::encode(key),
669 urlencoding::encode(value)
670 ));
671 }
672 }
673 }
674 if !query_parts.is_empty() {
675 let query = query_parts.join("&");
676 attributes.insert("url.query".to_string(), Value::String(query.into()));
677 }
678 }
679
680 // ALB can be HTTP or HTTPS, default to HTTP if not specified
681 attributes.insert(
682 "url.scheme".to_string(),
683 Value::String("http".to_string().into()),
684 );
685 attributes.insert(
686 "network.protocol.version".to_string(),
687 Value::String("1.1".to_string().into()),
688 ); // ALB uses HTTP/1.1
689
690 // Add ALB specific attributes
691 if let Some(target_group_arn) = &self.request_context.elb.target_group_arn {
692 attributes.insert(
693 "alb.target_group_arn".to_string(),
694 Value::String(target_group_arn.to_string().into()),
695 );
696 }
697
698 // Extract headers for context propagation
699 let carrier = self
700 .headers
701 .iter()
702 .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
703 .collect();
704
705 // Add source IP and user agent
706 if let Some(source_ip) = &self
707 .headers
708 .get("x-forwarded-for")
709 .and_then(|h| h.to_str().ok())
710 {
711 if let Some(client_ip) = source_ip.split(',').next() {
712 attributes.insert(
713 "client.address".to_string(),
714 Value::String(client_ip.trim().to_string().into()),
715 );
716 }
717 }
718 if let Some(user_agent) = self.headers.get("user-agent").and_then(|h| h.to_str().ok()) {
719 attributes.insert(
720 "user_agent.original".to_string(),
721 Value::String(user_agent.to_string().into()),
722 );
723 }
724
725 // Add domain name if available
726 if let Some(host) = self.headers.get("host").and_then(|h| h.to_str().ok()) {
727 attributes.insert(
728 "server.address".to_string(),
729 Value::String(host.to_string().into()),
730 );
731 }
732
733 SpanAttributes::builder()
734 .attributes(attributes)
735 .carrier(carrier)
736 .span_name(format!("{} {}", method, route))
737 .trigger(TriggerType::Http.to_string())
738 .build()
739 }
740}
741
742/// Default implementation for serde_json::Value.
743///
744/// This implementation provides a fallback for when the event type is not known
745/// or when working with raw JSON data. It returns default attributes with
746/// the trigger type set to "other".
747/// If there's a headers field, it will be used to populate the carrier.
748impl SpanAttributesExtractor for serde_json::Value {
749 fn extract_span_attributes(&self) -> SpanAttributes {
750 let carrier = self
751 .get("headers")
752 .and_then(|headers| headers.as_object())
753 .map(|obj| {
754 obj.iter()
755 .filter_map(|(k, v)| v.as_str().map(|v| (k.to_string(), v.to_string())))
756 .collect()
757 })
758 .unwrap_or_default();
759
760 SpanAttributes::builder()
761 .carrier(carrier)
762 .trigger(TriggerType::Other.to_string())
763 .build()
764 }
765}
766
767#[cfg(test)]
768mod tests {
769 use super::*;
770 use aws_lambda_events::http::Method;
771
772 #[test]
773 fn test_trigger_types() {
774 let attrs = SpanAttributes::default();
775 assert_eq!(attrs.trigger, TriggerType::Other.to_string());
776
777 let attrs = SpanAttributes::builder()
778 .trigger(TriggerType::Http.to_string())
779 .build();
780 assert_eq!(attrs.trigger, TriggerType::Http.to_string());
781
782 let attrs = SpanAttributes::builder().build();
783 assert_eq!(attrs.trigger, TriggerType::Other.to_string());
784 }
785
786 #[test]
787 fn test_apigw_v2_extraction() {
788 let request = ApiGatewayV2httpRequest {
789 raw_path: Some("/test".to_string()),
790 route_key: Some("GET /test".to_string()),
791 headers: aws_lambda_events::http::HeaderMap::new(),
792 request_context: aws_lambda_events::apigw::ApiGatewayV2httpRequestContext {
793 http: aws_lambda_events::apigw::ApiGatewayV2httpRequestContextHttpDescription {
794 method: Method::GET,
795 path: Some("/test".to_string()),
796 protocol: Some("HTTP/1.1".to_string()),
797 ..Default::default()
798 },
799 ..Default::default()
800 },
801 ..Default::default()
802 };
803
804 let attrs = request.extract_span_attributes();
805
806 assert_eq!(
807 attrs.attributes.get("http.request.method"),
808 Some(&Value::String("GET".to_string().into()))
809 );
810 assert_eq!(
811 attrs.attributes.get("url.path"),
812 Some(&Value::String("/test".to_string().into()))
813 );
814 assert_eq!(
815 attrs.attributes.get("http.route"),
816 Some(&Value::String("GET /test".to_string().into()))
817 );
818 assert_eq!(
819 attrs.attributes.get("url.scheme"),
820 Some(&Value::String("https".to_string().into()))
821 );
822 assert_eq!(
823 attrs.attributes.get("network.protocol.version"),
824 Some(&Value::String("1.1".to_string().into()))
825 );
826 }
827
828 #[test]
829 fn test_apigw_v1_extraction() {
830 let request = ApiGatewayProxyRequest {
831 path: Some("/test".to_string()),
832 http_method: Method::GET,
833 resource: Some("/test".to_string()),
834 headers: aws_lambda_events::http::HeaderMap::new(),
835 request_context: aws_lambda_events::apigw::ApiGatewayProxyRequestContext {
836 protocol: Some("HTTP/1.1".to_string()),
837 ..Default::default()
838 },
839 ..Default::default()
840 };
841
842 let attrs = request.extract_span_attributes();
843
844 assert_eq!(
845 attrs.attributes.get("http.request.method"),
846 Some(&Value::String("GET".to_string().into()))
847 );
848 assert_eq!(
849 attrs.attributes.get("url.path"),
850 Some(&Value::String("/test".to_string().into()))
851 );
852 assert_eq!(
853 attrs.attributes.get("http.route"),
854 Some(&Value::String("/test".to_string().into()))
855 );
856 assert_eq!(
857 attrs.attributes.get("url.scheme"),
858 Some(&Value::String("https".to_string().into()))
859 );
860 assert_eq!(
861 attrs.attributes.get("network.protocol.version"),
862 Some(&Value::String("1.1".to_string().into()))
863 );
864 }
865
866 #[test]
867 fn test_alb_extraction() {
868 let request = AlbTargetGroupRequest {
869 path: Some("/test".to_string()),
870 http_method: Method::GET,
871 headers: aws_lambda_events::http::HeaderMap::new(),
872 request_context: aws_lambda_events::alb::AlbTargetGroupRequestContext {
873 elb: aws_lambda_events::alb::ElbContext {
874 target_group_arn: Some("arn:aws:elasticloadbalancing:...".to_string()),
875 },
876 },
877 ..Default::default()
878 };
879
880 let attrs = request.extract_span_attributes();
881
882 assert_eq!(
883 attrs.attributes.get("http.request.method"),
884 Some(&Value::String("GET".to_string().into()))
885 );
886 assert_eq!(
887 attrs.attributes.get("url.path"),
888 Some(&Value::String("/test".to_string().into()))
889 );
890 assert_eq!(
891 attrs.attributes.get("url.scheme"),
892 Some(&Value::String("http".to_string().into()))
893 );
894 assert_eq!(
895 attrs.attributes.get("network.protocol.version"),
896 Some(&Value::String("1.1".to_string().into()))
897 );
898 assert_eq!(
899 attrs.attributes.get("alb.target_group_arn"),
900 Some(&Value::String(
901 "arn:aws:elasticloadbalancing:...".to_string().into()
902 ))
903 );
904 }
905
906 #[test]
907 fn test_xray_header_extraction() {
908 // Create API Gateway request with X-Ray header
909 let mut headers = aws_lambda_events::http::HeaderMap::new();
910 let xray_header =
911 "Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1";
912 headers.insert(
913 "x-amzn-trace-id",
914 aws_lambda_events::http::HeaderValue::from_str(xray_header).unwrap(),
915 );
916
917 // Also include a W3C traceparent header
918 let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
919 headers.insert(
920 "traceparent",
921 aws_lambda_events::http::HeaderValue::from_str(traceparent).unwrap(),
922 );
923
924 // Create API Gateway V2 request
925 let request = ApiGatewayV2httpRequest {
926 headers,
927 raw_path: Some("/test".to_string()),
928 route_key: Some("GET /test".to_string()),
929 request_context: aws_lambda_events::apigw::ApiGatewayV2httpRequestContext {
930 http: aws_lambda_events::apigw::ApiGatewayV2httpRequestContextHttpDescription {
931 method: Method::GET,
932 path: Some("/test".to_string()),
933 protocol: Some("HTTP/1.1".to_string()),
934 ..Default::default()
935 },
936 ..Default::default()
937 },
938 ..Default::default()
939 };
940
941 // Extract attributes
942 let attrs = request.extract_span_attributes();
943
944 // Verify carrier contains both headers
945 assert!(attrs.carrier.is_some());
946 let carrier = attrs.carrier.unwrap();
947
948 // X-Ray header should be present and unaltered
949 assert!(carrier.contains_key("x-amzn-trace-id"));
950 assert_eq!(carrier.get("x-amzn-trace-id").unwrap(), xray_header);
951
952 // W3C header should also be present
953 assert!(carrier.contains_key("traceparent"));
954 assert_eq!(carrier.get("traceparent").unwrap(), traceparent);
955 }
956
957 #[test]
958 fn test_json_extractor_with_xray_headers() {
959 // Create a JSON value with headers including X-Ray
960 let json_value = serde_json::json!({
961 "headers": {
962 "x-amzn-trace-id": "Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1",
963 "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
964 "content-type": "application/json"
965 },
966 "body": "{\"message\":\"Hello World\"}",
967 "requestContext": {
968 "requestId": "12345"
969 }
970 });
971
972 // Extract attributes
973 let attrs = json_value.extract_span_attributes();
974
975 // Verify carrier contains the headers
976 assert!(attrs.carrier.is_some());
977 let carrier = attrs.carrier.unwrap();
978
979 // Both trace headers should be present
980 assert!(carrier.contains_key("x-amzn-trace-id"));
981 assert_eq!(
982 carrier.get("x-amzn-trace-id").unwrap(),
983 "Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1"
984 );
985
986 assert!(carrier.contains_key("traceparent"));
987 assert_eq!(
988 carrier.get("traceparent").unwrap(),
989 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
990 );
991 }
992}