Skip to content

Commit 3304ad5

Browse files
committed
Add Application Signals runtime metrics
1 parent f003a1f commit 3304ad5

File tree

6 files changed

+309
-33
lines changed

6 files changed

+309
-33
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: 84 additions & 15 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,8 +21,10 @@
1921
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
2022
from amazon.opentelemetry.distro.otlp_udp_exporter import 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
2326
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
27+
from opentelemetry.metrics import set_meter_provider
2428
from opentelemetry.sdk._configuration import (
2529
_get_exporter_names,
2630
_get_id_generator,
@@ -29,7 +33,6 @@
2933
_import_id_generator,
3034
_import_sampler,
3135
_init_logging,
32-
_init_metrics,
3336
_OTelSDKConfigurator,
3437
)
3538
from opentelemetry.sdk.environment_variables import (
@@ -50,7 +53,13 @@
5053
ObservableUpDownCounter,
5154
UpDownCounter,
5255
)
53-
from opentelemetry.sdk.metrics.export import AggregationTemporality, PeriodicExportingMetricReader
56+
from opentelemetry.sdk.metrics.export import (
57+
AggregationTemporality,
58+
MetricExporter,
59+
MetricReader,
60+
PeriodicExportingMetricReader,
61+
)
62+
from opentelemetry.sdk.metrics.view import DefaultAggregation, DropAggregation, View
5463
from opentelemetry.sdk.resources import Resource, get_aggregated_resources
5564
from opentelemetry.sdk.trace import TracerProvider
5665
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
@@ -59,9 +68,10 @@
5968
from opentelemetry.semconv.resource import ResourceAttributes
6069
from opentelemetry.trace import set_tracer_provider
6170

