Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 96 additions & 17 deletions sentry_sdk/integrations/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import sentry_sdk
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.traces import SegmentSource, StreamedSpan
from sentry_sdk.tracing import TransactionSource
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.utils import (
HAS_REAL_CONTEXTVARS,
CONTEXTVARS_ERROR_MESSAGE,
Expand Down Expand Up @@ -34,10 +36,14 @@

if TYPE_CHECKING:
from typing import Any
from typing import ContextManager
from typing import Optional
from typing import Dict
from typing import Callable
from typing import Generator
from typing import Union

from sentry_sdk.tracing import Span

from sentry_sdk._types import Event, EventProcessor

Expand Down Expand Up @@ -101,6 +107,9 @@ def sentry_log_exception(
RequestHandler.log_exception = sentry_log_exception


_DEFAULT_TRANSACTION_NAME = "generic Tornado request"


@contextlib.contextmanager
def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]":
integration = sentry_sdk.get_client().get_integration(TornadoIntegration)
Expand All @@ -110,6 +119,8 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
return

weak_handler = weakref.ref(self)
client = sentry_sdk.get_client()
span_streaming = has_span_streaming_enabled(client.options)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is communicating a state rather than a "thing", would suggest a name like is_span_streaming_enabled


with sentry_sdk.isolation_scope() as scope:
headers = self.request.headers
Expand All @@ -118,22 +129,90 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
processor = _make_event_processor(weak_handler)
scope.add_event_processor(processor)

transaction = continue_trace(
headers,
op=OP.HTTP_SERVER,
# Like with all other integrations, this is our
# fallback transaction in case there is no route.
# sentry_urldispatcher_resolve is responsible for
# setting a transaction name later.
name="generic Tornado request",
source=TransactionSource.ROUTE,
origin=TornadoIntegration.origin,
)

with sentry_sdk.start_transaction(
transaction, custom_sampling_context={"tornado_request": self.request}
):
yield
span_ctx: "ContextManager[Union[Span, StreamedSpan, None]]"

if span_streaming:
sentry_sdk.traces.continue_trace(dict(headers))
scope.set_custom_sampling_context({"tornado_request": self.request})

span_ctx = sentry_sdk.traces.start_span(
name=_DEFAULT_TRANSACTION_NAME,
attributes={
"sentry.op": OP.HTTP_SERVER,
"sentry.origin": TornadoIntegration.origin,
"sentry.span.source": SegmentSource.ROUTE,
},
)
Comment on lines +138 to +145
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
span_ctx = sentry_sdk.traces.start_span(
name=_DEFAULT_TRANSACTION_NAME,
attributes={
"sentry.op": OP.HTTP_SERVER,
"sentry.origin": TornadoIntegration.origin,
"sentry.span.source": SegmentSource.ROUTE,
},
)
span_ctx = sentry_sdk.traces.start_span(
name=_DEFAULT_TRANSACTION_NAME,
attributes={
"sentry.op": OP.HTTP_SERVER,
"sentry.origin": TornadoIntegration.origin,
"sentry.span.source": SegmentSource.ROUTE,
},
parent_span=None,
)

to make sure this will be a segment

else:
transaction = continue_trace(
headers,
op=OP.HTTP_SERVER,
# Like with all other integrations, this is our
# fallback transaction in case there is no route.
# sentry_urldispatcher_resolve is responsible for
# setting a transaction name later.
name=_DEFAULT_TRANSACTION_NAME,
source=TransactionSource.ROUTE,
origin=TornadoIntegration.origin,
)
span_ctx = sentry_sdk.start_transaction(
transaction,
custom_sampling_context={"tornado_request": self.request},
)

with span_ctx as span:
if isinstance(span, StreamedSpan):
with capture_internal_exceptions():
for attr, value in _get_request_attributes(self.request).items():
span.set_attribute(attr, value)

method = getattr(self, self.request.method.lower(), None)
if method is not None:
tx_name = transaction_from_function(method) or ""
if tx_name:
span.name = tx_name
Comment on lines +171 to +173
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback is dead code and I'm not sure if we want transaction concepts to bleed into the streaming path when it comes to variable naming.

Suggested change
tx_name = transaction_from_function(method) or ""
if tx_name:
span.name = tx_name
span_name = transaction_from_function(method)
if span_name:
span.name = span_name

span.set_attribute(
"sentry.span.source",
SegmentSource.COMPONENT.value,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 : both approaches work, but I noticed that this spot uses .value and line 143 doesn't. As a clean up, would be good to make this consistent.

)

try:
yield
finally:
if isinstance(span, StreamedSpan):
with capture_internal_exceptions():
status_int = self.get_status()
span.set_attribute(SPANDATA.HTTP_STATUS_CODE, status_int)
span.status = "error" if status_int >= 400 else "ok"


def _get_request_attributes(request: "Any") -> "Dict[str, Any]":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we still capturing the request body in the streaming path? See https://getsentry.github.io/sentry-conventions/attributes/http/#http-request-body-data

For prior art IIRC @ericapisani did this for another integration, not sure which exactly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

starlette and aiohttp were the couple that I worked on that had this

attributes = {} # type: Dict[str, Any]

if request.method:
attributes[SPANDATA.HTTP_REQUEST_METHOD] = request.method.upper()

headers = _filter_headers(dict(request.headers), use_annotated_value=False)
for header, value in headers.items():
attributes[f"http.request.header.{header.lower()}"] = value

if request.query:
attributes[SPANDATA.HTTP_QUERY] = request.query

attributes[SPANDATA.URL_FULL] = "%s://%s%s" % (
request.protocol,
request.host,
request.path,
)

if request.protocol:
attributes["network.protocol.name"] = request.protocol

if should_send_default_pii() and request.remote_ip:
attributes["client.address"] = request.remote_ip
attributes["user.ip_address"] = request.remote_ip

return attributes


@ensure_integration_enabled(TornadoIntegration)
Expand Down
Loading
Loading