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 opentelemetry::KeyValue;
75use opentelemetry_sdk::Resource;
76use std::env;
77
78/// Get default Lambda resource attributes.
79///
80/// This function automatically detects and sets standard Lambda attributes from environment
81/// variables and allows for custom attribute configuration through `OTEL_RESOURCE_ATTRIBUTES`.
82///
83/// # Environment Variables
84///
85/// - `AWS_REGION`: Sets `cloud.region`
86/// - `AWS_LAMBDA_FUNCTION_NAME`: Sets `faas.name` and default `service.name`
87/// - `AWS_LAMBDA_FUNCTION_VERSION`: Sets `faas.version`
88/// - `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`: Sets `faas.max_memory`
89/// - `AWS_LAMBDA_LOG_STREAM_NAME`: Sets `faas.instance`
90/// - `OTEL_SERVICE_NAME`: Overrides default service name
91/// - `OTEL_RESOURCE_ATTRIBUTES`: Additional attributes in key=value format
92///
93/// # Returns
94///
95/// Returns a [`Resource`] containing all detected and configured attributes.
96///
97/// # Examples
98///
99/// Basic usage with environment variables:
100///
101/// ```no_run
102/// use lambda_otel_lite::resource::get_lambda_resource;
103/// use opentelemetry::KeyValue;
104///
105/// // Get resource with Lambda environment attributes
106/// let resource = get_lambda_resource();
107/// ```
108///
109/// Adding custom attributes:
110///
111/// ```no_run
112/// use lambda_otel_lite::resource::get_lambda_resource;
113/// use opentelemetry::KeyValue;
114/// use opentelemetry_sdk::Resource;
115///
116/// // Get Lambda resource
117/// let lambda_resource = get_lambda_resource();
118///
119/// // Create custom resource
120/// let extra_resource = Resource::builder()
121///     .with_attributes(vec![
122///         KeyValue::new("deployment.stage", "prod"),
123///         KeyValue::new("team", "backend"),
124///     ])
125///     .build();
126///
127/// // Combine resources (custom attributes take precedence)
128/// // Create a new resource with all attributes
129/// let mut all_attributes = vec![
130///     KeyValue::new("deployment.stage", "prod"),
131///     KeyValue::new("team", "backend"),
132/// ];
133///
134/// // Add lambda attributes (could be done more programmatically in real code)
135/// all_attributes.push(KeyValue::new("cloud.provider", "aws"));
136/// all_attributes.push(KeyValue::new("faas.name", "my-function"));
137///
138/// let final_resource = Resource::builder()
139///     .with_attributes(all_attributes)
140///     .build();
141/// ```
142///
143/// # Integration with Telemetry Config
144///
145/// This function is automatically called by [`init_telemetry`](crate::init_telemetry)
146/// when no custom resource is provided. To override or extend these attributes, use
147/// the [`TelemetryConfig`](crate::TelemetryConfig) builder:
148///
149/// ```no_run
150/// use lambda_otel_lite::{TelemetryConfig, init_telemetry};
151/// use opentelemetry_sdk::Resource;
152///
153/// # async fn example() -> Result<(), lambda_runtime::Error> {
154/// // Get base Lambda resource
155/// let base_resource = lambda_otel_lite::get_lambda_resource();
156///
157/// // Configure telemetry with the resource
158/// let config = TelemetryConfig::builder()
159///     .resource(base_resource)
160///     .build();
161///
162/// let _completion_handler = init_telemetry(config).await?;
163/// # Ok(())
164/// # }
165/// ```
166pub fn get_lambda_resource() -> Resource {
167    let mut attributes = Vec::new();
168
169    // Add standard Lambda attributes
170    if let Ok(region) = env::var("AWS_REGION") {
171        attributes.push(KeyValue::new("cloud.provider", "aws"));
172        attributes.push(KeyValue::new("cloud.region", region));
173    }
174
175    if let Ok(function_name) = env::var("AWS_LAMBDA_FUNCTION_NAME") {
176        attributes.push(KeyValue::new("faas.name", function_name.clone()));
177        // Use function name as service name if not set
178        if env::var("OTEL_SERVICE_NAME").is_err() {
179            attributes.push(KeyValue::new("service.name", function_name));
180        }
181    }
182
183    if let Ok(version) = env::var("AWS_LAMBDA_FUNCTION_VERSION") {
184        attributes.push(KeyValue::new("faas.version", version));
185    }
186
187    if let Ok(memory) = env::var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE") {
188        if let Ok(memory_mb) = memory.parse::<i64>() {
189            let memory_bytes = memory_mb * 1024 * 1024;
190            attributes.push(KeyValue::new("faas.max_memory", memory_bytes));
191        }
192    }
193
194    if let Ok(log_stream) = env::var("AWS_LAMBDA_LOG_STREAM_NAME") {
195        attributes.push(KeyValue::new("faas.instance", log_stream));
196    }
197
198    // create resource with standard attributes and merge with custom attributes
199    Resource::builder().with_attributes(attributes).build()
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use serial_test::serial;
206    use std::env;
207
208    fn cleanup_env() {
209        env::remove_var("AWS_REGION");
210        env::remove_var("AWS_LAMBDA_FUNCTION_NAME");
211        env::remove_var("AWS_LAMBDA_FUNCTION_VERSION");
212        env::remove_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE");
213        env::remove_var("AWS_LAMBDA_LOG_STREAM_NAME");
214        env::remove_var("OTEL_SERVICE_NAME");
215        env::remove_var("OTEL_RESOURCE_ATTRIBUTES");
216    }
217
218    #[test]
219    #[serial]
220    fn test_get_lambda_resource_with_standard_env() {
221        cleanup_env();
222
223        // Set up test environment
224        env::set_var("AWS_REGION", "us-west-2");
225        env::set_var("AWS_LAMBDA_FUNCTION_NAME", "test-function");
226        env::set_var("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST");
227        env::set_var("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128");
228        env::set_var("AWS_LAMBDA_LOG_STREAM_NAME", "2024/01/01/[$LATEST]abc123");
229
230        let resource = get_lambda_resource();
231        let schema = resource.schema_url().unwrap_or("");
232        assert!(schema.is_empty()); // Default resource has no schema URL
233
234        // Check attributes using the resource's attribute iterator
235        let attrs: Vec<_> = resource.iter().collect();
236
237        // Helper function to find an attribute by key
238        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
239            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
240        };
241
242        assert_eq!(
243            find_attr("cloud.provider"),
244            Some(&opentelemetry::Value::String("aws".into()))
245        );
246        assert_eq!(
247            find_attr("cloud.region"),
248            Some(&opentelemetry::Value::String("us-west-2".into()))
249        );
250        assert_eq!(
251            find_attr("faas.name"),
252            Some(&opentelemetry::Value::String("test-function".into()))
253        );
254        assert_eq!(
255            find_attr("service.name"),
256            Some(&opentelemetry::Value::String("test-function".into()))
257        );
258        assert_eq!(
259            find_attr("faas.version"),
260            Some(&opentelemetry::Value::String("$LATEST".into()))
261        );
262        assert_eq!(
263            find_attr("faas.max_memory"),
264            Some(&opentelemetry::Value::I64(134217728))
265        ); // 128 * 1024 * 1024
266        assert_eq!(
267            find_attr("faas.instance"),
268            Some(&opentelemetry::Value::String(
269                "2024/01/01/[$LATEST]abc123".into()
270            ))
271        );
272
273        cleanup_env();
274    }
275
276    #[test]
277    #[serial]
278    fn test_get_lambda_resource_with_custom_service_name() {
279        cleanup_env();
280
281        // Set up test environment
282        env::set_var("AWS_LAMBDA_FUNCTION_NAME", "test-function");
283        env::set_var("OTEL_SERVICE_NAME", "custom-service");
284
285        let resource = get_lambda_resource();
286        let attrs: Vec<_> = resource.iter().collect();
287
288        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
289            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
290        };
291
292        assert_eq!(
293            find_attr("service.name"),
294            Some(&opentelemetry::Value::String("custom-service".into()))
295        );
296        assert_eq!(
297            find_attr("faas.name"),
298            Some(&opentelemetry::Value::String("test-function".into()))
299        );
300
301        cleanup_env();
302    }
303
304    #[test]
305    #[serial]
306    fn test_get_lambda_resource_with_custom_attributes() {
307        cleanup_env();
308
309        // Set up test environment
310        env::set_var(
311            "OTEL_RESOURCE_ATTRIBUTES",
312            "custom.attr=value,deployment.stage=prod",
313        );
314
315        let resource = get_lambda_resource();
316        let attrs: Vec<_> = resource.iter().collect();
317
318        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
319            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
320        };
321
322        assert_eq!(
323            find_attr("custom.attr"),
324            Some(&opentelemetry::Value::String("value".into()))
325        );
326        assert_eq!(
327            find_attr("deployment.stage"),
328            Some(&opentelemetry::Value::String("prod".into()))
329        );
330
331        cleanup_env();
332    }
333
334    #[test]
335    #[serial]
336    fn test_get_lambda_resource_with_encoded_attributes() {
337        cleanup_env();
338
339        // Set up test environment
340        env::set_var(
341            "OTEL_RESOURCE_ATTRIBUTES",
342            "custom.attr=hello%20world,tag=value%3Dtest",
343        );
344
345        let resource = get_lambda_resource();
346        let attrs: Vec<_> = resource.iter().collect();
347
348        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
349            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
350        };
351
352        assert_eq!(
353            find_attr("custom.attr"),
354            Some(&opentelemetry::Value::String("hello%20world".into()))
355        );
356        assert_eq!(
357            find_attr("tag"),
358            Some(&opentelemetry::Value::String("value%3Dtest".into()))
359        );
360
361        cleanup_env();
362    }
363
364    #[test]
365    #[serial]
366    fn test_get_lambda_resource_with_empty_environment() {
367        cleanup_env();
368
369        let resource = get_lambda_resource();
370        assert!(resource.schema_url().unwrap_or("").is_empty());
371
372        let attrs: Vec<_> = resource.iter().collect();
373
374        let find_attr = |key: &str| -> Option<&opentelemetry::Value> {
375            attrs.iter().find(|kv| kv.0.as_str() == key).map(|kv| kv.1)
376        };
377
378        assert!(find_attr("cloud.provider").is_none());
379        assert!(find_attr("cloud.region").is_none());
380        assert!(find_attr("faas.name").is_none());
381
382        cleanup_env();
383    }
384}