Skip to content

Commit 5b76598

Browse files
authored
Change configuration for adding Application Signals Dimensions to EMF exporter (#552)
*Description of changes:* - Ports changes from: aws-observability/aws-otel-java-instrumentation#1264 - Adds new `OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS` environment variable. When enabled, the exporters automatically add Service and Environment dimensions. Defaults to `UnknownService` and `generic:default` respectively based on resource attributes and cloud platform. - Application Signals dimensions are enabled by default when OTEL_METRICS_EXPORTER is set to awsemf. Users can opt out or disable this feature by explicitly setting OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS to false. - Disable Application Signals Dimensions for Agent Observability path By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent ff89bc5 commit 5b76598

File tree

7 files changed

+194
-175
lines changed

7 files changed

+194
-175
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t
1717
([#547](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/547))
1818
- Add Service and Environment dimensions to EMF metrics when Application Signals EMF export is enabled
1919
([#548](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/548))
20+
- Refactor configuration for adding Application Signals Dimensions to EMF exporter
21+
([#552](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/552))
2022
- Fix CVE-2025-66471. No associated PR since `urllib3` dependency will auto-bump to `2.6.x` upon release.
2123

2224
## v0.14.0 - 2025-11-19

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
_logger: Logger = getLogger(__name__)
1212

1313
AGENT_OBSERVABILITY_ENABLED = "AGENT_OBSERVABILITY_ENABLED"
14+
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS = "OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS"
1415

1516

1617
def is_installed(req: str) -> bool:
@@ -38,6 +39,11 @@ def is_agent_observability_enabled() -> bool:
3839
return os.environ.get(AGENT_OBSERVABILITY_ENABLED, "false").lower() == "true"
3940

4041

42+
def should_add_application_signals_dimensions() -> bool:
43+
"""Should Service and Environment Application Signals dimensions be added to EMF logs?"""
44+
return os.environ.get(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "true").lower() == "true"
45+
46+
4147
IS_BOTOCORE_INSTALLED: bool = is_installed("botocore")
4248

4349

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
import sys
1717
from logging import ERROR, Logger, getLogger
1818

19-
from amazon.opentelemetry.distro._utils import get_aws_region, is_agent_observability_enabled, is_installed
19+
from amazon.opentelemetry.distro._utils import (
20+
OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS,
21+
get_aws_region,
22+
is_agent_observability_enabled,
23+
is_installed,
24+
)
2025
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import (
2126
APPLICATION_SIGNALS_ENABLED_CONFIG,
2227
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
@@ -158,6 +163,7 @@ def _configure(self, **kwargs):
158163

159164
# Disable AWS Application Signals by default
160165
os.environ.setdefault(APPLICATION_SIGNALS_ENABLED_CONFIG, "false")
166+
os.environ.setdefault(OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS, "false")
161167

162168
super(AwsOpenTelemetryDistro, self)._configure()
163169

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

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
import json
77
import logging
88
import math
9-
import os
109
import time
1110
from abc import ABC, abstractmethod
1211
from collections import defaultdict
1312
from typing import Any, Dict, List, Optional, Tuple
1413

15-
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
14+
from amazon.opentelemetry.distro._utils import should_add_application_signals_dimensions
1615
from opentelemetry.sdk.metrics import Counter
1716
from opentelemetry.sdk.metrics import Histogram as HistogramInstr
1817
from opentelemetry.sdk.metrics import ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter
@@ -30,6 +29,7 @@
3029
)
3130
from opentelemetry.sdk.metrics.view import ExponentialBucketHistogramAggregation
3231
from opentelemetry.sdk.resources import Resource
32+
from opentelemetry.semconv._incubating.attributes.cloud_attributes import CloudPlatformValues
3333
from opentelemetry.semconv.resource import ResourceAttributes
3434
from opentelemetry.util.types import Attributes
3535

@@ -39,9 +39,19 @@
3939
SERVICE_DIMENSION_NAME: str = "Service"
4040
ENVIRONMENT_DIMENSION_NAME: str = "Environment"
4141

42+
# Resource attribute constant for deployment.environment.name
43+
# deployment.environment is deprecated in favor of deployment.environment.name
44+
# but not yet available in current OTel Python version
45+
# https://github.com/open-telemetry/opentelemetry.io/commit/b04507d7be1e916f6705126c56d66dbe9536503e
46+
DEPLOYMENT_ENVIRONMENT_NAME: str = "deployment.environment.name"
47+
4248
# Constants
43-
LAMBDA_DEFAULT: str = "lambda:default"
4449
UNKNOWN_SERVICE: str = "UnknownService"
50+
UNKNOWN_ENVIRONMENT: str = "generic:default"
51+
EC2_DEFAULT: str = "ec2:default"
52+
ECS_DEFAULT: str = "ecs:default"
53+
EKS_DEFAULT: str = "eks:default"
54+
LAMBDA_DEFAULT: str = "lambda:default"
4555

4656

4757
class MetricRecord:
@@ -195,52 +205,58 @@ def _get_dimension_names(self, attributes: Attributes) -> List[str]:
195205
# For now, use all attributes as dimensions
196206
return list(attributes.keys())
197207

198-
def _has_dimension_case_insensitive(self, dimension_names: List[str], dimension_to_check: str) -> bool:
199-
"""Check if dimension already exists (case-insensitive match)."""
200-
dimension_lower = dimension_to_check.lower()
201-
return any(dim.lower() == dimension_lower for dim in dimension_names)
202-
203-
@staticmethod
204-
def _is_application_signals_emf_export_enabled() -> bool:
205-
"""Check if Application Signals EMF export is enabled.
206-
207-
Returns True only if BOTH:
208-
- OTEL_AWS_APPLICATION_SIGNALS_ENABLED is true
209-
- OTEL_AWS_APPLICATION_SIGNALS_EMF_EXPORT_ENABLED is true
210-
"""
211-
app_signals_enabled = os.environ.get("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "false").lower() == "true"
212-
emf_export_enabled = (
213-
os.environ.get("OTEL_AWS_APPLICATION_SIGNALS_EMF_EXPORT_ENABLED", "false").lower() == "true"
214-
)
215-
return app_signals_enabled and emf_export_enabled
216-
217208
def _add_application_signals_dimensions(
218-
self, dimension_names: List[str], emf_log: Dict, resource: Resource
209+
self, dimension_names: List[str], emf_log: Dict, resource_attributes: Optional[Attributes]
219210
) -> None:
220-
"""Add Service and Environment dimensions if not already present (case-insensitive)."""
221-
if not self._is_application_signals_emf_export_enabled():
211+
"""Add Service and Environment dimensions if not already present."""
212+
if not should_add_application_signals_dimensions():
222213
return
223214

224-
# Add Service dimension if not already set by user
225215
if not self._has_dimension_case_insensitive(dimension_names, SERVICE_DIMENSION_NAME):
226-
if resource:
227-
service_name, _ = get_service_attribute(resource)
228-
else:
216+
service_name = resource_attributes.get(ResourceAttributes.SERVICE_NAME) if resource_attributes else None
217+
service_name_str = str(service_name) if service_name else ""
218+
# https://github.com/open-telemetry/opentelemetry-python/blob/102fec2be1fe9d0a8e299598a21ad6ec3b96dcca/opentelemetry-semantic-conventions/src/opentelemetry/semconv/attributes/service_attributes.py#L20
219+
if (
220+
not service_name
221+
or service_name_str == "unknown_service"
222+
or service_name_str.startswith("unknown_service:")
223+
):
229224
service_name = UNKNOWN_SERVICE
230-
dimension_names.insert(0, SERVICE_DIMENSION_NAME)
231-
emf_log[SERVICE_DIMENSION_NAME] = str(service_name)
225+
dimension_names.append(SERVICE_DIMENSION_NAME)
226+
emf_log[SERVICE_DIMENSION_NAME] = service_name
232227

233-
# Add Environment dimension if not already set by user
234228
if not self._has_dimension_case_insensitive(dimension_names, ENVIRONMENT_DIMENSION_NAME):
235-
environment_value = None
236-
if resource and resource.attributes:
237-
environment_value = resource.attributes.get(ResourceAttributes.DEPLOYMENT_ENVIRONMENT)
238-
if not environment_value:
239-
environment_value = LAMBDA_DEFAULT
240-
# Insert after Service if it exists, otherwise at the beginning
241-
insert_pos = 1 if SERVICE_DIMENSION_NAME in dimension_names else 0
242-
dimension_names.insert(insert_pos, ENVIRONMENT_DIMENSION_NAME)
243-
emf_log[ENVIRONMENT_DIMENSION_NAME] = str(environment_value)
229+
environment_name = self._get_deployment_environment(resource_attributes)
230+
dimension_names.append(ENVIRONMENT_DIMENSION_NAME)
231+
emf_log[ENVIRONMENT_DIMENSION_NAME] = environment_name
232+
233+
def _has_dimension_case_insensitive(self, dimension_names: List[str], dimension_to_check: str) -> bool:
234+
"""Check if dimension already exists."""
235+
dimension_lower = dimension_to_check.lower()
236+
return any(dim.lower() == dimension_lower for dim in dimension_names)
237+
238+
def _get_deployment_environment(self, resource_attributes: Optional[Attributes]) -> str:
239+
"""Get deployment environment from resource attributes or cloud platform."""
240+
if not resource_attributes:
241+
return UNKNOWN_ENVIRONMENT
242+
243+
environment_name = resource_attributes.get(DEPLOYMENT_ENVIRONMENT_NAME)
244+
if not environment_name:
245+
environment_name = resource_attributes.get(ResourceAttributes.DEPLOYMENT_ENVIRONMENT)
246+
247+
if environment_name:
248+
return str(environment_name)
249+
250+
platform = resource_attributes.get(ResourceAttributes.CLOUD_PLATFORM)
251+
if platform:
252+
platform_defaults = {
253+
CloudPlatformValues.AWS_EC2.value: EC2_DEFAULT,
254+
CloudPlatformValues.AWS_ECS.value: ECS_DEFAULT,
255+
CloudPlatformValues.AWS_EKS.value: EKS_DEFAULT,
256+
CloudPlatformValues.AWS_LAMBDA.value: LAMBDA_DEFAULT,
257+
}
258+
return platform_defaults.get(str(platform), UNKNOWN_ENVIRONMENT)
259+
return UNKNOWN_ENVIRONMENT
244260

245261
def _get_attributes_key(self, attributes: Attributes) -> str:
246262
"""
@@ -552,7 +568,8 @@ def _create_emf_log(
552568
emf_log[name] = str(value)
553569

554570
# Add Service and Environment dimensions if Application Signals EMF export is enabled
555-
self._add_application_signals_dimensions(dimension_names, emf_log, resource)
571+
resource_attributes = resource.attributes if resource else {}
572+
self._add_application_signals_dimensions(dimension_names, emf_log, resource_attributes)
556573

557574
# Add CloudWatch Metrics if we have metrics, include dimensions only if they exist
558575
if metric_definitions:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,10 @@ def test_create_emf_log(self):
358358
# Check that the result is JSON serializable
359359
json.dumps(result) # Should not raise exception
360360

361+
@patch.dict("os.environ", {"OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS": "false"})
361362
def test_create_emf_log_with_resource(self):
362363
"""Test EMF log creation with resource attributes."""
364+
363365
# Create test records
364366
gauge_record = self.exporter._create_metric_record("gauge_metric", "Count", "Gauge")
365367
gauge_record.value = 50.0
@@ -395,8 +397,10 @@ def test_create_emf_log_with_resource(self):
395397
self.assertEqual(set(cw_metrics["Dimensions"][0]), {"env", "service"})
396398
self.assertEqual(cw_metrics["Metrics"][0]["Name"], "gauge_metric")
397399

400+
@patch.dict("os.environ", {"OTEL_METRICS_ADD_APPLICATION_SIGNALS_DIMENSIONS": "false"})
398401
def test_create_emf_log_without_dimensions(self):
399402
"""Test EMF log creation with metrics but no dimensions."""
403+
400404
# Create test record without attributes (no dimensions)
401405
gauge_record = self.exporter._create_metric_record("gauge_metric", "Count", "Gauge")
402406
gauge_record.value = 75.0

0 commit comments

Comments
 (0)