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}