diff --git a/docs/platforms/python/tracing/streamed-spans/index.mdx b/docs/platforms/python/tracing/streamed-spans/index.mdx index 4cebd494e7940f..eac7692e5412b3 100644 --- a/docs/platforms/python/tracing/streamed-spans/index.mdx +++ b/docs/platforms/python/tracing/streamed-spans/index.mdx @@ -1,14 +1,22 @@ --- title: Streamed Spans +h1_title: Streamed Spans for Python (SDK v3.x) description: "Learn how to use stream mode to send spans to Sentry as they finish, removing the 1,000-span limit and making trace data visible sooner." sidebar_order: 45 new: true --- -By default, the Sentry Python SDK collects all spans in memory and sends them to Sentry as a single transaction once the root span ends. This is called transaction mode. -Stream mode changes this by sending spans to Sentry in batches as they finish. Service spans, which represent a service's entry point, replace transactions as the main grouping for each service. + + +Version 3.0 of the Python SDK is coming in November 2026. This page describes upcoming behavior. For the SDK 2.x version of this page, select 2.x in the version selector in the sidebar. + + - +Starting with SDK 3.0, the Sentry Python SDK sends spans to Sentry in batches as they finish. This is called stream mode. Service spans, which represent a service's entry point, replace transactions as the main grouping for each service. + +In SDK 2.x, the default was transaction mode, which collects all spans in memory and sends them to Sentry as a single transaction once the root span ends. Transaction mode no longer exists in SDK 3.0. + + - **No 1,000-span limit.** In transaction mode, transactions are capped at 1,000 spans. Stream mode has no upper limit since spans are sent in batches. This is especially beneficial for complex generative AI pipelines that easily exceed standard span limits. - **Lower memory usage.** Spans are flushed periodically and don't need to be held in memory until the root span ends. This is especially useful for long-running processes like queue consumers or cron jobs. @@ -36,9 +44,9 @@ Trace └── Child span ``` - + -Stream mode requires the streamed Span API, so migrating to stream mode and migrating to the streamed Span API are the same step. If you have existing custom instrumentation, see the Migration Guide for a full list of changes. +Stream mode uses a different Span API than transaction mode. If you have existing custom instrumentation, see the Migration Guide for a full list of changes. @@ -48,40 +56,13 @@ You need: - Tracing configured in your app -- Sentry SDK `>=2.62.0` - -## Enable Stream Mode - - - - - -Opt in by adding the `trace_lifecycle` option when initializing the SDK: - - - - -```python -import sentry_sdk - -sentry_sdk.init( - dsn="___PUBLIC_DSN___", - traces_sample_rate=1.0, - # enables stream mode - trace_lifecycle="stream", -) - -``` - - - - +- Sentry SDK `>=3.0.0` -To revert to transaction mode, remove the `trace_lifecycle` option or set it to `"static"` (the default). +Stream mode is always on when tracing is enabled. No additional configuration is needed. -When stream mode is enabled, the SDK maintains an internal buffer that groups spans by trace ID. +The SDK maintains an internal buffer that groups spans by trace ID. Spans are flushed: @@ -101,7 +82,7 @@ Each flush sends only the spans accumulated since the last flush, grouped into e -Use `sentry_sdk.traces.start_span()` to create a span that ends automatically when the `with` block exits: +Use `sentry_sdk.start_span()` to create a span that ends automatically when the `with` block exits: @@ -109,7 +90,7 @@ Use `sentry_sdk.traces.start_span()` to create a span that ends automatically wh ```python import sentry_sdk -with sentry_sdk.traces.start_span(name="my-operation") as span: +with sentry_sdk.start_span(name="my-operation") as span: # Your code here do_work() ``` @@ -127,11 +108,11 @@ Child spans created inside an active span are automatically associated with the ```python import sentry_sdk -with sentry_sdk.traces.start_span(name="parent-operation"): - with sentry_sdk.traces.start_span(name="child-step-1"): +with sentry_sdk.start_span(name="parent-operation"): + with sentry_sdk.start_span(name="child-step-1"): step_one() - with sentry_sdk.traces.start_span(name="child-step-2"): + with sentry_sdk.start_span(name="child-step-2"): step_two() ``` @@ -149,7 +130,7 @@ If you want to force a new service span, regardless of whether it has a parent s ```python import sentry_sdk -with sentry_sdk.traces.start_span(name="task-name", parent_span=None) as span: +with sentry_sdk.start_span(name="task-name", parent_span=None) as span: do_work() ``` @@ -164,7 +145,7 @@ You can also use the `@trace` decorator to instrument a function. It accepts opt ```python -from sentry_sdk.traces import trace +from sentry_sdk import trace @trace(name="checkout", attributes={"flow.pipeline": "legacy"}) def checkout(): @@ -199,7 +180,7 @@ You can set attributes when starting a span: ```python import sentry_sdk -with sentry_sdk.traces.start_span( +with sentry_sdk.start_span( name="process-order", attributes={ "sentry.op": "queue.process", @@ -225,7 +206,7 @@ Or add them to an already running span: ```python import sentry_sdk -with sentry_sdk.traces.start_span(name="handle-request") as span: +with sentry_sdk.start_span(name="handle-request") as span: # Set a single attribute span.set_attribute("http.response.status_code", 200) @@ -244,7 +225,7 @@ For examples, see -If you need to start a completely new trace unconnected to the current one, use `sentry_sdk.traces.new_trace()`. This is useful for background jobs or scheduled tasks where you want a clean trace boundary: +If you need to start a completely new trace unconnected to the current one, use `sentry_sdk.new_trace()`. This is useful for background jobs or scheduled tasks where you want a clean trace boundary: @@ -292,12 +273,12 @@ If you need to start a completely new trace unconnected to the current one, use ```python import sentry_sdk -with sentry_sdk.traces.start_span(name="span in trace 1"): +with sentry_sdk.start_span(name="span in trace 1"): ... -sentry_sdk.traces.new_trace() +sentry_sdk.new_trace() -with sentry_sdk.traces.start_span(name="span in trace 2"): +with sentry_sdk.start_span(name="span in trace 2"): # This span is the root of a new, separate trace ... ``` @@ -337,7 +318,6 @@ def postprocess_span(span, hint): sentry_sdk.init( dsn="___PUBLIC_DSN___", traces_sample_rate=1.0, - trace_lifecycle="stream", before_send_span=postprocess_span, ) ``` @@ -370,7 +350,6 @@ import sentry_sdk sentry_sdk.init( dsn="___PUBLIC_DSN___", traces_sample_rate=1.0, - trace_lifecycle="stream", ignore_spans=[ # String match against span name "/health", @@ -402,13 +381,13 @@ If a matching span is a service span, all of its child spans are dropped as well ## Sampling (Optional) -If you use `traces_sample_rate`, no changes are needed — it works the same way in stream mode. +If you're upgrading from SDK 2.x and use `traces_sample_rate`, no changes are needed — it works the same way as in transaction mode. -If you use a custom `traces_sampler`, the shape of the sampling context is different in stream mode. See the Streamed Span API migration guide for details. +If you use a custom `traces_sampler`, not all keys in the sampling context might be populated. See the Streamed Span API migration guide for details. ## Verify Your Setup -To make sure you've enabled stream mode successfully: +To make sure your setup works: - **Check the Sentry dashboard**: Spans should appear in the Traces view shortly after they complete. Traces look similar to transaction mode, but contain only spans and no transactions. -- **Check your logs**: If the SDK logs warnings about unsupported span operations, you may still be using the legacy Span API somewhere in your code. +- **Check your custom instrumentation**: If your code fails with an `ImportError` or `AttributeError` related to `sentry_sdk.tracing`, `start_transaction`, or span methods like `set_data()`, you're still using the transaction-mode Span API. See the Migration Guide for how to update it. diff --git a/docs/platforms/python/tracing/streamed-spans/index__v2.x.mdx b/docs/platforms/python/tracing/streamed-spans/index__v2.x.mdx new file mode 100644 index 00000000000000..4cb7ae9d36f074 --- /dev/null +++ b/docs/platforms/python/tracing/streamed-spans/index__v2.x.mdx @@ -0,0 +1,415 @@ +--- +title: Streamed Spans +h1_title: Streamed Spans for Python +description: "Learn how to use stream mode to send spans to Sentry as they finish, removing the 1,000-span limit and making trace data visible sooner." +sidebar_order: 45 +new: true +--- + +By default, the Sentry Python SDK collects all spans in memory and sends them to Sentry as a single transaction once the root span ends. This is called transaction mode. +Stream mode changes this by sending spans to Sentry in batches as they finish. Service spans, which represent a service's entry point, replace transactions as the main grouping for each service. + + + +- **No 1,000-span limit.** In transaction mode, transactions are capped at 1,000 spans. Stream mode has no upper limit since spans are sent in batches. This is especially beneficial for complex generative AI pipelines that easily exceed standard span limits. +- **Lower memory usage.** Spans are flushed periodically and don't need to be held in memory until the root span ends. This is especially useful for long-running processes like queue consumers or cron jobs. +- **Faster visibility.** Span data arrives in Sentry as your application runs, instead of only after the entire operation completes. +- **Fewer spans lost to crashes.** If your process terminates unexpectedly, spans that were already flushed are emitted normally. In transaction mode, a crash before the transaction ends means all span data is lost. Stream mode emits spans incrementally as they finish, so more of them survive a crash. + + + +You can find the following span types mentioned throughout this page: + +- **Root span**: The topmost span in a trace. It has no parent span and is always a service span. +- **Service span**: A parent-level span at the entry of a service. In transaction mode, this is called a transaction. +- **Child span**: Any span nested under a parent span within the same trace. + +This graph shows how these span types relate to each other within a trace: + +``` +Trace +│ +└── Root span [service A] + ├── Child span + │ └── Child span + └── Service span [service B] + ├── Child span + └── Child span +``` + + + +Stream mode requires the streamed Span API, so migrating to stream mode and migrating to the streamed Span API are the same step. If you have existing custom instrumentation, see the Migration Guide for a full list of changes. + + + +## Prerequisites + +You need: + +- Tracing configured in + your app +- Sentry SDK `>=2.62.0` + +## Enable Stream Mode + + + + + +Opt in by adding the `trace_lifecycle` option when initializing the SDK: + + + + +```python +import sentry_sdk + +sentry_sdk.init( + dsn="___PUBLIC_DSN___", + traces_sample_rate=1.0, + # enables stream mode + trace_lifecycle="stream", +) + +``` + + + + + +To revert to transaction mode, remove the `trace_lifecycle` option or set it to `"static"` (the default). + + + +When stream mode is enabled, the SDK maintains an internal buffer that groups spans by trace ID. + +Spans are flushed: + +- On a regular interval (every 5 seconds by default). +- When a trace's buffer reaches 1,000 spans. +- When the SDK shuts down. + +Each flush sends only the spans accumulated since the last flush, grouped into envelopes by trace ID. + + + +## Manual Instrumentation (Optional) + +### Start a Span + + + + + +Use `sentry_sdk.traces.start_span()` to create a span that ends automatically when the `with` block exits: + + + + +```python +import sentry_sdk + +with sentry_sdk.traces.start_span(name="my-operation") as span: + # Your code here + do_work() +``` + + + + + + +Child spans created inside an active span are automatically associated with the parent: + + + + +```python +import sentry_sdk + +with sentry_sdk.traces.start_span(name="parent-operation"): + with sentry_sdk.traces.start_span(name="child-step-1"): + step_one() + + with sentry_sdk.traces.start_span(name="child-step-2"): + step_two() +``` + + + + + + +A span is automatically promoted to a service span (the equivalent of a transaction) if no other parent is currently active. +If you want to force a new service span, regardless of whether it has a parent span, set `parent_span=None`: + + + + +```python +import sentry_sdk + +with sentry_sdk.traces.start_span(name="task-name", parent_span=None) as span: + do_work() +``` + + + + + + +You can also use the `@trace` decorator to instrument a function. It accepts optional `name`, `attributes`, and `active` arguments: + + + + +```python +from sentry_sdk.traces import trace + +@trace(name="checkout", attributes={"flow.pipeline": "legacy"}) +def checkout(): + ... +``` + + + + + +For more details on span creation, see Custom Instrumentation. + +### Add Span Attributes + +Attach structured metadata to spans using `attributes`, which can be `str`, `int`, `float`, or `bool`, as well as arrays of these types. + + + +Sentry automatically sets several standard attributes on spans. To avoid accidentally overwriting these, refer to our Sentry Attribute Conventions. + + + + + + + +You can set attributes when starting a span: + + + + +```python +import sentry_sdk + +with sentry_sdk.traces.start_span( + name="process-order", + attributes={ + "sentry.op": "queue.process", + "order.id": "abc-123", + "order.item_count": 5, + "order.priority": True, + }, +): + process_order() +``` + + + + + + + +Or add them to an already running span: + + + + +```python +import sentry_sdk + +with sentry_sdk.traces.start_span(name="handle-request") as span: + # Set a single attribute + span.set_attribute("http.response.status_code", 200) + + # Set multiple attributes at once + span.set_attributes({ + "http.route": "/api/users", + "user.id": "user-42", + }) +``` + + + + + +Find more examples in our Sending Span Metrics documentation. + +## Breadcrumbs + +In stream mode, breadcrumbs are no longer sent with spans. +They remain attached to errors, so you don't need to change how you record them. + +## Distributed Tracing (Optional) + +### Continue a Trace + + + + + +When your service receives a request from an upstream service that includes Sentry trace headers, use `sentry_sdk.traces.continue_trace()` to connect your spans to the existing distributed trace. +Unlike the legacy `sentry_sdk.continue_trace()`, the new version is not a context manager. Instead, it sets the propagation context and the next span picks it up automatically. + + + + +```python +import sentry_sdk + +headers = { + "sentry-trace": "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0be902b7-1", + "baggage": "sentry-trace_id=...", +} + +sentry_sdk.traces.continue_trace(headers) +with sentry_sdk.traces.start_span(name="handle request"): + ... +``` + + + + + +### Start a New Trace + + + + + +If you need to start a completely new trace unconnected to the current one, use `sentry_sdk.traces.new_trace()`. This is useful for background jobs or scheduled tasks where you want a clean trace boundary: + + + + +```python +import sentry_sdk + +with sentry_sdk.traces.start_span(name="span in trace 1"): + ... + +sentry_sdk.traces.new_trace() + +with sentry_sdk.traces.start_span(name="span in trace 2"): + # This span is the root of a new, separate trace + ... +``` + + + + + +See custom instrumentation for more information on distributed tracing. + +## Extended Configuration (Optional) + +### Filter Spans + + + + + +To modify or redact span data before it's sent, use the `before_send_span` option: + + + + +```python +import sentry_sdk + +def postprocess_span(span, hint): + attributes_to_sanitize = [ + "http.request.header.custom-auth", + "http.request.header.custom-user-id", + ] + for attribute in attributes_to_sanitize: + if span["attributes"].get(attribute): + span["attributes"][attribute] = "[Sanitized]" + return span + +sentry_sdk.init( + dsn="___PUBLIC_DSN___", + traces_sample_rate=1.0, + trace_lifecycle="stream", + before_send_span=postprocess_span, +) +``` + + + + + + + +`before_send_span` can only modify span data — you cannot use it to drop spans (use [`ignore_spans`](#drop-spans) instead). + + + +### Drop Spans + + + + + +To prevent specific spans from being created, use the `ignore_spans` option. Rules are evaluated at span start, so only the span name and attributes set at creation time are taken into account. Rules can be strings, compiled regexes, or dictionaries with name and/or attributes conditions: + + + + +```python +import re +import sentry_sdk + +sentry_sdk.init( + dsn="___PUBLIC_DSN___", + traces_sample_rate=1.0, + 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"), + }, + }, + ], +) +``` + + + + + +If a matching span is a service span, all of its child spans are dropped as well. If a child span matches, only that span is dropped and its children are reparented to the nearest ancestor. + +## Sampling (Optional) + +If you use `traces_sample_rate`, no changes are needed — it works the same way in stream mode. + +If you use a custom `traces_sampler`, the shape of the sampling context is different in stream mode. See the Streamed Span API migration guide for details. + +## Verify Your Setup + +To make sure you've enabled stream mode successfully: + +- **Check the Sentry dashboard**: Spans should appear in the Traces view shortly after they complete. Traces look similar to transaction mode, but contain only spans and no transactions. +- **Check your logs**: If the SDK logs warnings about unsupported span operations, you may still be using the legacy Span API somewhere in your code. diff --git a/docs/platforms/python/tracing/streamed-spans/migration-guide.mdx b/docs/platforms/python/tracing/streamed-spans/migration-guide.mdx index 4f61b11d1dc9f8..31c056a97200a0 100644 --- a/docs/platforms/python/tracing/streamed-spans/migration-guide.mdx +++ b/docs/platforms/python/tracing/streamed-spans/migration-guide.mdx @@ -1,16 +1,23 @@ --- title: Migrate to Stream Mode +h1_title: Migrate to Stream Mode for Python (SDK v3.x) sidebar_order: 10 description: "Learn how to migrate your custom instrumentation from transaction mode to stream mode." --- -Stream mode requires the streamed Span API. If you use custom instrumentation (creating spans manually, setting span data, or filtering spans) you'll need to update that code before you can switch to stream mode. This guide walks through the changes. + + +Version 3.0 of the Python SDK is coming in November 2026. This guide describes upcoming behavior. For the SDK 2.x version of this guide, select 2.x in the version selector in the sidebar. + + + +Starting with SDK 3.0, stream mode is the only tracing mode: all spans are streamed, and the transaction-based Span API no longer exists. If you use custom instrumentation (creating spans manually, setting span data, or filtering spans) you'll need to update that code when upgrading. This guide walks through the changes. For an introduction to stream mode itself, see Streamed Spans. -## Enable Stream Mode +## Remove `trace_lifecycle` -Add the `trace_lifecycle` option when initializing the SDK: +The `trace_lifecycle` option no longer exists. If you enabled stream mode on SDK 2.x, remove the option, since passing an unknown option to `sentry_sdk.init()` raises a `TypeError`: ```python diff import sentry_sdk @@ -18,20 +25,22 @@ import sentry_sdk sentry_sdk.init( dsn="___PUBLIC_DSN___", traces_sample_rate=1.0, -+ trace_lifecycle="stream", +- trace_lifecycle="stream", ) ``` +If you already migrated to the `sentry_sdk.traces` API on SDK 2.x, your code keeps working. The top-level API (`sentry_sdk.start_span()`, `sentry_sdk.get_current_span()`, and so on) now uses the same streamed implementation, so we recommend switching to it. + ## Span Creation -Replace `start_span`, `start_transaction`, and `start_child` with `sentry_sdk.traces.start_span()`. Whether the resulting span is a service span, a child span, or a sibling depends on the `parent_span` argument and what's currently active. +Replace `start_transaction` and `start_child` with `sentry_sdk.start_span()`. Whether the resulting span is a service span, a child span, or a sibling depends on the `parent_span` argument and what's currently active. ```python diff import sentry_sdk # Starting a span - with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span: -+ with sentry_sdk.traces.start_span( ++ with sentry_sdk.start_span( + name="GET /api/users", + attributes={"sentry.op": "http.client"}, + ) as span: @@ -39,44 +48,45 @@ import sentry_sdk # Starting what used to be a transaction: pass parent_span=None to force a service span - with sentry_sdk.start_transaction(name="flow.checkout") as transaction: -+ with sentry_sdk.traces.start_span(name="flow.checkout", parent_span=None) as span: ++ with sentry_sdk.start_span(name="flow.checkout", parent_span=None) as span: ... # Starting a child span: just start a span while the parent is active - with parent.start_child(op="db", description="SELECT") as child: -+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}): ++ with sentry_sdk.start_span(name="SELECT", attributes={"sentry.op": "db"}): ... # Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly - with parent.start_child(op="db", description="SELECT") as child: -+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent): ++ with sentry_sdk.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent): ... ``` A few argument changes come along with this: +- `name` is now required. - `description` no longer exists — use `name` instead. - `op` is no longer a dedicated argument — set it as the `sentry.op` attribute instead. -If you use the `@trace` decorator, only the import changes: +If you use the `@trace` decorator, the import stays the same. The decorator accepts optional `name`, `attributes`, and `active` arguments. `op` and `template` no longer exist — set `sentry.op` as an attribute instead: ```python diff -- from sentry_sdk import trace -+ from sentry_sdk.traces import trace +from sentry_sdk import trace -@trace +- @trace(op="checkout") ++ @trace(attributes={"sentry.op": "checkout"}) def checkout(): ... ``` -If your code imports `Span` or `Transaction` directly, for example for type annotations, replace both with `StreamedSpan`: +The `sentry_sdk.tracing` module has been removed. If your code imports `Span` or `Transaction` from it, for example for type annotations, replace both with `Span` from `sentry_sdk.traces`: ```python diff - from sentry_sdk.tracing import Span, Transaction -+ from sentry_sdk.traces import StreamedSpan ++ from sentry_sdk.traces import Span -- def process(span: Span) -> None: -+ def process(span: StreamedSpan) -> None: +- def process(transaction: Transaction) -> None: ++ def process(span: Span) -> None: ... ``` @@ -104,7 +114,7 @@ Unlike the old methods, `set_attribute` only accepts primitive types (`str`, `in + }) ``` -Tags set on the scope with `sentry_sdk.set_tag()` aren't applied to spans in stream mode. Use `sentry_sdk.set_attribute()` to apply data to spans: +Tags set on the scope with `sentry_sdk.set_tag()` aren't applied to spans. Use `sentry_sdk.set_attribute()` to apply data to spans: ```python import sentry_sdk @@ -115,23 +125,17 @@ sentry_sdk.set_attribute("region", "Europe") # applied to spans, logs, metrics ## Accessing the Current Span -A few ways of referencing the current span or transaction change in stream mode: +`sentry_sdk.get_current_span()` works the same way, but returns a streamed span. Prefer it over reading the span off the scope: ```python diff import sentry_sdk -# Getting the current span -- span = sentry_sdk.get_current_span() -+ span = sentry_sdk.traces.get_current_span() - -# Getting the current span via the scope - scope = sentry_sdk.get_current_scope() - current_span = scope.span -+ current_span = sentry_sdk.traces.get_current_span() - ++ current_span = sentry_sdk.get_current_span() ``` -If your code reads specific fields off the trace context, access them as direct properties instead of calling `get_trace_context()`, which no longer exists on streaming spans: +If your code reads specific fields off the trace context, access them as direct properties instead of calling `get_trace_context()`, which no longer exists: ```python diff - ctx = span.get_trace_context() @@ -143,7 +147,7 @@ If your code reads specific fields off the trace context, access them as direct ## Trace Propagation -`sentry_sdk.traces.continue_trace()` replaces the legacy `continue_trace()`. It's no longer a context manager — it sets the propagation context, and the next span you start picks it up automatically: +`sentry_sdk.continue_trace()` now accepts a single dictionary with the incoming headers. It's no longer a context manager and doesn't return a transaction — it sets the propagation context, and the next span you start picks it up automatically: ```python diff import sentry_sdk @@ -155,19 +159,21 @@ headers = { - with sentry_sdk.continue_trace(headers) as transaction: - ... -+ sentry_sdk.traces.continue_trace(headers) -+ with sentry_sdk.traces.start_span(name="handle request"): ++ sentry_sdk.continue_trace(headers) ++ with sentry_sdk.start_span(name="handle request"): + ... ``` +To start a new trace that's unconnected to the current one, use `sentry_sdk.new_trace()`. Like `continue_trace()`, it doesn't start a span on its own. + ## Span Status -Status can only be `ok` (default) or `error` in stream mode: +Status can only be `ok` (default) or `error`: ```python -from sentry_sdk.traces import start_span +import sentry_sdk -with start_span(name="process") as span: +with sentry_sdk.start_span(name="process") as span: try: ... except Exception: @@ -178,7 +184,7 @@ with start_span(name="process") as span: If you use `traces_sample_rate`, no changes are needed. -If you use a custom `traces_sampler`, the sampling context has the same structure in stream mode, but not all keys might be populated. The `data` key under `transaction_context` contains span attributes known at span start. +If you use a custom `traces_sampler`, the sampling context has the same structure as before, but not all keys might be populated. The `data` key under `transaction_context` contains span attributes known at span start. ```python import sentry_sdk @@ -194,7 +200,6 @@ def traces_sampler(sampling_context): sentry_sdk.init( traces_sampler=traces_sampler, - trace_lifecycle="stream", ) ``` @@ -209,13 +214,13 @@ import sentry_sdk - ): - ... + sentry_sdk.get_current_scope().set_custom_sampling_context({"asgi_scope": asgi_scope}) -+ with sentry_sdk.traces.start_span(name="handle request"): ++ with sentry_sdk.start_span(name="handle request"): + ... ``` ## Filtering and Dropping Spans -`before_send_transaction` has no effect in stream mode, since spans are sent individually rather than batched into a transaction. Replace it with `ignore_spans` (to drop spans) and `before_send_span` (to modify them): +The `before_send_transaction` option has been removed, since spans are sent individually rather than batched into a transaction. Replace it with `ignore_spans` (to drop spans) and `before_send_span` (to modify them). If you previously set either of these under `_experiments`, move them to the top level: ```python diff import re @@ -230,13 +235,12 @@ sentry_sdk.init( dsn="___PUBLIC_DSN___", traces_sample_rate=1.0, - before_send_transaction=my_filter, -+ trace_lifecycle="stream", + ignore_spans=[ + "/health", + re.compile(r"^GET /api/v1/internal"), + {"attributes": {"service.id": "15def9a"}}, -+ ], -+ before_send_span=my_span_processor, ++ ], ++ before_send_span=my_span_processor, ) ``` diff --git a/docs/platforms/python/tracing/streamed-spans/migration-guide__v2.x.mdx b/docs/platforms/python/tracing/streamed-spans/migration-guide__v2.x.mdx new file mode 100644 index 00000000000000..c59d38ae5d2991 --- /dev/null +++ b/docs/platforms/python/tracing/streamed-spans/migration-guide__v2.x.mdx @@ -0,0 +1,245 @@ +--- +title: Migrate to Stream Mode +h1_title: Migrate to Stream Mode for Python +sidebar_order: 10 +description: "Learn how to migrate your custom instrumentation from transaction mode to stream mode." +--- + +Stream mode requires the streamed Span API. If you use custom instrumentation (creating spans manually, setting span data, or filtering spans) you'll need to update that code before you can switch to stream mode. This guide walks through the changes. + +For an introduction to stream mode itself, see Streamed Spans. + +## Enable Stream Mode + +Add the `trace_lifecycle` option when initializing the SDK: + +```python diff +import sentry_sdk + +sentry_sdk.init( + dsn="___PUBLIC_DSN___", + traces_sample_rate=1.0, ++ trace_lifecycle="stream", +) +``` + +## Span Creation + +Replace `start_span`, `start_transaction`, and `start_child` with `sentry_sdk.traces.start_span()`. Whether the resulting span is a service span, a child span, or a sibling depends on the `parent_span` argument and what's currently active. + +```python diff +import sentry_sdk + +# Starting a span +- with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span: ++ with sentry_sdk.traces.start_span( ++ name="GET /api/users", ++ attributes={"sentry.op": "http.client"}, ++ ) as span: + ... + +# Starting what used to be a transaction: pass parent_span=None to force a service span +- with sentry_sdk.start_transaction(name="flow.checkout") as transaction: ++ with sentry_sdk.traces.start_span(name="flow.checkout", parent_span=None) as span: + ... + +# Starting a child span: just start a span while the parent is active +- with parent.start_child(op="db", description="SELECT") as child: ++ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}): + ... + +# Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly +- with parent.start_child(op="db", description="SELECT") as child: ++ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent): + ... +``` + +A few argument changes come along with this: + +- `description` no longer exists — use `name` instead. +- `op` is no longer a dedicated argument — set it as the `sentry.op` attribute instead. + +If you use the `@trace` decorator, only the import changes: + +```python diff +- from sentry_sdk import trace ++ from sentry_sdk.traces import trace + +@trace +def checkout(): + ... +``` + +If your code imports `Span` or `Transaction` directly, for example for type annotations, replace both with `StreamedSpan`: + +```python diff +- from sentry_sdk.tracing import Span, Transaction ++ from sentry_sdk.traces import StreamedSpan + +- def process(span: Span) -> None: ++ def process(span: StreamedSpan) -> None: + ... +``` + +## Span Data + +In stream mode, spans have no contexts, data, or tags. Instead, everything is an attribute. +Replace `set_data()`, `set_tag()`, and `set_context()` with `set_attribute()` or `set_attributes()`: + +```python diff +- span.set_data("flow.step", "submit_payment") +- span.set_tag("http.status_code", 201) ++ span.set_attributes({ ++ "flow.step": "submit_payment", ++ "http.response.status_code": 201, ++ }) +``` + +Unlike the old methods, `set_attribute` only accepts primitive types (`str`, `int`, `float`, `bool`, or arrays of these). `None` isn't supported either. Flatten dictionaries into separate attributes, and stringify anything that can't be flattened: + +```python diff +- span.set_data("request", {"method": "POST", "path": "/api/checkout"}) ++ span.set_attributes({ ++ "request.method": "POST", ++ "request.path": "/api/checkout", ++ }) +``` + +Tags set on the scope with `sentry_sdk.set_tag()` aren't applied to spans in stream mode. Use `sentry_sdk.set_attribute()` to apply data to spans: + +```python +import sentry_sdk + +sentry_sdk.set_tag("region", "Europe") # applied to errors and other tag-supporting telemetry +sentry_sdk.set_attribute("region", "Europe") # applied to spans, logs, metrics +``` + +## Accessing the Current Span + +A few ways of referencing the current span or transaction change in stream mode: + +```python diff +import sentry_sdk + +# Getting the current span +- span = sentry_sdk.get_current_span() ++ span = sentry_sdk.traces.get_current_span() + +# Getting the current span via the scope +- scope = sentry_sdk.get_current_scope() +- current_span = scope.span ++ current_span = sentry_sdk.traces.get_current_span() + +``` + +If your code reads specific fields off the trace context, access them as direct properties instead of calling `get_trace_context()`, which no longer exists on streaming spans: + +```python diff +- ctx = span.get_trace_context() +- trace_id = ctx["trace_id"] +- span_id = ctx["span_id"] ++ trace_id = span.trace_id ++ span_id = span.span_id +``` + +## Trace Propagation + +`sentry_sdk.traces.continue_trace()` replaces the legacy `continue_trace()`. It's no longer a context manager — it sets the propagation context, and the next span you start picks it up automatically: + +```python diff +import sentry_sdk + +headers = { + "sentry-trace": "...", + "baggage": "...", +} + +- with sentry_sdk.continue_trace(headers) as transaction: +- ... ++ sentry_sdk.traces.continue_trace(headers) ++ with sentry_sdk.traces.start_span(name="handle request"): ++ ... +``` + +## Span Status + +Status can only be `ok` (default) or `error` in stream mode: + +```python +from sentry_sdk.traces import start_span + +with start_span(name="process") as span: + try: + ... + except Exception: + span.status = "error" +``` + +## Sampling + +If you use `traces_sample_rate`, no changes are needed. + +If you use a custom `traces_sampler`, the sampling context has the same structure in stream mode, but not all keys might be populated. The `data` key under `transaction_context` contains span attributes known at span start. + +```python +import sentry_sdk + +def traces_sampler(sampling_context): + if sampling_context["parent_sampled"] is not None: + return float(sampling_context["parent_sampled"]) + + if sampling_context["transaction_context"]["name"] in IGNORED_SPAN_NAMES: + return 0.0 + + return 1.0 + +sentry_sdk.init( + traces_sampler=traces_sampler, + trace_lifecycle="stream", +) +``` + +`custom_sampling_context` is no longer an argument to `start_span`. Set it on the scope instead, after `continue_trace` (which resets the propagation context) and before `start_span` (which is when sampling happens): + +```python diff +import sentry_sdk + +- with sentry_sdk.start_span( +- name="handle request", +- custom_sampling_context={"asgi_scope": asgi_scope}, +- ): +- ... ++ sentry_sdk.get_current_scope().set_custom_sampling_context({"asgi_scope": asgi_scope}) ++ with sentry_sdk.traces.start_span(name="handle request"): ++ ... +``` + +## Filtering and Dropping Spans + +`before_send_transaction` has no effect in stream mode, since spans are sent individually rather than batched into a transaction. Replace it with `ignore_spans` (to drop spans) and `before_send_span` (to modify them): + +```python diff +import re +import sentry_sdk + ++ def my_span_processor(span, hint): ++ if span["attributes"].get("sentry.op") == "db.query": ++ span["name"] = "[filtered]" ++ return span + +sentry_sdk.init( + dsn="___PUBLIC_DSN___", + traces_sample_rate=1.0, +- before_send_transaction=my_filter, ++ trace_lifecycle="stream", ++ ignore_spans=[ ++ "/health", ++ re.compile(r"^GET /api/v1/internal"), ++ {"attributes": {"service.id": "15def9a"}}, ++ ], ++ before_send_span=my_span_processor, +) +``` + +`ignore_spans` only has access to the span name and attributes set at creation time — not attributes added later in the span's lifetime, like an HTTP status code set after the request completes. If your `before_send_transaction` logic was used to drop spans based on late-set data, this can't be replicated in stream mode. Consider server-side filtering with Sentry [inbound data filters](/concepts/data-management/filtering/) or [Relay](/product/relay/) rules instead. +`before_send_span` runs after the span ends and has access to all attributes set during the span's lifetime.