Skip to content

Commit e902760

Browse files
authored
SigV4 Authentication support for http/protobuf exporter (#324)
*Issue #, if available:* Adding SigV4 Authentication extension for Exporting traces to OTLP CloudWatch endpoint without needing to explictily install the collector. *Description of changes:* Added a new class that extends upstream's OTLP http span exporter. Overrides the _export method so that if the endpoint is CW, we add an extra step of injecting SigV4 authentication to the headers. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
2 parents 0c3ade0 + 6cf44bd commit e902760

File tree

6 files changed

+355
-20
lines changed

6 files changed

+355
-20
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import sys
5+
from logging import Logger, getLogger
6+
7+
import pkg_resources
8+
9+
_logger: Logger = getLogger(__name__)
10+
11+
12+
def is_installed(req: str) -> bool:
13+
"""Is the given required package installed?"""
14+
15+
if req in sys.modules and sys.modules[req] is not None:
16+
return True
17+
18+
try:
19+
pkg_resources.get_distribution(req)
20+
except Exception as exc: # pylint: disable=broad-except
21+
_logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc)
22+
return False
23+
return True

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# SPDX-License-Identifier: Apache-2.0
33
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
44
import os
5+
import re
56
from logging import Logger, getLogger
67
from typing import ClassVar, Dict, List, Type, Union
78

@@ -19,6 +20,7 @@
1920
AwsMetricAttributesSpanExporterBuilder,
2021
)
2122
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
23+
from amazon.opentelemetry.distro.otlp_aws_span_exporter import OTLPAwsSpanExporter
2224
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter
2325
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2426
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
@@ -81,6 +83,7 @@
8183
OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED_CONFIG = "OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED"
8284
SYSTEM_METRICS_INSTRUMENTATION_SCOPE_NAME = "opentelemetry.instrumentation.system_metrics"
8385
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
86+
XRAY_OTLP_ENDPOINT_PATTERN = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$"
8487
# UDP package size is not larger than 64KB
8588
LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10
8689

@@ -315,6 +318,11 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span
315318
traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
316319
span_exporter = OTLPUdpSpanExporter(endpoint=traces_endpoint)
317320

321+
if isinstance(span_exporter, OTLPSpanExporter) and is_xray_otlp_endpoint(
322+
os.environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
323+
):
324+
span_exporter = OTLPAwsSpanExporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT))
325+
318326
if not _is_application_signals_enabled():
319327
return span_exporter
320328

@@ -328,6 +336,10 @@ def _customize_span_processors(provider: TracerProvider, resource: Resource) ->
328336
# Construct and set local and remote attributes span processor
329337
provider.add_span_processor(AttributePropagatingSpanProcessorBuilder().build())
330338

339+
# Do not export Application-Signals metrics if it's XRay OTLP endpoint
340+
if is_xray_otlp_endpoint():
341+
return
342+
331343
# Export 100% spans and not export Application-Signals metrics if on Lambda.
332344
if _is_lambda_environment():
333345
_export_unsampled_span_for_lambda(provider, resource)
@@ -437,6 +449,15 @@ def _is_lambda_environment():
437449
return AWS_LAMBDA_FUNCTION_NAME_CONFIG in os.environ
438450

439451

