opentelemetry_lambda_extension/
resource.rs1use opentelemetry::{KeyValue, Value};
11use opentelemetry_proto::tonic::resource::v1::Resource as ProtoResource;
12use opentelemetry_sdk::resource::{Resource, ResourceDetector};
13use opentelemetry_semantic_conventions::SCHEMA_URL;
14use opentelemetry_semantic_conventions::attribute as semconv_attr;
15use opentelemetry_semantic_conventions::resource as semconv_res;
16use std::borrow::Cow;
17use std::env;
18
19pub mod semconv {
21 pub use opentelemetry_semantic_conventions::attribute::{
22 CLOUD_PLATFORM, CLOUD_PROVIDER, CLOUD_REGION, FAAS_COLDSTART, FAAS_INVOCATION_ID,
23 FAAS_MAX_MEMORY, FAAS_NAME, FAAS_TRIGGER, FAAS_VERSION,
24 };
25 pub use opentelemetry_semantic_conventions::resource::{
26 AWS_LOG_GROUP_NAMES, AWS_LOG_STREAM_NAMES, FAAS_INSTANCE, SERVICE_NAME,
27 TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION,
28 };
29}
30
31const AWS_EXECUTION_ENV: &str = "AWS_EXECUTION_ENV";
33const AWS_REGION: &str = "AWS_REGION";
34const AWS_LAMBDA_FUNCTION_NAME: &str = "AWS_LAMBDA_FUNCTION_NAME";
35const AWS_LAMBDA_FUNCTION_VERSION: &str = "AWS_LAMBDA_FUNCTION_VERSION";
36const AWS_LAMBDA_FUNCTION_MEMORY_SIZE: &str = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE";
37const AWS_LAMBDA_LOG_GROUP_NAME: &str = "AWS_LAMBDA_LOG_GROUP_NAME";
38const AWS_LAMBDA_LOG_STREAM_NAME: &str = "AWS_LAMBDA_LOG_STREAM_NAME";
39
40#[derive(Debug, Default)]
58pub struct AwsLambdaDetector;
59
60impl AwsLambdaDetector {
61 pub fn new() -> Self {
63 Self
64 }
65}
66
67impl ResourceDetector for AwsLambdaDetector {
68 fn detect(&self) -> Resource {
69 let execution_env = env::var(AWS_EXECUTION_ENV).ok();
70 if !execution_env
71 .as_ref()
72 .map(|e| e.starts_with("AWS_Lambda_"))
73 .unwrap_or(false)
74 {
75 return Resource::builder_empty().build();
76 }
77
78 let mut attributes = vec![
79 KeyValue::new(semconv_attr::CLOUD_PROVIDER, "aws"),
80 KeyValue::new(semconv_attr::CLOUD_PLATFORM, "aws_lambda"),
81 ];
82
83 if let Ok(region) = env::var(AWS_REGION) {
84 attributes.push(KeyValue::new(semconv_attr::CLOUD_REGION, region));
85 }
86
87 if let Ok(name) = env::var(AWS_LAMBDA_FUNCTION_NAME) {
88 attributes.push(KeyValue::new(semconv_attr::FAAS_NAME, name));
89 }
90
91 if let Ok(version) = env::var(AWS_LAMBDA_FUNCTION_VERSION) {
92 attributes.push(KeyValue::new(semconv_attr::FAAS_VERSION, version));
93 }
94
95 if let Ok(memory) = env::var(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)
96 && let Ok(mb) = memory.parse::<i64>()
97 {
98 attributes.push(KeyValue::new(
99 semconv_attr::FAAS_MAX_MEMORY,
100 mb * 1024 * 1024,
101 ));
102 }
103
104 if let Ok(log_group) = env::var(AWS_LAMBDA_LOG_GROUP_NAME) {
105 use opentelemetry::StringValue;
106 attributes.push(KeyValue::new(
107 semconv_res::AWS_LOG_GROUP_NAMES,
108 Value::Array(vec![StringValue::from(log_group)].into()),
109 ));
110 }
111
112 if let Ok(log_stream) = env::var(AWS_LAMBDA_LOG_STREAM_NAME) {
113 attributes.push(KeyValue::new(semconv_res::FAAS_INSTANCE, log_stream));
114 }
115
116 Resource::builder_empty()
117 .with_schema_url(attributes.iter().cloned(), Cow::Borrowed(SCHEMA_URL))
118 .build()
119 }
120}
121
122#[derive(Debug, Default)]
127pub struct ExtensionDetector;
128
129impl ExtensionDetector {
130 pub fn new() -> Self {
132 Self
133 }
134}
135
136impl ResourceDetector for ExtensionDetector {
137 fn detect(&self) -> Resource {
138 let attributes = vec![
139 KeyValue::new(semconv_res::SERVICE_NAME, "opentelemetry-lambda-extension"),
140 KeyValue::new(
141 semconv_res::TELEMETRY_SDK_NAME,
142 "opentelemetry-lambda-extension",
143 ),
144 KeyValue::new(semconv_res::TELEMETRY_SDK_LANGUAGE, "rust"),
145 KeyValue::new(
146 semconv_res::TELEMETRY_SDK_VERSION,
147 env!("CARGO_PKG_VERSION"),
148 ),
149 ];
150
151 Resource::builder_empty()
152 .with_attributes(attributes)
153 .build()
154 }
155}
156
157pub fn detect_resource() -> Resource {
162 let lambda_detector = AwsLambdaDetector::new();
163 let extension_detector = ExtensionDetector::new();
164
165 Resource::builder_empty()
166 .with_detector(Box::new(lambda_detector))
167 .with_detector(Box::new(extension_detector))
168 .build()
169}
170
171pub fn to_proto_resource(resource: &Resource) -> ProtoResource {
173 resource.into()
174}
175
176#[derive(Debug)]
181#[must_use = "builders do nothing unless .build() is called"]
182pub struct ResourceBuilder {
183 inner: opentelemetry_sdk::resource::ResourceBuilder,
184}
185
186impl ResourceBuilder {
187 pub fn new() -> Self {
189 Self {
190 inner: Resource::builder_empty()
191 .with_detector(Box::new(AwsLambdaDetector::new()))
192 .with_detector(Box::new(ExtensionDetector::new())),
193 }
194 }
195
196 pub fn detect_lambda_environment(self) -> Self {
201 Self {
202 inner: self
203 .inner
204 .with_detector(Box::new(AwsLambdaDetector::new()))
205 .with_detector(Box::new(ExtensionDetector::new())),
206 }
207 }
208
209 pub fn add_string(self, key: impl Into<Cow<'static, str>>, value: impl Into<String>) -> Self {
211 Self {
212 inner: self
213 .inner
214 .with_attribute(KeyValue::new(key.into(), value.into())),
215 }
216 }
217
218 pub fn add_int(self, key: impl Into<Cow<'static, str>>, value: i64) -> Self {
220 Self {
221 inner: self.inner.with_attribute(KeyValue::new(key.into(), value)),
222 }
223 }
224
225 pub fn add_bool(self, key: impl Into<Cow<'static, str>>, value: bool) -> Self {
227 Self {
228 inner: self.inner.with_attribute(KeyValue::new(key.into(), value)),
229 }
230 }
231
232 pub fn build(self) -> Resource {
234 self.inner.build()
235 }
236
237 pub fn build_proto(self) -> ProtoResource {
239 to_proto_resource(&self.build())
240 }
241}
242
243impl Default for ResourceBuilder {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use opentelemetry_proto::tonic::common::v1::any_value;
253
254 fn get_string_value(resource: &Resource, key: &str) -> Option<String> {
255 use opentelemetry::Key;
256 let owned_key = Key::from(key.to_owned());
257 resource.get(&owned_key).and_then(|v| match v {
258 Value::String(s) => Some(s.to_string()),
259 _ => None,
260 })
261 }
262
263 fn get_int_value(resource: &Resource, key: &str) -> Option<i64> {
264 use opentelemetry::Key;
265 let owned_key = Key::from(key.to_owned());
266 resource.get(&owned_key).and_then(|v| match v {
267 Value::I64(i) => Some(i),
268 _ => None,
269 })
270 }
271
272 #[test]
273 fn test_lambda_detector_outside_lambda() {
274 temp_env::with_vars([(AWS_EXECUTION_ENV, None::<&str>)], || {
276 let detector = AwsLambdaDetector::new();
277 let resource = detector.detect();
278 assert!(resource.is_empty());
279 });
280 }
281
282 #[test]
283 fn test_lambda_detector_in_lambda() {
284 temp_env::with_vars(
285 [
286 (AWS_EXECUTION_ENV, Some("AWS_Lambda_nodejs18.x")),
287 (AWS_REGION, Some("us-east-1")),
288 (AWS_LAMBDA_FUNCTION_NAME, Some("test-function")),
289 (AWS_LAMBDA_FUNCTION_VERSION, Some("$LATEST")),
290 (AWS_LAMBDA_FUNCTION_MEMORY_SIZE, Some("128")),
291 ],
292 || {
293 let detector = AwsLambdaDetector::new();
294 let resource = detector.detect();
295
296 assert_eq!(
297 get_string_value(&resource, semconv::CLOUD_PROVIDER),
298 Some("aws".to_string())
299 );
300 assert_eq!(
301 get_string_value(&resource, semconv::CLOUD_PLATFORM),
302 Some("aws_lambda".to_string())
303 );
304 assert_eq!(
305 get_string_value(&resource, semconv::CLOUD_REGION),
306 Some("us-east-1".to_string())
307 );
308 assert_eq!(
309 get_string_value(&resource, semconv::FAAS_NAME),
310 Some("test-function".to_string())
311 );
312 assert_eq!(
313 get_string_value(&resource, semconv::FAAS_VERSION),
314 Some("$LATEST".to_string())
315 );
316 assert_eq!(
317 get_int_value(&resource, semconv::FAAS_MAX_MEMORY),
318 Some(128 * 1024 * 1024)
319 );
320 },
321 );
322 }
323
324 #[test]
325 fn test_extension_detector() {
326 let detector = ExtensionDetector::new();
327 let resource = detector.detect();
328
329 assert_eq!(
330 get_string_value(&resource, semconv::SERVICE_NAME),
331 Some("opentelemetry-lambda-extension".to_string())
332 );
333 assert_eq!(
334 get_string_value(&resource, semconv::TELEMETRY_SDK_NAME),
335 Some("opentelemetry-lambda-extension".to_string())
336 );
337 assert_eq!(
338 get_string_value(&resource, semconv::TELEMETRY_SDK_LANGUAGE),
339 Some("rust".to_string())
340 );
341 assert!(get_string_value(&resource, semconv::TELEMETRY_SDK_VERSION).is_some());
342 }
343
344 #[test]
345 fn test_resource_builder_custom_attributes() {
346 temp_env::with_vars([(AWS_EXECUTION_ENV, None::<&str>)], || {
347 let resource = ResourceBuilder::new()
348 .add_string("custom.string", "value")
349 .add_int("custom.int", 42)
350 .add_bool("custom.bool", true)
351 .build();
352
353 assert_eq!(
354 get_string_value(&resource, "custom.string"),
355 Some("value".to_string())
356 );
357 assert_eq!(get_int_value(&resource, "custom.int"), Some(42));
358 });
359 }
360
361 #[test]
362 fn test_detect_resource() {
363 temp_env::with_vars([(AWS_EXECUTION_ENV, None::<&str>)], || {
364 let resource = detect_resource();
365 assert!(get_string_value(&resource, semconv::SERVICE_NAME).is_some());
367 });
368 }
369
370 #[test]
371 fn test_to_proto_resource() {
372 let resource = Resource::builder_empty()
373 .with_attribute(KeyValue::new("test.key", "test.value"))
374 .build();
375
376 let proto = to_proto_resource(&resource);
377
378 assert!(!proto.attributes.is_empty());
379 let attr = &proto.attributes[0];
380 assert_eq!(attr.key, "test.key");
381 match &attr.value {
382 Some(v) => match &v.value {
383 Some(any_value::Value::StringValue(s)) => assert_eq!(s, "test.value"),
384 _ => panic!("Expected string value"),
385 },
386 None => panic!("Expected value"),
387 }
388 }
389
390 #[test]
391 fn test_service_name_is_extension_not_function() {
392 temp_env::with_vars(
393 [
394 (AWS_EXECUTION_ENV, Some("AWS_Lambda_nodejs18.x")),
395 (AWS_LAMBDA_FUNCTION_NAME, Some("my-lambda-function")),
396 ],
397 || {
398 let resource = detect_resource();
399
400 let service_name = get_string_value(&resource, semconv::SERVICE_NAME);
401 let faas_name = get_string_value(&resource, semconv::FAAS_NAME);
402
403 assert_eq!(
404 service_name,
405 Some("opentelemetry-lambda-extension".to_string()),
406 "service.name should be the extension name, not the function name"
407 );
408
409 assert_eq!(
410 faas_name,
411 Some("my-lambda-function".to_string()),
412 "faas.name should be the Lambda function name"
413 );
414 },
415 );
416 }
417
418 #[test]
419 fn test_faas_instance_from_log_stream() {
420 temp_env::with_vars(
421 [
422 (AWS_EXECUTION_ENV, Some("AWS_Lambda_nodejs18.x")),
423 (
424 AWS_LAMBDA_LOG_STREAM_NAME,
425 Some("2024/01/01/[$LATEST]abc123"),
426 ),
427 ],
428 || {
429 let detector = AwsLambdaDetector::new();
430 let resource = detector.detect();
431
432 assert_eq!(
433 get_string_value(&resource, semconv::FAAS_INSTANCE),
434 Some("2024/01/01/[$LATEST]abc123".to_string())
435 );
436 },
437 );
438 }
439}