diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 1db0b34979..c6e2e9f314 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -882,6 +882,11 @@ class SPANDATA: The messaging system's name, e.g. `kafka`, `aws_sqs` """ + MIDDLEWARE_NAME = "middleware.name" + """ + The middleware's name, e.g. `AuthenticationMiddleware` + """ + NETWORK_PROTOCOL_NAME = "network.protocol.name" """ The application layer protocol name used for the network connection. diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 253e51b1f1..21009daa63 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -1,11 +1,12 @@ from copy import deepcopy import sentry_sdk -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource +from sentry_sdk.tracing_utils import has_span_streaming_enabled from sentry_sdk.utils import ( ensure_integration_enabled, event_from_exception, @@ -141,16 +142,34 @@ async def _create_span_call( receive: "Receive", send: "Send", ) -> None: - if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: + client = sentry_sdk.get_client() + if client.get_integration(StarliteIntegration) is None: return await old_call(self, scope, receive, send) middleware_name = self.__class__.__name__ - with sentry_sdk.start_span( - op=OP.MIDDLEWARE_STARLITE, - name=middleware_name, - origin=StarliteIntegration.origin, + is_span_streaming_enabled = has_span_streaming_enabled(client.options) + + def _start_middleware_span(op: str, name: str) -> "Any": + if is_span_streaming_enabled: + return sentry_sdk.traces.start_span( + name=name, + attributes={ + "sentry.op": op, + "sentry.origin": StarliteIntegration.origin, + SPANDATA.MIDDLEWARE_NAME: middleware_name, + }, + ) + return sentry_sdk.start_span( + op=op, + name=name, + origin=StarliteIntegration.origin, + ) + + with _start_middleware_span( + op=OP.MIDDLEWARE_STARLITE, name=middleware_name ) as middleware_span: - middleware_span.set_tag("starlite.middleware_name", middleware_name) + if not is_span_streaming_enabled: + middleware_span.set_tag("starlite.middleware_name", middleware_name) # Creating spans for the "receive" callback async def _sentry_receive( @@ -158,12 +177,12 @@ async def _sentry_receive( ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: return await receive(*args, **kwargs) - with sentry_sdk.start_span( + with _start_middleware_span( op=OP.MIDDLEWARE_STARLITE_RECEIVE, name=getattr(receive, "__qualname__", str(receive)), - origin=StarliteIntegration.origin, ) as span: - span.set_tag("starlite.middleware_name", middleware_name) + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) return await receive(*args, **kwargs) receive_name = getattr(receive, "__name__", str(receive)) @@ -174,12 +193,12 @@ async def _sentry_receive( async def _sentry_send(message: "Message") -> None: if sentry_sdk.get_client().get_integration(StarliteIntegration) is None: return await send(message) - with sentry_sdk.start_span( + with _start_middleware_span( op=OP.MIDDLEWARE_STARLITE_SEND, name=getattr(send, "__qualname__", str(send)), - origin=StarliteIntegration.origin, ) as span: - span.set_tag("starlite.middleware_name", middleware_name) + if not is_span_streaming_enabled: + span.set_tag("starlite.middleware_name", middleware_name) return await send(message) send_name = getattr(send, "__name__", str(send)) diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index 5c038c482d..0e7e67fe2a 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -9,6 +9,7 @@ from starlite.middleware.session.memory_backend import MemoryBackendConfig from starlite.testing import TestClient +import sentry_sdk from sentry_sdk import capture_message from sentry_sdk.integrations.starlite import StarliteIntegration @@ -111,10 +112,14 @@ def test_catch_exceptions( assert event["exception"]["values"][0]["mechanism"]["type"] == "starlite" -def test_middleware_spans(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_middleware_spans(sentry_init, capture_events, capture_items, span_streaming): sentry_init( traces_sample_rate=1.0, integrations=[StarliteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) logging_config = LoggingMiddlewareConfig() @@ -128,32 +133,58 @@ def test_middleware_spans(sentry_init, capture_events): rate_limit_config.middleware, ] ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() client = TestClient( starlite_app, raise_server_exceptions=False, base_url="http://testserver.local" ) client.get("/message") - (_, transaction_event) = events - expected = {"SessionMiddleware", "LoggingMiddleware", "RateLimitMiddleware"} - found = set() - starlite_spans = ( - span - for span in transaction_event["spans"] - if span["op"] == "middleware.starlite" - ) + if span_streaming: + sentry_sdk.flush() + + middleware_spans = [ + item.payload + for item in items + if item.payload.get("attributes", {}).get("sentry.op") + == "middleware.starlite" + ] + assert len(middleware_spans) == 3 + + found = set() + for span in middleware_spans: + assert span["name"] in expected + assert span["name"] not in found + found.add(span["name"]) + assert span["name"] == span["attributes"]["middleware.name"] + else: + (_, transaction_event) = events + + found = set() + middleware_spans = [ + span + for span in transaction_event["spans"] + if span["op"] == "middleware.starlite" + ] + assert len(middleware_spans) == 3 - for span in starlite_spans: - assert span["description"] in expected - assert span["description"] not in found - found.add(span["description"]) - assert span["description"] == span["tags"]["starlite.middleware_name"] + for span in middleware_spans: + assert span["description"] in expected + assert span["description"] not in found + found.add(span["description"]) + assert span["description"] == span["tags"]["starlite.middleware_name"] -def test_middleware_callback_spans(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_middleware_callback_spans( + sentry_init, capture_events, capture_items, span_streaming +): class SampleMiddleware(AbstractMiddleware): async def __call__(self, scope, receive, send) -> None: async def do_stuff(message): @@ -167,15 +198,20 @@ async def do_stuff(message): sentry_init( traces_sample_rate=1.0, integrations=[StarliteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) starlite_app = starlite_app_factory(middleware=[SampleMiddleware]) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() client = TestClient(starlite_app, raise_server_exceptions=False) client.get("/message") - (_, transaction_events) = events - expected_starlite_spans = [ { "op": "middleware.starlite", @@ -194,25 +230,52 @@ async def do_stuff(message): }, ] - def is_matching_span(expected_span, actual_span): - return ( - expected_span["op"] == actual_span["op"] - and expected_span["description"] == actual_span["description"] - and expected_span["tags"] == actual_span["tags"] - ) + if span_streaming: + sentry_sdk.flush() - actual_starlite_spans = list( - span - for span in transaction_events["spans"] - if "middleware.starlite" in span["op"] - ) - assert len(actual_starlite_spans) == 3 - - for expected_span in expected_starlite_spans: - assert any( - is_matching_span(expected_span, actual_span) - for actual_span in actual_starlite_spans + actual_starlite_spans = [ + item.payload + for item in items + if "middleware.starlite" + in item.payload.get("attributes", {}).get("sentry.op", "") + ] + assert len(actual_starlite_spans) == 3 + + def is_matching_span_streaming(expected_span, actual_span): + return ( + expected_span["op"] == actual_span["attributes"]["sentry.op"] + and expected_span["description"] == actual_span["name"] + and expected_span["tags"]["starlite.middleware_name"] + == actual_span["attributes"]["middleware.name"] + ) + + for expected_span in expected_starlite_spans: + assert any( + is_matching_span_streaming(expected_span, actual_span) + for actual_span in actual_starlite_spans + ) + else: + (_, transaction_events) = events + + def is_matching_span(expected_span, actual_span): + return ( + expected_span["op"] == actual_span["op"] + and expected_span["description"] == actual_span["description"] + and expected_span["tags"] == actual_span["tags"] + ) + + actual_starlite_spans = list( + span + for span in transaction_events["spans"] + if "middleware.starlite" in span["op"] ) + assert len(actual_starlite_spans) == 3 + + for expected_span in expected_starlite_spans: + assert any( + is_matching_span(expected_span, actual_span) + for actual_span in actual_starlite_spans + ) def test_middleware_receive_send(sentry_init, capture_events): @@ -238,7 +301,10 @@ async def __call__(self, scope, receive, send): client.get("/message") -def test_middleware_partial_receive_send(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_middleware_partial_receive_send( + sentry_init, capture_events, capture_items, span_streaming +): class SamplePartialReceiveSendMiddleware(AbstractMiddleware): async def __call__(self, scope, receive, send): message = await receive() @@ -262,16 +328,21 @@ async def my_send(*args, **kwargs): sentry_init( traces_sample_rate=1.0, integrations=[StarliteIntegration()], + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) starlite_app = starlite_app_factory(middleware=[SamplePartialReceiveSendMiddleware]) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() client = TestClient(starlite_app, raise_server_exceptions=False) # See SamplePartialReceiveSendMiddleware.__call__ above for assertions of correct behavior client.get("/message") - (_, transaction_events) = events - expected_starlite_spans = [ { "op": "middleware.starlite", @@ -290,31 +361,62 @@ async def my_send(*args, **kwargs): }, ] - def is_matching_span(expected_span, actual_span): - return ( - expected_span["op"] == actual_span["op"] - and actual_span["description"].startswith(expected_span["description"]) - and expected_span["tags"] == actual_span["tags"] - ) - - actual_starlite_spans = list( - span - for span in transaction_events["spans"] - if "middleware.starlite" in span["op"] - ) - assert len(actual_starlite_spans) == 3 + if span_streaming: + sentry_sdk.flush() - for expected_span in expected_starlite_spans: - assert any( - is_matching_span(expected_span, actual_span) - for actual_span in actual_starlite_spans + actual_starlite_spans = [ + item.payload + for item in items + if "middleware.starlite" + in item.payload.get("attributes", {}).get("sentry.op", "") + ] + assert len(actual_starlite_spans) == 3 + + def is_matching_span_streaming(expected_span, actual_span): + return ( + expected_span["op"] == actual_span["attributes"]["sentry.op"] + and actual_span["name"].startswith(expected_span["description"]) + and expected_span["tags"]["starlite.middleware_name"] + == actual_span["attributes"]["middleware.name"] + ) + + for expected_span in expected_starlite_spans: + assert any( + is_matching_span_streaming(expected_span, actual_span) + for actual_span in actual_starlite_spans + ) + else: + (_, transaction_events) = events + + def is_matching_span(expected_span, actual_span): + return ( + expected_span["op"] == actual_span["op"] + and actual_span["description"].startswith(expected_span["description"]) + and expected_span["tags"] == actual_span["tags"] + ) + + actual_starlite_spans = list( + span + for span in transaction_events["spans"] + if "middleware.starlite" in span["op"] ) + assert len(actual_starlite_spans) == 3 + + for expected_span in expected_starlite_spans: + assert any( + is_matching_span(expected_span, actual_span) + for actual_span in actual_starlite_spans + ) -def test_span_origin(sentry_init, capture_events): +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_span_origin(sentry_init, capture_events, capture_items, span_streaming): sentry_init( integrations=[StarliteIntegration()], traces_sample_rate=1.0, + _experiments={ + "trace_lifecycle": "stream" if span_streaming else "static", + }, ) logging_config = LoggingMiddlewareConfig() @@ -328,18 +430,36 @@ def test_span_origin(sentry_init, capture_events): rate_limit_config.middleware, ] ) - events = capture_events() + + if span_streaming: + items = capture_items("span") + else: + events = capture_events() client = TestClient( starlite_app, raise_server_exceptions=False, base_url="http://testserver.local" ) client.get("/message") - (_, event) = events + if span_streaming: + sentry_sdk.flush() + + starlite_items = [ + item + for item in items + if "starlite" in item.payload.get("attributes", {}).get("sentry.op", "") + ] + assert len(starlite_items) > 0 + for item in starlite_items: + assert item.payload["attributes"]["sentry.origin"] == "auto.http.starlite" + else: + (_, event) = events - assert event["contexts"]["trace"]["origin"] == "auto.http.starlite" - for span in event["spans"]: - assert span["origin"] == "auto.http.starlite" + assert event["contexts"]["trace"]["origin"] == "auto.http.starlite" + starlite_spans = [span for span in event["spans"] if "starlite" in span["op"]] + assert len(starlite_spans) > 0 + for span in starlite_spans: + assert span["origin"] == "auto.http.starlite" @pytest.mark.parametrize(