Skip to content

Commit 7e5d401

Browse files
feat(tracing): Improve @trace decorator. (#4648)
Update the `@trace` decorator and make it more powerful. It accepts now the following parameters: `op`, `name`, `attributes`. Example usage: ```python import sentry_sdk from sentry_sdk.consts import OP # Simple usage (like before) @sentry_sdk.trace def process_data(): # Function implementation pass # With custom parameters @sentry_sdk.trace( op=OP.DB_QUERY, name="Get user data", attributes={"postgres": True} ) def make_db_query(sql): # Function implementation pass ``` This creates better DX for our users. --------- Co-authored-by: Daniel Szoke <[email protected]>
1 parent 19914cd commit 7e5d401

File tree

5 files changed

+120
-57
lines changed

5 files changed

+120
-57
lines changed

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Enriching Events
3737
Performance Monitoring
3838
======================
3939

40+
.. autofunction:: sentry_sdk.api.trace
4041
.. autofunction:: sentry_sdk.api.continue_trace
4142
.. autofunction:: sentry_sdk.api.get_current_span
4243
.. autofunction:: sentry_sdk.api.start_span

sentry_sdk/integrations/starlite.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from starlite.plugins.base import get_plugin_for_value # type: ignore
1818
from starlite.routes.http import HTTPRoute # type: ignore
1919
from starlite.utils import ConnectionDataExtractor, is_async_callable, Ref # type: ignore
20-
from pydantic import BaseModel # type: ignore
20+
from pydantic import BaseModel
2121
except ImportError:
2222
raise DidNotEnable("Starlite is not installed")
2323

sentry_sdk/tracing.py

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,43 +1340,85 @@ def _set_initial_sampling_decision(self, sampling_context):
13401340
if TYPE_CHECKING:
13411341

13421342
@overload
1343-
def trace(func=None):
1344-
# type: (None) -> Callable[[Callable[P, R]], Callable[P, R]]
1343+
def trace(func=None, *, op=None, name=None, attributes=None):
1344+
# type: (None, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]]
1345+
# Handles: @trace() and @trace(op="custom")
13451346
pass
13461347

13471348
@overload
13481349
def trace(func):
13491350
# type: (Callable[P, R]) -> Callable[P, R]
1351+
# Handles: @trace
13501352
pass
13511353

13521354

1353-
def trace(func=None):
1354-
# type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
1355+
def trace(func=None, *, op=None, name=None, attributes=None):
1356+
# type: (Optional[Callable[P, R]], Optional[str], Optional[str], Optional[dict[str, Any]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
13551357
"""
1356-
Decorator to start a child span under the existing current transaction.
1357-
If there is no current transaction, then nothing will be traced.
1358+
Decorator to start a child span around a function call.
13581359
1359-
.. code-block::
1360-
:caption: Usage
1360+
This decorator automatically creates a new span when the decorated function
1361+
is called, and finishes the span when the function returns or raises an exception.
1362+
1363+
:param func: The function to trace. When used as a decorator without parentheses,
1364+
this is the function being decorated. When used with parameters (e.g.,
1365+
``@trace(op="custom")``, this should be None.
1366+
:type func: Callable or None
1367+
1368+
:param op: The operation name for the span. This is a high-level description
1369+
of what the span represents (e.g., "http.client", "db.query").
1370+
You can use predefined constants from :py:class:`sentry_sdk.consts.OP`
1371+
or provide your own string. If not provided, a default operation will
1372+
be assigned based on the template.
1373+
:type op: str or None
1374+
1375+
:param name: The human-readable name/description for the span. If not provided,
1376+
defaults to the function name. This provides more specific details about
1377+
what the span represents (e.g., "GET /api/users", "process_user_data").
1378+
:type name: str or None
1379+
1380+
:param attributes: A dictionary of key-value pairs to add as attributes to the span.
1381+
Attribute values must be strings, integers, floats, or booleans. These
1382+
attributes provide additional context about the span's execution.
1383+
:type attributes: dict[str, Any] or None
1384+
1385+
:returns: When used as ``@trace``, returns the decorated function. When used as
1386+
``@trace(...)`` with parameters, returns a decorator function.
1387+
:rtype: Callable or decorator function
1388+
1389+
Example::
13611390
13621391
import sentry_sdk
1392+
from sentry_sdk.consts import OP
13631393
1394+
# Simple usage with default values
13641395
@sentry_sdk.trace
1365-
def my_function():
1366-
...
1396+
def process_data():
1397+
# Function implementation
1398+
pass
13671399
1368-
@sentry_sdk.trace
1369-
async def my_async_function():
1370-
...
1400+
# With custom parameters
1401+
@sentry_sdk.trace(
1402+
op=OP.DB_QUERY,
1403+
name="Get user data",
1404+
attributes={"postgres": True}
1405+
)
1406+
def make_db_query(sql):
1407+
# Function implementation
1408+
pass
13711409
"""
1372-
from sentry_sdk.tracing_utils import start_child_span_decorator
1410+
from sentry_sdk.tracing_utils import create_span_decorator
1411+
1412+
decorator = create_span_decorator(
1413+
op=op,
1414+
name=name,
1415+
attributes=attributes,
1416+
)
13731417

1374-
# This patterns allows usage of both @sentry_traced and @sentry_traced(...)
1375-
# See https://stackoverflow.com/questions/52126071/decorator-with-arguments-avoid-parenthesis-when-no-arguments/52126278
13761418
if func:
1377-
return start_child_span_decorator(func)
1419+
return decorator(func)
13781420
else:
1379-
return start_child_span_decorator
1421+
return decorator
13801422

13811423

13821424
# Circular imports

sentry_sdk/tracing_utils.py

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import contextlib
2+
import functools
23
import inspect
34
import os
45
import re
56
import sys
67
from collections.abc import Mapping
78
from datetime import timedelta
89
from decimal import ROUND_DOWN, Decimal, DefaultContext, localcontext
9-
from functools import wraps
1010
from random import Random
1111
from urllib.parse import quote, unquote
1212
import uuid
@@ -770,70 +770,86 @@ def normalize_incoming_data(incoming_data):
770770
return data
771771

772772

773-
def start_child_span_decorator(func):
774-
# type: (Any) -> Any
773+
def create_span_decorator(op=None, name=None, attributes=None):
774+
# type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any
775775
"""
776-
Decorator to add child spans for functions.
776+
Create a span decorator that can wrap both sync and async functions.
777777
778-
See also ``sentry_sdk.tracing.trace()``.
778+
:param op: The operation type for the span.
779+
:param name: The name of the span.
780+
:param attributes: Additional attributes to set on the span.
779781
"""
780-
# Asynchronous case
781-
if inspect.iscoroutinefunction(func):
782782

783-
@wraps(func)
784-
async def func_with_tracing(*args, **kwargs):
785-
# type: (*Any, **Any) -> Any
783+
def span_decorator(f):
784+
# type: (Any) -> Any
785+
"""
786+
Decorator to create a span for the given function.
787+
"""
786788

787-
span = get_current_span()
789+
@functools.wraps(f)
790+
async def async_wrapper(*args, **kwargs):
791+
# type: (*Any, **Any) -> Any
792+
current_span = get_current_span()
788793

789-
if span is None:
794+
if current_span is None:
790795
logger.debug(
791796
"Cannot create a child span for %s. "
792797
"Please start a Sentry transaction before calling this function.",
793-
qualname_from_function(func),
798+
qualname_from_function(f),
794799
)
795-
return await func(*args, **kwargs)
800+
return await f(*args, **kwargs)
801+
802+
span_op = op or OP.FUNCTION
803+
span_name = name or qualname_from_function(f) or ""
796804

797-
with span.start_child(
798-
op=OP.FUNCTION,
799-
name=qualname_from_function(func),
800-
):
801-
return await func(*args, **kwargs)
805+
with current_span.start_child(
806+
op=span_op,
807+
name=span_name,
808+
) as span:
809+
span.update_data(attributes or {})
810+
result = await f(*args, **kwargs)
811+
return result
802812

803813
try:
804-
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
814+
async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
805815
except Exception:
806816
pass
807817

808-
# Synchronous case
809-
else:
810-
811-
@wraps(func)
812-
def func_with_tracing(*args, **kwargs):
818+
@functools.wraps(f)
819+
def sync_wrapper(*args, **kwargs):
813820
# type: (*Any, **Any) -> Any
821+
current_span = get_current_span()
814822

815-
span = get_current_span()
816-
817-
if span is None:
823+
if current_span is None:
818824
logger.debug(
819825
"Cannot create a child span for %s. "
820826
"Please start a Sentry transaction before calling this function.",
821-
qualname_from_function(func),
827+
qualname_from_function(f),
822828
)
823-
return func(*args, **kwargs)
829+
return f(*args, **kwargs)
830+
831+
span_op = op or OP.FUNCTION
832+
span_name = name or qualname_from_function(f) or ""
824833

825-
with span.start_child(
826-
op=OP.FUNCTION,
827-
name=qualname_from_function(func),
828-
):
829-
return func(*args, **kwargs)
834+
with current_span.start_child(
835+
op=span_op,
836+
name=span_name,
837+
) as span:
838+
span.update_data(attributes or {})
839+
result = f(*args, **kwargs)
840+
return result
830841

831842
try:
832-
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
843+
sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
833844
except Exception:
834845
pass
835846

836-
return func_with_tracing
847+
if inspect.iscoroutinefunction(f):
848+
return async_wrapper
849+
else:
850+
return sync_wrapper
851+
852+
return span_decorator
837853

838854

839855
def get_current_span(scope=None):

tests/tracing/test_decorator.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pytest
55

66
from sentry_sdk.tracing import trace
7-
from sentry_sdk.tracing_utils import start_child_span_decorator
7+
from sentry_sdk.tracing_utils import create_span_decorator
88
from sentry_sdk.utils import logger
99
from tests.conftest import patch_start_tracing_child
1010

@@ -24,6 +24,7 @@ def test_trace_decorator():
2424
fake_start_child.assert_not_called()
2525
assert result == "return_of_sync_function"
2626

27+
start_child_span_decorator = create_span_decorator()
2728
result2 = start_child_span_decorator(my_example_function)()
2829
fake_start_child.assert_called_once_with(
2930
op="function", name="test_decorator.my_example_function"
@@ -38,6 +39,7 @@ def test_trace_decorator_no_trx():
3839
fake_debug.assert_not_called()
3940
assert result == "return_of_sync_function"
4041

42+
start_child_span_decorator = create_span_decorator()
4143
result2 = start_child_span_decorator(my_example_function)()
4244
fake_debug.assert_called_once_with(
4345
"Cannot create a child span for %s. "
@@ -55,6 +57,7 @@ async def test_trace_decorator_async():
5557
fake_start_child.assert_not_called()
5658
assert result == "return_of_async_function"
5759

60+
start_child_span_decorator = create_span_decorator()
5861
result2 = await start_child_span_decorator(my_async_example_function)()
5962
fake_start_child.assert_called_once_with(
6063
op="function",
@@ -71,6 +74,7 @@ async def test_trace_decorator_async_no_trx():
7174
fake_debug.assert_not_called()
7275
assert result == "return_of_async_function"
7376

77+
start_child_span_decorator = create_span_decorator()
7478
result2 = await start_child_span_decorator(my_async_example_function)()
7579
fake_debug.assert_called_once_with(
7680
"Cannot create a child span for %s. "

0 commit comments

Comments
 (0)