From a6a67a9fff3d8d2da7cffc5dbcff2acfde7086b4 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 12:01:29 +0200 Subject: [PATCH 1/2] feat(tornado): Add `sentry.is_localhost` for inbound filters --- sentry_sdk/integrations/tornado.py | 21 ++++++++++ tests/integrations/tornado/test_tornado.py | 48 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index d0be7fceb0..2080a60779 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -22,6 +22,7 @@ CONTEXTVARS_ERROR_MESSAGE, HAS_REAL_CONTEXTVARS, AnnotatedValue, + _is_localhost, capture_internal_exceptions, ensure_integration_enabled, event_from_exception, @@ -127,6 +128,15 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] processor = _make_event_processor(weak_handler) scope.add_event_processor(processor) + scope.set_attribute( + SPANDATA.SENTRY_IS_LOCALHOST, + _is_localhost( + client_ip=_get_client_ip(self.request), + host_header=self.request.headers.get("Host"), + forwarded_host_header=self.request.headers.get("X-Forwarded-Host"), + ), + ) + span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]" if is_span_streaming_enabled: @@ -198,6 +208,17 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] span.status = "error" if status_int >= 400 else "ok" +def _get_client_ip(request: "Any") -> "Optional[str]": + x_forwarded_for = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() + if x_forwarded_for: + return x_forwarded_for + + if request.headers.get("X-Real-IP"): + return request.headers["X-Real-IP"] + + return request.remote_ip + + def _get_request_attributes(request: "Any") -> "Dict[str, Any]": attributes = {} # type: Dict[str, Any] client_options = sentry_sdk.get_client().options diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index e995e3e2be..c57f6a8cfd 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -1118,6 +1118,54 @@ def test_client_address_span_attribute_data_collection( assert "client.address" not in server_span["attributes"] +@pytest.mark.parametrize( + "forwarded_for, host_header, is_localhost", + [ + pytest.param(None, None, True, id="loopback-ip"), + pytest.param( + "93.184.216.34", "localhost:8080", True, id="localhost-host-header" + ), + pytest.param("93.184.216.34", "example.com", False, id="non-local"), + ], +) +def test_is_localhost_span_attribute( + tornado_testcase, + sentry_init, + capture_items, + forwarded_for, + host_header, + is_localhost, +): + from sentry_sdk.consts import SPANDATA + + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + ) + + items = capture_items("span") + + client = tornado_testcase(Application([(r"/hi", ChildSpanHandler)])) + headers = {} + if forwarded_for: + headers["X-Forwarded-For"] = forwarded_for + if host_header: + headers["Host"] = host_header + client.fetch("/hi", headers=headers if headers else None) + + sentry_sdk.flush() + + child_spans = [item.payload for item in items if not item.payload.get("is_segment")] + server_spans = [item.payload for item in items if item.payload.get("is_segment")] + + assert len(server_spans) == 1 + assert len(child_spans) == 1 + + assert server_spans[0]["attributes"][SPANDATA.SENTRY_IS_LOCALHOST] == is_localhost + assert child_spans[0]["attributes"][SPANDATA.SENTRY_IS_LOCALHOST] == is_localhost + + @pytest.mark.parametrize( "init_kwargs, expect_remote_addr", DATA_COLLECTION_REMOTE_ADDR_CASES ) From 463c399fdd6cc7e2f7d8d3ddf6393a078e78c63c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 30 Sep 2026 13:02:11 +0200 Subject: [PATCH 2/2] . --- tests/integrations/tornado/test_tornado.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index c57f6a8cfd..561b1b22bb 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -7,6 +7,7 @@ import sentry_sdk from sentry_sdk import capture_message, start_transaction from sentry_sdk._types import SENSITIVE_DATA_SUBSTITUTE +from sentry_sdk.consts import SPANDATA from sentry_sdk.integrations.tornado import TornadoIntegration from tests.integrations.utils import ( DATA_COLLECTION_REMOTE_ADDR_CASES, @@ -1136,8 +1137,6 @@ def test_is_localhost_span_attribute( host_header, is_localhost, ): - from sentry_sdk.consts import SPANDATA - sentry_init( integrations=[TornadoIntegration()], traces_sample_rate=1.0,