Skip to content

Commit e9f772c

Browse files
committed
Add Application Signals runtime metrics
1 parent 90f7fa0 commit e9f772c

File tree

6 files changed

+320
-41
lines changed

6 files changed

+320
-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,
@@ -18,7 +20,9 @@
1820
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
1921
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpMetricExporter, OTLPUdpSpanExporter
2022
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
23+
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
2124
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
25+
from opentelemetry.metrics import set_meter_provider
2226
from opentelemetry.sdk._configuration import (
2327
_get_exporter_names,
2428
_get_id_generator,
@@ -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,7 +51,13 @@
4851
ObservableUpDownCounter,
4952
UpDownCounter,
5053
)
51-
from opentelemetry.sdk.metrics.export import AggregationTemporality, PeriodicExportingMetricReader
54+
from opentelemetry.sdk.metrics.export import (
55+
AggregationTemporality,
56+
MetricExporter,
57+
MetricReader,
58+
PeriodicExportingMetricReader,
59+
)
60+
from opentelemetry.sdk.metrics.view import DefaultAggregation, DropAggregation, View
5261
from opentelemetry.sdk.resources import Resource, get_aggregated_resources
5362
from opentelemetry.sdk.trace import TracerProvider
5463
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
@@ -57,9 +66,10 @@
5766
from opentelemetry.semconv.resource import ResourceAttributes
5867
from opentelemetry.trace import set_tracer_provider
5968

60-
APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
69+
DEPRECATED_APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
6170
APPLICATION_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"
62-
APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
71+
APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"
72+
DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
6373
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"
6474
METRIC_EXPORT_INTERVAL_CONFIG = "OTEL_METRIC_EXPORT_INTERVAL"
6575
DEFAULT_METRIC_EXPORT_INTERVAL = 60000.0
@@ -111,13 +121,16 @@ def _initialize_components():
111121

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

122135
sampler_name = _get_sampler()
123136
sampler = _custom_import_sampler(sampler_name, resource)
@@ -159,6 +172,27 @@ def _init_tracing(
159172
set_tracer_provider(trace_provider)
160173

161174

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

164198

@@ -264,14 +298,9 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
264298
# Construct meterProvider
265299
_logger.info("AWS Application Signals enabled")
266300
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
267-
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
268-
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
269-
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
270-
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
271-
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
272-
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
301+
273302
periodic_exporting_metric_reader = PeriodicExportingMetricReader(
274-
exporter=otel_metric_exporter, export_interval_millis=export_interval_millis
303+
exporter=otel_metric_exporter, export_interval_millis=_get_metric_export_interval()
275304
)
276305
meter_provider: MeterProvider = MeterProvider(resource=resource, metric_readers=[periodic_exporting_metric_reader])
277306
# Construct and set application signals metrics processor
@@ -280,25 +309,68 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
280309
return
281310

282311

312+
def _customize_metric_exporters(metric_readers: List[MetricReader], views: List[View]) -> None:
313+
if _is_application_signals_runtime_enabled():
314+
system_metrics_scope_name = "opentelemetry.instrumentation.system_metrics"
315+
if 0 == len(metric_readers):
316+
_logger.info("Registered scope %s", system_metrics_scope_name)
317+
views.append(View(meter_name=system_metrics_scope_name, aggregation=DefaultAggregation()))
318+
views.append(View(instrument_name="*", aggregation=DropAggregation()))
319+
320+
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
321+
scope_based_periodic_exporting_metric_reader = ScopeBasedPeriodicExportingMetricReader(
322+
exporter=otel_metric_exporter,
323+
export_interval_millis=_get_metric_export_interval(),
324+
registered_scope_names={system_metrics_scope_name},
325+
)
326+
metric_readers.append(scope_based_periodic_exporting_metric_reader)
327+
328+
283329
def _customize_versions(auto_resource: Dict[str, any]) -> Dict[str, any]:
284330
distro_version = version("aws-opentelemetry-distro")
285331
auto_resource[ResourceAttributes.TELEMETRY_AUTO_VERSION] = distro_version + "-aws"
286332
_logger.debug("aws-opentelementry-distro - version: %s", auto_resource[ResourceAttributes.TELEMETRY_AUTO_VERSION])
287333
return auto_resource
288334

289335

336+
def _customize_resource(resource: Resource) -> Resource:
337+
service_name, is_unknown = get_service_attribute(resource)
338+
if is_unknown:
339+
_logger.debug("No valid service name found")
340+
341+
return resource.merge(Resource.create({AWS_LOCAL_SERVICE: service_name}))
342+
343+
290344
def _is_application_signals_enabled():
291345
return (
292-
os.environ.get(APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(APP_SIGNALS_ENABLED_CONFIG, "false")).lower()
346+
os.environ.get(
347+
APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(DEPRECATED_APP_SIGNALS_ENABLED_CONFIG, "false")
348+
).lower()
293349
== "true"
294350
)
295351

296352

353+
def _is_application_signals_runtime_enabled():
354+
return _is_application_signals_enabled() and (
355+
os.environ.get(APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG, "true").lower() == "true"
356+
)
357+
358+
297359
def _is_lambda_environment():
298360
# detect if running in AWS Lambda environment
299361
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
300362

301363

364+
def _get_metric_export_interval():
365+
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
366+
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
367+
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
368+
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
369+
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
370+
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
371+
return export_interval_millis
372+
373+
302374
class ApplicationSignalsExporterProvider:
303375
_instance: ClassVar["ApplicationSignalsExporterProvider"] = None
304376

@@ -334,7 +406,7 @@ def create_exporter(self):
334406
if protocol == "http/protobuf":
335407
application_signals_endpoint = os.environ.get(
336408
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
337-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
409+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
338410
)
339411
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
340412
return OTLPHttpOTLPMetricExporter(
@@ -350,7 +422,7 @@ def create_exporter(self):
350422

351423
application_signals_endpoint = os.environ.get(
352424
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
353-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "localhost:4315"),
425+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "localhost:4315"),
354426
)
355427
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
356428
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)