Skip to content

Commit 8fd3f21

Browse files
authored
Suppress http receive/send spans from Starlette Instrumentor for Agent Observability (aws-observability#464)
*Description of changes:* `Starlette Instrumentor` utilizes the `OpenTelemetryMiddleware` instrumentation to capture all events generated by [ASGI](https://asgi.readthedocs.io/en/latest/implementations.html#starlette) `Request received, Response headers sent (http.response.start), Response body sent (http.response.body)` which leads to a lot of span noises for streaming events: <img width="1149" height="595" alt="image" src="https://github.com/user-attachments/assets/8076586e-5e09-46cc-ba94-772d81a18824" /> Luckily, the upstream `OpenTelemetryMiddleware` provides a configuration in the constructor to suppress these instrumentations: https://github.com/open-telemetry/opentelemetry-python-contrib/blob/51da0a766e5d3cbc746189e10c9573163198cfcd/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py#L573 Enabling this for Agent Observability to help filter out noise. <img width="1685" height="481" alt="image" src="https://github.com/user-attachments/assets/e639969f-af2a-4559-bec7-c84962a243ad" /> Related issue for removing the need for this patch in the long term, using the proposed `OTEL_PYTHON_STARLETTE_EXCLUDED_SPANS` environment variable: open-telemetry/opentelemetry-python-contrib#3725 By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 7e79092 commit 8fd3f21

File tree

3 files changed

+80
-26
lines changed

3 files changed

+80
-26
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def apply_instrumentation_patches() -> None:
6767
from amazon.opentelemetry.distro.patches._starlette_patches import _apply_starlette_instrumentation_patches
6868

6969
# Starlette auto-instrumentation v0.54b includes a strict dependency version check
70-
# This restriction was removed in v1.34.0/0.55b0. Applying temporary patch for Genesis launch
70+
# This restriction was removed in v1.34.0/0.55b0. Applying temporary patch for Bedrock AgentCore launch
7171
# TODO: Remove patch after syncing with upstream v1.34.0 or later
7272
_apply_starlette_instrumentation_patches()
7373

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from logging import Logger, getLogger
55
from typing import Collection
66

7+
from amazon.opentelemetry.distro._utils import is_agent_observability_enabled
8+
79
_logger: Logger = getLogger(__name__)
810

911

@@ -18,6 +20,7 @@ def _apply_starlette_instrumentation_patches() -> None:
1820
"""
1921
try:
2022
# pylint: disable=import-outside-toplevel
23+
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
2124
from opentelemetry.instrumentation.starlette import StarletteInstrumentor
2225

2326
# Patch starlette dependencies version check
@@ -28,6 +31,25 @@ def patched_instrumentation_dependencies(self) -> Collection[str]:
2831
# Apply the patch
2932
StarletteInstrumentor.instrumentation_dependencies = patched_instrumentation_dependencies
3033

34+
# pylint: disable=line-too-long
35+
# Patch to exclude http receive/send ASGI event spans from Bedrock AgentCore,
36+
# this Middleware instrumentation is injected internally by Starlette Instrumentor, see:
37+
# https://github.com/open-telemetry/opentelemetry-python-contrib/blob/51da0a766e5d3cbc746189e10c9573163198cfcd/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py#L573
38+
#
39+
# Issue for tracking a feature to customize this setting within Starlette:
40+
# https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3725
41+
if is_agent_observability_enabled():
42+
original_init = OpenTelemetryMiddleware.__init__
43+
44+
def patched_init(self, app, **kwargs):
45+
original_init(self, app, **kwargs)
46+
if hasattr(self, "exclude_receive_span"):
47+
self.exclude_receive_span = True
48+
if hasattr(self, "exclude_send_span"):
49+
self.exclude_send_span = True
50+
51+
OpenTelemetryMiddleware.__init__ = patched_init
52+
3153
_logger.debug("Successfully patched Starlette instrumentation_dependencies method")
3254
except Exception as exc: # pylint: disable=broad-except
3355
_logger.warning("Failed to apply Starlette instrumentation patches: %s", exc)

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_starlette_patches.py

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,63 @@ class TestStarlettePatch(TestCase):
1212
@patch("amazon.opentelemetry.distro.patches._starlette_patches._logger")
1313
def test_starlette_patch_applied_successfully(self, mock_logger):
1414
"""Test that the Starlette instrumentation patch is applied successfully."""
15-
# Create a mock StarletteInstrumentor class
16-
mock_instrumentor_class = MagicMock()
17-
mock_instrumentor_class.__name__ = "StarletteInstrumentor"
18-
19-
# Create a mock module
20-
mock_starlette_module = MagicMock()
21-
mock_starlette_module.StarletteInstrumentor = mock_instrumentor_class
22-
23-
# Mock the import
24-
with patch.dict("sys.modules", {"opentelemetry.instrumentation.starlette": mock_starlette_module}):
25-
# Apply the patch
26-
_apply_starlette_instrumentation_patches()
27-
28-
# Verify the instrumentation_dependencies method was replaced
29-
self.assertTrue(hasattr(mock_instrumentor_class, "instrumentation_dependencies"))
30-
31-
# Test the patched method returns the expected value
32-
mock_instance = MagicMock()
33-
result = mock_instrumentor_class.instrumentation_dependencies(mock_instance)
34-
self.assertEqual(result, ("starlette >= 0.13",))
35-
36-
# Verify logging
37-
mock_logger.debug.assert_called_once_with(
38-
"Successfully patched Starlette instrumentation_dependencies method"
39-
)
15+
for agent_enabled in [True, False]:
16+
with self.subTest(agent_enabled=agent_enabled):
17+
with patch.dict("os.environ", {"AGENT_OBSERVABILITY_ENABLED": "true" if agent_enabled else "false"}):
18+
# Create a mock StarletteInstrumentor class
19+
mock_instrumentor_class = MagicMock()
20+
mock_instrumentor_class.__name__ = "StarletteInstrumentor"
21+
22+
def create_middleware_class():
23+
class MockMiddleware:
24+
def __init__(self, app, **kwargs):
25+
pass
26+
27+
return MockMiddleware
28+
29+
mock_middleware_class = create_middleware_class()
30+
31+
mock_starlette_module = MagicMock()
32+
mock_starlette_module.StarletteInstrumentor = mock_instrumentor_class
33+
34+
mock_asgi_module = MagicMock()
35+
mock_asgi_module.OpenTelemetryMiddleware = mock_middleware_class
36+
37+
with patch.dict(
38+
"sys.modules",
39+
{
40+
"opentelemetry.instrumentation.starlette": mock_starlette_module,
41+
"opentelemetry.instrumentation.asgi": mock_asgi_module,
42+
},
43+
):
44+
# Apply the patch
45+
_apply_starlette_instrumentation_patches()
46+
47+
# Verify the instrumentation_dependencies method was replaced
48+
self.assertTrue(hasattr(mock_instrumentor_class, "instrumentation_dependencies"))
49+
50+
# Test the patched method returns the expected value
51+
mock_instance = MagicMock()
52+
result = mock_instrumentor_class.instrumentation_dependencies(mock_instance)
53+
self.assertEqual(result, ("starlette >= 0.13",))
54+
55+
mock_middleware_instance = MagicMock()
56+
mock_middleware_instance.exclude_receive_span = False
57+
mock_middleware_instance.exclude_send_span = False
58+
mock_middleware_class.__init__(mock_middleware_instance, "app")
59+
60+
# Test middleware patching sets exclude flags
61+
if agent_enabled:
62+
self.assertTrue(mock_middleware_instance.exclude_receive_span)
63+
self.assertTrue(mock_middleware_instance.exclude_send_span)
64+
else:
65+
self.assertFalse(mock_middleware_instance.exclude_receive_span)
66+
self.assertFalse(mock_middleware_instance.exclude_send_span)
67+
68+
# Verify logging
69+
mock_logger.debug.assert_called_with(
70+
"Successfully patched Starlette instrumentation_dependencies method"
71+
)
4072

4173
@patch("amazon.opentelemetry.distro.patches._starlette_patches._logger")
4274
def test_starlette_patch_handles_import_error(self, mock_logger):

0 commit comments

Comments
 (0)