Skip to content

Commit bfc6626

Browse files
committed
A nicer implementation
1 parent 4346db1 commit bfc6626

File tree

2 files changed

+174
-137
lines changed

2 files changed

+174
-137
lines changed

sentry_sdk/tracing.py

Lines changed: 15 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
11
from decimal import Decimal
2-
import functools
3-
import inspect
42
import uuid
53
import warnings
64
from datetime import datetime, timedelta, timezone
75
from enum import Enum
86

97
import sentry_sdk
10-
from sentry_sdk.consts import INSTRUMENTER, OP, SPANSTATUS, SPANDATA
8+
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA
119
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
10+
from sentry_sdk.tracing_utils import create_span_decorator
1211
from sentry_sdk.utils import (
1312
get_current_thread_meta,
1413
is_valid_sample_rate,
1514
logger,
1615
nanosecond_time,
1716
should_be_treated_as_error,
18-
qualname_from_function,
1917
)
2018

2119
from typing import TYPE_CHECKING
@@ -281,7 +279,6 @@ class Span:
281279
"_local_aggregator",
282280
"scope",
283281
"origin",
284-
"name",
285282
"_flags",
286283
"_flags_capacity",
287284
)
@@ -352,6 +349,11 @@ def __init__(
352349
self.update_active_thread()
353350
self.set_profiler_id(get_profiler_id())
354351

352+
@property
353+
def name(self):
354+
# type: () -> str
355+
return self.description
356+
355357
# TODO this should really live on the Transaction class rather than the Span
356358
# class
357359
def init_span_recorder(self, maxlen):
@@ -1374,144 +1376,20 @@ async def my_async_function():
13741376
return start_child_span_decorator
13751377

13761378

1377-
def _set_span_attributes(span, attributes):
1378-
# type: (Span, dict[str, Any]) -> None
1379-
"""
1380-
Set the given attributes on the given span.
1381-
1382-
:param span: The span to set attributes on.
1383-
:param attributes: The attributes to set on the span.
1384-
"""
1385-
for key, value in attributes.items():
1386-
span.set_data(key, value)
1387-
1388-
1389-
def _set_input_attributes(span, template, args, kwargs):
1390-
"""
1391-
Set LLM input attributes based on given information to the given span.
1392-
1393-
:param span: The span to set attributes on.
1394-
:param template: The template to use to set attributes on the span.
1395-
:param args: The arguments to the LLM call.
1396-
:param kwargs: The keyword arguments to the LLM call.
1397-
"""
1398-
pass
1399-
1400-
1401-
def _set_output_attributes(span, template, result):
1402-
"""
1403-
Set LLM output attributes based on given information to the given span.
1404-
1405-
:param span: The span to set attributes on.
1406-
:param template: The template to use to set attributes on the span.
1407-
:param result: The result of the LLM call.
1408-
"""
1409-
pass
1410-
1411-
1412-
def new_trace(func=None, *, as_type="span", name=None):
1379+
def new_trace(func=None, *, as_type="span", name=None, attributes=None):
14131380
"""
14141381
Decorator to start a child span.
14151382
14161383
:param func: The function to trace.
14171384
:param as_type: The type of span to create.
1418-
:param name: The name of the span.
1385+
:param name: The name of the span. (defaults to the function name)
1386+
:param attributes: Additional attributes to set on the span.
14191387
"""
1420-
1421-
def span_decorator(f, *a, **kw):
1422-
@functools.wraps(f)
1423-
async def async_wrapper(*args, **kwargs):
1424-
# type: (*Any, **Any) -> Any
1425-
op = kw.get("op", OP.FUNCTION)
1426-
span_name = kw.get("name", qualname_from_function(f))
1427-
attributes = kw.get("attributes", {})
1428-
1429-
with sentry_sdk.start_span(
1430-
op=op,
1431-
name=span_name,
1432-
) as span:
1433-
_set_span_attributes(span, attributes)
1434-
_set_input_attributes(span, as_type, args, kwargs)
1435-
result = await f(*args, **kwargs)
1436-
_set_output_attributes(span, as_type, result)
1437-
1438-
return result
1439-
1440-
@functools.wraps(f)
1441-
def sync_wrapper(*args, **kwargs):
1442-
# type: (*Any, **Any) -> Any
1443-
op = kw.get("op", OP.FUNCTION)
1444-
span_name = kw.get("name", qualname_from_function(f))
1445-
attributes = kw.get("attributes", {})
1446-
1447-
with sentry_sdk.start_span(
1448-
op=op,
1449-
name=span_name,
1450-
) as span:
1451-
_set_span_attributes(span, attributes)
1452-
_set_input_attributes(span, as_type, args, kwargs)
1453-
result = f(*args, **kwargs)
1454-
_set_output_attributes(span, as_type, result)
1455-
1456-
return result
1457-
1458-
if inspect.iscoroutinefunction(f):
1459-
wrapper = async_wrapper
1460-
else:
1461-
wrapper = sync_wrapper
1462-
1463-
return wrapper
1464-
1465-
def ai_chat_decorator(f):
1466-
# type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
1467-
attributes = {
1468-
"gen_ai.operation.name": "chat",
1469-
}
1470-
1471-
return span_decorator(
1472-
f,
1473-
op=OP.GEN_AI_CHAT,
1474-
name="chat [MODEL]",
1475-
attributes=attributes,
1476-
)
1477-
1478-
def ai_agent_decorator(f):
1479-
# type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
1480-
agent_name = name or qualname_from_function(f)
1481-
attributes = {
1482-
"gen_ai.operation.name": "invoke_agent",
1483-
"gen_ai.agent.name": agent_name,
1484-
}
1485-
1486-
return span_decorator(
1487-
f,
1488-
op=OP.GEN_AI_INVOKE_AGENT,
1489-
name=f"invoke_agent {agent_name}",
1490-
attributes=attributes,
1491-
)
1492-
1493-
def ai_tool_decorator(f):
1494-
# type: (Optional[Callable[P, R]]) -> Union[Callable[P, R], Callable[[Callable[P, R]], Callable[P, R]]]
1495-
tool_name = name or qualname_from_function(f)
1496-
attributes = {
1497-
"gen_ai.operation.name": "execute_tool",
1498-
"gen_ai.tool.name": tool_name,
1499-
}
1500-
1501-
return span_decorator(
1502-
f,
1503-
op=OP.GEN_AI_EXECUTE_TOOL,
1504-
name=f"execute_tool {tool_name}",
1505-
attributes=attributes,
1506-
)
1507-
1508-
# Select a type based decorator (with default fallback to span_decorator)
1509-
decorator_for_type = {
1510-
"ai_chat": ai_chat_decorator,
1511-
"ai_agent": ai_agent_decorator,
1512-
"ai_tool": ai_tool_decorator,
1513-
}
1514-
decorator = decorator_for_type.get(as_type, span_decorator)
1388+
decorator = create_span_decorator(
1389+
template=as_type,
1390+
name=name,
1391+
attributes=attributes,
1392+
)
15151393

15161394
if func:
15171395
return decorator(func)

sentry_sdk/tracing_utils.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import functools
23
import inspect
34
import os
45
import re
@@ -896,6 +897,164 @@ def _sample_rand_range(parent_sampled, sample_rate):
896897
return sample_rate, 1.0
897898

898899

900+
def _get_span_name(template, name):
901+
span_name = name
902+
if template == "ai_chat":
903+
span_name = "chat [MODEL]"
904+
elif template == "ai_agent":
905+
span_name = f"invoke_agent {name}"
906+
elif template == "ai_tool":
907+
span_name = f"execute_tool {name}"
908+
return span_name
909+
910+
911+
def _get_span_op(template):
912+
op = OP.FUNCTION
913+
914+
if template == "ai_chat":
915+
op = OP.GEN_AI_CHAT
916+
elif template == "ai_agent":
917+
op = OP.GEN_AI_INVOKE_AGENT
918+
elif template == "ai_tool":
919+
op = OP.GEN_AI_EXECUTE_TOOL
920+
921+
return op
922+
923+
924+
def _set_span_attributes(span, attributes):
925+
# type: (Any, dict[str, Any]) -> None
926+
"""
927+
Set the given attributes on the given span.
928+
929+
:param span: The span to set attributes on.
930+
:param attributes: The attributes to set on the span.
931+
"""
932+
attributes = attributes or {}
933+
934+
for key, value in attributes.items():
935+
span.set_data(key, value)
936+
937+
938+
def _set_input_attributes(span, template, args, kwargs):
939+
"""
940+
Set span input attributes based on the given template.
941+
942+
:param span: The span to set attributes on.
943+
:param template: The template to use to set attributes on the span.
944+
:param args: The arguments to the wrapped function.
945+
:param kwargs: The keyword arguments to the wrapped function.
946+
"""
947+
attributes = {}
948+
949+
if template == "ai_agent":
950+
attributes = {
951+
SPANDATA.GEN_AI_OPERATION_NAME: "invoke_agent",
952+
SPANDATA.GEN_AI_AGENT_NAME: span.name,
953+
}
954+
elif template == "ai_chat":
955+
attributes = {
956+
SPANDATA.GEN_AI_OPERATION_NAME: "chat",
957+
}
958+
elif template == "ai_tool":
959+
attributes = {
960+
SPANDATA.GEN_AI_OPERATION_NAME: "execute_tool",
961+
SPANDATA.GEN_AI_TOOL_NAME: span.name,
962+
}
963+
964+
_set_span_attributes(span, attributes)
965+
966+
967+
def _set_output_attributes(span, template, result):
968+
"""
969+
Set span output attributes based on the given template.
970+
971+
:param span: The span to set attributes on.
972+
:param template: The template to use to set attributes on the span.
973+
:param result: The result of the wrapped function.
974+
"""
975+
attributes = {}
976+
977+
_set_span_attributes(span, attributes)
978+
979+
980+
def create_span_decorator(template, op=None, name=None, attributes=None):
981+
# type: (str, Optional[str], Optional[str], Optional[dict[str, Any]]) -> Any
982+
"""
983+
Create a span decorator that can wrap both sync and async functions.
984+
985+
:param template: The type of span to create (used for input/output attributes).
986+
:param op: The operation type for the span.
987+
:param name: The name of the span.
988+
:param attributes: Additional attributes to set on the span.
989+
"""
990+
991+
def span_decorator(f):
992+
@functools.wraps(f)
993+
async def async_wrapper(*args, **kwargs):
994+
# type: (*Any, **Any) -> Any
995+
if get_current_span() is None:
996+
logger.debug(
997+
"Cannot create a child span for %s. "
998+
"Please start a Sentry transaction before calling this function.",
999+
qualname_from_function(f),
1000+
)
1001+
return await f(*args, **kwargs)
1002+
1003+
with sentry_sdk.start_span(
1004+
op=_get_span_op(template),
1005+
name=_get_span_name(template, name or qualname_from_function(f)),
1006+
) as span:
1007+
_set_input_attributes(span, template, args, kwargs)
1008+
1009+
result = await f(*args, **kwargs)
1010+
1011+
_set_output_attributes(span, template, result)
1012+
_set_span_attributes(span, attributes)
1013+
1014+
return result
1015+
1016+
try:
1017+
async_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
1018+
except Exception:
1019+
pass
1020+
1021+
@functools.wraps(f)
1022+
def sync_wrapper(*args, **kwargs):
1023+
# type: (*Any, **Any) -> Any
1024+
if get_current_span() is None:
1025+
logger.debug(
1026+
"Cannot create a child span for %s. "
1027+
"Please start a Sentry transaction before calling this function.",
1028+
qualname_from_function(f),
1029+
)
1030+
return f(*args, **kwargs)
1031+
1032+
with sentry_sdk.start_span(
1033+
op=_get_span_op(template),
1034+
name=_get_span_name(template, name or qualname_from_function(f)),
1035+
) as span:
1036+
_set_input_attributes(span, template, args, kwargs)
1037+
1038+
result = f(*args, **kwargs)
1039+
1040+
_set_output_attributes(span, template, result)
1041+
_set_span_attributes(span, attributes)
1042+
1043+
return result
1044+
1045+
try:
1046+
sync_wrapper.__signature__ = inspect.signature(f) # type: ignore[attr-defined]
1047+
except Exception:
1048+
pass
1049+
1050+
if inspect.iscoroutinefunction(f):
1051+
return async_wrapper
1052+
else:
1053+
return sync_wrapper
1054+
1055+
return span_decorator
1056+
1057+
8991058
# Circular imports
9001059
from sentry_sdk.tracing import (
9011060
BAGGAGE_HEADER_NAME,

0 commit comments

Comments
 (0)