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.
This page covers both transaction mode (default) and stream mode. See Streamed Spans to learn more.
Changed in 2.15.0
The parameter name in start_span() used to be called description. In version 2.15.0 description was deprecated and from 2.15.0 on, only name should be used. description will be removed in 3.0.0.
Adding transactions or service spans (in stream mode) will allow you to instrument and capture certain regions of your code.
If you're using one of Sentry's SDK integrations, transactions/service spans will be created for you automatically.
The following example creates a transaction/service span for an expensive operation (in this case, eat_pizza), and then sends the result to Sentry:
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())
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())
import sentry_sdk
def eat_slice(slice):
...
def eat_pizza(pizza):
with sentry_sdk.traces.start_span(
name="Eat Pizza",
attributes={"sentry.op": "task"},
# set parent span to None to create service span
parent_span=None,
):
while pizza.slices > 0:
span = sentry_sdk.traces.start_span(name="Eat Slice")
eat_slice(pizza.slices.pop())
span.end()
The API reference documents start_transaction and start_span, along with their parameters.
Note that sentry_sdk.start_transaction() (transaction mode) is meant be used as a context manager. This ensures that the transaction/service span will be properly set as active and any spans created within will be attached to it.
In stream mode, sentry_sdk.traces.start_span() can, but doesn't have to be, used as a context manager. If you don't use it as a context manager, make sure to call span.end() to finish the span.
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.
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())
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())
import sentry_sdk
def eat_slice(slice):
...
def eat_pizza(pizza):
with sentry_sdk.traces.start_span(
name="Eat Pizza",
attributes={"sentry.op": "task"},
parent_span=None,
):
while pizza.slices > 0:
with sentry_sdk.traces.start_span(name="Eat Slice"):
eat_slice(pizza.slices.pop())
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())
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())
import sentry_sdk
@sentry_sdk.traces.trace
def eat_slice(slice):
...
def eat_pizza(pizza):
with sentry_sdk.traces.start_span(
name="Eat Pizza",
attributes={"sentry.op": "task"},
parent_span=None,
):
while pizza.slices > 0:
eat_slice(pizza.slices.pop())
See the @sentry_sdk.trace decoration section below for more details.
Static & class methods
When tracing a static or class method, you must add the @sentry_sdk.trace (transaction mode)/@sentry_sdk.traces.trace (stream mode) decorator after the @staticmethod or @classmethod decorator (i.e., closer to the function definition). Otherwise, your function will break! This applies in both transaction mode and stream mode.
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()
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()
import sentry_sdk
def eat_slice(slice):
...
def eat_pizza(pizza):
with sentry_sdk.traces.start_span(
name="Eat Pizza",
attributes={"sentry.op": "task"},
parent_span=None,
):
while pizza.slices > 0:
span = sentry_sdk.traces.start_span(name="Eat Slice")
eat_slice(pizza.slices.pop())
span.end()
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.
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()
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()
import sentry_sdk
def chew():
...
def eat_slice(slice):
with sentry_sdk.traces.start_span(name="Eat Slice"):
with sentry_sdk.traces.start_span(name="Chew"):
chew()
import sentry_sdk
@sentry_sdk.trace
def chew():
...
@sentry_sdk.trace
def eat_slice(slice):
chew()
import sentry_sdk
@sentry_sdk.trace
def chew():
...
@sentry_sdk.trace
def eat_slice(slice):
chew()
import sentry_sdk
@sentry_sdk.traces.trace
def chew():
...
@sentry_sdk.traces.trace
def eat_slice(slice):
chew()
See the @sentry_sdk.trace decoration section below for more details.
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()
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()
import sentry_sdk
def chew():
...
def eat_slice(slice):
parent_span = sentry_sdk.traces.start_span(name="Eat Slice")
# pass the parent span via `parent_span` to create a child span
child_span = sentry_sdk.traces.start_span(name="Chew", parent_span=parent_span)
chew()
child_span.end()
parent_span.end()
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.
Only available in stream mode.
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:
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())
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.
Static & class methods
When tracing a static or class method, you must add the decorator after the @staticmethod or @classmethod decorator (i.e., closer to the function definition). Otherwise, your function will break.
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()
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()
import sentry_sdk
@sentry_sdk.traces.trace(name="Paul", attributes={"sentry.op": "my_op", "x": True})
def my_function(i):
...
@sentry_sdk.traces.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
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
The template parameter is only available in transaction mode.
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.
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()
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
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:
SPANTEMPLATE.AI_AGENT-> Invoke Agent SpanSPANTEMPLATE.AI_CHAT-> AI Request spanSPANTEMPLATE.AI_TOOL-> Execute Tool Span
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.
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,
)
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.
Important
To enable performance monitoring for the functions specified in functions_to_trace, the SDK needs to load the function modules. Be aware, there may be code being executed in modules during module loading. To avoid this, use the method described above to trace your functions.
Only available in transaction mode.
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.
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())
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.
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)
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)
import sentry_sdk
@sentry_sdk.traces.trace
def eat_slice(slice):
span = sentry_sdk.traces.get_current_span()
if span is not None:
span.set_attribute("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:
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])
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.
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])
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])
with sentry_sdk.traces.start_span(name="my-span") as span:
span.set_attribute("my-data-attribute-1", "value1")
span.set_attribute("my-data-attribute-2", 42)
span.set_attribute("my-data-attribute-3", True)
# or use set_attributes to set multiple attributes at once
span.set_attributes({
"my-data-attribute-4": ["value1", "value2", "value3"],
"my-data-attribute-5": [42, 43, 44],
"my-data-attribute-6": [True, False, True]
})
How you add data to every span depends on your tracing mode:
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
)
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
)
import sentry_sdk
def before_send_span(span, hint):
# Runs once per span, as it's flushed.
if "attributes" not in span:
span["attributes"] = {}
span["attributes"].update({
"component_version": "2.0.0",
"deployment_stage": "production",
})
return span
sentry_sdk.init(
# ...
trace_lifecycle="stream",
before_send_span=before_send_span,
)
before_send_span can only modify span data and you cannot use it to drop spans. See Dropping Spans below.
ignore_spans is only available in stream mode.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.
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"),
},
},
],
)
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"),
},
},
],
)
sentry_sdk.update_current_span() isn't available in stream mode. Retrieve the current span with sentry_sdk.traces.get_current_span() and update its attributes using set_attribute() or set_attributes().
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.
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()
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
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
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").