452+
def is_xray_otlp_endpoint(otlp_endpoint: str = None) -> bool:
453+
"""Is the given endpoint the XRay OTLP endpoint?"""
454+
455+
if not otlp_endpoint:
456+
return False
457+
458+
return bool(re.match(XRAY_OTLP_ENDPOINT_PATTERN, otlp_endpoint.lower()))
459+
460+
440461
def _get_metric_export_interval():
441462
export_interval_millis = float(os.environ.get(METRIC_EXPORT_INTERVAL_CONFIG, DEFAULT_METRIC_EXPORT_INTERVAL))
442463
_logger.debug("Span Metrics export interval: %s", export_interval_millis)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import logging
4+
from typing import Dict, Optional
5+
6+
import requests
7+
8+
from amazon.opentelemetry.distro._utils import is_installed
9+
from opentelemetry.exporter.otlp.proto.http import Compression
10+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
11+
12+
AWS_SERVICE = "xray"
13+
_logger = logging.getLogger(__name__)
14+
15+
16+
class OTLPAwsSpanExporter(OTLPSpanExporter):
17+
"""
18+
This exporter extends the functionality of the OTLPSpanExporter to allow spans to be exported to the
19+
XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the botocore
20+
library to sign and directly inject SigV4 Authentication to the exported request's headers.
21+
22+
https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html
23+
"""
24+
25+
def __init__(
26+
self,
27+
endpoint: Optional[str] = None,
28+
certificate_file: Optional[str] = None,
29+
client_key_file: Optional[str] = None,
30+
client_certificate_file: Optional[str] = None,
31+
headers: Optional[Dict[str, str]] = None,
32+
timeout: Optional[int] = None,
33+
compression: Optional[Compression] = None,
34+
rsession: Optional[requests.Session] = None,
35+
):
36+
37+
self._aws_region = None
38+
39+
# Requires botocore to be installed to sign the headers. However,
40+
# some users might not need to use this exporter. In order not conflict
41+
# with existing behavior, we check for botocore before initializing this exporter.
42+
43+
if endpoint and is_installed("botocore"):
44+
# pylint: disable=import-outside-toplevel
45+
from botocore import auth, awsrequest, session
46+
47+
self.boto_auth = auth
48+
self.boto_aws_request = awsrequest
49+
self.boto_session = session.Session()
50+
51+
# Assumes only valid endpoints passed are of XRay OTLP format.
52+
# The only usecase for this class would be for ADOT Python Auto Instrumentation and that already validates
53+
# the endpoint to be an XRay OTLP endpoint.
54+
self._aws_region = endpoint.split(".")[1]
55+
56+
else:
57+
_logger.error(
58+
"botocore is required to export traces to %s. Please install it using `pip install botocore`",
59+
endpoint,
60+
)
61+
62+
super().__init__(
63+
endpoint=endpoint,
64+
certificate_file=certificate_file,
65+
client_key_file=client_key_file,
66+
client_certificate_file=client_certificate_file,
67+
headers=headers,
68+
timeout=timeout,
69+
compression=compression,
70+
session=rsession,
71+
)
72+
73+
# Overrides upstream's private implementation of _export. All behaviors are
74+
# the same except if the endpoint is an XRay OTLP endpoint, we will sign the request
75+
# with SigV4 in headers before sending it to the endpoint. Otherwise, we will skip signing.
76+
def _export(self, serialized_data: bytes):
77+
request = self.boto_aws_request.AWSRequest(
78+
method="POST",
79+
url=self._endpoint,
80+
data=serialized_data,
81+
headers={"Content-Type": "application/x-protobuf"},
82+
)
83+
84+
credentials = self.boto_session.get_credentials()
85+
86+
if credentials is not None:
87+
signer = self.boto_auth.SigV4Auth(credentials, AWS_SERVICE, self._aws_region)
88+
89+
try:
90+
signer.add_auth(request)
91+
self._session.headers.update(dict(request.headers))
92+
93+
except Exception as signing_error: # pylint: disable=broad-except
94+
_logger.error("Failed to sign request: %s", signing_error)
95+
96+
return super()._export(serialized_data)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_instrumentation_patch.py

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
# SPDX-License-Identifier: Apache-2.0
33
# Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License.
44
import os
5-
import sys
65
from logging import Logger, getLogger
76

8-
import pkg_resources
9-
7+
from amazon.opentelemetry.distro._utils import is_installed
108
from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches
119

1210
# Env variable for determining whether we want to monkey patch gevent modules. Possible values are 'all', 'none', and
@@ -25,7 +23,7 @@ def apply_instrumentation_patches() -> None:
2523
2624
Where possible, automated testing should be run to catch upstream changes resulting in broken patches
2725
"""
28-
if _is_installed("gevent"):
26+
if is_installed("gevent"):
2927
try:
3028
gevent_patch_module = os.environ.get(AWS_GEVENT_PATCH_MODULES, "all")
3129

@@ -56,7 +54,7 @@ def apply_instrumentation_patches() -> None:
5654
except Exception as exc: # pylint: disable=broad-except
5755
_logger.info("Failed to monkey patch gevent, exception: %s", exc)
5856

59-
if _is_installed("botocore ~= 1.0"):
57+
if is_installed("botocore ~= 1.0"):
6058
# pylint: disable=import-outside-toplevel
6159
# Delay import to only occur if patches is safe to apply (e.g. the instrumented library is installed).
6260
from amazon.opentelemetry.distro.patches._botocore_patches import _apply_botocore_instrumentation_patches
@@ -66,15 +64,3 @@ def apply_instrumentation_patches() -> None:
6664
# No need to check if library is installed as this patches opentelemetry.sdk,
6765
# which must be installed for the distro to work at all.
6866
_apply_resource_detector_patches()
69-
70-
71-
def _is_installed(req: str) -> bool:
72-
if req in sys.modules:
73-
return True
74-
75-
try:
76-
pkg_resources.get_distribution(req)
77-
except Exception as exc: # pylint: disable=broad-except
78-
_logger.debug("Skipping instrumentation patch: package %s, exception: %s", req, exc)
79-
return False
80-
return True

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@
3838
_LAMBDA_SOURCE_MAPPING_ID: str = "lambdaEventSourceMappingID"
3939

4040
# Patch names
41-
GET_DISTRIBUTION_PATCH: str = (
42-
"amazon.opentelemetry.distro.patches._instrumentation_patch.pkg_resources.get_distribution"
43-
)
41+
GET_DISTRIBUTION_PATCH: str = "amazon.opentelemetry.distro._utils.pkg_resources.get_distribution"
4442

4543

4644
class TestInstrumentationPatch(TestCase):

0 commit comments

Comments
 (0)