Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce Transaction and Hub.start_transaction #747

Merged
merged 1 commit into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 11 additions & 1 deletion sentry_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Union

from sentry_sdk._types import Event, Hint, Breadcrumb, BreadcrumbHint, ExcInfo
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Span, Transaction

T = TypeVar("T")
F = TypeVar("F", bound=Callable[..., Any])
Expand All @@ -37,6 +37,7 @@ def overload(x):
"flush",
"last_event_id",
"start_span",
"start_transaction",
"set_tag",
"set_context",
"set_extra",
Expand Down Expand Up @@ -201,3 +202,12 @@ def start_span(
):
# type: (...) -> Span
return Hub.current.start_span(span=span, **kwargs)


@hubmethod
def start_transaction(
transaction=None, # type: Optional[Transaction]
**kwargs # type: Any
):
# type: (...) -> Transaction
return Hub.current.start_transaction(transaction, **kwargs)
92 changes: 71 additions & 21 deletions sentry_sdk/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sentry_sdk._compat import with_metaclass
from sentry_sdk.scope import Scope
from sentry_sdk.client import Client
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Span, Transaction
from sentry_sdk.sessions import Session
from sentry_sdk.utils import (
exc_info_from_error,
Expand Down Expand Up @@ -441,38 +441,88 @@ def start_span(
):
# type: (...) -> Span
"""
Create a new span whose parent span is the currently active
span, if any. The return value is the span object that can
be used as a context manager to start and stop timing.

Note that you will not see any span that is not contained
within a transaction. Create a transaction with
``start_span(transaction="my transaction")`` if an
integration doesn't already do this for you.
Create and start timing a new span whose parent is the currently active
span or transaction, if any. The return value is a span instance,
typically used as a context manager to start and stop timing in a `with`
block.

Only spans contained in a transaction are sent to Sentry. Most
integrations start a transaction at the appropriate time, for example
for every incoming HTTP request. Use `start_transaction` to start a new
transaction when one is not already in progress.
"""
# TODO: consider removing this in a future release.
Copy link
Member

Choose a reason for hiding this comment

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

As an aside for another PR: we should have a standard deprecated comment somewhere like we do in JS, so we can easily see what to remove in each major version.

# This is for backwards compatibility with releases before
# start_transaction existed, to allow for a smoother transition.
if isinstance(span, Transaction) or "transaction" in kwargs:
deprecation_msg = (
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need the redundant check here for isinstance(span, Transaction) or "transaction" in kwargs, we should just extract deprecation_msg to be a top level constant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intention here was to have a self-contained block with the TODO note, so it is clearer what it applies to.

"Deprecated: use start_transaction to start transactions and "
"Transaction.start_child to start spans."
)
if isinstance(span, Transaction):
logger.warning(deprecation_msg)
return self.start_transaction(span)
if "transaction" in kwargs:
logger.warning(deprecation_msg)
name = kwargs.pop("transaction")
return self.start_transaction(name=name, **kwargs)

client, scope = self._stack[-1]
if span is not None:
return span

kwargs.setdefault("hub", self)

if span is None:
span = scope.span
if span is not None:
span = span.new_span(**kwargs)
else:
span = Span(**kwargs)
span = self.scope.span
if span is not None:
return span.start_child(**kwargs)

return Span(**kwargs)

def start_transaction(
self,
transaction=None, # type: Optional[Transaction]
**kwargs # type: Any
):
# type: (...) -> Transaction
"""
Start and return a transaction.

Start an existing transaction if given, otherwise create and start a new
transaction with kwargs.

This is the entry point to manual tracing instrumentation.

A tree structure can be built by adding child spans to the transaction,
and child spans to other spans. To start a new child span within the
transaction or any span, call the respective `.start_child()` method.

Every child span must be finished before the transaction is finished,
otherwise the unfinished spans are discarded.

When used as context managers, spans and transactions are automatically
finished at the end of the `with` block. If not using context managers,
call the `.finish()` method.

When the transaction is finished, it will be sent to Sentry with all its
finished child spans.
"""
if transaction is None:
kwargs.setdefault("hub", self)
transaction = Transaction(**kwargs)

client, scope = self._stack[-1]

if span.sampled is None and span.transaction is not None:
if transaction.sampled is None:
sample_rate = client and client.options["traces_sample_rate"] or 0
span.sampled = random.random() < sample_rate
transaction.sampled = random.random() < sample_rate

if span.sampled:
if transaction.sampled:
max_spans = (
client and client.options["_experiments"].get("max_spans") or 1000
)
span.init_finished_spans(maxlen=max_spans)
transaction.init_span_recorder(maxlen=max_spans)

return span
return transaction

@overload # noqa
def push_scope(
Expand Down
22 changes: 12 additions & 10 deletions sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
_filter_headers,
request_body_within_bounds,
)
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Transaction
from sentry_sdk.utils import (
capture_internal_exceptions,
event_from_exception,
Expand Down Expand Up @@ -87,27 +87,29 @@ async def sentry_app_handle(self, request, *args, **kwargs):
scope.clear_breadcrumbs()
scope.add_event_processor(_make_request_processor(weak_request))

span = Span.continue_from_headers(request.headers)
span.op = "http.server"
# If this transaction name makes it to the UI, AIOHTTP's
# URL resolver did not find a route or died trying.
span.transaction = "generic AIOHTTP request"
transaction = Transaction.continue_from_headers(
request.headers,
op="http.server",
# If this transaction name makes it to the UI, AIOHTTP's
# URL resolver did not find a route or died trying.
name="generic AIOHTTP request",
)

with hub.start_span(span):
with hub.start_transaction(transaction):
try:
response = await old_handle(self, request)
except HTTPException as e:
span.set_http_status(e.status_code)
transaction.set_http_status(e.status_code)
raise
except asyncio.CancelledError:
span.set_status("cancelled")
transaction.set_status("cancelled")
raise
except Exception:
# This will probably map to a 500 but seems like we
# have no way to tell. Do not set span status.
reraise(*_capture_exception(hub))

span.set_http_status(response.status)
transaction.set_http_status(response.status)
return response

Application._handle = sentry_app_handle
Expand Down
16 changes: 8 additions & 8 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
HAS_REAL_CONTEXTVARS,
CONTEXTVARS_ERROR_MESSAGE,
)
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Transaction

if MYPY:
from typing import Dict
Expand Down Expand Up @@ -123,16 +123,16 @@ async def _run_app(self, scope, callback):
ty = scope["type"]

if ty in ("http", "websocket"):
span = Span.continue_from_headers(dict(scope["headers"]))
span.op = "{}.server".format(ty)
transaction = Transaction.continue_from_headers(
dict(scope["headers"]), op="{}.server".format(ty),
)
else:
span = Span()
span.op = "asgi.server"
transaction = Transaction(op="asgi.server")

span.set_tag("asgi.type", ty)
span.transaction = _DEFAULT_TRANSACTION_NAME
transaction.name = _DEFAULT_TRANSACTION_NAME
transaction.set_tag("asgi.type", ty)

with hub.start_span(span) as span:
with hub.start_transaction(transaction):
# XXX: Would be cool to have correct span status, but we
# would have to wrap send(). That is a bit hard to do with
# the current abstraction over ASGI 2/3.
Expand Down
16 changes: 9 additions & 7 deletions sentry_sdk/integrations/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from sentry_sdk.hub import Hub
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Transaction
from sentry_sdk._compat import reraise
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations.logging import ignore_logger
Expand Down Expand Up @@ -130,19 +130,21 @@ def _inner(*args, **kwargs):
scope.clear_breadcrumbs()
scope.add_event_processor(_make_event_processor(task, *args, **kwargs))

span = Span.continue_from_headers(args[3].get("headers") or {})
span.op = "celery.task"
span.transaction = "unknown celery task"
transaction = Transaction.continue_from_headers(
args[3].get("headers") or {},
op="celery.task",
name="unknown celery task",
)

# Could possibly use a better hook than this one
span.set_status("ok")
transaction.set_status("ok")

with capture_internal_exceptions():
# Celery task objects are not a thing to be trusted. Even
# something such as attribute access can fail.
span.transaction = task.name
transaction.name = task.name

with hub.start_span(span):
with hub.start_transaction(transaction):
return f(*args, **kwargs)

return _inner # type: ignore
Expand Down
13 changes: 7 additions & 6 deletions sentry_sdk/integrations/rq.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from sentry_sdk.hub import Hub
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Transaction
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception


Expand Down Expand Up @@ -61,15 +61,16 @@ def sentry_patched_perform_job(self, job, *args, **kwargs):
scope.clear_breadcrumbs()
scope.add_event_processor(_make_event_processor(weakref.ref(job)))

span = Span.continue_from_headers(
job.meta.get("_sentry_trace_headers") or {}
transaction = Transaction.continue_from_headers(
job.meta.get("_sentry_trace_headers") or {},
op="rq.task",
name="unknown RQ task",
)
span.op = "rq.task"

with capture_internal_exceptions():
span.transaction = job.func_name
transaction.name = job.func_name

with hub.start_span(span):
with hub.start_transaction(transaction):
rv = old_perform_job(self, job, *args, **kwargs)

if self.is_horse:
Expand Down
18 changes: 10 additions & 8 deletions sentry_sdk/integrations/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
event_from_exception,
)
from sentry_sdk._compat import PY2, reraise, iteritems
from sentry_sdk.tracing import Span
from sentry_sdk.tracing import Transaction
from sentry_sdk.sessions import auto_session_tracking
from sentry_sdk.integrations._wsgi_common import _filter_headers

Expand Down Expand Up @@ -113,15 +113,17 @@ def __call__(self, environ, start_response):
_make_wsgi_event_processor(environ)
)

span = Span.continue_from_environ(environ)
span.op = "http.server"
span.transaction = "generic WSGI request"
transaction = Transaction.continue_from_environ(
environ, op="http.server", name="generic WSGI request"
)

with hub.start_span(span) as span:
with hub.start_transaction(transaction):
try:
rv = self.app(
environ,
partial(_sentry_start_response, start_response, span),
partial(
_sentry_start_response, start_response, transaction
),
)
except BaseException:
reraise(*_capture_exception(hub))
Expand All @@ -133,15 +135,15 @@ def __call__(self, environ, start_response):

def _sentry_start_response(
old_start_response, # type: StartResponse
span, # type: Span
transaction, # type: Transaction
status, # type: str
response_headers, # type: WsgiResponseHeaders
exc_info=None, # type: Optional[WsgiExcInfo]
):
# type: (...) -> WsgiResponseIter
with capture_internal_exceptions():
status_int = int(status.split(" ", 1)[0])
span.set_http_status(status_int)
transaction.set_http_status(status_int)

if exc_info is None:
# The Django Rest Framework WSGI test client, and likely other
Expand Down