Skip to main content

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