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}