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 {status_code} response")));
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
295/// Trait for types that can provide span attributes.
296///
297/// This trait enables automatic extraction of OpenTelemetry span attributes from event types.
298/// The tracing layer automatically detects and uses implementations of this trait when
299/// processing Lambda events.
300///
301/// # Implementation Guidelines
302///
303/// When implementing this trait, follow these best practices:
304///
305/// 1. **Attribute Naming**:
306///    - Follow [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/)
307///    - Use lowercase with dots for namespacing (e.g., "http.method")
308///    - Keep names concise but descriptive
309///
310/// 2. **Span Kind**:
311///    - Set appropriate kind based on event type:
312///      - "SERVER" for inbound requests (default)
313///      - "CONSUMER" for event/message processing
314///      - "CLIENT" for outbound calls
315///
316/// 3. **Context Propagation**:
317///    - Extract both W3C Trace Context and AWS X-Ray headers
318///    - Handle `traceparent`, `tracestate`, and `x-amzn-trace-id` headers
319///    - Validate header values when possible
320///
321/// 4. **Performance**:
322///    - Minimize string allocations
323///    - Avoid unnecessary cloning
324///    - Filter out invalid or unnecessary headers
325///
326/// # Examples
327///
328/// Basic implementation for a custom event:
329///
330/// ```no_run
331/// use lambda_otel_lite::{SpanAttributes, SpanAttributesExtractor};
332/// use std::collections::HashMap;
333/// use opentelemetry::Value;
334///
335/// struct CustomEvent {
336///     operation: String,
337///     trace_parent: Option<String>,
338/// }
339///
340/// impl SpanAttributesExtractor for CustomEvent {
341///     fn extract_span_attributes(&self) -> SpanAttributes {
342///         let mut attributes = HashMap::new();
343///         attributes.insert("operation".to_string(), Value::String(self.operation.clone().into()));
344///
345///         // Add trace context if available
346///         let mut carrier = HashMap::new();
347///         if let Some(header) = &self.trace_parent {
348///             carrier.insert("traceparent".to_string(), header.clone());
349///         }
350///         // You can also handle X-Ray headers
351///         // if let Some(xray_header) = &self.xray_trace_id {
352///         //    carrier.insert("x-amzn-trace-id".to_string(), xray_header.clone());
353///         // }
354///
355///         SpanAttributes::builder()
356///             .attributes(attributes)
357///             .carrier(carrier)
358///             .build()
359///     }
360/// }
361/// ```
362pub trait SpanAttributesExtractor {
363    /// Extract span attributes from this type.
364    ///
365    /// This method should extract any relevant information from the implementing type
366    /// that should be included in the OpenTelemetry span. This includes:
367    /// - Custom attributes describing the event
368    /// - Span kind if different from SERVER
369    /// - Headers for context propagation
370    /// - Links to related traces
371    ///
372    /// # Returns
373    ///
374    /// Returns a `SpanAttributes` instance containing all extracted information.
375    /// If extraction fails in any way, it should return a default instance rather
376    /// than failing.
377    fn extract_span_attributes(&self) -> SpanAttributes;
378}
379
380/// Implementation for API Gateway V2 HTTP API events.
381///
382/// Extracts standard HTTP attributes following OpenTelemetry semantic conventions:
383/// - `http.request.method`: The HTTP method
384/// - `url.path`: The request path (from raw_path)
385/// - `url.query`: The query string if present (from raw_query_string)
386/// - `url.scheme`: The protocol scheme (always "https" for API Gateway)
387/// - `network.protocol.version`: The HTTP protocol version
388/// - `http.route`: The API Gateway route key (e.g. "$default" or "GET /users/{id}")
389/// - `client.address`: The client's IP address (from source_ip)
390/// - `user_agent.original`: The user agent header
391/// - `server.address`: The domain name
392///
393/// Also extracts W3C Trace Context headers and AWS X-Ray headers for distributed tracing.
394impl SpanAttributesExtractor for ApiGatewayV2httpRequest {
395    fn extract_span_attributes(&self) -> SpanAttributes {
396        let mut attributes = HashMap::new();
397        let method = self.request_context.http.method.to_string();
398        let path = self.raw_path.as_deref().unwrap_or("/");
399
400        // Add HTTP attributes following OTel semantic conventions
401        attributes.insert(
402            "http.request.method".to_string(),
403            Value::String(method.clone().into()),
404        );
405
406        // Use raw_path directly for url.path
407        if let Some(raw_path) = &self.raw_path {
408            attributes.insert(
409                "url.path".to_string(),
410                Value::String(raw_path.to_string().into()),
411            );
412        }
413
414        // Use raw_query_string directly for url.query
415        if let Some(query) = &self.raw_query_string {
416            if !query.is_empty() {
417                attributes.insert(
418                    "url.query".to_string(),
419                    Value::String(query.to_string().into()),
420                );
421            }
422        }
423
424        if let Some(protocol) = &self.request_context.http.protocol {
425            let protocol_lower = protocol.to_lowercase();
426            if protocol_lower.starts_with("http/") {
427                attributes.insert(
428                    "network.protocol.version".to_string(),
429                    Value::String(
430                        protocol_lower
431                            .trim_start_matches("http/")
432                            .to_string()
433                            .into(),
434                    ),
435                );
436            }
437            attributes.insert(
438                "url.scheme".to_string(),
439                Value::String("https".to_string().into()),
440            ); // API Gateway is always HTTPS
441        }
442
443        // Add route key as http.route
444        if let Some(route_key) = &self.route_key {
445            attributes.insert(
446                "http.route".to_string(),
447                Value::String(route_key.to_string().into()),
448            );
449        }
450
451        // Extract headers for context propagation
452        let carrier = self
453            .headers
454            .iter()
455            .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
456            .collect();
457
458        // Add source IP and user agent
459        if let Some(source_ip) = &self.request_context.http.source_ip {
460            attributes.insert(
461                "client.address".to_string(),
462                Value::String(source_ip.to_string().into()),
463            );
464        }
465        if let Some(user_agent) = self.headers.get("user-agent").and_then(|h| h.to_str().ok()) {
466            attributes.insert(
467                "user_agent.original".to_string(),
468                Value::String(user_agent.to_string().into()),
469            );
470        }
471
472        // Add domain name if available
473        if let Some(domain_name) = &self.request_context.domain_name {
474            attributes.insert(
475                "server.address".to_string(),
476                Value::String(domain_name.to_string().into()),
477            );
478        }
479
480        SpanAttributes::builder()
481            .attributes(attributes)
482            .carrier(carrier)
483            .span_name(format!("{method} {path}"))
484            .trigger(TriggerType::Http.to_string())
485            .build()
486    }
487}
488
489/// Implementation for API Gateway V1 REST API events.
490///
491/// Extracts standard HTTP attributes following OpenTelemetry semantic conventions:
492/// - `http.request.method`: The HTTP method
493/// - `url.path`: The request path
494/// - `url.query`: The query string (constructed from multi_value_query_string_parameters)
495/// - `url.scheme`: The protocol scheme (always "https" for API Gateway)
496/// - `network.protocol.version`: The HTTP protocol version
497/// - `http.route`: The API Gateway resource path
498/// - `client.address`: The client's IP address (from identity.source_ip)
499/// - `user_agent.original`: The user agent header
500/// - `server.address`: The domain name
501///
502/// Also extracts W3C Trace Context headers and AWS X-Ray headers for distributed tracing.
503impl SpanAttributesExtractor for ApiGatewayProxyRequest {
504    fn extract_span_attributes(&self) -> SpanAttributes {
505        let mut attributes = HashMap::new();
506        let method = self.http_method.to_string();
507        let route = self.resource.as_deref().unwrap_or("/");
508
509        // Add HTTP attributes following OTel semantic conventions
510        attributes.insert(
511            "http.request.method".to_string(),
512            Value::String(method.clone().into()),
513        );
514
515        // Use path directly
516        if let Some(path) = &self.path {
517            attributes.insert(
518                "url.path".to_string(),
519                Value::String(path.to_string().into()),
520            );
521        }
522
523        // Use multi_value_query_string_parameters and format query string manually
524        if !self.multi_value_query_string_parameters.is_empty() {
525            let mut query_parts = Vec::new();
526            for key in self
527                .multi_value_query_string_parameters
528                .iter()
529                .map(|(k, _)| k)
530            {
531                if let Some(values) = self.multi_value_query_string_parameters.all(key) {
532                    for value in values {
533                        query_parts.push(format!(
534                            "{}={}",
535                            urlencoding::encode(key),
536                            urlencoding::encode(value)
537                        ));
538                    }
539                }
540            }
541            if !query_parts.is_empty() {
542                let query = query_parts.join("&");
543                attributes.insert("url.query".to_string(), Value::String(query.into()));
544            }
545        }
546
547        if let Some(protocol) = &self.request_context.protocol {
548            let protocol_lower = protocol.to_lowercase();
549            if protocol_lower.starts_with("http/") {
550                attributes.insert(
551                    "network.protocol.version".to_string(),
552                    Value::String(
553                        protocol_lower
554                            .trim_start_matches("http/")
555                            .to_string()
556                            .into(),
557                    ),
558                );
559            }
560            attributes.insert(
561                "url.scheme".to_string(),
562                Value::String("https".to_string().into()),
563            ); // API Gateway is always HTTPS
564        }
565
566        // Add route
567        attributes.insert(
568            "http.route".to_string(),
569            Value::String(route.to_string().into()),
570        );
571
572        // Extract headers for context propagation
573        let carrier = self
574            .headers
575            .iter()
576            .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
577            .collect();
578
579        // Add source IP and user agent
580        if let Some(source_ip) = &self.request_context.identity.source_ip {
581            attributes.insert(
582                "client.address".to_string(),
583                Value::String(source_ip.to_string().into()),
584            );
585        }
586        if let Some(user_agent) = self.headers.get("user-agent").and_then(|h| h.to_str().ok()) {
587            attributes.insert(
588                "user_agent.original".to_string(),
589                Value::String(user_agent.to_string().into()),
590            );
591        }
592
593        // Add domain name if available
594        if let Some(domain_name) = &self.request_context.domain_name {
595            attributes.insert(
596                "server.address".to_string(),
597                Value::String(domain_name.to_string().into()),
598            );
599        }
600
601        SpanAttributes::builder()
602            .attributes(attributes)
603            .carrier(carrier)
604            .span_name(format!("{method} {route}"))
605            .trigger(TriggerType::Http.to_string())
606            .build()
607    }
608}
609
610/// Implementation for Application Load Balancer target group events.
611///
612/// Extracts standard HTTP attributes following OpenTelemetry semantic conventions:
613/// - `http.request.method`: The HTTP method
614/// - `url.path`: The request path
615/// - `url.query`: The query string (constructed from multi_value_query_string_parameters)
616/// - `url.scheme`: The protocol scheme (defaults to "http")
617/// - `network.protocol.version`: The HTTP protocol version (always "1.1" for ALB)
618/// - `http.route`: The request path
619/// - `client.address`: The client's IP address (from x-forwarded-for header)
620/// - `user_agent.original`: The user agent header
621/// - `server.address`: The host header
622/// - `alb.target_group_arn`: The ARN of the target group
623///
624/// Also extracts W3C Trace Context headers and AWS X-Ray headers for distributed tracing.
625impl SpanAttributesExtractor for AlbTargetGroupRequest {
626    fn extract_span_attributes(&self) -> SpanAttributes {
627        let mut attributes = HashMap::new();
628        let method = self.http_method.to_string();
629        let route = self.path.as_deref().unwrap_or("/");
630
631        // Add HTTP attributes following OTel semantic conventions
632        attributes.insert(
633            "http.request.method".to_string(),
634            Value::String(method.clone().into()),
635        );
636
637        // Use path directly
638        if let Some(path) = &self.path {
639            attributes.insert(
640                "url.path".to_string(),
641                Value::String(path.to_string().into()),
642            );
643        }
644
645        // Use multi_value_query_string_parameters and format query string manually
646        if !self.multi_value_query_string_parameters.is_empty() {
647            let mut query_parts = Vec::new();
648            for key in self
649                .multi_value_query_string_parameters
650                .iter()
651                .map(|(k, _)| k)
652            {
653                if let Some(values) = self.multi_value_query_string_parameters.all(key) {
654                    for value in values {
655                        query_parts.push(format!(
656                            "{}={}",
657                            urlencoding::encode(key),
658                            urlencoding::encode(value)
659                        ));
660                    }
661                }
662            }
663            if !query_parts.is_empty() {
664                let query = query_parts.join("&");
665                attributes.insert("url.query".to_string(), Value::String(query.into()));
666            }
667        }
668
669        // ALB can be HTTP or HTTPS, default to HTTP if not specified
670        attributes.insert(
671            "url.scheme".to_string(),
672            Value::String("http".to_string().into()),
673        );
674        attributes.insert(
675            "network.protocol.version".to_string(),
676            Value::String("1.1".to_string().into()),
677        ); // ALB uses HTTP/1.1
678
679        // Add ALB specific attributes
680        if let Some(target_group_arn) = &self.request_context.elb.target_group_arn {
681            attributes.insert(
682                "alb.target_group_arn".to_string(),
683                Value::String(target_group_arn.to_string().into()),
684            );
685        }
686
687        // Extract headers for context propagation
688        let carrier = self
689            .headers
690            .iter()
691            .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_string())))
692            .collect();
693
694        // Add source IP and user agent
695        if let Some(source_ip) = &self
696            .headers
697            .get("x-forwarded-for")
698            .and_then(|h| h.to_str().ok())
699        {
700            if let Some(client_ip) = source_ip.split(',').next() {
701                attributes.insert(
702                    "client.address".to_string(),
703                    Value::String(client_ip.trim().to_string().into()),
704                );
705            }
706        }
707        if let Some(user_agent) = self.headers.get("user-agent").and_then(|h| h.to_str().ok()) {
708            attributes.insert(
709                "user_agent.original".to_string(),
710                Value::String(user_agent.to_string().into()),
711            );
712        }
713
714        // Add domain name if available
715        if let Some(host) = self.headers.get("host").and_then(|h| h.to_str().ok()) {
716            attributes.insert(
717                "server.address".to_string(),
718                Value::String(host.to_string().into()),
719            );
720        }
721
722        SpanAttributes::builder()
723            .attributes(attributes)
724            .carrier(carrier)
725            .span_name(format!("{method} {route}"))
726            .trigger(TriggerType::Http.to_string())
727            .build()
728    }
729}
730
731/// Default implementation for serde_json::Value.
732///
733/// This implementation provides a fallback for when the event type is not known
734/// or when working with raw JSON data. It returns default attributes with
735/// the trigger type set to "other".
736/// If there's a headers field, it will be used to populate the carrier.
737impl SpanAttributesExtractor for serde_json::Value {
738    fn extract_span_attributes(&self) -> SpanAttributes {
739        let carrier = self
740            .get("headers")
741            .and_then(|headers| headers.as_object())
742            .map(|obj| {
743                obj.iter()
744                    .filter_map(|(k, v)| v.as_str().map(|v| (k.to_string(), v.to_string())))
745                    .collect()
746            })
747            .unwrap_or_default();
748
749        SpanAttributes::builder()
750            .carrier(carrier)
751            .trigger(TriggerType::Other.to_string())
752            .build()
753    }
754}
755
756#[cfg(test)]
757mod tests {
758    use super::*;
759    use aws_lambda_events::http::Method;
760
761    #[test]
762    fn test_trigger_types() {
763        let attrs = SpanAttributes::default();
764        assert_eq!(attrs.trigger, TriggerType::Other.to_string());
765
766        let attrs = SpanAttributes::builder()
767            .trigger(TriggerType::Http.to_string())
768            .build();
769        assert_eq!(attrs.trigger, TriggerType::Http.to_string());
770
771        let attrs = SpanAttributes::builder().build();
772        assert_eq!(attrs.trigger, TriggerType::Other.to_string());
773    }
774
775    #[test]
776    fn test_apigw_v2_extraction() {
777        let request = ApiGatewayV2httpRequest {
778            raw_path: Some("/test".to_string()),
779            route_key: Some("GET /test".to_string()),
780            headers: aws_lambda_events::http::HeaderMap::new(),
781            request_context: aws_lambda_events::apigw::ApiGatewayV2httpRequestContext {
782                http: aws_lambda_events::apigw::ApiGatewayV2httpRequestContextHttpDescription {
783                    method: Method::GET,
784                    path: Some("/test".to_string()),
785                    protocol: Some("HTTP/1.1".to_string()),
786                    ..Default::default()
787                },
788                ..Default::default()
789            },
790            ..Default::default()
791        };
792
793        let attrs = request.extract_span_attributes();
794
795        assert_eq!(
796            attrs.attributes.get("http.request.method"),
797            Some(&Value::String("GET".to_string().into()))
798        );
799        assert_eq!(
800            attrs.attributes.get("url.path"),
801            Some(&Value::String("/test".to_string().into()))
802        );
803        assert_eq!(
804            attrs.attributes.get("http.route"),
805            Some(&Value::String("GET /test".to_string().into()))
806        );
807        assert_eq!(
808            attrs.attributes.get("url.scheme"),
809            Some(&Value::String("https".to_string().into()))
810        );
811        assert_eq!(
812            attrs.attributes.get("network.protocol.version"),
813            Some(&Value::String("1.1".to_string().into()))
814        );
815    }
816
817    #[test]
818    fn test_apigw_v1_extraction() {
819        let request = ApiGatewayProxyRequest {
820            path: Some("/test".to_string()),
821            http_method: Method::GET,
822            resource: Some("/test".to_string()),
823            headers: aws_lambda_events::http::HeaderMap::new(),
824            request_context: aws_lambda_events::apigw::ApiGatewayProxyRequestContext {
825                protocol: Some("HTTP/1.1".to_string()),
826                ..Default::default()
827            },
828            ..Default::default()
829        };
830
831        let attrs = request.extract_span_attributes();
832
833        assert_eq!(
834            attrs.attributes.get("http.request.method"),
835            Some(&Value::String("GET".to_string().into()))
836        );
837        assert_eq!(
838            attrs.attributes.get("url.path"),
839            Some(&Value::String("/test".to_string().into()))
840        );
841        assert_eq!(
842            attrs.attributes.get("http.route"),
843            Some(&Value::String("/test".to_string().into()))
844        );
845        assert_eq!(
846            attrs.attributes.get("url.scheme"),
847            Some(&Value::String("https".to_string().into()))
848        );
849        assert_eq!(
850            attrs.attributes.get("network.protocol.version"),
851            Some(&Value::String("1.1".to_string().into()))
852        );
853    }
854
855    #[test]
856    fn test_alb_extraction() {
857        let request = AlbTargetGroupRequest {
858            path: Some("/test".to_string()),
859            http_method: Method::GET,
860            headers: aws_lambda_events::http::HeaderMap::new(),
861            request_context: aws_lambda_events::alb::AlbTargetGroupRequestContext {
862                elb: aws_lambda_events::alb::ElbContext {
863                    target_group_arn: Some("arn:aws:elasticloadbalancing:...".to_string()),
864                },
865            },
866            ..Default::default()
867        };
868
869        let attrs = request.extract_span_attributes();
870
871        assert_eq!(
872            attrs.attributes.get("http.request.method"),
873            Some(&Value::String("GET".to_string().into()))
874        );
875        assert_eq!(
876            attrs.attributes.get("url.path"),
877            Some(&Value::String("/test".to_string().into()))
878        );
879        assert_eq!(
880            attrs.attributes.get("url.scheme"),
881            Some(&Value::String("http".to_string().into()))
882        );
883        assert_eq!(
884            attrs.attributes.get("network.protocol.version"),
885            Some(&Value::String("1.1".to_string().into()))
886        );
887        assert_eq!(
888            attrs.attributes.get("alb.target_group_arn"),
889            Some(&Value::String(
890                "arn:aws:elasticloadbalancing:...".to_string().into()
891            ))
892        );
893    }
894
895    #[test]
896    fn test_xray_header_extraction() {
897        // Create API Gateway request with X-Ray header
898        let mut headers = aws_lambda_events::http::HeaderMap::new();
899        let xray_header =
900            "Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1";
901        headers.insert(
902            "x-amzn-trace-id",
903            aws_lambda_events::http::HeaderValue::from_str(xray_header).unwrap(),
904        );
905
906        // Also include a W3C traceparent header
907        let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
908        headers.insert(
909            "traceparent",
910            aws_lambda_events::http::HeaderValue::from_str(traceparent).unwrap(),
911        );
912
913        // Create API Gateway V2 request
914        let request = ApiGatewayV2httpRequest {
915            headers,
916            raw_path: Some("/test".to_string()),
917            route_key: Some("GET /test".to_string()),
918            request_context: aws_lambda_events::apigw::ApiGatewayV2httpRequestContext {
919                http: aws_lambda_events::apigw::ApiGatewayV2httpRequestContextHttpDescription {
920                    method: Method::GET,
921                    path: Some("/test".to_string()),
922                    protocol: Some("HTTP/1.1".to_string()),
923                    ..Default::default()
924                },
925                ..Default::default()
926            },
927            ..Default::default()
928        };
929
930        // Extract attributes
931        let attrs = request.extract_span_attributes();
932
933        // Verify carrier contains both headers
934        assert!(attrs.carrier.is_some());
935        let carrier = attrs.carrier.unwrap();
936
937        // X-Ray header should be present and unaltered
938        assert!(carrier.contains_key("x-amzn-trace-id"));
939        assert_eq!(carrier.get("x-amzn-trace-id").unwrap(), xray_header);
940
941        // W3C header should also be present
942        assert!(carrier.contains_key("traceparent"));
943        assert_eq!(carrier.get("traceparent").unwrap(), traceparent);
944    }
945
946    #[test]
947    fn test_json_extractor_with_xray_headers() {
948        // Create a JSON value with headers including X-Ray
949        let json_value = serde_json::json!({
950            "headers": {
951                "x-amzn-trace-id": "Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1",
952                "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
953                "content-type": "application/json"
954            },
955            "body": "{\"message\":\"Hello World\"}",
956            "requestContext": {
957                "requestId": "12345"
958            }
959        });
960
961        // Extract attributes
962        let attrs = json_value.extract_span_attributes();
963
964        // Verify carrier contains the headers
965        assert!(attrs.carrier.is_some());
966        let carrier = attrs.carrier.unwrap();
967
968        // Both trace headers should be present
969        assert!(carrier.contains_key("x-amzn-trace-id"));
970        assert_eq!(
971            carrier.get("x-amzn-trace-id").unwrap(),
972            "Root=1-58406520-a006649127e371903a2de979;Parent=4c721bf33e3caf8f;Sampled=1"
973        );
974
975        assert!(carrier.contains_key("traceparent"));
976        assert_eq!(
977            carrier.get("traceparent").unwrap(),
978            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
979        );
980    }
981}