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}