Skip to content

Commit 7c52e93

Browse files
authored
Enable logging in user created event_loop (#610)
* Inject _invocation_id with lowest priority * Add unit tests * Change back host version * Add logging test facility in testutil * Change file name * Add tests on old logging
1 parent 7f51869 commit 7c52e93

File tree

8 files changed

+147
-14
lines changed

8 files changed

+147
-14
lines changed

azure_functions_worker/dispatcher.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,10 @@ def get_current_invocation_id():
512512
if loop is not None:
513513
current_task = asyncio.Task.current_task(loop)
514514
if current_task is not None:
515-
return getattr(
515+
task_invocation_id = getattr(
516516
current_task, ContextEnabledTask._AZURE_INVOCATION_ID, None)
517+
if task_invocation_id is not None:
518+
return task_invocation_id
517519

518520
return getattr(_invocation_id_local, 'v', None)
519521

azure_functions_worker/testutils.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,25 @@ class WebHostTestCaseMeta(type(unittest.TestCase)):
126126
def __new__(mcls, name, bases, dct):
127127
for attrname, attr in dct.items():
128128
if attrname.startswith('test_') and callable(attr):
129-
130-
@functools.wraps(attr)
131-
def wrapper(self, *args, __meth__=attr, **kwargs):
132-
return self._run_test(__meth__, *args, **kwargs)
129+
test_case_name = attrname.lstrip('test_')
130+
test_case = attr
131+
132+
check_log_case_name = f'check_log_{test_case_name}'
133+
check_log_case = dct.get(check_log_case_name)
134+
135+
@functools.wraps(test_case)
136+
def wrapper(self, *args, __meth__=test_case,
137+
__check_log__=check_log_case, **kwargs):
138+
if __check_log__ is not None and callable(__check_log__):
139+
# Check logging output for unit test scenarios
140+
result = self._run_test(__meth__, *args, **kwargs)
141+
output_lines = self.host_out.splitlines()
142+
host_out = list(map(lambda s: s.strip(), output_lines))
143+
self._run_test(__check_log__, host_out=host_out)
144+
return result
145+
else:
146+
# Check normal unit test
147+
return self._run_test(__meth__, *args, **kwargs)
133148

134149
dct[attrname] = wrapper
135150

@@ -188,16 +203,20 @@ def _run_test(self, test, *args, **kwargs):
188203
self.host_stdout.read()
189204
last_pos = self.host_stdout.tell()
190205

206+
test_exception = None
191207
try:
192208
test(self, *args, **kwargs)
193-
except Exception:
194-
try:
195-
self.host_stdout.seek(last_pos)
196-
host_out = self.host_stdout.read()
197-
self.host_stdout_logger.error(
198-
f'Captured WebHost stdout:\n{host_out}')
199-
finally:
200-
raise
209+
except Exception as e:
210+
test_exception = e
211+
212+
try:
213+
self.host_stdout.seek(last_pos)
214+
self.host_out = self.host_stdout.read()
215+
self.host_stdout_logger.error(
216+
f'Captured WebHost stdout:\n{self.host_out}')
217+
finally:
218+
if test_exception is not None:
219+
raise test_exception
201220

202221

203222
class _MockWebHostServicer(protos.FunctionRpcServicer):
@@ -725,7 +744,8 @@ def _setup_func_app(app_root):
725744
f.write(HOST_JSON_TEMPLATE)
726745

727746
_symlink_dir(TESTS_ROOT / 'common' / 'ping', ping_func)
728-
_symlink_dir(EXTENSIONS_PATH, extensions)
747+
if EXTENSIONS_PATH.exists():
748+
_symlink_dir(EXTENSIONS_PATH, extensions)
729749

730750

731751
def _teardown_func_app(app_root):
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"authLevel": "anonymous",
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"name": "req",
9+
"methods": [
10+
"get"
11+
]
12+
},
13+
{
14+
"type": "http",
15+
"direction": "out",
16+
"name": "$return"
17+
}
18+
]
19+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import logging
2+
import asyncio
3+
4+
import azure.functions as func
5+
6+
7+
logger = logging.getLogger('my function')
8+
9+
10+
async def try_log():
11+
logger.info("try_log")
12+
13+
14+
def main(req: func.HttpRequest) -> func.HttpResponse:
15+
loop = asyncio.SelectorEventLoop()
16+
asyncio.set_event_loop(loop)
17+
18+
# This line should throws an asyncio RuntimeError exception
19+
loop.run_until_complete(try_log())
20+
loop.close()
21+
return 'OK-user-event-loop'

