Skip to content

Commit f6a8d49

Browse files
committed
add Application Signals runtime metrics
1 parent 0aa7eb4 commit f6a8d49

File tree

6 files changed

+262
-38
lines changed

6 files changed

+262
-38
lines changed

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
AWS_SQS_QUEUE_NAME,
2323
AWS_SQS_QUEUE_URL,
2424
)
25+
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
2526
from amazon.opentelemetry.distro._aws_span_processing_util import (
2627
GEN_AI_REQUEST_MODEL,
2728
LOCAL_ROOT,
@@ -30,7 +31,6 @@
3031
UNKNOWN_OPERATION,
3132
UNKNOWN_REMOTE_OPERATION,
3233
UNKNOWN_REMOTE_SERVICE,
33-
UNKNOWN_SERVICE,
3434
extract_api_path_value,
3535
get_egress_operation,
3636
get_ingress_operation,
@@ -52,7 +52,6 @@
5252
from opentelemetry.semconv.trace import SpanAttributes
5353

5454
# Pertinent OTEL attribute keys
55-
_SERVICE_NAME: str = ResourceAttributes.SERVICE_NAME
5655
_DB_CONNECTION_STRING: str = SpanAttributes.DB_CONNECTION_STRING
5756
_DB_NAME: str = SpanAttributes.DB_NAME
5857
_DB_OPERATION: str = SpanAttributes.DB_OPERATION
@@ -92,10 +91,6 @@
9291
# Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
9392
_GRAPHQL: str = "graphql"
9493

95-
# As per https://opentelemetry.io/docs/specs/semconv/resource/#service, if service name is not specified, SDK defaults
96-
# the service name to unknown_service:<process name> or just unknown_service.
97-
_OTEL_UNKNOWN_SERVICE_PREFIX: str = "unknown_service"
98-
9994
_logger: Logger = getLogger(__name__)
10095

10196

@@ -141,15 +136,11 @@ def _generate_dependency_metric_attributes(span: ReadableSpan, resource: Resourc
141136

142137

143138
def _set_service(resource: Resource, span: ReadableSpan, attributes: BoundedAttributes) -> None:
144-
"""Service is always derived from SERVICE_NAME"""
145-
service: str = resource.attributes.get(_SERVICE_NAME)
146-
147-
# In practice the service name is never None, but we can be defensive here.
148-
if service is None or service.startswith(_OTEL_UNKNOWN_SERVICE_PREFIX):
139+
service_name, is_unknown = get_service_attribute(resource)
140+
if is_unknown:
149141
_log_unknown_attribute(AWS_LOCAL_SERVICE, span)
150-
service = UNKNOWN_SERVICE
151142

152-
attributes[AWS_LOCAL_SERVICE] = service
143+
attributes[AWS_LOCAL_SERVICE] = service_name
153144

154145

155146
def _set_ingress_operation(span: ReadableSpan, attributes: BoundedAttributes) -> None:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from amazon.opentelemetry.distro._aws_span_processing_util import UNKNOWN_SERVICE
2+
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
3+
4+
# As per https://opentelemetry.io/docs/specs/semconv/resource/#service, if service name is not specified, SDK defaults
5+
# the service name to unknown_service:<process name> or just unknown_service.
6+
_OTEL_UNKNOWN_SERVICE_PREFIX: str = "unknown_service"
7+
8+
def get_service_attribute(resource: Resource) -> (str, bool):
9+
"""Service is always derived from SERVICE_NAME"""
10+
service: str = resource.attributes.get(SERVICE_NAME)
11+
12+
# In practice the service name is never None, but we can be defensive here.
13+
if service is None or service.startswith(_OTEL_UNKNOWN_SERVICE_PREFIX):
14+
return UNKNOWN_SERVICE, True
15+
16+
return service, False

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

Lines changed: 94 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
44
import os
55
from logging import Logger, getLogger
6-
from typing import ClassVar, Dict, Type
6+
from typing import ClassVar, Dict, Type, Union
77

8+
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
89
from importlib_metadata import version
10+
from opentelemetry.metrics import set_meter_provider
11+
from opentelemetry.sdk.metrics.view import View, DefaultAggregation, DropAggregation
912
from typing_extensions import override
10-
13+
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE
1114
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
1215
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
1316
AttributePropagatingSpanProcessorBuilder,
1417
)
1518
from amazon.opentelemetry.distro.aws_metric_attributes_span_exporter_builder import (
1619
AwsMetricAttributesSpanExporterBuilder,
1720
)
21+
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
1822
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
1923
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
2024
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
@@ -27,7 +31,6 @@
2731
_import_id_generator,
2832
_import_sampler,
2933
_init_logging,
30-
_init_metrics,
3134
_OTelSDKConfigurator,
3235
)
3336
from opentelemetry.sdk.environment_variables import (
@@ -48,18 +51,19 @@
4851
ObservableUpDownCounter,
4952
UpDownCounter,
5053
)
51-
from opentelemetry.sdk.metrics.export import AggregationTemporality, PeriodicExportingMetricReader
52-
from opentelemetry.sdk.resources import Resource, get_aggregated_resources
54+
from opentelemetry.sdk.metrics.export import MetricExporter, MetricReader, AggregationTemporality, PeriodicExportingMetricReader
55+
from opentelemetry.sdk.resources import Resource, get_aggregated_resources, SERVICE_NAME
5356
from opentelemetry.sdk.trace import TracerProvider
5457
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
5558
from opentelemetry.sdk.trace.id_generator import IdGenerator
5659
from opentelemetry.sdk.trace.sampling import Sampler
5760
from opentelemetry.semconv.resource import ResourceAttributes
5861
from opentelemetry.trace import set_tracer_provider
5962

