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 or service spans (in stream mode) will allow you to instrument and capture certain regions of your code.

The following example creates a transaction/service span 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 start_span, along with their parameters.

If you want to have more fine-grained performance monitoring, you can add child spans to your transaction/service span, 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() in transaction mode or sentry_sdk.traces.start_span() in stream mode will find the current active transaction/span and attach the new 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() (transaction mode) or span.end() (stream mode) 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()

In transaction mode, the parameters of start_span() and start_child() are the same. See the API reference for more details. In stream mode, however, there's no separate start_child() method. Instead, the span will become the child of the currently active span. Alternatively, you can pass the parent span explicitly via parent_span when starting the child (as shown in the example above).

When you create your span manually, make sure to call span.finish() (transaction mode) or span.end() (stream mode) 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.

In stream mode, a span normally attaches to whatever span is currently active. To control parentage explicitly, for example to make a span a sibling instead of a child, pass parent_span directly:

Copied
import sentry_sdk

def eat_slice(slice):
    ...

def chew():
    ...

def eat_pizza(pizza):
    with sentry_sdk.traces.start_span(name="Eat Pizza") as pizza_span:
        with sentry_sdk.traces.start_span(name="Eat Slice"):
            with sentry_sdk.traces.start_span(name="Chew", parent_span=pizza_span):
                # "Chew" is a sibling of "Eat Slice", not its child
                chew()
            eat_slice(pizza.slices.pop())

In transaction mode, 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.

In stream mode, the decorator is @sentry_sdk.traces.trace and accepts name, attributes, and active. Note that there's no op parameter; set the operation as the sentry.op key in attributes instead. 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.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:

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 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 agent and will create the following span tree that is compatible with the OpenTelemetry Semantic Conventions and the Sentry conventions for 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:

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 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="___PUBLIC_DSN___",
    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() (transaction mode) or sentry_sdk.traces.get_current_span() (stream mode). This function will return a span if there's one running, otherwise it will return None.

In this example, we'll set custom data 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)

In transaction mode, 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 any span 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])

How you add data to every span depends on your tracing mode:

Copied
import sentry_sdk
from sentry_sdk.types import Event, Hint

def before_send_transaction(event: Event, hint: Hint) -> Event | None:
    # Add attributes to the root span (transaction)
    if "trace" in event.get("contexts", {}):
        if "data" not in event["contexts"]["trace"]:
            event["contexts"]["trace"]["data"] = {}

        event["contexts"]["trace"]["data"].update({
            "app_version": "1.2.3",
            "environment_region": "us-west-2"
        })

    # Add attributes to all child spans
    for span in event.get("spans", []):
        if "data" not in span:
            span["data"] = {}

        span["data"].update({
            "component_version": "2.0.0",
            "deployment_stage": "production"
        })

    return event

sentry_sdk.init(
    # ...
    before_send_transaction=before_send_transaction
)

In stream mode, you can prevent specific spans from being created using ignore_spans. It evaluates rules at span start, which can be strings, compiled regexes, or dictionaries with name and/or attributes conditions. If the dropped span is a service span, its children are dropped too. If it's a child span, only that span is dropped and its children are reparented to the nearest ancestor.

Copied
import re
import sentry_sdk

sentry_sdk.init(
    trace_lifecycle="stream",
    ignore_spans=[
        # String match against span name
        "/health",

        # Regex match against span name
        re.compile(r"/flow/.*"),

        # Match by attributes (all must match)
        {
            "attributes": {
                "service.id": "15def9a",
                "flow.pipeline": "legacy",
            }
        },

        # Match by name and attributes
        {
            "name": re.compile(r"/flow/.*"),
            "attributes": {
                "service.id": re.compile(r".*\.facade"),
            },
        },
    ],
)

In transaction mode, 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:

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").