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/// - `OTLP_STDOUT_SPAN_EXPORTER_COMPRESSION_LEVEL`: Sets `lambda_otel_lite.otlp_stdout_span_exporter.compression_level`
103///
104/// # Returns
105///
106/// Returns a [`Resource`] containing all detected and configured attributes.
107///
108/// # Examples
109///
110/// Basic usage with environment variables:
111///
112/// ```no_run
113/// use lambda_otel_lite::resource::get_lambda_resource;
114/// use opentelemetry::KeyValue;
115///
116/// // Get resource with Lambda environment attributes
117/// let resource = get_lambda_resource();
118/// ```
119///
120/// Adding custom attributes:
121///
122/// ```no_run
123/// use lambda_otel_lite::resource::get_lambda_resource;
124/// use opentelemetry::KeyValue;
125/// use opentelemetry_sdk::Resource;
126///
127/// // Get Lambda resource
128/// let lambda_resource = get_lambda_resource();
129///
130/// // Create custom resource
131/// let extra_resource = Resource::builder()
132///     .with_attributes(vec![
133///         KeyValue::new("deployment.stage", "prod"),
134///         KeyValue::new("team", "backend"),
135///     ])
136///     .build();
137///
138/// // Combine resources (custom attributes take precedence)
139/// // Create a new resource with all attributes
140/// let mut all_attributes = vec![
141///     KeyValue::new("deployment.stage", "prod"),
142///     KeyValue::new("team", "backend"),
143/// ];
144///
145/// // Add lambda attributes (could be done more programmatically in real code)
146/// all_attributes.push(KeyValue::new("cloud.provider", "aws"));
147/// all_attributes.push(KeyValue::new("faas.name", "my-function"));
148///
149/// let final_resource = Resource::builder()
150///     .with_attributes(all_attributes)
151///     .build();
152/// ```
153///
154/// # Integration with Telemetry Config
155///
156/// This function is automatically called by [`init_telemetry`](crate::init_telemetry)
157/// when no custom resource is provided. To override or extend these attributes, use
158/// the [`TelemetryConfig`](crate::TelemetryConfig) builder:
159///
160/// ```no_run
161/// use lambda_otel_lite::{TelemetryConfig, init_telemetry};
162/// use opentelemetry_sdk::Resource;
163///
164/// # async fn example() -> Result<(), lambda_runtime::Error> {
165/// // Get base Lambda resource
166/// let base_resource = lambda_otel_lite::get_lambda_resource();
167///
168/// // Configure telemetry with the resource
169/// let config = TelemetryConfig::builder()
170///     .resource(base_resource)
171///     .build();
172///
173/// let _completion_handler = init_telemetry(config).await?;
174/// # Ok(())
175/// # }
176/// ```
177pub fn get_lambda_resource() -> Resource {
178    let mut attributes = Vec::new();
179
180    // Add standard Lambda attributes
181    if let Ok(region) = env::var("AWS_REGION") {
182        attributes.push(KeyValue::new("cloud.provider", "aws"));
183        attributes.push(KeyValue::new("cloud.region", region));
184    }
185
186    if let Ok(function_name) = env::var(env_vars::AWS_LAMBDA_FUNCTION_NAME) {
187        attributes.push(KeyValue::new("faas.name", function_name.clone()));
188    }
189
190    if let Ok(version) = env::var("AWS_LAMBDA_FUNCTION_VERSION") {
191        attributes.push(KeyValue::new("faas.version", version));
192    }
193
194    if let Ok(memory) = env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") {
195        if let Ok(memory_mb) = memory.parse::<i64>() {
196            let memory_bytes = memory_mb * 1024 * 1024;
197            attributes.push(KeyValue::new("faas.max_memory", memory_bytes));
198        }
199    }
200
201    if let Ok(log_stream) = env::var("AWS_LAMBDA_LOG_STREAM_NAME") {
202        attributes.push(KeyValue::new("faas.instance", log_stream));
203    }
204
205    // Set service name with fallback logic:
206    // 1. Use OTEL_SERVICE_NAME if defined
207    // 2. Fall back to AWS_LAMBDA_FUNCTION_NAME if available
208    // 3. Fall back to "unknown_service" if neither is available
209    let service_name = env::var(env_vars::SERVICE_NAME)
210        .or_else(|_| env::var(env_vars::AWS_LAMBDA_FUNCTION_NAME))
211        .unwrap_or_else(|_| defaults::SERVICE_NAME.to_string());
212
213    attributes.push(KeyValue::new("service.name", service_name));
214
215    // Add configuration attributes only when environment variables are explicitly set
216    if let Ok(mode) = env::var(env_vars::PROCESSOR_MODE) {
217        attributes.push(KeyValue::new(resource_attributes::PROCESSOR_MODE, mode));
218    }
219
220    if let Ok(queue_size) = env::var(env_vars::QUEUE_SIZE) {
221        if let Ok(size) = queue_size.parse::<i64>() {
222            attributes.push(KeyValue::new(resource_attributes::QUEUE_SIZE, size));
223        }
224    }
225
226    if let Ok(compression_level) = env::var(env_vars::COMPRESSION_LEVEL) {
227        if let Ok(level) = compression_level.parse::<i64>() {
228            attributes.push(KeyValue::new(resource_attributes::COMPRESSION_LEVEL, level));
229        }
230    }
231
232    // create resource with standard attributes and merge with custom attributes
233    Resource::builder().with_attributes(attributes).build()
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use serial_test::serial;
240    use std::env;
241
242    fn cleanup_env() {
243        env::remove_var("AWS_REGION");
244        env::remove_var(env_vars::AWS_LAMBDA_FUNCTION_NAME);
245        env::remove_var("AWS_LAMBDA_FUNCTION_VERSION");
246        env::remove_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE");
247        env::remove_var("AWS_LAMBDA_LOG_STREAM_NAME");
248        env::remove_var(env_vars::SERVICE_NAME);
249        env::remove_var(env_vars::RESOURCE_ATTRIBUTES);
250        env::remove_var(env_vars::QUEUE_SIZE);
251        env::remove_var(env_vars::PROCESSOR_MODE);
252        env::remove_var(env_vars::COMPRESSION_LEVEL);
253    }
254
255    // Helper function to find an attribute by key
256    fn find_attr<'a>(
257        attrs: &'a [(&'a str, &'a opentelemetry::Value)],
258        key: &str,
259    ) -> Option<&'a opentelemetry::Value> {
260        attrs.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
261    }
262
263    #[test]
264    #[serial]
265    fn test_get_lambda_resource_with_standard_env() {
266        cleanup_env();
267
268        // Set up test environment
269        env::set_var("AWS_REGION", "us-west-2");
270        env::set_var(env_vars::AWS_LAMBDA_FUNCTION_NAME, "test-function");
271        env::set_var("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST");
272        env::set_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128");
273        env::set_var("AWS_LAMBDA_LOG_STREAM_NAME", "2024/01/01/[$LATEST]abc123");
274
275        let resource = get_lambda_resource();
276        let schema = resource.schema_url().unwrap_or("");
277        assert!(schema.is_empty()); // Default resource has no schema URL
278
279        // Check attributes using the resource's attribute iterator
280        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
281
282        assert_eq!(
283            find_attr(&attrs, "cloud.provider"),
284            Some(&opentelemetry::Value::String("aws".into()))
285        );
286        assert_eq!(
287            find_attr(&attrs, "cloud.region"),
288            Some(&opentelemetry::Value::String("us-west-2".into()))
289        );
290        assert_eq!(
291            find_attr(&attrs, "faas.name"),
292            Some(&opentelemetry::Value::String("test-function".into()))
293        );
294        assert_eq!(
295            find_attr(&attrs, "faas.version"),
296            Some(&opentelemetry::Value::String("$LATEST".into()))
297        );
298
299        // Verify memory is converted to bytes
300        assert_eq!(
301            find_attr(&attrs, "faas.max_memory"),
302            Some(&opentelemetry::Value::I64(128 * 1024 * 1024))
303        );
304        assert_eq!(
305            find_attr(&attrs, "faas.instance"),
306            Some(&opentelemetry::Value::String(
307                "2024/01/01/[$LATEST]abc123".into()
308            ))
309        );
310
311        cleanup_env();
312    }
313
314    #[test]
315    #[serial]
316    fn test_get_lambda_resource_with_no_env() {
317        cleanup_env();
318
319        let resource = get_lambda_resource();
320        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
321
322        // No attributes should be set
323        assert!(find_attr(&attrs, "cloud.provider").is_none());
324        assert!(find_attr(&attrs, "cloud.region").is_none());
325        assert!(find_attr(&attrs, "faas.name").is_none());
326
327        cleanup_env();
328    }
329
330    #[test]
331    #[serial]
332    fn test_get_lambda_resource_with_custom_service_name() {
333        cleanup_env();
334
335        // Set up test environment
336        env::set_var("AWS_LAMBDA_FUNCTION_NAME", "test-function");
337        env::set_var("OTEL_SERVICE_NAME", "custom-service");
338
339        let resource = get_lambda_resource();
340        let attrs: Vec<_> = resource.iter().collect();
341
342        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
343            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
344        };
345
346        assert_eq!(
347            find_attr("service.name"),
348            Some(&opentelemetry::Value::String("custom-service".into()))
349        );
350        assert_eq!(
351            find_attr("faas.name"),
352            Some(&opentelemetry::Value::String("test-function".into()))
353        );
354
355        cleanup_env();
356    }
357
358    #[test]
359    #[serial]
360    fn test_get_lambda_resource_with_custom_attributes() {
361        cleanup_env();
362
363        // Set up test environment
364        env::set_var(
365            "OTEL_RESOURCE_ATTRIBUTES",
366            "custom.attr=value,deployment.stage=prod",
367        );
368
369        let resource = get_lambda_resource();
370        let attrs: Vec<_> = resource.iter().collect();
371
372        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
373            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
374        };
375
376        assert_eq!(
377            find_attr("custom.attr"),
378            Some(&opentelemetry::Value::String("value".into()))
379        );
380        assert_eq!(
381            find_attr("deployment.stage"),
382            Some(&opentelemetry::Value::String("prod".into()))
383        );
384
385        cleanup_env();
386    }
387
388    #[test]
389    #[serial]
390    fn test_get_lambda_resource_with_encoded_attributes() {
391        cleanup_env();
392
393        // Set up test environment
394        env::set_var(
395            "OTEL_RESOURCE_ATTRIBUTES",
396            "custom.attr=hello%20world,tag=value%3Dtest",
397        );
398
399        let resource = get_lambda_resource();
400        let attrs: Vec<_> = resource.iter().collect();
401
402        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
403            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
404        };
405
406        assert_eq!(
407            find_attr("custom.attr"),
408            Some(&opentelemetry::Value::String("hello%20world".into()))
409        );
410        assert_eq!(
411            find_attr("tag"),
412            Some(&opentelemetry::Value::String("value%3Dtest".into()))
413        );
414
415        cleanup_env();
416    }
417
418    #[test]
419    #[serial]
420    fn test_resource_attributes_only_set_when_env_vars_present() {
421        cleanup_env();
422
423        // Create resource with no environment variables set
424        let resource = get_lambda_resource();
425        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
426
427        // Verify that configuration attributes are not set
428        assert!(find_attr(&attrs, resource_attributes::QUEUE_SIZE).is_none());
429        assert!(find_attr(&attrs, resource_attributes::PROCESSOR_MODE).is_none());
430        assert!(find_attr(&attrs, resource_attributes::COMPRESSION_LEVEL).is_none());
431
432        // Set environment variables
433        env::set_var(env_vars::QUEUE_SIZE, "4096");
434        env::set_var(env_vars::PROCESSOR_MODE, "async");
435        env::set_var(env_vars::COMPRESSION_LEVEL, "9");
436
437        // Create resource with environment variables set
438        let resource_with_env = get_lambda_resource();
439        let attrs_with_env: Vec<_> = resource_with_env
440            .iter()
441            .map(|(k, v)| (k.as_str(), v))
442            .collect();
443
444        // Verify that configuration attributes are set with correct values
445        assert_eq!(
446            find_attr(&attrs_with_env, resource_attributes::QUEUE_SIZE),
447            Some(&opentelemetry::Value::I64(4096))
448        );
449        assert_eq!(
450            find_attr(&attrs_with_env, resource_attributes::PROCESSOR_MODE),
451            Some(&opentelemetry::Value::String("async".into()))
452        );
453        assert_eq!(
454            find_attr(&attrs_with_env, resource_attributes::COMPRESSION_LEVEL),
455            Some(&opentelemetry::Value::I64(9))
456        );
457
458        cleanup_env();
459    }
460
461    #[test]
462    #[serial]
463    fn test_resource_attributes_not_set_with_invalid_env_vars() {
464        cleanup_env();
465
466        // Set invalid environment variables
467        env::set_var(env_vars::QUEUE_SIZE, "not_a_number");
468        env::set_var(env_vars::COMPRESSION_LEVEL, "high");
469
470        // Create resource with invalid environment variables
471        let resource = get_lambda_resource();
472        let attrs: Vec<_> = resource.iter().map(|(k, v)| (k.as_str(), v)).collect();
473
474        // Verify that configuration attributes with invalid values are not set
475        assert!(find_attr(&attrs, resource_attributes::QUEUE_SIZE).is_none());
476        assert!(find_attr(&attrs, resource_attributes::COMPRESSION_LEVEL).is_none());
477
478        // But the mode attribute should be set since it's a string
479        env::set_var(env_vars::PROCESSOR_MODE, "custom_mode");
480        let resource_with_mode = get_lambda_resource();
481        let attrs_with_mode: Vec<_> = resource_with_mode
482            .iter()
483            .map(|(k, v)| (k.as_str(), v))
484            .collect();
485
486        assert_eq!(
487            find_attr(&attrs_with_mode, resource_attributes::PROCESSOR_MODE),
488            Some(&opentelemetry::Value::String("custom_mode".into()))
489        );
490
491        cleanup_env();
492    }
493}