Skip to content

Commit b8eb812

Browse files
authored
AGE-148 : Get full function body details, when exception occurs (#56)
* recording function body * Updated Record Exception flow to collect logical unit of exception
1 parent 316c5bc commit b8eb812

File tree

3 files changed

+89
-4
lines changed

3 files changed

+89
-4
lines changed

middleware/__init__.py

+24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from middleware.distro import mw_tracker, record_exception
2+
from typing import Collection
3+
import sys
4+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
5+
from opentelemetry import trace
6+
from typing import Optional
27
from middleware.options import (
38
MWOptions,
49
DETECT_AWS_BEANSTALK,
@@ -28,3 +33,22 @@
2833
"DETECT_GCP",
2934
"DETECT_ENVVARS",
3035
]
36+
37+
tracer = trace.get_tracer(__name__)
38+
39+
class ExceptionInstrumentor(BaseInstrumentor):
40+
def instrumentation_dependencies(self) -> Collection[str]:
41+
"""Return dependencies if this instrumentor requires any."""
42+
return []
43+
44+
def _instrument(self, **kwargs):
45+
"""Automatically sets sys.excepthook when the instrumentor is loaded."""
46+
sys.excepthook = record_exception
47+
48+
def _uninstrument(self, **kwargs):
49+
"""Restores default sys.excepthook if needed."""
50+
sys.excepthook = sys.__excepthook__
51+
52+
# Load the instrumentor
53+
ExceptionInstrumentor().instrument()
54+

middleware/distro.py

+64-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import logging
2+
import inspect
3+
import traceback
4+
import sys
25
from logging import getLogger
36
from typing import Optional
47
from opentelemetry.instrumentation.distro import BaseDistro
@@ -9,7 +12,8 @@
912
from middleware.log import create_logger_handler
1013
from middleware.profiler import collect_profiling
1114
from opentelemetry import trace
12-
from opentelemetry.trace import Tracer, get_current_span, get_tracer
15+
from opentelemetry.trace import Tracer, get_current_span, get_tracer, Span
16+
1317

1418
_logger = getLogger(__name__)
1519

@@ -80,6 +84,63 @@ def mw_tracker(
8084

8185
mw_tracker_called = True
8286

87+
def extract_function_code(tb_frame):
88+
"""Extracts the full function body where the exception occurred."""
89+
try:
90+
source_lines, _ = inspect.getsourcelines(tb_frame)
91+
return "".join(source_lines) # Convert to a string
92+
except Exception:
93+
return "Could not retrieve source code."
94+
95+
# Replacement of span.record_exception to include function source code
96+
def custom_record_exception(span: Span, exc: Exception):
97+
"""Custom exception recording that captures function source code."""
98+
exc_type, exc_value, exc_tb = exc.__class__, str(exc), exc.__traceback__
99+
100+
if exc_tb is None:
101+
span.set_attribute("exception.warning", "No traceback available")
102+
span.record_exception(exc)
103+
return
104+
105+
tb_details = traceback.extract_tb(exc_tb)
106+
107+
if not tb_details:
108+
span.set_attribute("exception.warning", "Traceback is empty")
109+
span.record_exception(exc)
110+
return
111+
112+
last_tb = tb_details[-1] # Get the last traceback entry (where exception occurred)
113+
filename, lineno, func_name, _ = last_tb
114+
115+
# Extract the correct frame from the traceback
116+
tb_frame = None
117+
for frame, _ in traceback.walk_tb(exc_tb):
118+
if frame.f_code.co_name == func_name:
119+
tb_frame = frame
120+
break
121+
122+
123+
124+
function_code = extract_function_code(tb_frame) if tb_frame else "Function source not found."
125+
126+
# Determine if the exception is escaping
127+
current_exc = sys.exc_info()[1] # Get the currently active exception
128+
exception_escaped = current_exc is exc # True if it's still propagating
129+
130+
# Add extra details in the existing "exception" event
131+
span.add_event(
132+
"exception", # Keep the event name as "exception"
133+
{
134+
"exception.type": str(exc_type.__name__),
135+
"exception.message": exc_value,
136+
"exception.stacktrace": traceback.format_exc(),
137+
"exception.function_name": func_name,
138+
"exception.file": filename,
139+
"exception.line": lineno,
140+
"exception.function_body": function_code,
141+
"exception.escaped": exception_escaped,
142+
}
143+
)
83144

84145
def record_exception(exc: Exception, span_name: Optional[str] = None) -> None:
85146
"""
@@ -102,15 +163,15 @@ def record_exception(exc: Exception, span_name: Optional[str] = None) -> None:
102163

103164
span = get_current_span()
104165
if span.is_recording():
105-
span.record_exception(exc)
166+
custom_record_exception(span, exc)
106167
return
107168

108169
tracer: Tracer = get_tracer("mw-tracer")
109170
if span_name is None:
110171
span_name = type(exc).__name__
111172

112173
span = tracer.start_span(span_name)
113-
span.record_exception(exc)
174+
custom_record_exception(span, exc)
114175
span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
115176
span.end()
116177

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "middleware-io"
7-
version = "2.1.1"
7+
version = "2.1.2rc1"
88
requires-python = ">=3.8"
99
description = "Middleware's APM tool enables Python developers to effortlessly monitor their applications, gathering distributed tracing, metrics, logs, and profiling data for valuable insights and performance optimization."
1010
authors = [{ name = "middleware-dev" }]

0 commit comments

Comments
 (0)