Skip to content

Commit 2febebf

Browse files
committed
refactor: move stack trace functionality into a separate module
Signed-off-by: Varsha GS <varsha.gs@ibm.com>
1 parent 47af3ed commit 2febebf

File tree

4 files changed

+430
-299
lines changed

4 files changed

+430
-299
lines changed

src/instana/span/span.py

Lines changed: 3 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@
1414
- RegisteredSpan: Class that represents a Registered type span
1515
"""
1616

17-
import os
18-
import re
19-
import traceback
2017
from threading import Lock
2118
from time import time_ns
2219
from typing import Dict, Optional, Sequence, Union
@@ -37,14 +34,11 @@
3734

3835
from instana.log import logger
3936
from instana.recorder import StanRecorder
40-
from instana.span.kind import HTTP_SPANS, EXIT_SPANS
37+
from instana.span.kind import HTTP_SPANS
4138
from instana.span.readable_span import Event, ReadableSpan
39+
from instana.span.stack_trace import add_stack_trace_if_needed
4240
from instana.span_context import SpanContext
4341

44-
# Used by _add_stack for filtering Instana internal frames
45-
_re_tracer_frame = re.compile(r"/instana/.*\.py$")
46-
_re_with_stan_frame = re.compile("with_instana")
47-
4842

4943
class InstanaSpan(Span, ReadableSpan):
5044
def __init__(
@@ -199,81 +193,12 @@ def _readable_span(self) -> ReadableSpan:
199193
# kind=self.kind,
200194
)
201195

202-
def _should_collect_stack(self, level: str, is_errored: bool) -> bool:
203-
"""Determine if stack trace should be collected based on level and error state."""
204-
if level == "all":
205-
return True
206-
if level == "error" and is_errored:
207-
return True
208-
return False
209-
210-
def _should_exclude_frame(self, frame) -> bool:
211-
"""Check if a frame should be excluded from the stack trace."""
212-
if "INSTANA_DEBUG" in os.environ:
213-
return False
214-
if _re_tracer_frame.search(frame[0]):
215-
return True
216-
if _re_with_stan_frame.search(frame[2]):
217-
return True
218-
return False
219-
220-
def _apply_stack_limit(self, sanitized_stack: list, limit: int, use_full_stack: bool) -> list:
221-
"""Apply frame limit to the sanitized stack."""
222-
if use_full_stack or len(sanitized_stack) <= limit:
223-
return sanitized_stack
224-
# (limit * -1) gives us negative form of <limit> used for
225-
# slicing from the end of the list. e.g. stack[-25:]
226-
return sanitized_stack[(limit * -1) :]
227-
228-
def _add_stack(self, is_errored: bool = False) -> None:
229-
"""
230-
Adds a backtrace to <span> based on configuration.
231-
"""
232-
try:
233-
# Get configuration from agent options
234-
options = self._span_processor.agent.options
235-
level = options.stack_trace_level
236-
limit = options.stack_trace_length
237-
238-
# Determine if we should collect stack trace
239-
if not self._should_collect_stack(level, is_errored):
240-
return
241-
242-
# For erroneous EXIT spans, MAY consider the whole stack
243-
use_full_stack = is_errored
244-
245-
# Enforce hard limit of 40 frames (unless errored and using full stack)
246-
if not use_full_stack and limit > 40:
247-
limit = 40
248-
249-
sanitized_stack = []
250-
trace_back = traceback.extract_stack()
251-
trace_back.reverse()
252-
253-
for frame in trace_back:
254-
if self._should_exclude_frame(frame):
255-
continue
256-
sanitized_stack.append({"c": frame[0], "n": frame[1], "m": frame[2]})
257-
258-
# Apply limit (unless it's an errored span and we want full stack)
259-
self.stack = self._apply_stack_limit(sanitized_stack, limit, use_full_stack)
260-
261-
except Exception:
262-
logger.debug("span._add_stack: ", exc_info=True)
263-
264-
def _add_stack_trace_if_needed(self) -> None:
265-
"""Add stack trace based on configuration before span ends."""
266-
if self.name in EXIT_SPANS:
267-
# Check if span is errored
268-
is_errored = self.attributes.get("ec", 0) > 0
269-
self._add_stack(is_errored=is_errored)
270-
271196
def end(self, end_time: Optional[int] = None) -> None:
272197
with self._lock:
273198
self._end_time = end_time if end_time else time_ns()
274199
self._duration = self._end_time - self._start_time
275200

276-
self._add_stack_trace_if_needed()
201+
add_stack_trace_if_needed(self)
277202

278203
self._span_processor.record_span(self._readable_span())
279204

src/instana/span/stack_trace.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# (c) Copyright IBM Corp. 2025
2+
3+
"""
4+
Stack trace collection functionality for spans.
5+
6+
This module provides utilities for capturing and filtering stack traces
7+
for EXIT spans based on configuration settings.
8+
"""
9+
10+
import os
11+
import re
12+
import traceback
13+
from typing import List, Optional, TYPE_CHECKING
14+
15+
from instana.log import logger
16+
from instana.span.kind import EXIT_SPANS
17+
18+
if TYPE_CHECKING:
19+
from instana.span.span import InstanaSpan
20+
21+
# Regex patterns for filtering Instana internal frames
22+
_re_tracer_frame = re.compile(r"/instana/.*\.py$")
23+
_re_with_stan_frame = re.compile("with_instana")
24+
25+
26+
def _should_collect_stack(level: str, is_errored: bool) -> bool:
27+
"""
28+
Determine if stack trace should be collected based on level and error state.
29+
30+
Args:
31+
level: Stack trace collection level ("all", "error", or "none")
32+
is_errored: Whether the span has errors (ec > 0)
33+
34+
Returns:
35+
True if stack trace should be collected, False otherwise
36+
"""
37+
if level == "all":
38+
return True
39+
if level == "error" and is_errored:
40+
return True
41+
return False
42+
43+
44+
def _should_exclude_frame(frame) -> bool:
45+
"""
46+
Check if a frame should be excluded from the stack trace.
47+
48+
Frames are excluded if they are part of Instana's internal code,
49+
unless INSTANA_DEBUG is set.
50+
51+
Args:
52+
frame: A frame from traceback.extract_stack()
53+
54+
Returns:
55+
True if frame should be excluded, False otherwise
56+
"""
57+
if "INSTANA_DEBUG" in os.environ:
58+
return False
59+
if _re_tracer_frame.search(frame[0]):
60+
return True
61+
if _re_with_stan_frame.search(frame[2]):
62+
return True
63+
return False
64+
65+
66+
def _apply_stack_limit(
67+
sanitized_stack: List[dict], limit: int, use_full_stack: bool
68+
) -> List[dict]:
69+
"""
70+
Apply frame limit to the sanitized stack.
71+
72+
Args:
73+
sanitized_stack: List of stack frames
74+
limit: Maximum number of frames to include
75+
use_full_stack: If True, ignore the limit
76+
77+
Returns:
78+
Limited stack trace
79+
"""
80+
if use_full_stack or len(sanitized_stack) <= limit:
81+
return sanitized_stack
82+
# (limit * -1) gives us negative form of <limit> used for
83+
# slicing from the end of the list. e.g. stack[-25:]
84+
return sanitized_stack[(limit * -1) :]
85+
86+
87+
def add_stack(
88+
level: str, limit: int, is_errored: bool = False
89+
) -> Optional[List[dict]]:
90+
"""
91+
Capture and return a stack trace based on configuration.
92+
93+
This function collects the current call stack, filters out Instana
94+
internal frames, and applies the configured limit.
95+
96+
Args:
97+
level: Stack trace collection level ("all", "error", or "none")
98+
limit: Maximum number of frames to include (1-40)
99+
is_errored: Whether the span has errors (ec > 0)
100+
101+
Returns:
102+
List of stack frames in format [{"c": file, "n": line, "m": method}, ...]
103+
or None if stack trace should not be collected
104+
"""
105+
try:
106+
# Determine if we should collect stack trace
107+
if not _should_collect_stack(level, is_errored):
108+
return None
109+
110+
# For erroneous EXIT spans, MAY consider the whole stack
111+
use_full_stack = is_errored
112+
113+
# Enforce hard limit of 40 frames (unless errored and using full stack)
114+
if not use_full_stack and limit > 40:
115+
limit = 40
116+
117+
sanitized_stack = []
118+
trace_back = traceback.extract_stack()
119+
trace_back.reverse()
120+
121+
for frame in trace_back:
122+
if _should_exclude_frame(frame):
123+
continue
124+
sanitized_stack.append({"c": frame[0], "n": frame[1], "m": frame[2]})
125+
126+
# Apply limit (unless it's an errored span and we want full stack)
127+
return _apply_stack_limit(sanitized_stack, limit, use_full_stack)
128+
129+
except Exception:
130+
logger.debug("add_stack: ", exc_info=True)
131+
return None
132+
133+
134+
def add_stack_trace_if_needed(span: "InstanaSpan") -> None:
135+
"""
136+
Add stack trace to span based on configuration before span ends.
137+
138+
This function checks if the span is an EXIT span and if so, captures
139+
a stack trace based on the configured level and limit.
140+
141+
Args:
142+
span: The InstanaSpan to potentially add stack trace to
143+
"""
144+
if span.name in EXIT_SPANS:
145+
# Get configuration from agent options
146+
options = span._span_processor.agent.options
147+
148+
# Check if span is errored
149+
is_errored = span.attributes.get("ec", 0) > 0
150+
151+
# Capture stack trace using add_stack function
152+
span.stack = add_stack(
153+
level=options.stack_trace_level,
154+
limit=options.stack_trace_length,
155+
is_errored=is_errored
156+
)

0 commit comments

Comments
 (0)