Skip to content

Commit e512c07

Browse files
Add Service and Environment dimensions to EMF metrics for Lambda
When running on AWS Lambda, EMF metrics now include "Service" and "Environment" dimensions to enable better metric filtering and correlation in CloudWatch. - Service: Uses service.name from OTel Resource (set via OTEL_SERVICE_NAME) - Environment: Hardcoded to "lambda:default" This change only applies when using ConsoleEmfExporter in Lambda environments, ensuring non-Lambda deployments are unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 72625db commit e512c07

File tree

6 files changed

+149
-4
lines changed

6 files changed

+149
-4
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ def _create_emf_exporter():
835835
# pylint: disable=import-outside-toplevel
836836
from amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter import ConsoleEmfExporter
837837

838-
return ConsoleEmfExporter(namespace=log_header_setting.namespace)
838+
return ConsoleEmfExporter(namespace=log_header_setting.namespace, is_lambda=True)
839839

840840
# For non-Lambda environment or Lambda with valid headers - use CloudWatch EMF exporter
841841
session = get_aws_session()

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/aws/metrics/base_emf_exporter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@
3030
from opentelemetry.sdk.resources import Resource
3131
from opentelemetry.util.types import Attributes
3232

33+
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
34+
3335
logger = logging.getLogger(__name__)
3436

37+
# Constants for Lambda EMF dimensions
38+
LAMBDA_ENVIRONMENT_DEFAULT = "lambda:default"
39+
3540

