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}