60-
APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
63+
DEPRECATED_APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
6164
APPLICATION_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"
62-
APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
65+
APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"
66+
DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
6367
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"
6468
METRIC_EXPORT_INTERVAL_CONFIG = "OTEL_METRIC_EXPORT_INTERVAL"
6569
DEFAULT_METRIC_EXPORT_INTERVAL = 60000.0
@@ -105,13 +109,15 @@ def _initialize_components():
105109

106110
auto_resource: Dict[str, any] = {}
107111
auto_resource = _customize_versions(auto_resource)
108-
resource = get_aggregated_resources(
109-
[
110-
AwsEc2ResourceDetector(),
111-
AwsEksResourceDetector(),
112-
AwsEcsResourceDetector(),
113-
]
114-
).merge(Resource.create(auto_resource))
112+
# auto_resource = _set_aws_attributes(auto_resource)
113+
resource = _customize_resource(
114+
get_aggregated_resources(
115+
[
116+
AwsEc2ResourceDetector(),
117+
AwsEksResourceDetector(),
118+
AwsEcsResourceDetector(),
119+
]
120+
).merge(Resource.create(auto_resource)))
115121

116122
sampler_name = _get_sampler()
117123
sampler = _custom_import_sampler(sampler_name, resource)
@@ -153,6 +159,49 @@ def _init_tracing(
153159
set_tracer_provider(trace_provider)
154160

155161

162+
def _init_metrics(
163+
exporters_or_readers: Dict[
164+
str, Union[Type[MetricExporter], Type[MetricReader]]
165+
],
166+
resource: Resource = None,
167+
):
168+
metric_readers = []
169+
views = []
170+
171+
for _, exporter_or_reader_class in exporters_or_readers.items():
172+
exporter_args = {}
173+
174+
if issubclass(exporter_or_reader_class, MetricReader):
175+
metric_readers.append(exporter_or_reader_class(**exporter_args))
176+
else:
177+
metric_readers.append(
178+
PeriodicExportingMetricReader(
179+
exporter_or_reader_class(**exporter_args)
180+
)
181+
)
182+
183+
if _is_application_signals_runtime_enabled():
184+
system_metrics_scope_name = "opentelemetry.instrumentation.system_metrics"
185+
if 0 == len(metric_readers):
186+
_logger.info("Registered scope %s", system_metrics_scope_name)
187+
views.append(View(
188+
meter_name=system_metrics_scope_name, aggregation=DefaultAggregation()
189+
))
190+
views.append(View(
191+
instrument_name="*",aggregation=DropAggregation()
192+
))
193+
194+
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
195+
scope_based_periodic_exporting_metric_reader = ScopeBasedPeriodicExportingMetricReader(
196+
exporter=otel_metric_exporter, export_interval_millis=_get_metric_export_interval(),
197+
registered_scope_names={system_metrics_scope_name},
198+
)
199+
metric_readers.append(scope_based_periodic_exporting_metric_reader)
200+
201+
provider = MeterProvider(resource=resource, metric_readers=metric_readers, views=views)
202+
set_meter_provider(provider)
203+
204+
156205
# END The OpenTelemetry Authors code
157206

158207

@@ -237,14 +286,9 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
237286
# Construct meterProvider
238287
_logger.info("AWS Application Signals enabled")
239288
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
240-
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
241-
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
242-
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
243-
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
244-
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
245-
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
289+
246290
periodic_exporting_metric_reader = PeriodicExportingMetricReader(
247-
exporter=otel_metric_exporter, export_interval_millis=export_interval_millis
291+
exporter=otel_metric_exporter, export_interval_millis=_get_metric_export_interval()
248292
)
249293
meter_provider: MeterProvider = MeterProvider(resource=resource, metric_readers=[periodic_exporting_metric_reader])
250294
# Construct and set application signals metrics processor
@@ -260,9 +304,25 @@ def _customize_versions(auto_resource: Dict[str, any]) -> Dict[str, any]:
260304
return auto_resource
261305

262306

307+
def _customize_resource(resource: Resource) -> Resource:
308+
service_name, is_unknown = get_service_attribute(resource)
309+
if is_unknown:
310+
_logger.debug("No valid service name found")
311+
312+
return resource.merge(Resource.create({AWS_LOCAL_SERVICE: service_name}))
313+
314+
263315
def _is_application_signals_enabled():
264316
return (
265-
os.environ.get(APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(APP_SIGNALS_ENABLED_CONFIG, "false")).lower()
317+
os.environ.get(APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(DEPRECATED_APP_SIGNALS_ENABLED_CONFIG, "false")).lower()
318+
== "true"
319+
)
320+
321+
322+
323+
def _is_application_signals_runtime_enabled():
324+
return _is_application_signals_enabled and (
325+
os.environ.get(APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG, "true").lower()
266326
== "true"
267327
)
268328

@@ -272,6 +332,16 @@ def _is_lambda_environment():
272332
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
273333

274334

335+
def _get_metric_export_interval():
336+
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
337+
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
338+
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
339+
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
340+
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
341+
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
342+
return export_interval_millis
343+
344+
275345
class ApplicationSignalsExporterProvider:
276346
_instance: ClassVar["ApplicationSignalsExporterProvider"] = None
277347

@@ -307,7 +377,7 @@ def create_exporter(self):
307377
if protocol == "http/protobuf":
308378
application_signals_endpoint = os.environ.get(
309379
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
310-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
380+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
311381
)
312382
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
313383
return OTLPHttpOTLPMetricExporter(
@@ -323,7 +393,7 @@ def create_exporter(self):
323393

324394
application_signals_endpoint = os.environ.get(
325395
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
326-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "localhost:4315"),
396+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "localhost:4315"),
327397
)
328398
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
329399
return OTLPGrpcOTLPMetricExporter(
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from logging import Logger, getLogger
2+
from typing import Optional
3+
4+
from opentelemetry.context import attach, detach, set_value, _SUPPRESS_INSTRUMENTATION_KEY
5+
from opentelemetry.sdk.metrics.export import ResourceMetrics, MetricsData, MetricExporter, PeriodicExportingMetricReader
6+
7+
_logger: Logger = getLogger(__name__)
8+
9+
class ScopeBasedPeriodicExportingMetricReader(PeriodicExportingMetricReader):
10+
11+
def __init__(
12+
self,
13+
exporter: MetricExporter,
14+
export_interval_millis: Optional[float] = None,
15+
export_timeout_millis: Optional[float] = None,
16+
registered_scope_names: set[str] = None,
17+
):
18+
super().__init__(exporter, export_interval_millis, export_timeout_millis)
19+
self._registered_scope_names = registered_scope_names
20+
21+
def _receive_metrics(
22+
self,
23+
metrics_data: MetricsData,
24+
timeout_millis: float = 10_000,
25+
**kwargs,
26+
) -> None:
27+
28+
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
29+
# pylint: disable=broad-exception-caught,invalid-name
30+
try:
31+
with self._export_lock:
32+
exporting_resource_metrics = []
33+
for metric in metrics_data.resource_metrics:
34+
exporting_scope_metrics = []
35+
for scope_metric in metric.scope_metrics:
36+
if scope_metric.scope.name in self._registered_scope_names:
37+
exporting_scope_metrics.append(scope_metric)
38+
if len(exporting_scope_metrics) > 0:
39+
exporting_resource_metrics.append(ResourceMetrics(
40+
resource=metric.resource,
41+
scope_metrics=exporting_scope_metrics,
42+
schema_url=metric.schema_url,
43+
))
44+
if len(exporting_resource_metrics) > 0:
45+
new_metrics_data = MetricsData(
46+
resource_metrics=exporting_resource_metrics
47+
)
48+
self._exporter.export(
49+
new_metrics_data, timeout_millis=timeout_millis
50+
)
51+
except Exception as e:
52+
_logger.exception("Exception while exporting metrics %s", str(e))
53+
detach(token)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
_customize_exporter,
1616
_customize_sampler,
1717
_customize_span_processors,
18-
_is_application_signals_enabled,
18+
_is_application_signals_enabled, APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG,
1919
)
2020
from amazon.opentelemetry.distro.aws_opentelemetry_distro import AwsOpenTelemetryDistro
2121
from amazon.opentelemetry.distro.aws_span_metrics_processor import AwsSpanMetricsProcessor
@@ -53,6 +53,7 @@ def setUpClass(cls):
5353
os.environ[OTEL_TRACES_EXPORTER] = "none"
5454
os.environ[OTEL_METRICS_EXPORTER] = "none"
5555
os.environ[OTEL_LOGS_EXPORTER] = "none"
56+
os.environ[APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG] = "false"
5657
os.environ[OTEL_TRACES_SAMPLER] = "traceidratio"
5758
os.environ[OTEL_TRACES_SAMPLER_ARG] = "0.01"
5859

0 commit comments

Comments
 (0)