Skip to content

Commit dff793d

Browse files
committed
Merge branch 'ivana/random-perf-improvements' into ivana/remove-decimal
2 parents 566eca3 + 2866369 commit dff793d

File tree

5 files changed

+120
-58
lines changed

5 files changed

+120
-58
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
@@ -1338,43 +1338,85 @@ def _set_initial_sampling_decision(self, sampling_context):
13381338
if TYPE_CHECKING:
13391339

13401340
@overload
1341-
def trace(func=None):
1342-
# type: (None) -> Callable[[Callable[P, R]], Callable[P, R]]
1341+
def trace(func=None, *, op=None, name=None, attributes=None):
1342+
# type: (None, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Callable[[Callable[P, R]], Callable[P, R]]
1343+
# Handles: @trace() and @trace(op="custom")
13431344
pass
13441345

13451346
@overload
13461347
def trace(func):
13471348
# type: (Callable[P, R]) -> Callable[P, R]
1349+
# Handles: @trace
13481350
pass
13491351

13501352

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

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

13791421

13801422
# Circular imports

sentry_sdk/tracing_utils.py

Lines changed: 53 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
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
8-
9-
from functools import wraps
109
from random import Random
1110
from urllib.parse import quote, unquote
1211
import uuid
@@ -770,70 +769,86 @@ def normalize_incoming_data(incoming_data):
770769
return data
771770

772771

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

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

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

789-
if span is None:
793+
if current_span is None:
790794
logger.debug(
791795
"Cannot create a child span for %s. "
792796
"Please start a Sentry transaction before calling this function.",
793-
qualname_from_function(func),
797+
qualname_from_function(f),
794798
)
795-
return await func(*args, **kwargs)
799+
return await f(*args, **kwargs)
800+
801+
span_op = op or OP.FUNCTION
802+
span_name = name or qualname_from_function(f) or ""
796803

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

803812
try:
804-
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
813+
async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
805814
except Exception:
806815
pass
807816

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

815-
span = get_current_span()
816-
817-
if span is None:
822+
if current_span is None:
818823
logger.debug(
819824
"Cannot create a child span for %s. "
820825
"Please start a Sentry transaction before calling this function.",
821-
qualname_from_function(func),
826+
qualname_from_function(f),
822827
)
823-
return func(*args, **kwargs)
828+
return f(*args, **kwargs)
824829

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

831841
try:
832-
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
842+
sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
833843
except Exception:
834844
pass
835845

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

838853

839854
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)