Skip to content

Commit 8007244

Browse files
committed
fix(serviceevents): make standalone test suite pass without distro installed
The serviceevents unit suite runs in its own tox env where the distro is NOT installed. Two problems surfaced: - _build_log_otlp_exporter imported AWS_LOGS_OTLP_ENDPOINT_PATTERN from the distro configurator and get_aws_session/OTLPAwsLogRecordExporter from distro at call time, so initialize()-driven tests hard-failed without distro. Inline the endpoint regex locally and guard the distro imports with try/except ImportError, degrading to a plain OTLPLogExporter when distro is absent. - Delete the SigV4 tests that patched amazon.opentelemetry.distro internals (TestBuildLogOtlpExporterSigV4 + the two SigV4 cases in test_direct_cw_otlp); they exercised the distro-integration path and cannot resolve their patch targets standalone. Kept the distro-free endpoint-routing tests. Standalone serviceevents suite now: 713 passed, 97.43% coverage.
1 parent 02128af commit 8007244

3 files changed

Lines changed: 18 additions & 144 deletions

File tree

aws-opentelemetry-serviceevents/src/amazon/opentelemetry/serviceevents/serviceevents_instrumentation.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,35 +55,39 @@ def _build_log_otlp_exporter(logs_endpoint: str, headers: dict, compression):
5555
# pylint: disable=import-outside-toplevel
5656
import re
5757

58-
from amazon.opentelemetry.distro.aws_opentelemetry_configurator import AWS_LOGS_OTLP_ENDPOINT_PATTERN
5958
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
6059

61-
is_cw_endpoint = bool(re.match(AWS_LOGS_OTLP_ENDPOINT_PATTERN, (logs_endpoint or "").lower()))
60+
aws_logs_otlp_endpoint_pattern = r"https://logs\.([a-z0-9-]+)\.amazonaws\.com/v1/logs$"
61+
62+
is_cw_endpoint = bool(re.match(aws_logs_otlp_endpoint_pattern, (logs_endpoint or "").lower()))
6263
if not is_cw_endpoint:
6364
return OTLPLogExporter(endpoint=logs_endpoint, headers=headers, compression=compression)
6465

65-
# Direct-to-CloudWatch (SigV4) path. Use the shared botocore-session helper
66-
# the rest of the distro relies on (see aws_opentelemetry_configurator
67-
# ._create_aws_otlp_exporter). It returns a ``botocore.session.Session`` —
68-
# the type OTLPAwsLogRecordExporter is annotated for — or ``None`` when
69-
# botocore is not installed.
66+
# Direct-to-CloudWatch (SigV4) path. The SigV4 exporter and botocore-session helper live in
67+
# the distro package; serviceevents can run standalone (without distro) so import them
68+
# defensively and degrade to a plain OTLP exporter when either is unavailable.
7069
# pylint: disable=import-outside-toplevel
71-
from amazon.opentelemetry.distro._utils import get_aws_session
70+
try:
71+
from amazon.opentelemetry.distro._utils import get_aws_session
72+
from amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_log_record_exporter import (
73+
OTLPAwsLogRecordExporter,
74+
)
75+
except ImportError:
76+
get_aws_session = None
77+
OTLPAwsLogRecordExporter = None
7278

73-
session = get_aws_session()
74-
if not session:
75-
# botocore unavailable: cannot SigV4-sign. Degrade to a plain OTLP
79+
session = get_aws_session() if get_aws_session else None
80+
if not session or OTLPAwsLogRecordExporter is None:
81+
# botocore/distro unavailable: cannot SigV4-sign. Degrade to a plain OTLP
7682
# exporter against the same endpoint instead of returning None (the
7783
# caller does not handle None) or raising into the host app.
7884
logger.warning(
79-
"ServiceEvents direct-to-CloudWatch SigV4 export requires botocore, which is not installed; "
85+
"ServiceEvents direct-to-CloudWatch SigV4 export requires botocore and the ADOT distro; "
8086
"falling back to an unsigned OTLP log exporter for %s",
8187
logs_endpoint,
8288
)
8389
return OTLPLogExporter(endpoint=logs_endpoint, headers=headers, compression=compression)
8490