62-
APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
71+
DEPRECATED_APP_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APP_SIGNALS_ENABLED"
6372
APPLICATION_SIGNALS_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_ENABLED"
64-
APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
73+
APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED"
74+
DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APP_SIGNALS_EXPORTER_ENDPOINT"
6575
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG = "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT"
6676
METRIC_EXPORT_INTERVAL_CONFIG = "OTEL_METRIC_EXPORT_INTERVAL"
6777
DEFAULT_METRIC_EXPORT_INTERVAL = 60000.0
@@ -171,6 +181,27 @@ def _init_tracing(
171181
set_tracer_provider(trace_provider)
172182

173183

184+
def _init_metrics(
185+
exporters_or_readers: Dict[str, Union[Type[MetricExporter], Type[MetricReader]]],
186+
resource: Resource = None,
187+
):
188+
metric_readers = []
189+
views = []
190+
191+
for _, exporter_or_reader_class in exporters_or_readers.items():
192+
exporter_args = {}
193+
194+
if issubclass(exporter_or_reader_class, MetricReader):
195+
metric_readers.append(exporter_or_reader_class(**exporter_args))
196+
else:
197+
metric_readers.append(PeriodicExportingMetricReader(exporter_or_reader_class(**exporter_args)))
198+
199+
_customize_metric_exporters(metric_readers, views)
200+
201+
provider = MeterProvider(resource=resource, metric_readers=metric_readers, views=views)
202+
set_meter_provider(provider)
203+
204+
174205
# END The OpenTelemetry Authors code
175206

176207

@@ -303,14 +334,9 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
303334
# Construct meterProvider
304335
_logger.info("AWS Application Signals enabled")
305336
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
306-
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
307-
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
308-
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
309-
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
310-
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
311-
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
337+
312338
periodic_exporting_metric_reader = PeriodicExportingMetricReader(
313-
exporter=otel_metric_exporter, export_interval_millis=export_interval_millis
339+
exporter=otel_metric_exporter, export_interval_millis=_get_metric_export_interval()
314340
)
315341
meter_provider: MeterProvider = MeterProvider(resource=resource, metric_readers=[periodic_exporting_metric_reader])
316342
# Construct and set application signals metrics processor
@@ -319,25 +345,68 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
319345
return
320346

321347

348+
def _customize_metric_exporters(metric_readers: List[MetricReader], views: List[View]) -> None:
349+
if _is_application_signals_runtime_enabled():
350+
system_metrics_scope_name = "opentelemetry.instrumentation.system_metrics"
351+
if 0 == len(metric_readers):
352+
_logger.info("Registered scope %s", system_metrics_scope_name)
353+
views.append(View(meter_name=system_metrics_scope_name, aggregation=DefaultAggregation()))
354+
views.append(View(instrument_name="*", aggregation=DropAggregation()))
355+
356+
otel_metric_exporter = ApplicationSignalsExporterProvider().create_exporter()
357+
scope_based_periodic_exporting_metric_reader = ScopeBasedPeriodicExportingMetricReader(
358+
exporter=otel_metric_exporter,
359+
export_interval_millis=_get_metric_export_interval(),
360+
registered_scope_names={system_metrics_scope_name},
361+
)
362+
metric_readers.append(scope_based_periodic_exporting_metric_reader)
363+
364+
322365
def _customize_versions(auto_resource: Dict[str, any]) -> Dict[str, any]:
323366
distro_version = version("aws-opentelemetry-distro")
324367
auto_resource[ResourceAttributes.TELEMETRY_AUTO_VERSION] = distro_version + "-aws"
325368
_logger.debug("aws-opentelementry-distro - version: %s", auto_resource[ResourceAttributes.TELEMETRY_AUTO_VERSION])
326369
return auto_resource
327370

328371

372+
def _customize_resource(resource: Resource) -> Resource:
373+
service_name, is_unknown = get_service_attribute(resource)
374+
if is_unknown:
375+
_logger.debug("No valid service name found")
376+
377+
return resource.merge(Resource.create({AWS_LOCAL_SERVICE: service_name}))
378+
379+
329380
def _is_application_signals_enabled():
330381
return (
331-
os.environ.get(APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(APP_SIGNALS_ENABLED_CONFIG, "false")).lower()
382+
os.environ.get(
383+
APPLICATION_SIGNALS_ENABLED_CONFIG, os.environ.get(DEPRECATED_APP_SIGNALS_ENABLED_CONFIG, "false")
384+
).lower()
332385
== "true"
333386
)
334387

335388

389+
def _is_application_signals_runtime_enabled():
390+
return _is_application_signals_enabled() and (
391+
os.environ.get(APPLICATION_SIGNALS_RUNTIME_ENABLED_CONFIG, "true").lower() == "true"
392+
)
393+
394+
336395
def _is_lambda_environment():
337396
# detect if running in AWS Lambda environment
338397
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
339398

340399

400+
def _get_metric_export_interval():
401+
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
402+
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
403+
# Cap export interval to 60 seconds. This is currently required for metrics-trace correlation to work correctly.
404+
if export_interval_millis > DEFAULT_METRIC_EXPORT_INTERVAL:
405+
export_interval_millis = DEFAULT_METRIC_EXPORT_INTERVAL
406+
_logger.info("AWS Application Signals metrics export interval capped to %s", export_interval_millis)
407+
return export_interval_millis
408+
409+
341410
def _span_export_batch_size():
342411
return LAMBDA_SPAN_EXPORT_BATCH_SIZE if _is_lambda_environment() else None
343412

@@ -372,7 +441,7 @@ def create_exporter(self):
372441
if protocol == "http/protobuf":
373442
application_signals_endpoint = os.environ.get(
374443
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
375-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
444+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "http://localhost:4316/v1/metrics"),
376445
)
377446
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
378447
return OTLPHttpOTLPMetricExporter(
@@ -388,7 +457,7 @@ def create_exporter(self):
388457

389458
application_signals_endpoint = os.environ.get(
390459
APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG,
391-
os.environ.get(APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "localhost:4315"),
460+
os.environ.get(DEPRECATED_APP_SIGNALS_EXPORTER_ENDPOINT_CONFIG, "localhost:4315"),
392461
)
393462
_logger.debug("AWS Application Signals export endpoint: %s", application_signals_endpoint)
394463
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)