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

Add decorator for Sentry tracing #1089

Merged
merged 51 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
bd86719
Add decorator for Sentry tracing
ynouri Apr 15, 2021
23d23de
Update decorators.py
ynouri Dec 30, 2021
af136a7
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 28, 2022
146c8ad
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 3, 2023
c0bc0d8
Linting
antonpirker Mar 3, 2023
fa6c550
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 3, 2023
9d1fb9a
Fixed syntax errors
antonpirker Mar 3, 2023
c272e17
Merge branch 'feature/tracing-decorator' of https://github.com/ynouri…
antonpirker Mar 3, 2023
b795e19
Fixed syntax error
antonpirker Mar 3, 2023
e839e4c
Try to make it work in Python 2.x
antonpirker Mar 3, 2023
99d5485
Removed transaction generation and made OP set to .
antonpirker Mar 6, 2023
1a6958f
Added function name as descripion to span.
antonpirker Mar 6, 2023
aecfda9
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 6, 2023
f8138b8
Try to make it run in Python 2
antonpirker Mar 6, 2023
c885e4c
Merge branch 'feature/tracing-decorator' of https://github.com/ynouri…
antonpirker Mar 6, 2023
05a7caa
Trying something else.
antonpirker Mar 6, 2023
33ecfcf
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 6, 2023
0160be0
Reverted changes to tracing_utils
antonpirker Mar 6, 2023
17eb234
Merge branch 'feature/tracing-decorator' of https://github.com/ynouri…
antonpirker Mar 6, 2023
1589a56
Added warning message in case there is no transaction
antonpirker Mar 6, 2023
639e190
Update sentry_sdk/tracing.py
antonpirker Mar 7, 2023
37a1b9c
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 7, 2023
0b26cf1
Added tests
antonpirker Mar 8, 2023
6635d62
Updated how to run tests
antonpirker Mar 8, 2023
10fe566
Fixed invocation of tests
antonpirker Mar 8, 2023
b293047
Fixed invocation (again)
antonpirker Mar 8, 2023
b40578a
Fixed tests
antonpirker Mar 8, 2023
333286f
Linting
antonpirker Mar 8, 2023
3e0e769
Fixed typing
antonpirker Mar 8, 2023
1bdf047
.
antonpirker Mar 8, 2023
95e877e
Create span under current span
antonpirker Mar 8, 2023
4c7f8eb
.
antonpirker Mar 8, 2023
6cf968a
More test coverage
antonpirker Mar 8, 2023
0ec4f56
Cleanup
antonpirker Mar 8, 2023
a882f54
Fixed typing
antonpirker Mar 8, 2023
e51a93f
Better naming
antonpirker Mar 8, 2023
4af3170
Added top level api to get current span and transaction
antonpirker Mar 14, 2023
4efec57
Split out function in separate PR
antonpirker Mar 14, 2023
0b3c227
Merge branch 'antonpirker/top-level-get-current-span-transaction' int…
antonpirker Mar 14, 2023
a1e49c8
Always install pytest-asyncio in Python3 to run new async tests in co…
antonpirker Mar 14, 2023
3265709
Updated test config to make it possible to have dependencies only nee…
antonpirker Mar 15, 2023
933535f
Fixed minimum Python versions the tests should run in.
antonpirker Mar 15, 2023
c124a7d
Dont run common tests in py3.4 (like it was before)
antonpirker Mar 15, 2023
d5e10fb
Updated test matrix
antonpirker Mar 15, 2023
2ede736
Trying normal fixture
antonpirker Mar 15, 2023
cefd2bc
Run asyncio tests only in python >3.6
antonpirker Mar 15, 2023
535eb74
Remove check because it is not really useful and breaks when asyncio …
antonpirker Mar 15, 2023
c1b6a6b
Fixed some tests
antonpirker Mar 15, 2023
62e5719
Use get_current_span, because it also returns the transaction if no s…
antonpirker Mar 15, 2023
6b90c09
Merge branch 'master' into feature/tracing-decorator
antonpirker Mar 15, 2023
524fb88
Linting
antonpirker Mar 15, 2023
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
2 changes: 2 additions & 0 deletions sentry_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from sentry_sdk.consts import VERSION # noqa

