|
1 | 1 | import logging
|
| 2 | +import inspect |
| 3 | +import traceback |
| 4 | +from typing import Optional, Type |
| 5 | +import sys |
2 | 6 | from logging import getLogger
|
3 | 7 | from typing import Optional
|
4 | 8 | from opentelemetry.instrumentation.distro import BaseDistro
|
|
9 | 13 | from middleware.log import create_logger_handler
|
10 | 14 | from middleware.profiler import collect_profiling
|
11 | 15 | from opentelemetry import trace
|
12 |
| -from opentelemetry.trace import Tracer, get_current_span, get_tracer |
| 16 | +from opentelemetry.trace import Tracer, get_current_span, get_tracer, get_tracer, Status, StatusCode |
| 17 | +from opentelemetry.sdk.trace import Span |
| 18 | +import os |
| 19 | +import json |
13 | 20 |
|
14 | 21 | _logger = getLogger(__name__)
|
15 | 22 |
|
@@ -80,7 +87,6 @@ def mw_tracker(
|
80 | 87 |
|
81 | 88 | mw_tracker_called = True
|
82 | 89 |
|
83 |
| - |
84 | 90 | def record_exception(exc: Exception, span_name: Optional[str] = None) -> None:
|
85 | 91 | """
|
86 | 92 | Reports an exception as a span event creating a dummy span if necessary.
|
@@ -114,6 +120,136 @@ def record_exception(exc: Exception, span_name: Optional[str] = None) -> None:
|
114 | 120 | span.set_status(trace.Status(trace.StatusCode.ERROR, str(exc)))
|
115 | 121 | span.end()
|
116 | 122 |
|
| 123 | +def extract_function_code(tb_frame, lineno): |
| 124 | + """Extracts the full function body where the exception occurred.""" |
| 125 | + try: |
| 126 | + # Get the source lines and the starting line number of the function |
| 127 | + source_lines, start_line = inspect.getsourcelines(tb_frame) |
| 128 | + end_line = start_line + len(source_lines) - 1 |
| 129 | + |
| 130 | + # If the function body is too long, limit the number of lines |
| 131 | + if len(source_lines) > 20: |
| 132 | + # Define the number of lines to show before and after the exception line |
| 133 | + lines_before = 10 |
| 134 | + lines_after = 10 |
| 135 | + |
| 136 | + # Calculate the start and end indices for slicing |
| 137 | + start_idx = max(0, lineno - start_line - lines_before) |
| 138 | + end_idx = min(len(source_lines), lineno - start_line + lines_after) |
| 139 | + |
| 140 | + # Extract the relevant lines |
| 141 | + source_lines = source_lines[start_idx:end_idx] |
| 142 | + |
| 143 | + # Adjust the start and end line numbers |
| 144 | + start_line += start_idx |
| 145 | + end_line = start_line + len(source_lines) - 1 |
| 146 | + |
| 147 | + # Convert the list of lines to a single string |
| 148 | + function_code = "".join(source_lines) |
| 149 | + |
| 150 | + return { |
| 151 | + "function_code": function_code, |
| 152 | + "function_start_line": start_line, |
| 153 | + "function_end_line": end_line, |
| 154 | + } |
| 155 | + |
| 156 | + except Exception as e: |
| 157 | + # Handle cases where the source code cannot be extracted |
| 158 | + return { |
| 159 | + "function_code": f"Error extracting function code: {e}", |
| 160 | + "function_start_line": None, |
| 161 | + "function_end_line": None, |
| 162 | + } |
| 163 | + |
| 164 | +def custom_record_exception_wrapper(self: Span, |
| 165 | + exception: BaseException, |
| 166 | + attributes=None, |
| 167 | + timestamp: int = None, |
| 168 | + escaped: bool = False) -> None: |
| 169 | + """ |
| 170 | + Custom wrapper for Span.record_exception. |
| 171 | + This calls our custom_record_exception to add extra details before delegating |
| 172 | + to the original record_exception method. |
| 173 | + """ |
| 174 | + # Check for a recursion marker |
| 175 | + if self.attributes.get("exception.is_recursion") == "true": |
| 176 | + return _original_record_exception(self, exception, attributes, timestamp, escaped) |
| 177 | + |
| 178 | + # Mark the span to prevent infinite recursion. |
| 179 | + self.set_attribute("exception.is_recursion", "true") |
| 180 | + |
| 181 | + # Call our custom exception recording logic. |
| 182 | + custom_record_exception(self, exception) |
| 183 | + |
| 184 | + # Optionally, call the original record_exception for default behavior. |
| 185 | + return _original_record_exception(self, exception, attributes, timestamp, escaped) |
| 186 | + |
| 187 | +# Replacement of span.record_exception to include function source code |
| 188 | +def custom_record_exception(span: Span, exc: Exception): |
| 189 | + """Custom exception recording that captures function source code.""" |
| 190 | + exc_type, exc_value, exc_tb = exc.__class__, str(exc), exc.__traceback__ |
| 191 | + |
| 192 | + if exc_tb is None: |
| 193 | + # span.set_attribute("exception.warning", "No traceback available") |
| 194 | + span.record_exception(exc) |
| 195 | + return |
| 196 | + |
| 197 | + tb_details = traceback.extract_tb(exc_tb) |
| 198 | + |
| 199 | + if not tb_details: |
| 200 | + # span.set_attribute("exception.warning", "Traceback is empty") |
| 201 | + span.record_exception(exc) |
| 202 | + return |
| 203 | + |
| 204 | + stack_info = [] |
| 205 | + |
| 206 | + for (frame, _), (filename, lineno, func_name, _) in zip(traceback.walk_tb(exc_tb), tb_details): |
| 207 | + function_details = extract_function_code(frame, lineno) if frame else "Function source not found." |
| 208 | + |
| 209 | + stack_entry = { |
| 210 | + "exception.file": filename, |
| 211 | + "exception.line": lineno, |
| 212 | + "exception.function_name": func_name, |
| 213 | + "exception.function_body": function_details["function_code"], |
| 214 | + "exception.start_line": function_details["function_start_line"], |
| 215 | + "exception.end_line": function_details["function_end_line"], |
| 216 | + } |
| 217 | + |
| 218 | + # Check if the file is from site-packages |
| 219 | + if "site-packages" in filename: |
| 220 | + stack_entry["exception.is_file_external"] = "true" |
| 221 | + else: |
| 222 | + stack_entry["exception.is_file_external"] = "false" |
| 223 | + |
| 224 | + stack_info.insert(0, stack_entry) # Prepend instead of append |
| 225 | + |
| 226 | + # Determine if the exception is escaping |
| 227 | + current_exc = sys.exc_info()[1] # Get the currently active exception |
| 228 | + exception_escaped = current_exc is exc # True if it's still propagating |
| 229 | + |
| 230 | + mw_vcs_repository_url = os.getenv("MW_VCS_REPOSITORY_URL") |
| 231 | + mw_vcs_commit_sha = os.getenv("MW_VCS_COMMIT_SHA") |
| 232 | + |
| 233 | + # Serialize stack info as JSON string since OpenTelemetry only supports string values |
| 234 | + stack_info_str = json.dumps(stack_info, indent=2) |
| 235 | + |
| 236 | + # Add extra details in the existing "exception" event |
| 237 | + span.add_event( |
| 238 | + "exception", |
| 239 | + { |
| 240 | + "exception.type": str(exc_type.__name__), |
| 241 | + "exception.message": exc_value, |
| 242 | + "exception.language": "python", |
| 243 | + "exception.stacktrace": traceback.format_exc(), |
| 244 | + "exception.escaped": exception_escaped, |
| 245 | + "exception.vcs.commit_sha": mw_vcs_commit_sha or "", |
| 246 | + "exception.vcs.repository_url": mw_vcs_repository_url or "", |
| 247 | + "exception.stack_details": stack_info_str, # Attach full stacktrace details |
| 248 | + } |
| 249 | + ) |
| 250 | + |
| 251 | + |
| 252 | + |
117 | 253 |
|
118 | 254 | # pylint: disable=too-few-public-methods
|
119 | 255 | class MiddlewareDistro(BaseDistro):
|
|
0 commit comments