lambda_otel_lite/
resource.rs

1//! Resource attribute management for Lambda functions.
2//!
3//! This module provides functionality for managing OpenTelemetry resource attributes
4//! in a Lambda environment. It automatically detects and sets standard Lambda attributes
5//! and allows for custom attribute configuration through environment variables.
6//!
7//! # Automatic FAAS Attributes
8//!
9//! The module automatically sets relevant FAAS attributes based on the Lambda context:
10//!
11//! ## Resource Attributes
12//! - `cloud.provider`: Set to "aws"
13//! - `cloud.region`: From AWS_REGION
14//! - `faas.name`: From AWS_LAMBDA_FUNCTION_NAME
15//! - `faas.version`: From AWS_LAMBDA_FUNCTION_VERSION
16//! - `faas.instance`: From AWS_LAMBDA_LOG_STREAM_NAME
17//! - `faas.max_memory`: From AWS_LAMBDA_FUNCTION_MEMORY_SIZE
18//! - `service.name`: From OTEL_SERVICE_NAME or function name
19//!
20//! # Configuration
21//!
22//! ## Custom Attributes
23//!
24//! Additional attributes can be set via the `OTEL_RESOURCE_ATTRIBUTES` environment variable
25//! in the format: `key=value,key2=value2`. Values can be URL-encoded if they contain
26//! special characters:
27//!
28//! ```bash
29//! # Setting custom attributes
30//! OTEL_RESOURCE_ATTRIBUTES="deployment.stage=prod,custom.tag=value%20with%20spaces"
31//! ```
32//!
33//! ## Service Name
34//!
35//! The service name can be configured in two ways:
36//!
37//! 1. Using `OTEL_SERVICE_NAME` environment variable:
38//! ```bash
39//! OTEL_SERVICE_NAME="my-custom-service"
40//! ```
41//!
42//! 2. Through the [`TelemetryConfig`](crate::TelemetryConfig) builder:
43//! ```no_run
44//! use lambda_otel_lite::{TelemetryConfig, init_telemetry};
45//! use opentelemetry::KeyValue;
46//! use opentelemetry_sdk::Resource;
47//!
48//! # async fn example() -> Result<(), lambda_runtime::Error> {
49//! let resource = Resource::builder()
50//!     .with_attributes(vec![
51//!         KeyValue::new("service.name", "my-service"),
52//!         KeyValue::new("service.version", "1.0.0"),
53//!     ])
54//!     .build();
55//!
56//! let config = TelemetryConfig::builder()
57//!     .resource(resource)
58//!     .build();
59//!
60//! let _completion_handler = init_telemetry(config).await?;
61//! # Ok(())
62//! # }
63//! ```
64//!
65//! # Integration
66//!
67//! This module is primarily used by the [`init_telemetry`](crate::init_telemetry) function
68//! to configure the OpenTelemetry tracer provider. The detected resource attributes are
69//! automatically attached to all spans created by the tracer.
70//!
71//! See the [`telemetry`](crate::telemetry) module for more details on initialization
72//! and configuration options.
73
74use crate::constants::defaults;
75use crate::constants::{env_vars, resource_attributes};
76use opentelemetry::KeyValue;
77use opentelemetry_sdk::Resource;
78use std::env;
79
80/// Get default Lambda resource attributes.
81///
82/// This function automatically detects and sets standard Lambda attributes from environment
83/// variables and allows for custom attribute configuration through `OTEL_RESOURCE_ATTRIBUTES`.
84///
85/// # Environment Variables
86///
87/// - `AWS_REGION`: Sets `cloud.region`
88/// - `AWS_LAMBDA_FUNCTION_NAME`: Sets `faas.name` and default `service.name`
89/// - `AWS_LAMBDA_FUNCTION_VERSION`: Sets `faas.version`
90/// - `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`: Sets `faas.max_memory`
91/// - `AWS_LAMBDA_LOG_STREAM_NAME`: Sets `faas.instance`
92/// - `OTEL_SERVICE_NAME`: Overrides default service name
93/// - `OTEL_RESOURCE_ATTRIBUTES`: Additional attributes in key=value format
94///
95/// # Configuration Attributes
96///
97/// The following configuration attributes are set in the resource **only when**
98/// the corresponding environment variables are explicitly set:
99///
100/// - `LAMBDA_EXTENSION_SPAN_PROCESSOR_MODE`: Sets `lambda_otel_lite.extension.span_processor_mode`
101/// - `LAMBDA_SPAN_PROCESSOR_QUEUE_SIZE`: Sets `lambda_otel_lite.lambda_span_processor.queue_size`
102/// - `LAMBDA_SPAN_PROCESSOR_BATCH_SIZE`: Sets `lambda_otel_lite.lambda_span_processor.batch_size`
103/// - `OTLP_STDOUT_SPAN_EXPORTER_COMPRESSION_LEVEL`: Sets `lambda_otel_lite.otlp_stdout_span_exporter.compression_level`
104///
105/// # Returns
106///
107/// Returns a [`Resource`] containing all detected and configured attributes.
108///
109/// # Examples
110///
111/// Basic usage with environment variables:
112///
113/// ```no_run
114/// use lambda_otel_lite::resource::get_lambda_resource;
115/// use opentelemetry::KeyValue;
116///
117/// // Get resource with Lambda environment attributes
118/// let resource = get_lambda_resource();
119/// ```
120///
121/// Adding custom attributes:
122///
123/// ```no_run
124/// use lambda_otel_lite::resource::get_lambda_resource;
125/// use opentelemetry::KeyValue;
126/// use opentelemetry_sdk::Resource;
127///
128/// // Get Lambda resource
129/// let lambda_resource = get_lambda_resource();
130///
131/// // Create custom resource
132/// let extra_resource = Resource::builder()
133///     .with_attributes(vec![
134///         KeyValue::new("deployment.stage", "prod"),
135///         KeyValue::new("team", "backend"),
136///     ])
137///     .build();
138///
139/// // Combine resources (custom attributes take precedence)
140/// // Create a new resource with all attributes
141/// let mut all_attributes = vec![
142///     KeyValue::new("deployment.stage", "prod"),
143///     KeyValue::new("team", "backend"),
144/// ];
145///
146/// // Add lambda attributes (could be done more programmatically in real code)
147/// all_attributes.push(KeyValue::new("cloud.provider", "aws"));
148/// all_attributes.push(KeyValue::new("faas.name", "my-function"));
149///
150/// let final_resource = Resource::builder()
151///     .with_attributes(all_attributes)
152///     .build();
153/// ```
154///
155/// # Integration with Telemetry Config
156///
157/// This function is automatically called by [`init_telemetry`](crate::init_telemetry)
158/// when no custom resource is provided. To override or extend these attributes, use
159/// the [`TelemetryConfig`](crate::TelemetryConfig) builder:
160///
161/// ```no_run
162/// use lambda_otel_lite::{TelemetryConfig, init_telemetry};
163/// use opentelemetry_sdk::Resource;
164///
165/// # async fn example() -> Result<(), lambda_runtime::Error> {
166/// // Get base Lambda resource
167/// let base_resource = lambda_otel_lite::get_lambda_resource();
168///
169/// // Configure telemetry with the resource
170/// let config = TelemetryConfig::builder()
171///     .resource(base_resource)
172///     .build();
173///
174/// let _completion_handler = init_telemetry(config).await?;
175/// # Ok(())
176/// # }
177/// ```
178pub fn get_lambda_resource() -> Resource {
179    let mut attributes = Vec::new();
180
181    // Add standard Lambda attributes
182    if let Ok(region) = env::var("AWS_REGION") {
183        attributes.push(KeyValue::new("cloud.provider", "aws"));
184        attributes.push(KeyValue::new("cloud.region", region));
185    }
186
187    if let Ok(function_name) = env::var(env_vars::AWS_LAMBDA_FUNCTION_NAME) {
188        attributes.push(KeyValue::new("faas.name", function_name.clone()));
189    }
190
191    if let Ok(version) = env::var("AWS_LAMBDA_FUNCTION_VERSION") {
192        attributes.push(KeyValue::new("faas.version", version));
193    }
194
195    if let Ok(memory) = env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") {
196        if let Ok(memory_mb) = memory.parse::<i64>() {
197            let memory_bytes = memory_mb * 1024 * 1024;
198            attributes.push(KeyValue::new("faas.max_memory", memory_bytes));
199        }
200    }
201
202    if let Ok(log_stream) = env::var("AWS_LAMBDA_LOG_STREAM_NAME") {
203        attributes.push(KeyValue::new("faas.instance", log_stream));
204    }
205
206    // Set service name with fallback logic:
207    // 1. Use OTEL_SERVICE_NAME if defined
208    // 2. Fall back to AWS_LAMBDA_FUNCTION_NAME if available
209    // 3. Fall back to "unknown_service" if neither is available
210    let service_name = env::var(env_vars::SERVICE_NAME)
211        .or_else(|_| env::var(env_vars::AWS_LAMBDA_FUNCTION_NAME))
212        .unwrap_or_else(|_| defaults::SERVICE_NAME.to_string());
213
214    attributes.push(KeyValue::new("service.name", service_name));
215
216    // Add configuration attributes only when environment variables are explicitly set
217    if let Ok(mode) = env::var(env_vars::PROCESSOR_MODE) {
218        attributes.push(KeyValue::new(resource_attributes::PROCESSOR_MODE, mode));
219    }
220
221    if let Ok(queue_size) = env::var(env_vars::QUEUE_SIZE) {
222        if let Ok(size) = queue_size.parse::<i64>() {
223            attributes.push(KeyValue::new(resource_attributes::QUEUE_SIZE, size));
224        }
225    }
226
227    if let Ok(batch_size) = env::var(env_vars::BATCH_SIZE) {
228        if let Ok(size) = batch_size.parse::<i64>() {
229            attributes.push(KeyValue::new(resource_attributes::BATCH_SIZE, size));
230        }
231    }
232
233    if let Ok(compression_level) = env::var(env_vars::COMPRESSION_LEVEL) {
234        if let Ok(level) = compression_level.parse::<i64>() {
235            attributes.push(KeyValue::new(resource_attributes::COMPRESSION_LEVEL, level));
236        }
237    }
238
239    // create resource with standard attributes and merge with custom attributes
240    Resource::builder().with_attributes(attributes).build()
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use serial_test::serial;
247    use std::env;
248
249    fn cleanup_env() {
250        env::remove_var("AWS_REGION");
251        env::remove_var(env_vars::AWS_LAMBDA_FUNCTION_NAME);
252        env::remove_var("AWS_LAMBDA_FUNCTION_VERSION");
253        env::remove_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE");
254        env::remove_var("AWS_LAMBDA_LOG_STREAM_NAME");
255        env::remove_var(env_vars::SERVICE_NAME);
256        env::remove_var(env_vars::RESOURCE_ATTRIBUTES);
257        env::remove_var(env_vars::BATCH_SIZE);
258        env::remove_var(env_vars::QUEUE_SIZE);
259        env::remove_var(env_vars::PROCESSOR_MODE);
260        env::remove_var(env_vars::COMPRESSION_LEVEL);
261    }
262
263    // Helper function to find an attribute by key
264    fn find_attr<'a>(
265        attrs: &'a [(&'a str, &'a opentelemetry::Value)],
266        key: &str,
267    ) -> Option<&'a opentelemetry::Value> {
268        attrs.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
269    }
270
271    #[test]
272    #[serial]
273    fn test_get_lambda_resource_with_standard_env() {
274        cleanup_env();
275
276        // Set up test environment
277        env::set_var("AWS_REGION", "us-west-2");
278        env::set_var(env_vars::AWS_LAMBDA_FUNCTION_NAME, "test-function");
279        env::set_var("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST");
280        env::set_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128");
281        env::set_var("AWS_LAMBDA_LOG_STREAM_NAME", "2024/01/01/[$LATEST]abc123");
282
283        let resource = get_lambda_resource();
284        let schema = resource.schema_url().unwrap_or("");
285        assert!(schema.is_empty()); // Default resource has no schema URL
286
287        // Check attributes using the resource's attribute iterator
288        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
289
290        assert_eq!(
291            find_attr(&attrs, "cloud.provider"),
292            Some(&opentelemetry::Value::String("aws".into()))
293        );
294        assert_eq!(
295            find_attr(&attrs, "cloud.region"),
296            Some(&opentelemetry::Value::String("us-west-2".into()))
297        );
298        assert_eq!(
299            find_attr(&attrs, "faas.name"),
300            Some(&opentelemetry::Value::String("test-function".into()))
301        );
302        assert_eq!(
303            find_attr(&attrs, "faas.version"),
304            Some(&opentelemetry::Value::String("$LATEST".into()))
305        );
306
307        // Verify memory is converted to bytes
308        assert_eq!(
309            find_attr(&attrs, "faas.max_memory"),
310            Some(&opentelemetry::Value::I64(128 * 1024 * 1024))
311        );
312        assert_eq!(
313            find_attr(&attrs, "faas.instance"),
314            Some(&opentelemetry::Value::String(
315                "2024/01/01/[$LATEST]abc123".into()
316            ))
317        );
318
319        cleanup_env();
320    }
321
322    #[test]
323    #[serial]
324    fn test_get_lambda_resource_with_no_env() {
325        cleanup_env();
326
327        let resource = get_lambda_resource();
328        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
329
330        // No attributes should be set
331        assert!(find_attr(&attrs, "cloud.provider").is_none());
332        assert!(find_attr(&attrs, "cloud.region").is_none());
333        assert!(find_attr(&attrs, "faas.name").is_none());
334
335        cleanup_env();
336    }
337
338    #[test]
339    #[serial]
340    fn test_get_lambda_resource_with_custom_service_name() {
341        cleanup_env();
342
343        // Set up test environment
344        env::set_var("AWS_LAMBDA_FUNCTION_NAME", "test-function");
345        env::set_var("OTEL_SERVICE_NAME", "custom-service");
346
347        let resource = get_lambda_resource();
348        let attrs: Vec<_> = resource.iter().collect();
349
350        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
351            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
352        };
353
354        assert_eq!(
355            find_attr("service.name"),
356            Some(&opentelemetry::Value::String("custom-service".into()))
357        );
358        assert_eq!(
359            find_attr("faas.name"),
360            Some(&opentelemetry::Value::String("test-function".into()))
361        );
362
363        cleanup_env();
364    }
365
366    #[test]
367    #[serial]
368    fn test_get_lambda_resource_with_custom_attributes() {
369        cleanup_env();
370
371        // Set up test environment
372        env::set_var(
373            "OTEL_RESOURCE_ATTRIBUTES",
374            "custom.attr=value,deployment.stage=prod",
375        );
376
377        let resource = get_lambda_resource();
378        let attrs: Vec<_> = resource.iter().collect();
379
380        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
381            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
382        };
383
384        assert_eq!(
385            find_attr("custom.attr"),
386            Some(&opentelemetry::Value::String("value".into()))
387        );
388        assert_eq!(
389            find_attr("deployment.stage"),
390            Some(&opentelemetry::Value::String("prod".into()))
391        );
392
393        cleanup_env();
394    }
395
396    #[test]
397    #[serial]
398    fn test_get_lambda_resource_with_encoded_attributes() {
399        cleanup_env();
400
401        // Set up test environment
402        env::set_var(
403            "OTEL_RESOURCE_ATTRIBUTES",
404            "custom.attr=hello%20world,tag=value%3Dtest",
405        );
406
407        let resource = get_lambda_resource();
408        let attrs: Vec<_> = resource.iter().collect();
409
410        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
411            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
412        };
413
414        assert_eq!(
415            find_attr("custom.attr"),
416            Some(&opentelemetry::Value::String("hello%20world".into()))
417        );
418        assert_eq!(
419            find_attr("tag"),
420            Some(&opentelemetry::Value::String("value%3Dtest".into()))
421        );
422
423        cleanup_env();
424    }
425
426    #[test]
427    #[serial]
428    fn test_resource_attributes_only_set_when_env_vars_present() {
429        cleanup_env();
430
431        // Create resource with no environment variables set
432        let resource = get_lambda_resource();
433        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
434
435        // Verify that configuration attributes are not set
436        assert!(find_attr(&attrs, resource_attributes::QUEUE_SIZE).is_none());
437        assert!(find_attr(&attrs, resource_attributes::BATCH_SIZE).is_none());
438        assert!(find_attr(&attrs, resource_attributes::PROCESSOR_MODE).is_none());
439        assert!(find_attr(&attrs, resource_attributes::COMPRESSION_LEVEL).is_none());
440
441        // Set environment variables
442        env::set_var(env_vars::QUEUE_SIZE, "4096");
443        env::set_var(env_vars::BATCH_SIZE, "1024");
444        env::set_var(env_vars::PROCESSOR_MODE, "async");
445        env::set_var(env_vars::COMPRESSION_LEVEL, "9");
446
447        // Create resource with environment variables set
448        let resource_with_env = get_lambda_resource();
449        let attrs_with_env: Vec<_> = resource_with_env
450            .iter()
451            .map(|(k, v)| (k.as_str(), v))
452            .collect();
453
454        // Verify that configuration attributes are set with correct values
455        assert_eq!(
456            find_attr(&attrs_with_env, resource_attributes::QUEUE_SIZE),
457            Some(&opentelemetry::Value::I64(4096))
458        );
459        assert_eq!(
460            find_attr(&attrs_with_env, resource_attributes::BATCH_SIZE),
461            Some(&opentelemetry::Value::I64(1024))
462        );
463        assert_eq!(
464            find_attr(&attrs_with_env, resource_attributes::PROCESSOR_MODE),
465            Some(&opentelemetry::Value::String("async".into()))
466        );
467        assert_eq!(
468            find_attr(&attrs_with_env, resource_attributes::COMPRESSION_LEVEL),
469            Some(&opentelemetry::Value::I64(9))
470        );
471
472        cleanup_env();
473    }
474
475    #[test]
476    #[serial]
477    fn test_resource_attributes_not_set_with_invalid_env_vars() {
478        cleanup_env();
479
480        // Set invalid environment variables
481        env::set_var(env_vars::QUEUE_SIZE, "not_a_number");
482        env::set_var(env_vars::BATCH_SIZE, "invalid");
483        env::set_var(env_vars::COMPRESSION_LEVEL, "high");
484
485        // Create resource with invalid environment variables
486        let resource = get_lambda_resource();
487        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
488
489        // Verify that configuration attributes with invalid values are not set
490        assert!(find_attr(&attrs, resource_attributes::QUEUE_SIZE).is_none());
491        assert!(find_attr(&attrs, resource_attributes::BATCH_SIZE).is_none());
492        assert!(find_attr(&attrs, resource_attributes::COMPRESSION_LEVEL).is_none());
493
494        // But the mode attribute should be set since it's a string
495        env::set_var(env_vars::PROCESSOR_MODE, "custom_mode");
496        let resource_with_mode = get_lambda_resource();
497        let attrs_with_mode: Vec<_> = resource_with_mode
498            .iter()
499            .map(|(k, v)| (k.as_str(), v))
500            .collect();
501
502        assert_eq!(
503            find_attr(&attrs_with_mode, resource_attributes::PROCESSOR_MODE),
504            Some(&opentelemetry::Value::String("custom_mode".into()))
505        );
506
507        cleanup_env();
508    }
509}