tests/unittests/test_http_functions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import hashlib
22
import pathlib
33
import filecmp
4+
import typing
45
import os
56

67
from azure_functions_worker import testutils
@@ -88,12 +89,19 @@ def test_async_logging(self):
8889
self.assertEqual(r.status_code, 200)
8990
self.assertEqual(r.text, 'OK-async')
9091

92+
def check_log_async_logging(self, host_out: typing.List[str]):
93+
self.assertIn('hello info', host_out)
94+
self.assertIn('and another error', host_out)
95+
9196
def test_sync_logging(self):
9297
# Test that logging doesn't *break* things.
9398
r = self.webhost.request('GET', 'sync_logging')
9499
self.assertEqual(r.status_code, 200)
95100
self.assertEqual(r.text, 'OK-sync')
96101

102+
def check_log_sync_logging(self, host_out: typing.List[str]):
103+
self.assertIn('a gracefully handled error')
104+
97105
def test_return_context(self):
98106
r = self.webhost.request('GET', 'return_context')
99107
self.assertEqual(r.status_code, 200)
@@ -263,3 +271,12 @@ def test_application_octet_stream_content_type(self):
263271
finally:
264272
if (os.path.exists(received_img_file)):
265273
os.remove(received_img_file)
274+
275+
def test_user_event_loop_error(self):
276+
# User event loop is not supported in HTTP trigger
277+
r = self.webhost.request('GET', 'user_event_loop/')
278+
self.assertEqual(r.status_code, 200)
279+
self.assertEqual(r.text, 'OK-user-event-loop')
280+
281+
def check_log_user_event_loop_error(self, host_out: typing.List[str]):
282+
self.assertIn('try_log', host_out)

tests/unittests/test_mock_timer_functions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,28 @@ async def call_and_check(due: bool):
3838

3939
await call_and_check(True)
4040
await call_and_check(False)
41+
42+
async def test_mock_timer__user_event_loop(self):
43+
async with testutils.start_mockhost(
44+
script_root=self.timer_funcs_dir) as host:
45+
46+
func_id, r = await host.load_function('user_event_loop_timer')
47+
48+
self.assertEqual(r.response.function_id, func_id)
49+
self.assertEqual(r.response.result.status,
50+
protos.StatusResult.Success)
51+
52+
async def call_and_check():
53+
_, r = await host.invoke_function(
54+
'user_event_loop_timer', [
55+
protos.ParameterBinding(
56+
name='timer',
57+
data=protos.TypedData(
58+
json=json.dumps({
59+
'IsPastDue': False
60+
})))
61+
])
62+
self.assertEqual(r.response.result.status,
63+
protos.StatusResult.Success)
64+
65+
await call_and_check()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"scriptFile": "main.py",
3+
"bindings": [
4+
{
5+
"type": "timerTrigger",
6+
"direction": "in",
7+
"name": "timer",
8+
"schedule": "*/5 * * * * *"
9+
}
10+
]
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import logging
2+
import asyncio
3+
4+
import azure.functions as func
5+
6+
7+
logger = logging.getLogger('my function')
8+
9+
10+
async def try_log():
11+
logger.info("try_log")
12+
13+
14+
def main(timer: func.TimerRequest):
15+
loop = asyncio.SelectorEventLoop()
16+
asyncio.set_event_loop(loop)
17+
loop.run_until_complete(try_log())
18+
loop.close()

0 commit comments

Comments
 (0)