Skip to content

Commit 972f07f

Browse files
authored
Collecting Function Body and Metadata when exception occurs (#57)
* Updated exception handling logic to handle more generic errors * Added support for MW_GIT_COMMIT_SHA and MW_REPOSITORY_URL env variables * Adding function line, code etc. for whole traceback * Removed print statements * reverted extra change * Added function start_line, end_line details
1 parent b8eb812 commit 972f07f

File tree

3 files changed

+74
-43
lines changed

3 files changed

+74
-43
lines changed

middleware/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from middleware.distro import mw_tracker, record_exception
22
from typing import Collection
33
import sys
4+
import threading
45
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
56
from opentelemetry import trace
67
from typing import Optional
@@ -36,6 +37,10 @@
3637

3738
tracer = trace.get_tracer(__name__)
3839

40+
# Hook for threading exceptions (Python 3.8+)
41+
def thread_excepthook(args):
42+
record_exception(args.exc_type, args.exc_value, args.exc_traceback)
43+
3944
class ExceptionInstrumentor(BaseInstrumentor):
4045
def instrumentation_dependencies(self) -> Collection[str]:
4146
"""Return dependencies if this instrumentor requires any."""
@@ -44,6 +49,7 @@ def instrumentation_dependencies(self) -> Collection[str]:
4449
def _instrument(self, **kwargs):
4550
"""Automatically sets sys.excepthook when the instrumentor is loaded."""
4651
sys.excepthook = record_exception
52+
threading.excepthook = thread_excepthook
4753

4854
def _uninstrument(self, **kwargs):
4955
"""Restores default sys.excepthook if needed."""

middleware/distro.py

+67-42
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import inspect
33
import traceback
4+
from typing import Optional, Type
45
import sys
56
from logging import getLogger
67
from typing import Optional
@@ -12,8 +13,9 @@
1213
from middleware.log import create_logger_handler
1314
from middleware.profiler import collect_profiling
1415
from opentelemetry import trace
15-
from opentelemetry.trace import Tracer, get_current_span, get_tracer, Span
16-
16+
from opentelemetry.trace import Tracer, get_current_span, get_tracer, Span, get_tracer, Status, StatusCode
17+
import os
18+
import json
1719

1820
_logger = getLogger(__name__)
1921

@@ -84,13 +86,32 @@ def mw_tracker(
8486

8587
mw_tracker_called = True
8688

87-
def extract_function_code(tb_frame):
89+
def extract_function_code(tb_frame, lineno):
8890
"""Extracts the full function body where the exception occurred."""
8991
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."
92+
source_lines, start_line = inspect.getsourcelines(tb_frame)
93+
end_line = start_line + len(source_lines) - 1
94+
95+
if len(source_lines) > 20:
96+
# Get 10 lines above and 10 below the exception line
97+
start_idx = max(0, lineno - start_line - 10)
98+
end_idx = min(len(source_lines), lineno - start_line + 10)
99+
source_lines = source_lines[start_idx:end_idx]
100+
101+
function_code = "".join(source_lines) # Convert to a string
102+
103+
return {
104+
"function_code": function_code,
105+
"function_start_line": start_line if len(source_lines) <= 20 else None,
106+
"function_end_line": end_line if len(source_lines) <= 20 else None,
107+
}
108+
109+
except Exception as e:
110+
return {
111+
"function_code": f"Error extracting function code: {e}",
112+
"function_start_line": None,
113+
"function_end_line": None
114+
}
94115

95116
# Replacement of span.record_exception to include function source code
96117
def custom_record_exception(span: Span, exc: Exception):
@@ -109,71 +130,75 @@ def custom_record_exception(span: Span, exc: Exception):
109130
span.record_exception(exc)
110131
return
111132

112-
last_tb = tb_details[-1] # Get the last traceback entry (where exception occurred)
113-
filename, lineno, func_name, _ = last_tb
133+
stack_info = []
114134

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-
135+
for (frame, _), (filename, lineno, func_name, _) in zip(traceback.walk_tb(exc_tb), tb_details):
136+
function_details = extract_function_code(frame, lineno) if frame else "Function source not found."
137+
138+
stack_info.append({
139+
"exception.file": filename,
140+
"exception.line": lineno,
141+
"exception.function_name": func_name,
142+
"exception.function_body": function_details["function_code"],
143+
"exception.start_line": function_details["function_start_line"],
144+
"exception.end_line": function_details["function_end_line"],
145+
})
123146

124-
function_code = extract_function_code(tb_frame) if tb_frame else "Function source not found."
125-
126-
# Determine if the exception is escaping
147+
# Determine if the exception is escaping
127148
current_exc = sys.exc_info()[1] # Get the currently active exception
128149
exception_escaped = current_exc is exc # True if it's still propagating
150+
151+
mw_git_repository_url = os.getenv("MW_GIT_REPOSITORY_URL")
152+
mw_git_commit_sha = os.getenv("MW_GIT_COMMIT_SHA")
153+
154+
# Serialize stack info as JSON string since OpenTelemetry only supports string values
155+
stack_info_str = json.dumps(stack_info, indent=2)
129156

130157
# Add extra details in the existing "exception" event
131158
span.add_event(
132-
"exception", # Keep the event name as "exception"
159+
"exception",
133160
{
134161
"exception.type": str(exc_type.__name__),
135162
"exception.message": exc_value,
136163
"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,
141164
"exception.escaped": exception_escaped,
165+
"exception.github.commit_sha": mw_git_commit_sha or "",
166+
"exception.github.repository_url": mw_git_repository_url or "",
167+
"exception.stack_details": stack_info_str, # Attach full stacktrace details
142168
}
143169
)
144170

145-
def record_exception(exc: Exception, span_name: Optional[str] = None) -> None:
171+
def record_exception(exc_type: Type[BaseException], exc_value: BaseException, exc_traceback) -> None:
146172
"""
147-
Reports an exception as a span event creating a dummy span if necessary.
173+
Reports an exception as a span event, creating a dummy span if necessary.
148174
149175
Args:
150-
exc (Exception): Pass Exception to record as in a current span.
151-
span_name (String,Optional): Span Name to use if no current span found,
152-
defaults to Exception Name.
176+
exc_type (Type[BaseException]): The type of the exception.
177+
exc_value (BaseException): The exception instance.
178+
exc_traceback: The traceback object.
153179
154180
Example
155181
--------
156-
>>> from middleware import record_exception
182+
>>> import sys
157183
>>> try:
158-
>>> print("Divide by zero:",1/0)
184+
>>> print("Divide by zero:", 1 / 0)
159185
>>> except Exception as e:
160-
>>> record_exception(e)
186+
>>> sys.excepthook(*sys.exc_info())
161187
162188
"""
163-
189+
# Retrieve the current span if available
164190
span = get_current_span()
165-
if span.is_recording():
166-
custom_record_exception(span, exc)
191+
if span and span.is_recording():
192+
custom_record_exception(span, exc_value)
167193
return
168194

195+
# Create a new span if none is found
169196
tracer: Tracer = get_tracer("mw-tracer")
170-
if span_name is None:
171-
span_name = type(exc).__name__
197+
span_name = exc_type.__name__ if exc_type else "UnknownException"
172198

173-
span = tracer.start_span(span_name)
174-
custom_record_exception(span, exc)
175-
span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
176-
span.end()
199+
with tracer.start_span(span_name) as span:
200+
custom_record_exception(span, exc_value)
201+
span.set_status(Status(StatusCode.ERROR, str(exc_value)))
177202

178203

179204
# pylint: disable=too-few-public-methods

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.2rc1"
7+
version = "2.1.2rc3"
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)