3641
class MetricRecord:
3742
"""The metric data unified representation of all OTel metrics for OTel to CW EMF conversion."""
@@ -121,6 +126,7 @@ def __init__(
121126
namespace: str = "default",
122127
preferred_temporality: Optional[Dict[type, AggregationTemporality]] = None,
123128
preferred_aggregation: Optional[Dict[type, Any]] = None,
129+
is_lambda: bool = False,
124130
):
125131
"""
126132
Initialize the base EMF exporter.
@@ -129,7 +135,9 @@ def __init__(
129135
namespace: CloudWatch namespace for metrics
130136
preferred_temporality: Optional dictionary mapping instrument types to aggregation temporality
131137
preferred_aggregation: Optional dictionary mapping instrument types to preferred aggregation
138+
is_lambda: Whether the exporter is running in AWS Lambda environment
132139
"""
140+
self._is_lambda = is_lambda
133141
# Set up temporality preference default to DELTA if customers not set
134142
if preferred_temporality is None:
135143
preferred_temporality = {
@@ -493,6 +501,15 @@ def _create_emf_log(
493501
for name, value in all_attributes.items():
494502
emf_log[name] = str(value)
495503

504+
# Add Service and Environment dimensions for Lambda
505+
if self._is_lambda:
506+
service_name, _ = get_service_attribute(resource) if resource else ("UnknownService", True)
507+
# Add Service and Environment to dimension names (at the beginning for consistency)
508+
dimension_names = ["Service", "Environment"] + dimension_names
509+
# Add dimension values to the EMF log root
510+
emf_log["Service"] = service_name
511+
emf_log["Environment"] = LAMBDA_ENVIRONMENT_DEFAULT
512+
496513
# Add CloudWatch Metrics if we have metrics, include dimensions only if they exist
497514
if metric_definitions:
498515
cloudwatch_metric = {"Namespace": self.namespace, "Metrics": metric_definitions}

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/exporter/aws/metrics/console_emf_exporter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(
3030
namespace: str = "default",
3131
preferred_temporality: Optional[Dict[type, AggregationTemporality]] = None,
3232
preferred_aggregation: Optional[Dict[type, Any]] = None,
33+
is_lambda: bool = False,
3334
**kwargs: Any,
3435
) -> None:
3536
"""
@@ -39,12 +40,13 @@ def __init__(
3940
namespace: CloudWatch namespace for metrics (defaults to "default")
4041
preferred_temporality: Optional dictionary mapping instrument types to aggregation temporality
4142
preferred_aggregation: Optional dictionary mapping instrument types to preferred aggregation
43+
is_lambda: Whether the exporter is running in AWS Lambda environment
4244
**kwargs: Additional arguments (unused, kept for compatibility)
4345
"""
4446
# No need to check for None since namespace has a default value
4547
if namespace is None:
4648
namespace = "default"
47-
super().__init__(namespace, preferred_temporality, preferred_aggregation)
49+
super().__init__(namespace, preferred_temporality, preferred_aggregation, is_lambda=is_lambda)
4850

4951
def _export(self, log_event: Dict[str, Any]) -> None:
5052
"""

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/exporter/aws/metrics/test_base_emf_exporter.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,113 @@ def test_export_failure_handling(self):
286286
result = self.exporter.export(metrics_data)
287287
self.assertEqual(result, MetricExportResult.FAILURE)
288288

289+
def test_is_lambda_initialization(self):
290+
"""Test is_lambda parameter initialization."""
291+
# Default should be False
292+
exporter = ConcreteEmfExporter()
293+
self.assertFalse(exporter._is_lambda)
294+
295+
# Explicit False
296+
exporter = ConcreteEmfExporter(is_lambda=False)
297+
self.assertFalse(exporter._is_lambda)
298+
299+
# Explicit True
300+
exporter = ConcreteEmfExporter(is_lambda=True)
301+
self.assertTrue(exporter._is_lambda)
302+
303+
def test_create_emf_log_with_lambda_dimensions(self):
304+
"""Test EMF log creation includes Service and Environment dimensions in Lambda mode."""
305+
# Create Lambda exporter
306+
lambda_exporter = ConcreteEmfExporter(namespace="TestNamespace", is_lambda=True)
307+
308+
# Create a simple metric record
309+
record = lambda_exporter._create_metric_record("test_metric", "Count", "Test")
310+
record.value = 50.0
311+
record.timestamp = 1234567890
312+
record.attributes = {"env": "test"}
313+
314+
records = [record]
315+
resource = Resource.create({"service.name": "my-lambda-service"})
316+
317+
result = lambda_exporter._create_emf_log(records, resource, 1234567890)
318+
319+
# Check that Service and Environment dimensions are added
320+
self.assertIn("Service", result)
321+
self.assertEqual(result["Service"], "my-lambda-service")
322+
self.assertIn("Environment", result)
323+
self.assertEqual(result["Environment"], "lambda:default")
324+
325+
# Check that dimensions include Service and Environment
326+
cw_metrics = result["_aws"]["CloudWatchMetrics"][0]
327+
dimensions = cw_metrics["Dimensions"][0]
328+
self.assertIn("Service", dimensions)
329+
self.assertIn("Environment", dimensions)
330+
# Service and Environment should be at the beginning
331+
self.assertEqual(dimensions[0], "Service")
332+
self.assertEqual(dimensions[1], "Environment")
333+
334+
def test_create_emf_log_without_lambda_dimensions(self):
335+
"""Test EMF log creation does NOT include Service and Environment dimensions when not Lambda."""
336+
# Create non-Lambda exporter (default)
337+
non_lambda_exporter = ConcreteEmfExporter(namespace="TestNamespace", is_lambda=False)
338+
339+
# Create a simple metric record
340+
record = non_lambda_exporter._create_metric_record("test_metric", "Count", "Test")
341+
record.value = 50.0
342+
record.timestamp = 1234567890
343+
record.attributes = {"env": "test"}
344+
345+
records = [record]
346+
resource = Resource.create({"service.name": "my-service"})
347+
348+
result = non_lambda_exporter._create_emf_log(records, resource, 1234567890)
349+
350+
# Check that Service and Environment dimensions are NOT added
351+
self.assertNotIn("Service", result)
352+
self.assertNotIn("Environment", result)
353+
354+
# Check that dimensions do NOT include Service and Environment
355+
cw_metrics = result["_aws"]["CloudWatchMetrics"][0]
356+
dimensions = cw_metrics["Dimensions"][0]
357+
self.assertNotIn("Service", dimensions)
358+
self.assertNotIn("Environment", dimensions)
359+
360+
def test_create_emf_log_lambda_with_unknown_service(self):
361+
"""Test EMF log creation uses UnknownService when service.name is not set in Lambda mode."""
362+
lambda_exporter = ConcreteEmfExporter(namespace="TestNamespace", is_lambda=True)
363+
364+
record = lambda_exporter._create_metric_record("test_metric", "Count", "Test")
365+
record.value = 50.0
366+
record.timestamp = 1234567890
367+
record.attributes = {}
368+
369+
records = [record]
370+
# Resource with unknown_service prefix
371+
resource = Resource.create({"service.name": "unknown_service:python"})
372+
373+
result = lambda_exporter._create_emf_log(records, resource, 1234567890)
374+
375+
# Should fall back to UnknownService
376+
self.assertEqual(result["Service"], "UnknownService")
377+
self.assertEqual(result["Environment"], "lambda:default")
378+
379+
def test_create_emf_log_lambda_with_none_resource(self):
380+
"""Test EMF log creation handles None resource in Lambda mode."""
381+
lambda_exporter = ConcreteEmfExporter(namespace="TestNamespace", is_lambda=True)
382+
383+
record = lambda_exporter._create_metric_record("test_metric", "Count", "Test")
384+
record.value = 50.0
385+
record.timestamp = 1234567890
386+
record.attributes = {}
387+
388+
records = [record]
389+
390+
result = lambda_exporter._create_emf_log(records, None, 1234567890)
391+
392+
# Should handle None resource gracefully
393+
self.assertEqual(result["Service"], "UnknownService")
394+
self.assertEqual(result["Environment"], "lambda:default")
395+
289396

290397
if __name__ == "__main__":
291398
unittest.main()

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/exporter/aws/metrics/test_console_emf_exporter.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ def test_initialization_with_parameters(self):
5252
exporter = ConsoleEmfExporter(namespace="TestNamespace", extra_param="ignored") # Should be ignored
5353
self.assertEqual(exporter.namespace, "TestNamespace")
5454

55+
def test_is_lambda_initialization(self):
56+
"""Test is_lambda parameter initialization."""
57+
# Default should be False
58+
exporter = ConsoleEmfExporter()
59+
self.assertFalse(exporter._is_lambda)
60+
61+
# Explicit False
62+
exporter = ConsoleEmfExporter(is_lambda=False)
63+
self.assertFalse(exporter._is_lambda)
64+
65+
# Explicit True
66+
exporter = ConsoleEmfExporter(is_lambda=True)
67+
self.assertTrue(exporter._is_lambda)
68+
69+
# With other parameters
70+
exporter = ConsoleEmfExporter(namespace="TestNamespace", is_lambda=True)
71+
self.assertEqual(exporter.namespace, "TestNamespace")
72+
self.assertTrue(exporter._is_lambda)
73+
5574
def test_export_log_event_success(self):
5675
"""Test that log events are properly sent to console output."""
5776
# Create a simple log event with EMF-formatted message

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/test_aws_opentelementry_configurator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,7 +1327,7 @@ def test_create_emf_exporter_lambda_without_valid_headers(
13271327
result = _create_emf_exporter()
13281328

13291329
self.assertEqual(result, mock_exporter_instance)
1330-
mock_console_exporter.assert_called_once_with(namespace="test-namespace")
1330+
mock_console_exporter.assert_called_once_with(namespace="test-namespace", is_lambda=True)
13311331

13321332
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._fetch_logs_header")
13331333
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._is_lambda_environment")
@@ -1474,7 +1474,7 @@ def test_create_emf_exporter_lambda_without_valid_headers_none_namespace(
14741474
result = _create_emf_exporter()
14751475

14761476
self.assertEqual(result, mock_exporter_instance)
1477-
mock_console_exporter.assert_called_once_with(namespace=None)
1477+
mock_console_exporter.assert_called_once_with(namespace=None, is_lambda=True)
14781478

14791479
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._fetch_logs_header")
14801480
@patch("amazon.opentelemetry.distro.aws_opentelemetry_configurator._is_lambda_environment")

0 commit comments

Comments
 (0)