from sentry_sdk.tracing import trace # noqa

__all__ = [ # noqa
"Hub",
"Scope",
Expand Down
38 changes: 35 additions & 3 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@
import sentry_sdk
from sentry_sdk.consts import INSTRUMENTER
from sentry_sdk.utils import logger, nanosecond_time
from sentry_sdk._compat import PY2
from sentry_sdk._types import TYPE_CHECKING


if TYPE_CHECKING:
import typing

from typing import Optional
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple
from typing import Iterator

import sentry_sdk.profiler
from sentry_sdk._types import Event, SamplingContext, MeasurementUnit
from sentry_sdk._types import Event, MeasurementUnit, SamplingContext


BAGGAGE_HEADER_NAME = "baggage"
SENTRY_TRACE_HEADER_NAME = "sentry-trace"
Expand Down Expand Up @@ -803,6 +805,36 @@ def finish(self, hub=None, end_timestamp=None):
pass


def trace(func=None):
# type: (Any) -> Any
"""
Decorator to start a child span under the existing current transaction.
If there is no current transaction, than nothing will be traced.

Usage:
import sentry_sdk

@sentry_sdk.trace
def my_function():
...

@sentry_sdk.trace
async def my_async_function():
...
"""
if PY2:
sl0thentr0py marked this conversation as resolved.
Show resolved Hide resolved
from sentry_sdk.tracing_utils_py2 import start_child_span_decorator
else:
from sentry_sdk.tracing_utils_py3 import start_child_span_decorator

# This patterns allows usage of both @sentry_traced and @sentry_traced(...)
# See https://stackoverflow.com/questions/52126071/decorator-with-arguments-avoid-parenthesis-when-no-arguments/52126278
if func:
return start_child_span_decorator(func)
else:
return start_child_span_decorator


# Circular imports

from sentry_sdk.tracing_utils import (
Expand Down
10 changes: 10 additions & 0 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,16 @@ def should_propagate_trace(hub, url):
return False


def get_running_span_or_transaction(hub):
sl0thentr0py marked this conversation as resolved.
Show resolved Hide resolved
# type: (sentry_sdk.Hub) -> Optional[Union[Span, Transaction]]
current_span = hub.scope.span
if current_span is not None:
return current_span

transaction = hub.scope.transaction
return transaction


# Circular imports
from sentry_sdk.tracing import LOW_QUALITY_TRANSACTION_SOURCES

Expand Down
45 changes: 45 additions & 0 deletions sentry_sdk/tracing_utils_py2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from functools import wraps

import sentry_sdk
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.tracing_utils import get_running_span_or_transaction
from sentry_sdk.consts import OP
from sentry_sdk.utils import logger, qualname_from_function


if TYPE_CHECKING:
from typing import Any


def start_child_span_decorator(func):
# type: (Any) -> Any
"""
Decorator to add child spans for functions.

This is the Python 2 compatible version of the decorator.
Duplicated code from ``sentry_sdk.tracing_utils_python3.start_child_span_decorator``.

See also ``sentry_sdk.tracing.trace()``.
"""

@wraps(func)
def func_with_tracing(*args, **kwargs):
# type: (*Any, **Any) -> Any

span_or_trx = get_running_span_or_transaction(sentry_sdk.Hub.current)

if span_or_trx is None:
logger.warning(
"No transaction found. Not creating a child span for %s. "
"Please start a Sentry transaction before calling this function.",
qualname_from_function(func),
)
return func(*args, **kwargs)

with span_or_trx.start_child(
op=OP.FUNCTION,
description=qualname_from_function(func),
):
return func(*args, **kwargs)

return func_with_tracing
72 changes: 72 additions & 0 deletions sentry_sdk/tracing_utils_py3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import inspect
from functools import wraps

import sentry_sdk
from sentry_sdk._types import TYPE_CHECKING
from sentry_sdk.consts import OP
from sentry_sdk.tracing_utils import get_running_span_or_transaction
from sentry_sdk.utils import logger, qualname_from_function


if TYPE_CHECKING:
from typing import Any


def start_child_span_decorator(func):
# type: (Any) -> Any
"""
Decorator to add child spans for functions.

This is the Python 3 compatible version of the decorator.
For Python 2 there is duplicated code here: ``sentry_sdk.tracing_utils_python2.start_child_span_decorator()``.

See also ``sentry_sdk.tracing.trace()``.
"""

# Asynchronous case
if inspect.iscoroutinefunction(func):

@wraps(func)
async def func_with_tracing(*args, **kwargs):
# type: (*Any, **Any) -> Any

span_or_trx = get_running_span_or_transaction(sentry_sdk.Hub.current)

if span_or_trx is None:
logger.warning(
"No transaction found. Not creating a child span for %s. "
"Please start a Sentry transaction before calling this function.",
qualname_from_function(func),
)
return await func(*args, **kwargs)

with span_or_trx.start_child(
op=OP.FUNCTION,
description=qualname_from_function(func),
):
return await func(*args, **kwargs)

# Synchronous case
else:

@wraps(func)
def func_with_tracing(*args, **kwargs):
# type: (*Any, **Any) -> Any

span_or_trx = get_running_span_or_transaction(sentry_sdk.Hub.current)

if span_or_trx is None:
logger.warning(
"No transaction found. Not creating a child span for %s. "
"Please start a Sentry transaction before calling this function.",
qualname_from_function(func),
)
return func(*args, **kwargs)

with span_or_trx.start_child(
op=OP.FUNCTION,
description=qualname_from_function(func),
):
return func(*args, **kwargs)

return func_with_tracing
50 changes: 50 additions & 0 deletions tests/tracing/test_decorator_py2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import mock

from sentry_sdk.tracing_utils_py2 import (
start_child_span_decorator as start_child_span_decorator_py2,
)
from sentry_sdk.utils import logger


def my_example_function():
return "return_of_sync_function"


def test_trace_decorator_py2():
fake_start_child = mock.MagicMock()
fake_transaction = mock.MagicMock()
fake_transaction.start_child = fake_start_child

with mock.patch(
"sentry_sdk.tracing_utils_py2.get_running_span_or_transaction",
return_value=fake_transaction,
):
result = my_example_function()
fake_start_child.assert_not_called()
assert result == "return_of_sync_function"

result2 = start_child_span_decorator_py2(my_example_function)()
fake_start_child.assert_called_once_with(
op="function", description="test_decorator_py2.my_example_function"
)
assert result2 == "return_of_sync_function"


def test_trace_decorator_py2_no_trx():
fake_transaction = None

with mock.patch(
"sentry_sdk.tracing_utils_py2.get_running_span_or_transaction",
return_value=fake_transaction,
):
with mock.patch.object(logger, "warning", mock.Mock()) as fake_warning:
result = my_example_function()
fake_warning.assert_not_called()
assert result == "return_of_sync_function"

result2 = start_child_span_decorator_py2(my_example_function)()
fake_warning.assert_called_once_with(
"No transaction found. Not creating a child span for %s. Please start a Sentry transaction before calling this function.",
"test_decorator_py2.my_example_function",
)
assert result2 == "return_of_sync_function"
101 changes: 101 additions & 0 deletions tests/tracing/test_decorator_py3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import mock
import pytest
import sys

from sentry_sdk.tracing_utils_py3 import (
start_child_span_decorator as start_child_span_decorator_py3,
)
from sentry_sdk.utils import logger

if sys.version_info < (3, 6):
pytest.skip("Async decorator only works on Python 3.6+", allow_module_level=True)


def my_example_function():
return "return_of_sync_function"


async def my_async_example_function():
return "return_of_async_function"


def test_trace_decorator_sync_py3():
fake_start_child = mock.MagicMock()
fake_transaction = mock.MagicMock()
fake_transaction.start_child = fake_start_child

with mock.patch(
"sentry_sdk.tracing_utils_py3.get_running_span_or_transaction",
return_value=fake_transaction,
):
result = my_example_function()
fake_start_child.assert_not_called()
assert result == "return_of_sync_function"

result2 = start_child_span_decorator_py3(my_example_function)()
fake_start_child.assert_called_once_with(
op="function", description="test_decorator_py3.my_example_function"
)
assert result2 == "return_of_sync_function"


def test_trace_decorator_sync_py3_no_trx():
fake_transaction = None

with mock.patch(
"sentry_sdk.tracing_utils_py3.get_running_span_or_transaction",
return_value=fake_transaction,
):
with mock.patch.object(logger, "warning", mock.Mock()) as fake_warning:
result = my_example_function()
fake_warning.assert_not_called()
assert result == "return_of_sync_function"

result2 = start_child_span_decorator_py3(my_example_function)()
fake_warning.assert_called_once_with(
"No transaction found. Not creating a child span for %s. Please start a Sentry transaction before calling this function.",
"test_decorator_py3.my_example_function",
)
assert result2 == "return_of_sync_function"


@pytest.mark.asyncio
async def test_trace_decorator_async_py3():
fake_start_child = mock.MagicMock()
fake_transaction = mock.MagicMock()
fake_transaction.start_child = fake_start_child

with mock.patch(
"sentry_sdk.tracing_utils_py3.get_running_span_or_transaction",
return_value=fake_transaction,
):
result = await my_async_example_function()
fake_start_child.assert_not_called()
assert result == "return_of_async_function"

result2 = await start_child_span_decorator_py3(my_async_example_function)()
fake_start_child.assert_called_once_with(
op="function", description="test_decorator_py3.my_async_example_function"
)
assert result2 == "return_of_async_function"


@pytest.mark.asyncio
async def test_trace_decorator_async_py3_no_trx():
fake_transaction = None

with mock.patch(
"sentry_sdk.tracing_utils_py3.get_running_span_or_transaction",
return_value=fake_transaction,
):
with mock.patch.object(logger, "warning", mock.Mock()) as fake_warning:
result = await my_async_example_function()
fake_warning.assert_not_called()
assert result == "return_of_async_function"

result2 = await start_child_span_decorator_py3(my_async_example_function)()
fake_warning.assert_called_once_with(
"No transaction found. Not creating a child span for %s. Please start a Sentry transaction before calling this function.",
"test_decorator_py3.my_async_example_function",
)
assert result2 == "return_of_async_function"
22 changes: 21 additions & 1 deletion tests/tracing/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from sentry_sdk import Hub, start_span, start_transaction, set_measurement
from sentry_sdk.consts import MATCH_ALL
from sentry_sdk.tracing import Span, Transaction
from sentry_sdk.tracing_utils import should_propagate_trace
from sentry_sdk.tracing_utils import (
should_propagate_trace,
get_running_span_or_transaction,
)

try:
from unittest import mock # python 3.3 and above
Expand Down Expand Up @@ -306,3 +309,20 @@ def test_should_propagate_trace(
hub.client.options = {"trace_propagation_targets": trace_propagation_targets}

assert should_propagate_trace(hub, url) == expected_propagation_decision


def test_get_running_span_or_transaction():
fake_hub = mock.MagicMock()
fake_hub.scope = mock.MagicMock()

fake_hub.scope.span = mock.MagicMock()
fake_hub.scope.transaction = None
assert get_running_span_or_transaction(fake_hub) == fake_hub.scope.span

fake_hub.scope.span = None
fake_hub.scope.transaction = mock.MagicMock()
assert get_running_span_or_transaction(fake_hub) == fake_hub.scope.transaction

fake_hub.scope.span = None
fake_hub.scope.transaction = None
assert get_running_span_or_transaction(fake_hub) is None