1
1
import logging
2
2
import inspect
3
3
import traceback
4
+ from typing import Optional , Type
4
5
import sys
5
6
from logging import getLogger
6
7
from typing import Optional
12
13
from middleware .log import create_logger_handler
13
14
from middleware .profiler import collect_profiling
14
15
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
17
19
18
20
_logger = getLogger (__name__ )
19
21
@@ -84,13 +86,32 @@ def mw_tracker(
84
86
85
87
mw_tracker_called = True
86
88
87
- def extract_function_code (tb_frame ):
89
+ def extract_function_code (tb_frame , lineno ):
88
90
"""Extracts the full function body where the exception occurred."""
89
91
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
+ }
94
115
95
116
# Replacement of span.record_exception to include function source code
96
117
def custom_record_exception (span : Span , exc : Exception ):
@@ -109,71 +130,75 @@ def custom_record_exception(span: Span, exc: Exception):
109
130
span .record_exception (exc )
110
131
return
111
132
112
- last_tb = tb_details [- 1 ] # Get the last traceback entry (where exception occurred)
113
- filename , lineno , func_name , _ = last_tb
133
+ stack_info = []
114
134
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
+ })
123
146
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
127
148
current_exc = sys .exc_info ()[1 ] # Get the currently active exception
128
149
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 )
129
156
130
157
# Add extra details in the existing "exception" event
131
158
span .add_event (
132
- "exception" , # Keep the event name as "exception"
159
+ "exception" ,
133
160
{
134
161
"exception.type" : str (exc_type .__name__ ),
135
162
"exception.message" : exc_value ,
136
163
"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
164
"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
142
168
}
143
169
)
144
170
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 :
146
172
"""
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.
148
174
149
175
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 .
153
179
154
180
Example
155
181
--------
156
- >>> from middleware import record_exception
182
+ >>> import sys
157
183
>>> try:
158
- >>> print("Divide by zero:",1/ 0)
184
+ >>> print("Divide by zero:", 1 / 0)
159
185
>>> except Exception as e:
160
- >>> record_exception(e )
186
+ >>> sys.excepthook(*sys.exc_info() )
161
187
162
188
"""
163
-
189
+ # Retrieve the current span if available
164
190
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 )
167
193
return
168
194
195
+ # Create a new span if none is found
169
196
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"
172
198
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 )))
177
202
178
203
179
204
# pylint: disable=too-few-public-methods
0 commit comments