Skip to content

Commit dd37193

Browse files
committed
implement sigv4 support for logs exporter
1 parent 6e438d6 commit dd37193

File tree

5 files changed

+200
-103
lines changed

5 files changed

+200
-103
lines changed

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

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

99
from importlib_metadata import version
@@ -21,11 +21,13 @@
2121
AwsMetricAttributesSpanExporterBuilder,
2222
)
2323
from amazon.opentelemetry.distro.aws_span_metrics_processor_builder import AwsSpanMetricsProcessorBuilder
24-
from amazon.opentelemetry.distro.otlp_aws_span_exporter import OTLPAwsSpanExporter
24+
from amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_logs_exporter import OTLPAwsLogExporter
25+
from amazon.opentelemetry.distro.exporter.otlp.aws.traces.otlp_aws_span_exporter import OTLPAwsSpanExporter
2526
from amazon.opentelemetry.distro.otlp_udp_exporter import OTLPUdpSpanExporter
2627
from amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler import AwsXRayRemoteSampler
2728
from amazon.opentelemetry.distro.scope_based_exporter import ScopeBasedPeriodicExportingMetricReader
2829
from amazon.opentelemetry.distro.scope_based_filtering_view import ScopeBasedRetainingView
30+
from opentelemetry._logs import set_logger_provider
2931
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as OTLPHttpOTLPMetricExporter
3032
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
3133
from opentelemetry.metrics import set_meter_provider
@@ -36,9 +38,10 @@
3638
_import_exporters,
3739
_import_id_generator,
3840
_import_sampler,
39-
_init_logging,
4041
_OTelSDKConfigurator,
4142
)
43+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
44+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExporter
4245
from opentelemetry.sdk.environment_variables import (
4346
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
4447
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
@@ -84,6 +87,8 @@
8487
OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED_CONFIG = "OTEL_AWS_PYTHON_DEFER_TO_WORKERS_ENABLED"
8588
SYSTEM_METRICS_INSTRUMENTATION_SCOPE_NAME = "opentelemetry.instrumentation.system_metrics"
8689
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"
90+
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"
91+
8792
XRAY_OTLP_ENDPOINT_PATTERN = r"https://xray\.([a-z0-9-]+)\.amazonaws\.com/v1/traces$"
8893
# UDP package size is not larger than 64KB
8994
LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10
@@ -160,6 +165,24 @@ def _initialize_components():
160165
_init_logging(log_exporters, resource)
161166

162167

168+
def _init_logging(
169+
exporters: Dict[str, Type[LogExporter]],
170+
resource: Resource = None,
171+
):
172+
173+
provider = LoggerProvider(resource=resource)
174+
set_logger_provider(provider)
175+
176+
for _, exporter_class in exporters.items():
177+
exporter_args = {}
178+
log_exporter = _customize_logs_exporter(exporter_class(**exporter_args), resource)
179+
provider.add_log_record_processor(BatchLogRecordProcessor(exporter=log_exporter))
180+
181+
handler = LoggingHandler(level=NOTSET, logger_provider=provider)
182+
183+
getLogger().addHandler(handler)
184+
185+
163186
def _init_tracing(
164187
exporters: Dict[str, Type[SpanExporter]],
165188
id_generator: IdGenerator = None,
@@ -338,6 +361,10 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span
338361
return AwsMetricAttributesSpanExporterBuilder(span_exporter, resource).build()
339362

340363

364+
def _customize_logs_exporter(log_exporter: LogExporter, resource: Resource) -> LogExporter:
365+
return OTLPAwsLogExporter(endpoint=os.getenv(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT))
366+
367+
341368
def _customize_span_processors(provider: TracerProvider, resource: Resource) -> None:
342369
# Add LambdaSpanProcessor to list of processors regardless of application signals.
343370
if _is_lambda_environment():
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import logging
2+
from abc import ABC, abstractmethod
3+
from typing import Optional
4+
5+
import requests
6+
7+
from amazon.opentelemetry.distro._utils import is_installed
8+
9+
_logger = logging.getLogger(__name__)
10+
11+
12+
class OTLPBaseAwsExporter(ABC):
13+
def __init__(
14+
self,
15+
endpoint: Optional[str] = None,
16+
rsession: Optional[requests.Session] = None,
17+
):
18+
19+
self._aws_region = None
20+
self._has_required_dependencies = False
21+
self._endpoint = endpoint
22+
self._session = rsession
23+
24+
# Requires botocore to be installed to sign the headers. However,
25+
# some users might not need to use this exporter. In order not conflict
26+
# with existing behavior, we check for botocore before initializing this exporter.
27+
28+
if endpoint and is_installed("botocore"):
29+
# pylint: disable=import-outside-toplevel
30+
from botocore import auth, awsrequest, session
31+
32+
self._boto_auth = auth
33+
self._boto_aws_request = awsrequest
34+
self._boto_session = session.Session()
35+
36+
# Assumes only valid endpoints passed are of XRay OTLP format.
37+
# The only usecase for this class would be for ADOT Python Auto Instrumentation and that already validates
38+
# the endpoint to be an XRay OTLP endpoint.
39+
self._aws_region = endpoint.split(".")[1]
40+
self._has_required_dependencies = True
41+
42+
else:
43+
_logger.error(
44+
"botocore is required to export to %s. Please install it using `pip install botocore`",
45+
endpoint,
46+
)
47+
48+
@abstractmethod
49+
def get_service(self):
50+
pass
51+
52+
def sigv4_auth(self, serialized_data):
53+
if self._has_required_dependencies:
54+
request = self._boto_aws_request.AWSRequest(
55+
method="POST",
56+
url=self._endpoint,
57+
data=serialized_data,
58+
headers={"Content-Type": "application/x-protobuf"},
59+
)
60+
61+
credentials = self._boto_session.get_credentials()
62+
63+
if credentials is not None:
64+
signer = self._boto_auth.SigV4Auth(credentials, self.get_service(), self._aws_region)
65+
66+
try:
67+
signer.add_auth(request)
68+
self._session.headers.update(dict(request.headers))
69+
70+
except Exception as signing_error: # pylint: disable=broad-except
71+
_logger.error("Failed to sign request: %s", signing_error)
72+
else:
73+
_logger.debug("botocore is not installed. Failed to sign request to export traces to: %s", self._endpoint)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from typing import Dict, Optional
5+
6+
import requests
7+
from opentelemetry.exporter.otlp.proto.http import Compression
8+
9+
from amazon.opentelemetry.distro.exporter.otlp.aws.common.otlp_aws_exporter import OTLPBaseAwsExporter
10+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
11+
12+
13+
class OTLPAwsLogExporter(OTLPLogExporter, OTLPBaseAwsExporter):
14+
def __init__(
15+
self,
16+
endpoint: Optional[str] = None,
17+
certificate_file: Optional[str] = None,
18+
client_key_file: Optional[str] = None,
19+
client_certificate_file: Optional[str] = None,
20+
headers: Optional[Dict[str, str]] = None,
21+
timeout: Optional[int] = None,
22+
compression: Optional[Compression] = None,
23+
session: Optional[requests.Session] = None,
24+
):
25+
OTLPBaseAwsExporter.__init__(self, endpoint, session)
26+
OTLPLogExporter.__init__(
27+
self,
28+
endpoint,
29+
certificate_file,
30+
client_key_file,
31+
client_certificate_file,
32+
headers,
33+
timeout,
34+
compression,
35+
session,
36+
)
37+
38+
def get_service(self):
39+
return "logs"
40+
41+
def _export(self, serialized_data: bytes):
42+
self.sigv4_auth(serialized_data)
43+
return OTLPLogExporter._export(self, serialized_data)
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 typing import Dict, Optional
4+
5+
import requests
6+
7+
from amazon.opentelemetry.distro.exporter.otlp.aws.common.otlp_aws_exporter import OTLPBaseAwsExporter
8+
from opentelemetry.exporter.otlp.proto.http import Compression
9+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
10+
11+
12+
class OTLPAwsSpanExporter(OTLPSpanExporter, OTLPBaseAwsExporter):
13+
"""
14+
This exporter extends the functionality of the OTLPSpanExporter to allow spans to be exported to the
15+
XRay OTLP endpoint https://xray.[AWSRegion].amazonaws.com/v1/traces. Utilizes the botocore
16+
library to sign and directly inject SigV4 Authentication to the exported request's headers.
17+
18+
https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-OTLPEndpoint.html
19+
"""
20+
21+
def __init__(
22+
self,
23+
endpoint: Optional[str] = None,
24+
certificate_file: Optional[str] = None,
25+
client_key_file: Optional[str] = None,
26+
client_certificate_file: Optional[str] = None,
27+
headers: Optional[Dict[str, str]] = None,
28+
timeout: Optional[int] = None,
29+
compression: Optional[Compression] = None,
30+
rsession: Optional[requests.Session] = None,
31+
):
32+
33+
OTLPBaseAwsExporter.__init__(self, endpoint, rsession)
34+
OTLPSpanExporter.__init__(
35+
self,
36+
endpoint,
37+
certificate_file,
38+
client_key_file,
39+
client_certificate_file,
40+
headers,
41+
timeout,
42+
compression,
43+
rsession,
44+
)
45+
46+
def get_service(self):
47+
return "xray"
48+
49+
# Overrides upstream's private implementation of _export. All behaviors are
50+
# the same except if the endpoint is an XRay OTLP endpoint, we will sign the request
51+
# with SigV4 in headers before sending it to the endpoint. Otherwise, we will skip signing.
52+
def _export(self, serialized_data: bytes):
53+
self.sigv4_auth(serialized_data)
54+
return OTLPSpanExporter._export(self, serialized_data)

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

Lines changed: 0 additions & 100 deletions
This file was deleted.

0 commit comments

Comments
 (0)