Skip to content

Commit da2b264

Browse files
authored
Attach troubleshooting guide for specific exception types (#665)
* Attach troubleshooting guide for specific exception types * Fix flake8 on broken test cases
1 parent c3b6ad9 commit da2b264

File tree

12 files changed

+149
-1
lines changed

12 files changed

+149
-1
lines changed

azure_functions_worker/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@
99

1010
# Feature Flags (app settings)
1111
PYTHON_ROLLBACK_CWD_PATH = "PYTHON_ROLLBACK_CWD_PATH"
12+
13+
# External Site URLs
14+
MODULE_NOT_FOUND_TS_URL = "https://aka.ms/functions-modulenotfound"

azure_functions_worker/dispatcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
from .logging import error_logger, logger, is_system_log_category
2626
from .logging import enable_console_logging, disable_console_logging
27-
from .tracing import marshall_exception_trace
27+
from .utils.tracing import marshall_exception_trace
2828
from .utils.wrappers import disable_feature_by
2929

3030

azure_functions_worker/loader.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import sys
1111
import typing
1212

13+
from .constants import MODULE_NOT_FOUND_TS_URL
14+
from .utils.wrappers import attach_message_to_exception
15+
1316

1417
_AZURE_NAMESPACE = '__app__'
1518

@@ -33,6 +36,10 @@ def uninstall():
3336
pass
3437

3538

39+
@attach_message_to_exception(
40+
expt_type=ImportError,
41+
message=f'Troubleshooting Guide: {MODULE_NOT_FOUND_TS_URL}'
42+
)
3643
def load_function(name: str, directory: str, script_file: str,
3744
entry_point: typing.Optional[str]):
3845
dir_path = pathlib.Path(directory)

azure_functions_worker/tracing.py renamed to azure_functions_worker/utils/tracing.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
import traceback
33

44

5+
def extend_exception_message(exc: Exception, msg: str) -> Exception:
6+
# Reconstruct exception message
7+
# From: ImportModule: no module name
8+
# To: ImportModule: no module name. msg
9+
old_tb = exc.__traceback__
10+
old_msg = getattr(exc, 'msg', None) or str(exc) or ''
11+
new_msg = (old_msg.rstrip('.') + '. ' + msg).rstrip()
12+
new_excpt = type(exc)(new_msg).with_traceback(old_tb)
13+
return new_excpt
14+
15+
516
def marshall_exception_trace(exc: Exception) -> str:
617
stack_summary: traceback.StackSummary = traceback.extract_tb(
718
exc.__traceback__)

azure_functions_worker/utils/wrappers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .common import is_envvar_true
2+
from .tracing import extend_exception_message
23

34

45
def enable_feature_by(flag: str, default=None):
@@ -19,3 +20,14 @@ def call(*args, **kwargs):
1920
return default
2021
return call
2122
return decorate
23+
24+
25+
def attach_message_to_exception(expt_type: Exception, message: str):
26+
def decorate(func):
27+
def call(*args, **kwargs):
28+
try:
29+
return func(*args, **kwargs)
30+
except expt_type as e:
31+
raise extend_exception_message(e, message)
32+
return call
33+
return decorate
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"type": "httpTrigger",
6+
"direction": "in",
7+
"name": "req"
8+
},
9+
{
10+
"type": "http",
11+
"direction": "out",
12+
"name": "$return"
13+
}
14+
]
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import logging
2+
3+
import azure.functions
4+
import does_not_exist # Noqa
5+
6+
7+
logger = logging.getLogger('my function')
8+
9+
10+
def main(req: azure.functions.HttpRequest):
11+
logger.info('Function should fail before hitting main')
12+
return 'OK-async'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"scriptFile": "main.py",
3+
"entryPoint": "customentry",
4+
"bindings": [
5+
{
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"name": "req"
9+
},
10+
{
11+
"type": "http",
12+
"direction": "out",
13+
"name": "$return"
14+
}
15+
]
16+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Import simple module with implicit directory import statement should fail
2+
from simple.main import main as s_main
3+
4+
5+
def brokenimplicit(req) -> str:
6+
return f's_main = {s_main(req)}'

tests/unittests/test_http_functions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,13 @@ def test_user_event_loop_error(self):
282282

283283
def check_log_user_event_loop_error(self, host_out: typing.List[str]):
284284
self.assertIn('try_log', host_out)
285+
286+
def test_import_module_troubleshooting_url(self):
287+
r = self.webhost.request('GET', 'missing_module/')
288+
self.assertEqual(r.status_code, 500)
289+
290+
def check_log_import_module_troubleshooting_url(self, host_out):
291+
self.assertIn("Exception: ModuleNotFoundError: "
292+
"No module named 'does_not_exist'. "
293+
"Troubleshooting Guide: "
294+
"https://aka.ms/functions-modulenotfound", host_out)

0 commit comments

Comments
 (0)