Custom Instrumentation

Learn how to capture performance data on any action in your app.

The Sentry SDK for Python does a very good job of auto instrumenting your application. If you use one of the popular frameworks, we've got you covered because well-known operations like HTTP calls and database queries will be instrumented out of the box. The Sentry SDK will also check your installed Python packages and auto-enable the matching SDK integrations. If you want to enable tracing in a piece of code that performs some other operations, add the @sentry_sdk.trace decorator.

Adding transactions will allow you to instrument and capture certain regions of your code.

The following example creates a transaction for an expensive operation (in this case, eat_pizza), and then sends the result to Sentry:

Copied
import sentry_sdk

def eat_slice(slice):
    ...

def eat_pizza(pizza):
    with sentry_sdk.start_transaction(op="task", name="Eat Pizza"):
        while pizza.slices > 0:
            eat_slice(pizza.slices.pop())

The API reference documents start_transaction and all its parameters.

If you want to have more fine-grained performance monitoring, you can add child spans to your transaction, which can be done by either:

  • Using a context manager
  • Using a decorator (this works on sync and async functions)
  • Manually starting and finishing a span

Calling sentry_sdk.start_span() will find the current active transaction and attach the span to it.

Copied
import sentry_sdk

def eat_slice(slice):
    ...

def eat_pizza(pizza):
    with sentry_sdk.start_transaction(op="task", name="Eat Pizza"):
        while pizza.slices > 0:
            with sentry_sdk.start_span(name="Eat Slice"):
                eat_slice(pizza.slices.pop())

Copied
import sentry_sdk

@sentry_sdk.trace
def eat_slice(slice):
    ...

def eat_pizza(pizza):
    with sentry_sdk.start_transaction(op="task", name="Eat Pizza"):
        while pizza.slices > 0:
            eat_slice(pizza.slices.pop())

See the @sentry_sdk.trace decoration section below for more details.

Copied
import sentry_sdk

def eat_slice(slice):
    ...

def eat_pizza(pizza):
    with sentry_sdk.start_transaction(op="task", name="Eat Pizza"):
        while pizza.slices > 0:
            span = sentry_sdk.start_span(name="Eat Slice")
            eat_slice(pizza.slices.pop())
            span.finish()

When you create your span manually, make sure to call span.finish() after the block of code you want to wrap in a span to finish the span. If you do not finish the span it will not be sent to Sentry.

Spans can be nested to form a span tree. If you'd like to learn more, read our distributed tracing documentation.

Copied
import sentry_sdk

def chew():
    ...

def eat_slice(slice):
    with sentry_sdk.start_span(name="Eat Slice"):
        with sentry_sdk.start_span(name="Chew"):
            chew()

Copied
import sentry_sdk

@sentry_sdk.trace
def chew():
    ...

@sentry_sdk.trace
def eat_slice(slice):
    chew()

See the @sentry_sdk.trace decoration section below for more details.

Copied
import sentry_sdk

def chew():
    ...

def eat_slice(slice):
    parent_span = sentry_sdk.start_span(name="Eat Slice")

    child_span = parent_span.start_child(name="Chew")
    chew()
    child_span.finish()

    parent_span.finish()

The parameters of start_span() and start_child() are the same. See the API reference for more details.

When you create your span manually, make sure to call span.finish() after the block of code you want to wrap in a span to finish the span. If you do not finish the span it will not be sent to Sentry.

You can set op, name and attributes parameters in the @sentry_sdk.trace decorator to customize your spans. Attribute values can only be primitive types (like int, float, bool, str) or a list of those types without mixing types.

When tracing a static or class method, you must add the @sentry_sdk.trace decorator after the @staticmethod or @classmethod decorator (i.e., closer to the function definition). Otherwise, your function will break.

Copied
 import sentry_sdk

+@sentry_sdk.trace(op="my_op", name="Paul", attributes={"x": True})
 def my_function(i):
     ...

 @sentry_sdk.trace
 def root_function():
     for i in range(3):
         my_function(i)

 root_function()

The code above will customize the my_function spans like this:

Copied
gantt
    dateFormat DD
    todayMarker off
    axisFormat %
    SPAN(op=function, name=root_function)   :a1, 01, 7d
    SPAN(op=my_op, name=Paul, x=True)    :a2, 02, 2d
    SPAN(op=my_op, name=Paul, x=True)    :a3, 04, 2d
    SPAN(op=my_op, name=Paul, x=True)    :a4, 06, 2d

In the @sentry_sdk.trace decorator you can also specify a template. This helps create spans that follow a certain template. Currently this is only available for spans that are created for the AI Agents instrumentation of Sentry.

Available templates are AI_AGENT, AI_TOOL, and AI_CHAT.

Copied
 import sentry_sdk
 from sentry_sdk.consts import SPANTEMPLATE

+@sentry_sdk.trace(template=SPANTEMPLATE.AI_AGENT)
 def my_function(i):
     ...

 @sentry_sdk.trace
 def root_function():
     for i in range(3):
         my_function(i)

 root_function()

This will treat my_function as an AI agent and will create the following span tree that is compatible with the OpenTelemetry Semantic Conventions and the Sentry conventions for AI Agents instrumentation. Depending on the template, there will also be a couple of attributes set by default, but those are omitted in the graph below for readability reasons:

