Skip to content

Commit ada153b

Browse files
authored
ref: Add cont profiling support to span first (#5672)
We'll only support continuous profiling in span first. Note: Span-first profiling is not yet supported server-side.
1 parent 0b5f4f8 commit ada153b

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

sentry_sdk/traces.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515

1616
import sentry_sdk
1717
from sentry_sdk.consts import SPANDATA
18+
from sentry_sdk.profiler.continuous_profiler import (
19+
get_profiler_id,
20+
try_autostart_continuous_profiler,
21+
try_profile_lifecycle_trace_start,
22+
)
1823
from sentry_sdk.tracing_utils import Baggage
1924
from sentry_sdk.utils import (
2025
capture_internal_exceptions,
@@ -28,6 +33,7 @@
2833
if TYPE_CHECKING:
2934
from typing import Any, Callable, Iterator, Optional, ParamSpec, TypeVar, Union
3035
from sentry_sdk._types import Attributes, AttributeValue
36+
from sentry_sdk.profiler.continuous_profiler import ContinuousProfile
3137

3238
P = ParamSpec("P")
3339
R = TypeVar("R")
@@ -232,6 +238,7 @@ class StreamedSpan:
232238
"_baggage",
233239
"_sample_rand",
234240
"_sample_rate",
241+
"_continuous_profile",
235242
)
236243

237244
def __init__(
@@ -284,6 +291,10 @@ def __init__(
284291

285292
self._update_active_thread()
286293

294+
self._continuous_profile: "Optional[ContinuousProfile]" = None
295+
self._start_profile()
296+
self._set_profile_id(get_profiler_id())
297+
287298
self._start()
288299

289300
def __repr__(self) -> str:
@@ -340,6 +351,11 @@ def _end(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None
340351
# This span is already finished, ignore.
341352
return
342353

354+
# Stop the profiler
355+
if self._is_segment() and self._continuous_profile is not None:
356+
with capture_internal_exceptions():
357+
self._continuous_profile.stop()
358+
343359
# Detach from scope
344360
if self._active:
345361
with capture_internal_exceptions():
@@ -528,6 +544,18 @@ def _get_trace_context(self) -> "dict[str, Any]":
528544

529545
return context
530546

547+
def _set_profile_id(self, profiler_id: "Optional[str]") -> None:
548+
if profiler_id is not None:
549+
self.set_attribute("sentry.profiler_id", profiler_id)
550+
551+
def _start_profile(self) -> None:
552+
if not self._is_segment():
553+
return
554+
555+
try_autostart_continuous_profiler()
556+
557+
self._continuous_profile = try_profile_lifecycle_trace_start()
558+
531559

532560
class NoOpStreamedSpan(StreamedSpan):
533561
__slots__ = (

tests/tracing/test_span_streaming.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import asyncio
22
import re
33
import sys
4+
import time
45
from typing import Any
56
from unittest import mock
67

78
import pytest
89

910
import sentry_sdk
11+
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
1012
from sentry_sdk.traces import NoOpStreamedSpan, SpanStatus, StreamedSpan
1113

1214
minimum_python_38 = pytest.mark.skipif(
@@ -1322,6 +1324,102 @@ def test_ignore_spans_reparenting(sentry_init, capture_envelopes):
13221324
assert span5["parent_span_id"] == span3["span_id"]
13231325

13241326

1327+
@mock.patch("sentry_sdk.profiler.continuous_profiler.DEFAULT_SAMPLING_FREQUENCY", 21)
1328+
def test_segment_span_has_profiler_id(
1329+
sentry_init, capture_envelopes, teardown_profiling
1330+
):
1331+
sentry_init(
1332+
traces_sample_rate=1.0,
1333+
profile_lifecycle="trace",
1334+
profiler_mode="thread",
1335+
profile_session_sample_rate=1.0,
1336+
_experiments={
1337+
"trace_lifecycle": "stream",
1338+
"continuous_profiling_auto_start": True,
1339+
},
1340+
)
1341+
envelopes = capture_envelopes()
1342+
1343+
with sentry_sdk.traces.start_span(name="profiled segment"):
1344+
time.sleep(0.1)
1345+
1346+
sentry_sdk.get_client().flush()
1347+
time.sleep(0.3) # wait for profiler to flush
1348+
1349+
spans = envelopes_to_spans(envelopes)
1350+
assert len(spans) == 1
1351+
assert "sentry.profiler_id" in spans[0]["attributes"]
1352+
1353+
profile_chunks = [
1354+
item
1355+
for envelope in envelopes
1356+
for item in envelope.items
1357+
if item.type == "profile_chunk"
1358+
]
1359+
assert len(profile_chunks) > 0
1360+
1361+
1362+
def test_segment_span_no_profiler_id_when_unsampled(
1363+
sentry_init, capture_envelopes, teardown_profiling
1364+
):
1365+
sentry_init(
1366+
traces_sample_rate=1.0,
1367+
profile_lifecycle="trace",
1368+
profiler_mode="thread",
1369+
profile_session_sample_rate=0.0,
1370+
_experiments={
1371+
"trace_lifecycle": "stream",
1372+
"continuous_profiling_auto_start": True,
1373+
},
1374+
)
1375+
envelopes = capture_envelopes()
1376+
1377+
with sentry_sdk.traces.start_span(name="segment"):
1378+
time.sleep(0.05)
1379+
1380+
sentry_sdk.get_client().flush()
1381+
time.sleep(0.2)
1382+
1383+
spans = envelopes_to_spans(envelopes)
1384+
assert len(spans) == 1
1385+
assert "sentry.profiler_id" not in spans[0]["attributes"]
1386+
1387+
profile_chunks = [
1388+
item
1389+
for envelope in envelopes
1390+
for item in envelope.items
1391+
if item.type == "profile_chunk"
1392+
]
1393+
assert len(profile_chunks) == 0
1394+
1395+
1396+
@mock.patch("sentry_sdk.profiler.continuous_profiler.DEFAULT_SAMPLING_FREQUENCY", 21)
1397+
def test_profile_stops_when_segment_ends(
1398+
sentry_init, capture_envelopes, teardown_profiling
1399+
):
1400+
sentry_init(
1401+
traces_sample_rate=1.0,
1402+
profile_lifecycle="trace",
1403+
profiler_mode="thread",
1404+
profile_session_sample_rate=1.0,
1405+
_experiments={
1406+
"trace_lifecycle": "stream",
1407+
"continuous_profiling_auto_start": True,
1408+
},
1409+
)
1410+
capture_envelopes()
1411+
1412+
with sentry_sdk.traces.start_span(name="segment") as span:
1413+
time.sleep(0.1)
1414+
assert span._continuous_profile is not None
1415+
assert span._continuous_profile.active is True
1416+
1417+
assert span._continuous_profile.active is False
1418+
1419+
time.sleep(0.3)
1420+
assert get_profiler_id() is None, "profiler should have stopped"
1421+
1422+
13251423
def test_transport_format(sentry_init, capture_envelopes):
13261424
sentry_init(
13271425
server_name="test-server",

0 commit comments

Comments
 (0)