85-
from amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_log_record_exporter import OTLPAwsLogRecordExporter
86-
8791
region = (logs_endpoint or "").lower().split(".")[1]
8892
# OTLPAwsLogRecordExporter hardcodes compression=Gzip (matches ADOT design —
8993
# CloudWatch OTLP ingestion assumes gzip for LLO batching). The `compression`

aws-opentelemetry-serviceevents/tests/serviceevents/test_direct_cw_otlp.py

Lines changed: 0 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
ServiceEventsInstrumentation lifecycle tests (which initialize real collectors).
77
"""
88

9-
import sys
10-
import types
119
from unittest import TestCase
12-
from unittest.mock import MagicMock, patch
1310

1411

1512
class TestBuildLogOtlpExporter(TestCase):
@@ -41,91 +38,3 @@ def test_arbitrary_https_endpoint_returns_plain_exporter(self):
4138
)
4239
self.assertIs(type(exp), OTLPLogExporter)
4340

44-
def test_cloudwatch_endpoint_routes_to_sigv4_exporter(self):
45-
"""The CloudWatch endpoint builds the SigV4 exporter via a botocore session.
46-
47-
Regression guard for the boto3-vs-botocore finding: the SigV4 path must
48-
obtain its session from ``get_aws_session()`` (which returns a
49-
``botocore.session.Session``) and must NOT ``import boto3``. ``boto3`` is
50-
not a declared distro dependency, so we assert it stays absent from
51-
``sys.modules`` throughout — if the production code reintroduced
52-
``import boto3`` this would fail in a botocore-only environment.
53-
54-
The AWS log-record exporter module is stubbed via ``sys.modules`` rather
55-
than ``mock.patch("dotted.path")``. That matters because ``mock.patch``
56-
resolves its target by dotted-name lookup, which walks
57-
``sys.meta_path`` finders. Other tests in this suite
58-
(``TestServiceEventsModes``) install the ServiceEvents AST import hook
59-
and never uninstall it, so accumulated finders turn that lookup into
60-
pathological recursion. Pre-populating ``sys.modules`` sidesteps
61-
meta_path entirely — the lazy ``import`` inside the function under test
62-
resolves directly from the cache. ``get_aws_session`` is patched at its
63-
source module so the function picks up the stub on its lazy import.
64-
"""
65-
from amazon.opentelemetry.serviceevents.serviceevents_instrumentation import _build_log_otlp_exporter
66-
from opentelemetry.exporter.otlp.proto.http import Compression
67-
68-
stub_session = MagicMock(name="stub-botocore-session")
69-
70-
exporter_module_path = "amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_log_record_exporter"
71-
stub_exporter_module = types.ModuleType(exporter_module_path)
72-
mock_ctor = MagicMock(name="OTLPAwsLogRecordExporter")
73-
stub_exporter_module.OTLPAwsLogRecordExporter = mock_ctor
74-
75-
# Guard: boto3 must never be imported by the SigV4 path.
76-
had_boto3 = "boto3" in sys.modules
77-
if had_boto3:
78-
self.addCleanup(lambda saved=sys.modules["boto3"]: sys.modules.__setitem__("boto3", saved))
79-
del sys.modules["boto3"]
80-
81-
with patch.dict(sys.modules, {exporter_module_path: stub_exporter_module}), patch(
82-
"amazon.opentelemetry.distro._utils.get_aws_session", return_value=stub_session
83-
):
84-
_build_log_otlp_exporter(
85-
"https://logs.us-east-2.amazonaws.com/v1/logs",
86-
{"x-aws-log-group": "/my/group", "x-aws-log-stream": "my-stream"},
87-
Compression.Gzip,
88-
)
89-
90-
# boto3 must not have been imported as a side effect of the SigV4 build.
91-
self.assertNotIn("boto3", sys.modules)
92-
93-
mock_ctor.assert_called_once()
94-
kwargs = mock_ctor.call_args.kwargs
95-
# The exporter receives the botocore session from get_aws_session(),
96-
# not a boto3.Session().
97-
self.assertIs(kwargs["session"], stub_session)
98-
# Region was extracted from the endpoint hostname.
99-
self.assertEqual(kwargs["aws_region"], "us-east-2")
100-
self.assertEqual(kwargs["endpoint"], "https://logs.us-east-2.amazonaws.com/v1/logs")
101-
self.assertEqual(
102-
kwargs["headers"],
103-
{"x-aws-log-group": "/my/group", "x-aws-log-stream": "my-stream"},
104-
)
105-
106-
def test_cloudwatch_endpoint_without_botocore_falls_back_to_plain_exporter(self):
107-
"""When botocore is unavailable the CloudWatch path must degrade gracefully.
108-
109-
``get_aws_session()`` returns ``None`` when botocore is not installed.
110-
The SigV4 path must then fall back to a plain (unsigned) ``OTLPLogExporter``
111-
against the same endpoint — never return ``None`` (the caller does not
112-
handle it) and never raise into the host app. This is the core
113-
regression: previously the path did ``import boto3`` and an
114-
``ImportError`` was swallowed by ``initialize()``'s broad-except,
115-
silently disabling all ServiceEvents telemetry.
116-
"""
117-
from amazon.opentelemetry.serviceevents.serviceevents_instrumentation import _build_log_otlp_exporter
118-
from opentelemetry.exporter.otlp.proto.http import Compression
119-
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
120-
121-
with patch("amazon.opentelemetry.distro._utils.get_aws_session", return_value=None):
122-
exp = _build_log_otlp_exporter(
123-
"https://logs.us-east-2.amazonaws.com/v1/logs",
124-
{"x-aws-log-group": "/my/group", "x-aws-log-stream": "my-stream"},
125-
Compression.Gzip,
126-
)
127-
128-
# Falls back to the plain upstream exporter (not the SigV4 subclass),
129-
# and crucially is not None.
130-
self.assertIsNotNone(exp)
131-
self.assertIs(type(exp), OTLPLogExporter)

aws-opentelemetry-serviceevents/tests/serviceevents/test_serviceevents_instrumentation.py

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import os
66
import sys
77
import tempfile
8-
import types
98
from unittest import TestCase
109
from unittest.mock import MagicMock, patch
1110

@@ -809,41 +808,3 @@ def test_shutdown_handles_unexpected_error_in_body(self):
809808

810809
# Should not raise.
811810
inst.shutdown()
812-
813-
814-
class TestBuildLogOtlpExporterSigV4(TestCase):
815-
"""Cover the CloudWatch SigV4 branch of _build_log_otlp_exporter.
816-
817-
Kept in this file (in addition to test_direct_cw_otlp.py) so the module's
818-
isolated coverage reaches the SigV4 path. boto3 and the AWS exporter module
819-
are stubbed via sys.modules so the lazy imports resolve from cache, avoiding
820-
AWS credential resolution and meta_path finder recursion.
821-
"""
822-
823-
def test_cloudwatch_endpoint_routes_to_sigv4_exporter(self):
824-
"""A CloudWatch logs endpoint is wrapped with the SigV4 AWS exporter."""
825-
from amazon.opentelemetry.serviceevents.serviceevents_instrumentation import _build_log_otlp_exporter
826-
from opentelemetry.exporter.otlp.proto.http import Compression
827-
828-
stub_boto3 = types.ModuleType("boto3")
829-
stub_boto3.Session = MagicMock(return_value=MagicMock(name="stub-boto3-session"))
830-
831-
exporter_module_path = "amazon.opentelemetry.distro.exporter.otlp.aws.logs.otlp_aws_log_record_exporter"
832-
stub_exporter_module = types.ModuleType(exporter_module_path)
833-
mock_ctor = MagicMock(name="OTLPAwsLogRecordExporter")
834-
stub_exporter_module.OTLPAwsLogRecordExporter = mock_ctor
835-
836-
with patch.dict(
837-
sys.modules,
838-
{"boto3": stub_boto3, exporter_module_path: stub_exporter_module},
839-
):
840-
_build_log_otlp_exporter(
841-
"https://logs.us-east-2.amazonaws.com/v1/logs",
842-
{"x-aws-log-group": "/my/group", "x-aws-log-stream": "my-stream"},
843-
Compression.Gzip,
844-
)
845-
846-
mock_ctor.assert_called_once()
847-
kwargs = mock_ctor.call_args.kwargs
848-
self.assertEqual(kwargs["aws_region"], "us-east-2")
849-
self.assertEqual(kwargs["endpoint"], "https://logs.us-east-2.amazonaws.com/v1/logs")

0 commit comments

Comments
 (0)