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}