Skip to content

Commit 9a80374

Browse files
committed
ref: Add streaming trace decorator
1 parent 157d123 commit 9a80374

File tree

2 files changed

+134
-1
lines changed

2 files changed

+134
-1
lines changed

sentry_sdk/traces.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
from sentry_sdk.utils import format_attribute, logger
1515

1616
if TYPE_CHECKING:
17-
from typing import Any, Optional, Union
17+
from typing import Any, Callable, Optional, ParamSpec, TypeVar, Union
1818
from sentry_sdk._types import Attributes, AttributeValue
1919

20+
P = ParamSpec("P")
21+
R = TypeVar("R")
22+
2023

2124
class SpanStatus(str, Enum):
2225
OK = "ok"
@@ -342,3 +345,74 @@ def trace_id(self) -> str:
342345
@property
343346
def sampled(self) -> "Optional[bool]":
344347
return False
348+
349+
350+
def trace(
351+
func: "Optional[Callable[P, R]]" = None,
352+
*,
353+
name: "Optional[str]" = None,
354+
attributes: "Optional[dict[str, Any]]" = None,
355+
active: bool = True,
356+
) -> "Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]":
357+
"""
358+
Decorator to start a span around a function call.
359+
360+
This decorator automatically creates a new span when the decorated function
361+
is called, and finishes the span when the function returns or raises an exception.
362+
363+
:param func: The function to trace. When used as a decorator without parentheses,
364+
this is the function being decorated. When used with parameters (e.g.,
365+
``@trace(op="custom")``, this should be None.
366+
:type func: Callable or None
367+
368+
:param name: The human-readable name/description for the span. If not provided,
369+
defaults to the function name. This provides more specific details about
370+
what the span represents (e.g., "GET /api/users", "process_user_data").
371+
:type name: str or None
372+
373+
:param attributes: A dictionary of key-value pairs to add as attributes to the span.
374+
Attribute values must be strings, integers, floats, or booleans. These
375+
attributes provide additional context about the span's execution.
376+
:type attributes: dict[str, Any] or None
377+
378+
:param active: Controls whether spans started while this span is running
379+
will automatically become its children. That's the default behavior. If
380+
you want to create a span that shouldn't have any children (unless
381+
provided explicitly via the `parent_span` argument), set this to False.
382+
:type active: bool
383+
384+
:returns: When used as ``@trace``, returns the decorated function. When used as
385+
``@trace(...)`` with parameters, returns a decorator function.
386+
:rtype: Callable or decorator function
387+
388+
Example::
389+
390+
import sentry_sdk
391+
392+
# Simple usage with default values
393+
@sentry_sdk.trace
394+
def process_data():
395+
# Function implementation
396+
pass
397+
398+
# With custom parameters
399+
@sentry_sdk.trace(
400+
name="Get user data",
401+
attributes={"postgres": True}
402+
)
403+
def make_db_query(sql):
404+
# Function implementation
405+
pass
406+
"""
407+
from sentry_sdk.tracing_utils import create_streaming_span_decorator
408+
409+
decorator = create_streaming_span_decorator(
410+
name=name,
411+
attributes=attributes,
412+
active=active,
413+
)
414+
415+
if func:
416+
return decorator(func)
417+
else:
418+
return decorator

sentry_sdk/tracing_utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,58 @@ def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
942942
return span_decorator
943943

944944

945+
def create_streaming_span_decorator(
946+
name: "Optional[str]" = None,
947+
attributes: "Optional[dict[str, Any]]" = None,
948+
active: bool = True,
949+
) -> "Any":
950+
"""
951+
Create a span creating decorator that can wrap both sync and async functions.
952+
"""
953+
from sentry_sdk.scope import should_send_default_pii
954+
955+
def span_decorator(f: "Any") -> "Any":
956+
"""
957+
Decorator to create a span for the given function.
958+
"""
959+
960+
@functools.wraps(f)
961+
async def async_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
962+
span_name = name or qualname_from_function(f) or ""
963+
964+
with start_streaming_span(
965+
name=span_name, attributes=attributes, active=active
966+
):
967+
result = await f(*args, **kwargs)
968+
return result
969+
970+
try:
971+
async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
972+
except Exception:
973+
pass
974+
975+
@functools.wraps(f)
976+
def sync_wrapper(*args: "Any", **kwargs: "Any") -> "Any":
977+
span_name = name or qualname_from_function(f) or ""
978+
979+
with start_streaming_span(
980+
name=span_name, attributes=attributes, active=active
981+
):
982+
return f(*args, **kwargs)
983+
984+
try:
985+
sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
986+
except Exception:
987+
pass
988+
989+
if inspect.iscoroutinefunction(f):
990+
return async_wrapper
991+
else:
992+
return sync_wrapper
993+
994+
return span_decorator
995+
996+
945997
def get_current_span(scope: "Optional[sentry_sdk.Scope]" = None) -> "Optional[Span]":
946998
"""
947999
Returns the currently active span if there is one running, otherwise `None`
@@ -1320,3 +1372,10 @@ def add_sentry_baggage_to_headers(
13201372

13211373
if TYPE_CHECKING:
13221374
from sentry_sdk.tracing import Span
1375+
1376+
1377+
from sentry_sdk.traces import (
1378+
LOW_QUALITY_SEGMENT_SOURCES,
1379+
start_span as start_streaming_span,
1380+
StreamedSpan,
1381+
)

0 commit comments

Comments
 (0)