Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 24 additions & 25 deletions runtimes/v1/azure_functions_runtime_v1/utils/tracing.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import traceback

from traceback import StackSummary, extract_tb
from typing import List


def extend_exception_message(exc: Exception, msg: str) -> Exception:
# Reconstruct exception message
# From: ImportModule: no module name
# To: ImportModule: no module name. msg
# To: ImportModule: no module name. msg
old_tb = exc.__traceback__
old_msg = getattr(exc, 'msg', None) or str(exc) or ''
new_msg = (old_msg.rstrip('.') + '. ' + msg).rstrip()
Expand All @@ -19,26 +15,27 @@ def extend_exception_message(exc: Exception, msg: str) -> Exception:


def marshall_exception_trace(exc: Exception) -> str:
stack_summary: StackSummary = extract_tb(exc.__traceback__)
if isinstance(exc, ModuleNotFoundError):
stack_summary = _marshall_module_not_found_error(stack_summary)
return ''.join(stack_summary.format())


def _marshall_module_not_found_error(tbss: StackSummary) -> StackSummary:
tbss = _remove_frame_from_stack(tbss, '<frozen importlib._bootstrap>')
tbss = _remove_frame_from_stack(
tbss, '<frozen importlib._bootstrap_external>')
return tbss


def _remove_frame_from_stack(tbss: StackSummary,
framename: str) -> StackSummary:
filtered_stack_list: List[traceback.FrameSummary] = \
list(filter(lambda frame: getattr(frame,
'filename') != framename, tbss))
filtered_stack: StackSummary = StackSummary.from_list(filtered_stack_list)
return filtered_stack
try:
# Use traceback.format_exception to capture the full exception chain
# This includes __cause__ and __context__ chained exceptions
full_traceback = traceback.format_exception(type(exc), exc, exc.__traceback__)

# If it's a ModuleNotFoundError, we might want to clean up the traceback
if isinstance(exc, ModuleNotFoundError):
# For consistency with the original logic, we'll still filter
# but we need to work with the formatted strings
filtered_lines = []
for line in full_traceback:
if '<frozen importlib._bootstrap>' not in line and \
'<frozen importlib._bootstrap_external>' not in line:
filtered_lines.append(line)
if filtered_lines:
return ''.join(filtered_lines)

return ''.join(full_traceback)
except Exception as sub_exc:
return (f'Could not extract traceback. '
f'Sub-exception: {type(sub_exc).__name__}: {str(sub_exc)}')


def serialize_exception(exc: Exception, protos):
Expand All @@ -47,10 +44,12 @@ def serialize_exception(exc: Exception, protos):
except Exception:
message = ('Unhandled exception in function. '
'Could not serialize original exception message.')

try:
stack_trace = marshall_exception_trace(exc)
except Exception:
stack_trace = ''

return protos.RpcException(message=message, stack_trace=stack_trace)


Expand Down
111 changes: 111 additions & 0 deletions runtimes/v1/tests/unittests/test_tracing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import unittest
import traceback
from azure_functions_runtime_v1.utils.tracing import (extend_exception_message,
marshall_exception_trace,
serialize_exception,
serialize_exception_as_str)


class MockProtos:
class RpcException:
def __init__(self, message, stack_trace):
self.message = message
self.stack_trace = stack_trace


class TestExceptionUtils(unittest.TestCase):

def test_extend_exception_message_basic(self):
exc = ValueError("Original message")
new_msg = "Extra info"
new_exc = extend_exception_message(exc, new_msg)
self.assertIsInstance(new_exc, ValueError)
self.assertIn("Original message", str(new_exc))
self.assertIn("Extra info", str(new_exc))
self.assertTrue(str(new_exc).endswith(new_msg))

def test_extend_exception_message_no_dot(self):
exc = ValueError("Message without dot")
new_exc = extend_exception_message(exc, "added")
self.assertEqual(str(new_exc), "Message without dot. added")

def test_marshall_exception_trace_basic(self):
try:
raise ValueError("Test")
except ValueError as exc:
trace = marshall_exception_trace(exc)
self.assertIn("ValueError: Test", trace)
self.assertIn("raise ValueError", trace)

def test_marshall_exception_trace_module_not_found(self):
try:
import non_existent_module # noqa: F401
except ModuleNotFoundError as exc:
trace = marshall_exception_trace(exc)
self.assertIn("ModuleNotFoundError", trace)
self.assertNotIn("<frozen importlib._bootstrap>", trace)

