diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index ed34142c84..7258e8dedb 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -23,7 +23,6 @@ from typing import Dict from typing import List from typing import Tuple - from typing import Iterator _traceparent_header_format_re = re.compile( "^[ \t]*" # whitespace @@ -66,27 +65,28 @@ def __iter__(self): class _SpanRecorder(object): - __slots__ = ("maxlen", "finished_spans") + __slots__ = ("maxlen", "finished_spans", "open_span_count") - def __init__(self): - # type: () -> None - self.maxlen = 1000 - self.finished_spans = None # type: Optional[List[Span]] + def __init__(self, maxlen): + # type: (int) -> None + self.maxlen = maxlen + self.open_span_count = 0 # type: int + self.finished_spans = [] # type: List[Span] - def append(self, span): + def start_span(self, span): # type: (Span) -> None - if self.finished_spans is not None: - if len(self.finished_spans) >= self.maxlen: - # If the span tree grows too large we decided it's better to - # discard all spans instead of trying to apply some expensive - # trimming. - self.finished_spans = None - else: - self.finished_spans.append(span) - def __iter__(self): - # type: () -> Iterator[Span] - return iter(self.finished_spans or ()) + # This is just so that we don't run out of memory while recording a lot + # of spans. At some point we just stop and flush out the start of the + # trace tree (i.e. the first n spans with the smallest + # start_timestamp). + self.open_span_count += 1 + if self.open_span_count > self.maxlen: + span._span_recorder = None + + def finish_span(self, span): + # type: (Span) -> None + self.finished_spans.append(span) class Span(object): @@ -132,18 +132,18 @@ def __init__( self.hub = hub self._tags = {} # type: Dict[str, str] self._data = {} # type: Dict[str, Any] - self._span_recorder = _SpanRecorder() # type: _SpanRecorder self.start_timestamp = datetime.now() #: End timestamp of span self.timestamp = None # type: Optional[datetime] + self._span_recorder = None # type: Optional[_SpanRecorder] + def init_finished_spans(self, maxlen): # type: (int) -> None - if self._span_recorder.finished_spans is None: - self._span_recorder.finished_spans = [] - - self._span_recorder.maxlen = maxlen + if self._span_recorder is None: + self._span_recorder = _SpanRecorder(maxlen) + self._span_recorder.start_span(self) def __repr__(self): # type: () -> str @@ -189,6 +189,7 @@ def new_span(self, **kwargs): sampled=self.sampled, **kwargs ) + rv._span_recorder = self._span_recorder return rv @@ -279,10 +280,13 @@ def finish(self, hub=None): self.timestamp = datetime.now() - self._span_recorder.append(self) - _maybe_create_breadcrumbs_from_span(hub, self) + if self._span_recorder is None: + return None + + self._span_recorder.finish_span(self) + if self.transaction is None: # If this has no transaction set we assume there's a parent # transaction for this span that would be flushed out eventually. @@ -310,7 +314,11 @@ def finish(self, hub=None): "contexts": {"trace": self.get_trace_context()}, "timestamp": self.timestamp, "start_timestamp": self.start_timestamp, - "spans": [s.to_json() for s in self._span_recorder if s is not self], + "spans": [ + s.to_json() + for s in self._span_recorder.finished_spans + if s is not self + ], } ) diff --git a/tests/test_tracing.py b/tests/test_tracing.py index 2e118cd2d9..0bb3e1c972 100644 --- a/tests/test_tracing.py +++ b/tests/test_tracing.py @@ -127,3 +127,18 @@ def foo(): gc.collect() assert len(references) == expected_refcount + + +def test_span_trimming(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0, _experiments={"max_spans": 3}) + events = capture_events() + + with Hub.current.start_span(transaction="hi"): + for i in range(10): + with Hub.current.start_span(op="foo{}".format(i)): + pass + + event, = events + span1, span2 = event["spans"] + assert span1["op"] == "foo0" + assert span2["op"] == "foo1"