Skip to content

Commit 73bc66d

Browse files
committed
Add Application Signals runtime metrics
1 parent ec06e1c commit 73bc66d

File tree

6 files changed

+319
-41
lines changed

6 files changed

+319
-41
lines changed

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

Lines changed: 5 additions & 14 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,
@@ -47,12 +47,11 @@
4747
MetricAttributeGenerator,
4848
)
4949
from amazon.opentelemetry.distro.sqs_url_parser import SqsUrlParser
50-
from opentelemetry.sdk.resources import Resource, ResourceAttributes
50+
from opentelemetry.sdk.resources import Resource
5151
from opentelemetry.sdk.trace import BoundedAttributes, ReadableSpan
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from amazon.opentelemetry.distro._aws_span_processing_util import UNKNOWN_SERVICE
4+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
5+
6+
# As per https://opentelemetry.io/docs/specs/semconv/resource/#service, if service name is not specified, SDK defaults
7+
# the service name to unknown_service:<process name> or just unknown_service.
8+
_OTEL_UNKNOWN_SERVICE_PREFIX: str = "unknown_service"
9+
10+
11+
def get_service_attribute(resource: Resource) -> (str, bool):
12+
"""Service is always derived from SERVICE_NAME"""
13+
service: str = resource.attributes.get(SERVICE_NAME)
14+
15+
# In practice the service name is never None, but we can be defensive here.
16+
if service is None or service.startswith(_OTEL_UNKNOWN_SERVICE_PREFIX):
17+
return UNKNOWN_SERVICE, True
18+
19+
return service, False

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

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
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, List, Type, Union
77

88
from importlib_metadata import version
99
from typing_extensions import override
1010

