Skip to content

Commit c207167

Browse files
committed
tests + linting fix
1 parent de0e89f commit c207167

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
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
56
from logging import Logger, getLogger
67

8+
import pkg_resources # noqa: F401
9+
710
from amazon.opentelemetry.distro._utils import is_installed
811
from amazon.opentelemetry.distro.patches._resource_detector_patches import _apply_resource_detector_patches
912

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import os
4+
from unittest import TestCase
5+
from unittest.mock import ANY, MagicMock, PropertyMock, patch
6+
7+
import requests
8+
from botocore.credentials import Credentials
9+
10+
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import OTLPAwsSpanExporter
11+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
12+
DEFAULT_COMPRESSION,
13+
DEFAULT_ENDPOINT,
14+
DEFAULT_TIMEOUT,
15+
DEFAULT_TRACES_EXPORT_PATH,
16+
OTLPSpanExporter,
17+
)
18+
from opentelemetry.exporter.otlp.proto.http.version import __version__
19+
from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
20+
from opentelemetry.sdk.trace import SpanContext, _Span
21+
from opentelemetry.trace import SpanKind, TraceFlags
22+
23+
OTLP_XRAY_ENDPOINT = "https://xray.us-east-1.amazonaws.com/v1/traces"
24+
USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__
25+
CONTENT_TYPE = "application/x-protobuf"
26+
AUTHORIZATION_HEADER = "Authorization"
27+
X_AMZ_DATE_HEADER = "X-Amz-Date"
28+
X_AMZ_SECURITY_TOKEN_HEADER = "X-Amz-Security-Token"
29+
30+
31+
class TestAwsSpanExporter(TestCase):
32+
def setUp(self):
33+
self.testing_spans = [
34+
self.create_span("test_span1", SpanKind.INTERNAL),
35+
self.create_span("test_span2", SpanKind.SERVER),
36+
self.create_span("test_span3", SpanKind.CLIENT),
37+
self.create_span("test_span4", SpanKind.PRODUCER),
38+
self.create_span("test_span5", SpanKind.CONSUMER),
39+
]
40+
41+
self.invalid_otlp_tracing_endpoints = [
42+
"https://xray.bad-region-1.amazonaws.com/v1/traces",
43+
"https://xray.us-east-1.amaz.com/v1/traces",
44+
"https://logs.us-east-1.amazonaws.com/v1/logs",
45+
"https://test-endpoint123.com/test",
46+
"xray.us-east-1.amazonaws.com/v1/traces",
47+
"https://test-endpoint123.com/test https://xray.us-east-1.amazonaws.com/v1/traces",
48+
"https://xray.us-east-1.amazonaws.com/v1/tracesssda",
49+
]
50+
51+
self.expected_auth_header = "AWS4-HMAC-SHA256 Credential=test_key/some_date/us-east-1/xray/aws4_request"
52+
self.expected_auth_x_amz_date = "some_date"
53+
self.expected_auth_security_token = "test_token"
54+
55+
@patch.dict(os.environ, {}, clear=True)
56+
def test_sigv4_exporter_init_default(self):
57+
"""Tests that the default exporter is OTLP protobuf/http Span Exporter if no endpoint is set"""
58+
59+
exporter = OTLPAwsSpanExporter()
60+
self.validate_exporter_extends_http_span_exporter(exporter, DEFAULT_ENDPOINT + DEFAULT_TRACES_EXPORT_PATH)
61+
self.assertIsNone(exporter._aws_region)
62+
self.assertIsInstance(exporter._session, requests.Session)
63+
64+
@patch.dict("sys.modules", {"botocore": None}, clear=False)
65+
@patch("pkg_resources.get_distribution")
66+
def test_no_botocore_valid_xray_endpoint(self, mock_get_distribution):
67+
"""Test that exporter defaults when using OTLP CW endpoint without botocore"""
68+
69+
def throw_exception():
70+
raise Exception()
71+
72+
mock_get_distribution.side_effect = throw_exception
73+
74+
exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT)
75+
self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT)
76+
self.assertIsNone(exporter._aws_region)
77+
78+
@patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT}, clear=True)
79+
@patch("botocore.session.Session")
80+
def test_sigv4_exporter_init_valid_cw_otlp_endpoint(self, session_mock):
81+
"""Tests that the endpoint is validated and sets the aws_region but still uses the OTLP protobuf/http
82+
Span Exporter exporter constructor behavior if a valid OTLP CloudWatch endpoint is set."""
83+
84+
mock_session = MagicMock()
85+
session_mock.return_value = mock_session
86+
87+
exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT)
88+
89+
self.assertEqual(exporter._aws_region, "us-east-1")
90+
self.validate_exporter_extends_http_span_exporter(exporter, OTLP_XRAY_ENDPOINT)
91+
92+
@patch("botocore.session.Session")
93+
def test_sigv4_exporter_init_invalid_cw_otlp_endpoint(self, botocore_mock):
94+
"""Tests that the exporter constructor behavior is set by OTLP protobuf/http Span Exporter
95+
if an invalid OTLP CloudWatch endpoint is set"""
96+
for bad_endpoint in self.invalid_otlp_tracing_endpoints:
97+
with self.subTest(endpoint=bad_endpoint):
98+
with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}):
99+
exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint)
100+
self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint)
101+
102+
self.assertIsNone(exporter._aws_region)
103+
104+
@patch("requests.Session.post")
105+
@patch("botocore.auth.SigV4Auth.add_auth")
106+
def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_cw_endpoint(self, mock_sigv4_auth, requests_mock):
107+
"""Tests that if the OTLP endpoint is not a valid XRay endpoint but the credentials are valid,
108+
SigV4 authentication method is called but fails so NO headers are injected into the existing Session headers."""
109+
110+
# Setting the exporter response
111+
mock_response = MagicMock()
112+
mock_response.status_code = 200
113+
type(mock_response).ok = PropertyMock(return_value=True)
114+
115+
# Setting the request session headers to make the call to endpoint
116+
mock_session = MagicMock()
117+
mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE}
118+
requests_mock.return_value = mock_session
119+
mock_session.post.return_value = mock_response
120+
121+
# SigV4 mock authentication injection
122+
mock_sigv4_auth.side_effect = self.mock_add_auth
123+
124+
# Initialize and call exporter
125+
exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT)
126+
exporter.export(self.testing_spans)
127+
128+
# For each invalid CW OTLP endpoint, vdalidate that SigV4 is not injected
129+
for bad_endpoint in self.invalid_otlp_tracing_endpoints:
130+
with self.subTest(endpoint=bad_endpoint):
131+
with patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: bad_endpoint}):
132+
133+
exporter = OTLPAwsSpanExporter(endpoint=bad_endpoint)
134+
135+
self.validate_exporter_extends_http_span_exporter(exporter, bad_endpoint)
136+
137+
exporter.export(self.testing_spans)
138+
139+
# Verify that SigV4 request headers were not injected
140+
actual_headers = mock_session.headers
141+
self.assertNotIn(AUTHORIZATION_HEADER, actual_headers)
142+
self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers)
143+
self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers)
144+
145+
requests_mock.assert_called_with(
146+
url=bad_endpoint,
147+
data=ANY,
148+
verify=ANY,
149+
timeout=ANY,
150+
cert=ANY,
151+
)
152+
153+
@patch("botocore.session.Session")
154+
@patch("requests.Session")
155+
@patch("botocore.auth.SigV4Auth.add_auth")
156+
@patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT})
157+
def test_sigv4_exporter_export_does_not_add_sigv4_if_not_valid_credentials(
158+
self, mock_sigv4_auth, requests_posts_mock, botocore_mock
159+
):
160+
"""Tests that if the OTLP endpoint is a valid CW endpoint but no credentials are returned,
161+
SigV4 authentication method is NOT called and is NOT injected into the existing
162+
Session headers."""
163+
# Setting the exporter response
164+
mock_response = MagicMock()
165+
mock_response.status_code = 200
166+
type(mock_response).ok = PropertyMock(return_value=True)
167+
168+
# Setting the request session headers to make the call to endpoint
169+
mock_session = MagicMock()
170+
mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE}
171+
requests_posts_mock.return_value = mock_session
172+
mock_session.post.return_value = mock_response
173+
174+
mock_botocore_session = MagicMock()
175+
botocore_mock.return_value = mock_botocore_session
176+
177+
# Test case, return None for get credentials
178+
mock_botocore_session.get_credentials.return_value = None
179+
180+
# Initialize and call exporter
181+
exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT)
182+
183+
# Validate that the region is valid
184+
self.assertEqual(exporter._aws_region, "us-east-1")
185+
186+
exporter.export(self.testing_spans)
187+
188+
# Verify SigV4 auth was not called
189+
mock_sigv4_auth.assert_not_called()
190+
191+
# Verify that SigV4 request headers were properly injected
192+
actual_headers = mock_session.headers
193+
self.assertNotIn(AUTHORIZATION_HEADER, actual_headers)
194+
self.assertNotIn(X_AMZ_DATE_HEADER, actual_headers)
195+
self.assertNotIn(X_AMZ_SECURITY_TOKEN_HEADER, actual_headers)
196+
197+
@patch("botocore.session.Session")
198+
@patch("requests.Session")
199+
@patch("botocore.auth.SigV4Auth.add_auth")
200+
@patch.dict(os.environ, {OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: OTLP_XRAY_ENDPOINT})
201+
def test_sigv4_exporter_export_adds_sigv4_authentication_if_valid_cw_endpoint(
202+
self, mock_sigv4_auth, requests_posts_mock, botocore_mock
203+
):
204+
"""Tests that if the OTLP endpoint is valid and credentials are valid,
205+
SigV4 authentication method is called and is
206+
injected into the existing Session headers."""
207+
208+
# Setting the exporter response
209+
mock_response = MagicMock()
210+
mock_response.status_code = 200
211+
type(mock_response).ok = PropertyMock(return_value=True)
212+
213+
# Setting the request session headers to make the call to endpoint
214+
mock_session = MagicMock()
215+
mock_session.headers = {"User-Agent": USER_AGENT, "Content-Type": CONTENT_TYPE}
216+
requests_posts_mock.return_value = mock_session
217+
mock_session.post.return_value = mock_response
218+
219+
mock_botocore_session = MagicMock()
220+
botocore_mock.return_value = mock_botocore_session
221+
mock_botocore_session.get_credentials.return_value = Credentials(
222+
access_key="test_key", secret_key="test_secret", token="test_token"
223+
)
224+
225+
# SigV4 mock authentication injection
226+
mock_sigv4_auth.side_effect = self.mock_add_auth
227+
228+
# Initialize and call exporter
229+
exporter = OTLPAwsSpanExporter(endpoint=OTLP_XRAY_ENDPOINT)
230+
exporter.export(self.testing_spans)
231+
232+
# Verify SigV4 auth was called
233+
mock_sigv4_auth.assert_called_once_with(ANY)
234+
235+
# Verify that SigV4 request headers were properly injected
236+
actual_headers = mock_session.headers
237+
self.assertIn("Authorization", actual_headers)
238+
self.assertIn("X-Amz-Date", actual_headers)
239+
self.assertIn("X-Amz-Security-Token", actual_headers)
240+
241+
self.assertEqual(actual_headers[AUTHORIZATION_HEADER], self.expected_auth_header)
242+
self.assertEqual(actual_headers[X_AMZ_DATE_HEADER], self.expected_auth_x_amz_date)
243+
self.assertEqual(actual_headers[X_AMZ_SECURITY_TOKEN_HEADER], self.expected_auth_security_token)
244+
245+
def validate_exporter_extends_http_span_exporter(self, exporter, endpoint):
246+
self.assertIsInstance(exporter, OTLPSpanExporter)
247+
self.assertEqual(exporter._endpoint, endpoint)
248+
self.assertEqual(exporter._certificate_file, True)
249+
self.assertEqual(exporter._client_certificate_file, None)
250+
self.assertEqual(exporter._client_key_file, None)
251+
self.assertEqual(exporter._timeout, DEFAULT_TIMEOUT)
252+
self.assertIs(exporter._compression, DEFAULT_COMPRESSION)
253+
self.assertEqual(exporter._headers, {})
254+
self.assertIn("User-Agent", exporter._session.headers)
255+
self.assertEqual(
256+
exporter._session.headers.get("Content-Type"),
257+
CONTENT_TYPE,
258+
)
259+
self.assertEqual(exporter._session.headers.get("User-Agent"), USER_AGENT)
260+
261+
@staticmethod
262+
def create_span(name="test_span", kind=SpanKind.INTERNAL):
263+
span = _Span(
264+
name=name,
265+
context=SpanContext(
266+
trace_id=0x1234567890ABCDEF,
267+
span_id=0x9876543210,
268+
is_remote=False,
269+
trace_flags=TraceFlags(TraceFlags.SAMPLED),
270+
),
271+
kind=kind,
272+
)
273+
return span
274+
275+
def mock_add_auth(self, request):
276+
request.headers._headers.extend(
277+
[
278+
(AUTHORIZATION_HEADER, self.expected_auth_header),
279+
(X_AMZ_DATE_HEADER, self.expected_auth_x_amz_date),
280+
(X_AMZ_SECURITY_TOKEN_HEADER, self.expected_auth_security_token),
281+
]
282+
)

0 commit comments

Comments
 (0)