Copied
gantt
    dateFormat DD
    todayMarker off
    axisFormat %
    SPAN(op=function, name=root_function)   :a1, 01, 7d
    SPAN(op=gen_ai.invoke_agent, name="invoke_agent my_function")    :a2, 02, 2d
    SPAN(op=gen_ai.invoke_agent, name="invoke_agent my_function")    :a3, 04, 2d
    SPAN(op=gen_ai.invoke_agent, name="invoke_agent my_function")    :a4, 06, 2d

For the span attributes that are set for the different available templates, see the AI Agents instrumentation documentation:

Currently it is not possible to define custom span templates.

To avoid having custom performance instrumentation code scattered all over your code base, pass a parameter functions_to_trace to your sentry_sdk.init() call.

Copied
import sentry_sdk

functions_to_trace = [
    {"qualified_name": "myrootmodule.eat_slice"},
    {"qualified_name": "myrootmodule.swallow"},
    {"qualified_name": "myrootmodule.chew"},
    {"qualified_name": "myrootmodule.someothermodule.another.some_function"},
    {"qualified_name": "myrootmodule.SomePizzaClass.some_method"},
]

sentry_sdk.init(
    dsn="https://examplePublicKey@o0.ingest.sentry.io/0
example-org / example-project
"
,
functions_to_trace=functions_to_trace, )

Now, whenever a function specified in functions_to_trace will be executed, a span will be created and attached as a child to the currently running span.

The sentry_sdk.get_current_scope().transaction property returns the active transaction or None if no transaction is active. You can use this property to modify data on the transaction.

Copied
import sentry_sdk

def eat_pizza(pizza):
    transaction = sentry_sdk.get_current_scope().transaction

    if transaction is not None:
        transaction.set_tag("num_of_slices", len(pizza.slices))

    while pizza.slices > 0:
        eat_slice(pizza.slices.pop())

To change data in the current span, use sentry_sdk.get_current_span(). This function will return a span if there's one running, otherwise it will return None.

In this example, we'll set a tag in the span created by the @sentry_sdk.trace decorator.

Copied
import sentry_sdk

@sentry_sdk.trace
def eat_slice(slice):
    span = sentry_sdk.get_current_span()

    if span is not None:
        span.set_tag("slice_id", slice.id)

You can add data attributes to your transactions. This data is visible in the trace explorer in Sentry. Data attributes can be of type string, number or boolean, as well as (non-mixed) arrays of these types:

Copied
with sentry_sdk.start_transaction(name="my-transaction") as transaction:
    transaction.set_data("my-data-attribute-1", "value1")
    transaction.set_data("my-data-attribute-2", 42)
    transaction.set_data("my-data-attribute-3", True)

    transaction.set_data("my-data-attribute-4", ["value1", "value2", "value3"])
    transaction.set_data("my-data-attribute-5", [42, 43, 44])
    transaction.set_data("my-data-attribute-6", [True, False, True])

You can add data attributes to your spans the same way, with the same type restrictions as described above.

Copied
with sentry_sdk.start_span(name="my-span") as span:
    span.set_data("my-data-attribute-1", "value1")
    span.set_data("my-data-attribute-2", 42)
    span.set_data("my-data-attribute-3", True)

    span.set_data("my-data-attribute-4", ["value1", "value2", "value3"])
    span.set_data("my-data-attribute-5", [42, 43, 44])
    span.set_data("my-data-attribute-6", [True, False, True])

To attach data attributes to the transaction and all its spans, you can use before_send_transaction:

Copied
import sentry_sdk
from sentry_sdk.types import Event, Hint

def before_send_transaction(event: Event, hint: Hint) -> Event | None:
    # Set the data attribute "foo" to "bar" on every span belonging to this
    # transaction event
    for span in event["spans"]:
        span["data"]["foo"] = "bar"

    # Set the data on the transaction itself, too
    event["contexts"]["trace"]["data"]["foo"] = "bar"

    return event

sentry_sdk.init(
    traces_sample_rate=1.0,
    before_send_transaction=before_send_transaction,
)

You can update the data of the currently running span using the sentry_sdk.update_current_span() function. You can set op, name and attributes to update your span. Attribute values can only be primitive types (like int, float, bool, str) or a list of those types without mixing types.

Copied
 import sentry_sdk

 @sentry_sdk.trace(op="my_op", name="Paul", attributes={"x": True})
 def my_function(i):
+    sentry_sdk.update_current_span(
+        op="myOp",
+        name=f"Paul{i}",
+        attributes={"y": i},
+    )
     ...

 @sentry_sdk.trace
 def root_function():
     for i in range(3):
         my_function(i)

 root_function()

The code above will update the my_function (now my_op) spans with custom data like this:

Copied
gantt
    dateFormat DD
    todayMarker off
    axisFormat %
    SPAN(op=function, name=root_function)   :a1, 01, 7d
    SPAN(op=myOp, name=Paul0, x=True, y=0)    :a2, 02, 2d
    SPAN(op=myOp, name=Paul1, x=True, y=1)    :a3, 04, 2d
    SPAN(op=myOp, name=Paul2, x=True, y=2)    :a4, 06, 2d
Was this helpful?
Help improve this content
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").