11+
from amazon.opentelemetry.distro._aws_attribute_keys import AWS_LOCAL_SERVICE
12+
from amazon.opentelemetry.distro._aws_resource_attribute_configurator import get_service_attribute
1113
from amazon.opentelemetry.distro.always_record_sampler import AlwaysRecordSampler
1214
from amazon.opentelemetry.distro.attribute_propagating_span_processor_builder import (
1315
AttributePropagatingSpanProcessorBuilder,
@@ -19,7 +21,9 @@
1921
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
2022
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
2123
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
24+
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
2225
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
26+
from opentelemetry.metrics import set_meter_provider
2327
from opentelemetry.sdk._configuration import (
2428
_get_exporter_names,
2529
_get_id_generator,
@@ -28,7 +32,6 @@
2832
_import_id_generator,
2933
_import_sampler,
3034
_init_logging,
31-
_init_metrics,
3235
_OTelSDKConfigurator,
3336
)
3437
from opentelemetry.sdk.environment_variables import (
@@ -49,7 +52,13 @@
4952
ObservableUpDownCounter,
5053
UpDownCounter,
5154
)
52-
from opentelemetry.sdk.metrics.export import AggregationTemporality, PeriodicExportingMetricReader
55+
from opentelemetry.sdk.metrics.export import (
56+
AggregationTemporality,
57+
MetricExporter,
58+
MetricReader,
59+
PeriodicExportingMetricReader,
60+
)
61+
from opentelemetry.sdk.metrics.view import DefaultAggregation, DropAggregation, View
5362
from opentelemetry.sdk.resources import Resource, get_aggregated_resources
5463
from opentelemetry.sdk.trace import TracerProvider
5564
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
@@ -58,9 +67,10 @@
5867
from opentelemetry.semconv.resource import ResourceAttributes
5968
from opentelemetry.trace import set_tracer_provider
6069

61-
APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
70+
DEPRECATED_APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
6271
APPLICATION_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"
63-
APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
72+
APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"
73+
DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
6474
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"
6575
METRIC_EXPORT_INTERVAL_CONFIG = "OTEL_METRIC_EXPORT_INTERVAL"
6676
DEFAULT_METRIC_EXPORT_INTERVAL = 60000.0
@@ -112,13 +122,16 @@ def _initialize_components():
112122

113123
auto_resource: Dict[str, any] = {}
114124
auto_resource = _customize_versions(auto_resource)
115-
resource = get_aggregated_resources(
116-
[
117-
AwsEc2ResourceDetector(),
118-
AwsEksResourceDetector(),
119-
AwsEcsResourceDetector(),
120-
]
121-
).merge(Resource.create(auto_resource))
125+
# auto_resource = _set_aws_attributes(auto_resource)
126+
resource = _customize_resource(
127+
get_aggregated_resources(
128+
[
129+
AwsEc2ResourceDetector(),
130+
AwsEksResourceDetector(),
131+
AwsEcsResourceDetector(),
132+
]
133+
).merge(Resource.create(auto_resource))
134+
)
122135

123136
sampler_name = _get_sampler()
124137
sampler = _custom_import_sampler(sampler_name, resource)
@@ -160,6 +173,27 @@ def _init_tracing(
160173
set_tracer_provider(trace_provider)
161174

162175

176+
def _init_metrics(
177+
exporters_or_readers: Dict[str, Union[Type[MetricExporter], Type[MetricReader]]],
178+
resource: Resource = None,
179+
):
180+
metric_readers = []
181+
views = []
182+
183+
for _, exporter_or_reader_class in exporters_or_readers.items():
184+
exporter_args = {}
185+
186+
if issubclass(exporter_or_reader_class, MetricReader):
187+
metric_readers.append(exporter_or_reader_class(**exporter_args))
188+
else:
189+
metric_readers.append(PeriodicExportingMetricReader(exporter_or_reader_class(**exporter_args)))
190+
191+
_customize_metric_exporters(metric_readers, views)
192+
193+
provider = MeterProvider(resource=resource, metric_readers=metric_readers, views=views)
194+
set_meter_provider(provider)
195+
196+
163197
# END The OpenTelemetry Authors code
164198

165199

@@ -283,14 +317,9 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
283317
# Construct meterProvider
284318
_logger.info("AWS Application Signals enabled")
285319
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
286-
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
287-
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
288-
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
289-
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
290-
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
291-
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
320+
292321
periodic_exporting_metric_reader = PeriodicExportingMetricReader(
293-
exporter=otel_metric_exporter, export_interval_millis=export_interval_millis
322+
exporter=otel_metric_exporter, export_interval_millis=_get_metric_export_interval()
294323
)
295324
meter_provider: MeterProvider = MeterProvider(resource=resource, metric_readers=[periodic_exporting_metric_reader])
296325
# Construct and set application signals metrics processor
@@ -299,25 +328,68 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
299328
return
300329

301330

331+
def _customize_metric_exporters(metric_readers: List[MetricReader], views: List[View]) -> None:
332+
if _is_application_signals_runtime_enabled():
333+
system_metrics_scope_name = "opentelemetry.instrumentation.system_metrics"
334+
if 0 == len(metric_readers):
335+
_logger.info("Registered scope %s", system_metrics_scope_name)
336+
views.append(View(meter_name=system_metrics_scope_name, aggregation=DefaultAggregation()))
337+
views.append(View(instrument_name="*", aggregation=DropAggregation()))
338+
339+
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
340+
scope_based_periodic_exporting_metric_reader = ScopeBasedPeriodicExportingMetricReader(
341+
exporter=otel_metric_exporter,
342+
export_interval_millis=_get_metric_export_interval(),
343+
registered_scope_names={system_metrics_scope_name},
344+
)
345+
metric_readers.append(scope_based_periodic_exporting_metric_reader)
346+
347+
302348
def _customize_versions(auto_resource: Dict[str, any]) -> Dict[str, any]:
303349
distro_version = version("aws-opentelemetry-distro")
304350
auto_resource[ResourceAttributes.TELEMETRY_AUTO_VERSION] = distro_version + "-aws"
305351
_logger.debug("aws-opentelementry-distro - version: %s", auto_resource[ResourceAttributes.TELEMETRY_AUTO_VERSION])
306352
return auto_resource
307353

308354

355+
def _customize_resource(resource: Resource) -> Resource:
356+
service_name, is_unknown = get_service_attribute(resource)
357+
if is_unknown:
358+
_logger.debug("No valid service name found")
359+
360+
return resource.merge(Resource.create({AWS_LOCAL_SERVICE: service_name}))
361+
362+
309363
def _is_application_signals_enabled():
310364
return (
311-
os.environ.get(APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(APP_SIGNALS_ENABLED_CONFIG, "false")).lower()
365+
os.environ.get(
366+
APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(DEPRECATED_APP_SIGNALS_ENABLED_CONFIG, "false")
367+
).lower()
312368
== "true"
313369
)
314370

315371

372+
def _is_application_signals_runtime_enabled():
373+
return _is_application_signals_enabled() and (
374+
os.environ.get(APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG, "true").lower() == "true"
375+
)
376+
377+
316378
def _is_lambda_environment():
317379
# detect if running in AWS Lambda environment
318380
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
319381

320382

383+
def _get_metric_export_interval():
384+
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
385+
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
386+
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
387+
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
388+
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
389+
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
390+
return export_interval_millis
391+
392+
321393
class ApplicationSignalsExporterProvider:
322394
_instance: ClassVar["ApplicationSignalsExporterProvider"] = None
323395

@@ -353,7 +425,7 @@ def create_exporter(self):
353425
if protocol == "http/protobuf":
354426
application_signals_endpoint = os.environ.get(
355427
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
356-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
428+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
357429
)
358430
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
359431
return OTLPHttpOTLPMetricExporter(
@@ -369,7 +441,7 @@ def create_exporter(self):
369441

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

0 commit comments

Comments
 (0)