Skip to content

Commit 0962302

Browse files
authored
Merge branch 'master' into antonpirker/django-db-improvements
2 parents fba0b46 + 7e5d401 commit 0962302

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
@@ -776,70 +776,86 @@ def normalize_incoming_data(incoming_data):
776776
return data
777777

778778

779-
def start_child_span_decorator(func):
780-
# type: (Any) -> Any
779+
def create_span_decorator(op=None, name=None, attributes=None):
780+
# type: (Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any
781781
"""
782-
Decorator to add child spans for functions.
782+
Create a span decorator that can wrap both sync and async functions.
783783
784-
See also ``sentry_sdk.tracing.trace()``.
784+
:param op: The operation type for the span.
785+
:param name: The name of the span.
786+
:param attributes: Additional attributes to set on the span.
785787
"""
786-
# Asynchronous case
787-
if inspect.iscoroutinefunction(func):
788788

789-
@wraps(func)
790-
async def func_with_tracing(*args, **kwargs):
791-
# type: (*Any, **Any) -> Any
789+
def span_decorator(f):
790+
# type: (Any) -> Any
791+
"""
792+
Decorator to create a span for the given function.
793+
"""
792794

793-
span = get_current_span()
795+
@functools.wraps(f)
796+
async def async_wrapper(*args, **kwargs):
797+
# type: (*Any, **Any) -> Any
798+
current_span = get_current_span()
794799

795-
if span is None:
800+
if current_span is None:
796801
logger.debug(
797802
"Cannot create a child span for %s. "
798803
"Please start a Sentry transaction before calling this function.",
799-
qualname_from_function(func),
804+
qualname_from_function(f),
800805
)
801-
return await func(*args, **kwargs)
806+
return await f(*args, **kwargs)
807+
808+
span_op = op or OP.FUNCTION
809+
span_name = name or qualname_from_function(f) or ""
802810

803-
with span.start_child(
804-
op=OP.FUNCTION,
805-
name=qualname_from_function(func),
806-
):
807-
return await func(*args, **kwargs)
811+
with current_span.start_child(
812+
op=span_op,
813+
name=span_name,
814+
) as span:
815+
span.update_data(attributes or {})
816+
result = await f(*args, **kwargs)
817+
return result
808818

809819
try:
810-
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
820+
async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
811821
except Exception:
812822
pass
813823

814-
# Synchronous case
815-
else:
816-
817-
@wraps(func)
818-
def func_with_tracing(*args, **kwargs):
824+
@functools.wraps(f)
825+
def sync_wrapper(*args, **kwargs):
819826
# type: (*Any, **Any) -> Any
827+
current_span = get_current_span()
820828

821-
span = get_current_span()
822-
823-
if span is None:
829+
if current_span is None:
824830
logger.debug(
825831
"Cannot create a child span for %s. "
826832
"Please start a Sentry transaction before calling this function.",
827-
qualname_from_function(func),
833+
qualname_from_function(f),
828834
)
829-
return func(*args, **kwargs)
835+
return f(*args, **kwargs)
836+
837+
span_op = op or OP.FUNCTION
838+
span_name = name or qualname_from_function(f) or ""
830839

831-
with span.start_child(
832-
op=OP.FUNCTION,
833-
name=qualname_from_function(func),
834-
):
835-
return func(*args, **kwargs)
840+
with current_span.start_child(
841+
op=span_op,
842+
name=span_name,
843+
) as span:
844+
span.update_data(attributes or {})
845+
result = f(*args, **kwargs)
846+
return result
836847

837848
try:
838-
func_with_tracing.__signature__ = inspect.signature(func) # type: ignore[attr-defined]
849+
sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
839850
except Exception:
840851
pass
841852

842-
return func_with_tracing
853+
if inspect.iscoroutinefunction(f):
854+
return async_wrapper
855+
else:
856+
return sync_wrapper
857+
858+
return span_decorator
843859

844860

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