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