def test_marshall_exception_trace_chained_exceptions(self):
try:
try:
raise ValueError("Inner error")
except ValueError as inner:
raise RuntimeError("Outer error") from inner
except RuntimeError as exc:
trace = marshall_exception_trace(exc)
# Outer exception must appear
self.assertIn("RuntimeError: Outer error", trace)
# Inner exception must also appear
self.assertIn("ValueError: Inner error", trace)
# Ensure 'The above exception was the direct cause' appears
self.assertIn("The above exception was the direct cause", trace)

def test_serialize_exception_returns_rpc_exception(self):
try:
raise ValueError("Error for proto")
except ValueError as exc:
result = serialize_exception(exc, MockProtos)
self.assertIsInstance(result, MockProtos.RpcException)
self.assertIn("ValueError", result.message)
self.assertIn("Error for proto", result.message)
self.assertIn("raise ValueError", result.stack_trace)

def test_serialize_exception_as_str_basic(self):
try:
raise RuntimeError("Runtime issue")
except RuntimeError as exc:
result = serialize_exception_as_str(exc)
self.assertIn("RuntimeError: Runtime issue", result)
self.assertIn("Stack Trace:", result)
self.assertIn("raise RuntimeError", result)

def test_serialize_exception_with_unserializable_exception(self):
class BadExc(Exception):
def __str__(self):
raise ValueError("Cannot stringify")

exc = BadExc()
result_str = serialize_exception_as_str(exc)
self.assertIn("Could not serialize original exception message", result_str)

result_proto = serialize_exception(exc, MockProtos)
self.assertIn("Could not serialize original exception message",
result_proto.message)

def test_marshall_exception_trace_sub_exception(self):
# Patch traceback.format_exception to raise inside marshall_exception_trace
original_format_exception = traceback.format_exception

def bad_format(*args, **kwargs):
raise RuntimeError("fail inside traceback")
traceback.format_exception = bad_format
try:
exc = ValueError("test")
result = marshall_exception_trace(exc)
self.assertIn("Could not extract traceback", result)
self.assertIn("RuntimeError", result)
finally:
traceback.format_exception = original_format_exception
46 changes: 22 additions & 24 deletions runtimes/v2/azure_functions_runtime/utils/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
# Licensed under the MIT License.
import traceback

from traceback import StackSummary, extract_tb
from typing import List


def extend_exception_message(exc: Exception, msg: str) -> Exception:
# Reconstruct exception message
# From: ImportModule: no module name
# To: ImportModule: no module name. msg
# To: ImportModule: no module name. msg
old_tb = exc.__traceback__
old_msg = getattr(exc, 'msg', None) or str(exc) or ''
new_msg = (old_msg.rstrip('.') + '. ' + msg).rstrip()
Expand All @@ -18,26 +15,27 @@ def extend_exception_message(exc: Exception, msg: str) -> Exception:


def marshall_exception_trace(exc: Exception) -> str:
stack_summary: StackSummary = extract_tb(exc.__traceback__)
if isinstance(exc, ModuleNotFoundError):
stack_summary = _marshall_module_not_found_error(stack_summary)
return ''.join(stack_summary.format())


def _marshall_module_not_found_error(tbss: StackSummary) -> StackSummary:
tbss = _remove_frame_from_stack(tbss, '<frozen importlib._bootstrap>')
tbss = _remove_frame_from_stack(
tbss, '<frozen importlib._bootstrap_external>')
return tbss


def _remove_frame_from_stack(tbss: StackSummary,
framename: str) -> StackSummary:
filtered_stack_list: List[traceback.FrameSummary] = \
list(filter(lambda frame: getattr(frame,
'filename') != framename, tbss))
filtered_stack: StackSummary = StackSummary.from_list(filtered_stack_list)
return filtered_stack
try:
# Use traceback.format_exception to capture the full exception chain
# This includes __cause__ and __context__ chained exceptions
full_traceback = traceback.format_exception(type(exc), exc, exc.__traceback__)

# If it's a ModuleNotFoundError, we might want to clean up the traceback
if isinstance(exc, ModuleNotFoundError):
# For consistency with the original logic, we'll still filter
# but we need to work with the formatted strings
filtered_lines = []
for line in full_traceback:
if '<frozen importlib._bootstrap>' not in line and \
'<frozen importlib._bootstrap_external>' not in line:
filtered_lines.append(line)
if filtered_lines:
return ''.join(filtered_lines)

return ''.join(full_traceback)
except Exception as sub_exc:
return (f'Could not extract traceback. '
f'Sub-exception: {type(sub_exc).__name__}: {str(sub_exc)}')


def serialize_exception(exc: Exception, protos):
Expand Down
4 changes: 2 additions & 2 deletions runtimes/v2/tests/unittests/test_threadpool.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import importlib
import types
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from azure_functions_runtime.utils import threadpool as tp

Expand Down
111 changes: 111 additions & 0 deletions runtimes/v2/tests/unittests/test_tracing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import unittest
import traceback
from azure_functions_runtime.utils.tracing import (extend_exception_message,
marshall_exception_trace,
serialize_exception,
serialize_exception_as_str)


class MockProtos:
class RpcException:
def __init__(self, message, stack_trace):
self.message = message
self.stack_trace = stack_trace


class TestExceptionUtils(unittest.TestCase):

def test_extend_exception_message_basic(self):
exc = ValueError("Original message")
new_msg = "Extra info"
new_exc = extend_exception_message(exc, new_msg)
self.assertIsInstance(new_exc, ValueError)
self.assertIn("Original message", str(new_exc))
self.assertIn("Extra info", str(new_exc))
self.assertTrue(str(new_exc).endswith(new_msg))

def test_extend_exception_message_no_dot(self):
exc = ValueError("Message without dot")
new_exc = extend_exception_message(exc, "added")
self.assertEqual(str(new_exc), "Message without dot. added")

def test_marshall_exception_trace_basic(self):
try:
raise ValueError("Test")
except ValueError as exc:
trace = marshall_exception_trace(exc)
self.assertIn("ValueError: Test", trace)
self.assertIn("raise ValueError", trace)

def test_marshall_exception_trace_module_not_found(self):
try:
import non_existent_module # noqa: F401
except ModuleNotFoundError as exc:
trace = marshall_exception_trace(exc)
self.assertIn("ModuleNotFoundError", trace)
self.assertNotIn("<frozen importlib._bootstrap>", trace)

def test_marshall_exception_trace_chained_exceptions(self):
try:
try:
raise ValueError("Inner error")
except ValueError as inner:
raise RuntimeError("Outer error") from inner
except RuntimeError as exc:
trace = marshall_exception_trace(exc)
# Outer exception must appear
self.assertIn("RuntimeError: Outer error", trace)
# Inner exception must also appear
self.assertIn("ValueError: Inner error", trace)
# Ensure 'The above exception was the direct cause' appears
self.assertIn("The above exception was the direct cause", trace)

def test_serialize_exception_returns_rpc_exception(self):
try:
raise ValueError("Error for proto")
except ValueError as exc:
result = serialize_exception(exc, MockProtos)
self.assertIsInstance(result, MockProtos.RpcException)
self.assertIn("ValueError", result.message)
self.assertIn("Error for proto", result.message)
self.assertIn("raise ValueError", result.stack_trace)

def test_serialize_exception_as_str_basic(self):
try:
raise RuntimeError("Runtime issue")
except RuntimeError as exc:
result = serialize_exception_as_str(exc)
self.assertIn("RuntimeError: Runtime issue", result)
self.assertIn("Stack Trace:", result)
self.assertIn("raise RuntimeError", result)

def test_serialize_exception_with_unserializable_exception(self):
class BadExc(Exception):
def __str__(self):
raise ValueError("Cannot stringify")

exc = BadExc()
result_str = serialize_exception_as_str(exc)
self.assertIn("Could not serialize original exception message", result_str)

result_proto = serialize_exception(exc, MockProtos)
self.assertIn("Could not serialize original exception message",
result_proto.message)

def test_marshall_exception_trace_sub_exception(self):
# Patch traceback.format_exception to raise inside marshall_exception_trace
original_format_exception = traceback.format_exception

def bad_format(*args, **kwargs):
raise RuntimeError("fail inside traceback")
traceback.format_exception = bad_format
try:
exc = ValueError("test")
result = marshall_exception_trace(exc)
self.assertIn("Could not extract traceback", result)
self.assertIn("RuntimeError", result)
finally:
traceback.format_exception = original_format_exception
Loading