Skip to content

Commit 7c9bcc4

Browse files
authored
Improve logging in python worker (#637)
* Improve logging * Update protobuf to v1.3.7 * Fix unit test scenarios * Future proof azure_functions_worker system logs
1 parent 1824612 commit 7c9bcc4

File tree

6 files changed

+80
-25
lines changed

6 files changed

+80
-25
lines changed

azure_functions_worker/dispatcher.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
from . import protos
2323
from . import constants
2424

25-
from .logging import error_logger, logger
25+
from .logging import error_logger, logger, is_system_log_category
26+
from .logging import enable_console_logging, disable_console_logging
2627
from .tracing import marshall_exception_trace
2728
from .utils.wrappers import disable_feature_by
2829

@@ -115,13 +116,21 @@ async def dispatch_forever(self):
115116
self._loop.set_task_factory(
116117
lambda loop, coro: ContextEnabledTask(coro, loop=loop))
117118

119+
# Attach gRPC logging to the root logger
118120
logging_handler = AsyncLoggingHandler()
119121
root_logger = logging.getLogger()
120122
root_logger.setLevel(logging.INFO)
121123
root_logger.addHandler(logging_handler)
124+
125+
# Since gRPC channel is established, should use it for logging
126+
disable_console_logging()
127+
logger.info('Detach console logging. Switch to gRPC logging')
128+
122129
try:
123130
await forever
124131
finally:
132+
# Reenable console logging when there's an exception
133+
enable_console_logging()
125134
root_logger.removeHandler(logging_handler)
126135
finally:
127136
DispatcherMeta.__current_dispatcher__ = None
@@ -155,10 +164,16 @@ def _on_logging(self, record: logging.LogRecord, formatted_msg: str):
155164
else:
156165
log_level = getattr(protos.RpcLog, 'None')
157166

167+
if is_system_log_category(record.name):
168+
log_category = protos.RpcLog.RpcLogCategory.System
169+
else:
170+
log_category = protos.RpcLog.RpcLogCategory.User
171+
158172
log = dict(
159173
level=log_level,
160174
message=formatted_msg,
161175
category=record.name,
176+
log_category=log_category
162177
)
163178

164179
invocation_id = get_current_invocation_id()
@@ -310,12 +325,17 @@ async def _handle__invocation_request(self, req):
310325
args[name] = bindings.Out()
311326

312327
if fi.is_async:
328+
logger.info('Function is async, request ID: %s,'
329+
'function ID: %s, invocation ID: %s',
330+
self.request_id, function_id, invocation_id)
313331
call_result = await fi.func(**args)
314332
else:
333+
logger.info('Function is sync, request ID: %s,'
334+
'function ID: %s, invocation ID: %s',
335+
self.request_id, function_id, invocation_id)
315336
call_result = await self._loop.run_in_executor(
316337
self._sync_call_tp,
317338
self.__run_sync_func, invocation_id, fi.func, args)
318-
319339
if call_result is not None and not fi.has_return:
320340
raise RuntimeError(
321341
f'function {fi.name!r} without a $return binding '
@@ -499,10 +519,10 @@ def gen(resp_queue):
499519
class AsyncLoggingHandler(logging.Handler):
500520

501521
def emit(self, record):
502-
if not record.name.startswith('azure_functions_worker'):
503-
# Skip worker system logs
504-
msg = self.format(record)
505-
Dispatcher.current._on_logging(record, msg)
522+
# Since we disable console log after gRPC channel is initiated
523+
# We should redirect all the messages into dispatcher
524+
msg = self.format(record)
525+
Dispatcher.current._on_logging(record, msg)
506526

507527

508528
class ContextEnabledTask(asyncio.Task):

azure_functions_worker/logging.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
logger = logging.getLogger('azure_functions_worker')
77
error_logger = logging.getLogger('azure_functions_worker_errors')
88

9+
handler = None
10+
error_handler = None
11+
912

1013
def setup(log_level, log_destination):
1114
if log_level == 'TRACE':
@@ -14,9 +17,6 @@ def setup(log_level, log_destination):
1417
formatter = logging.Formatter(
1518
'LanguageWorkerConsoleLog %(levelname)s: %(message)s')
1619

17-
error_handler = None
18-
handler = None
19-
2020
if log_destination is None:
2121
# With no explicit log destination we do split logging,
2222
# errors go into stderr, everything else -- to stdout.
@@ -46,3 +46,28 @@ def setup(log_level, log_destination):
4646

4747
error_logger.addHandler(error_handler)
4848
error_logger.setLevel(getattr(logging, log_level))
49+
50+
51+
def disable_console_logging():
52+
if logger and handler:
53+
logger.removeHandler(handler)
54+
55+
if error_logger and error_handler:
56+
error_logger.removeHandler(error_handler)
57+
58+
59+
def enable_console_logging():
60+
if logger and handler:
61+
logger.addHandler(handler)
62+
63+
if error_logger and error_handler:
64+
error_logger.addHandler(error_handler)
65+
66+
67+
def is_system_log_category(ctg: str):
68+
return any(
69+
[ctg.lower().startswith(c) for c in (
70+
'azure_functions_worker',
71+
'azure_functions_worker_errors'
72+
)]
73+
)

azure_functions_worker/protos/_src/src/proto/FunctionRpc.proto

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,12 @@ message RpcException {
431431

432432
// Http cookie type. Note that only name and value are used for Http requests
433433
message RpcHttpCookie {
434-
// Enum that lets servers require that a cookie shouoldn't be sent with cross-site requests
434+
// Enum that lets servers require that a cookie shouldn't be sent with cross-site requests
435435
enum SameSite {
436436
None = 0;
437437
Lax = 1;
438438
Strict = 2;
439+
ExplicitNone = 3;
439440
}
440441

441442
// Cookie name

azure_functions_worker/testutils.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,13 @@ class WebHostTestCaseMeta(type(unittest.TestCase)):
129129
def __new__(mcls, name, bases, dct):
130130
for attrname, attr in dct.items():
131131
if attrname.startswith('test_') and callable(attr):
132-
test_case_name = attrname.lstrip('test_')
133132
test_case = attr
134-
135-
check_log_case_name = f'check_log_{test_case_name}'
136-
check_log_case = dct.get(check_log_case_name)
133+
check_log_name = attrname.replace('test_', 'check_log_', 1)
134+
check_log_case = dct.get(check_log_name)
137135

138136
@functools.wraps(test_case)
139137
def wrapper(self, *args, __meth__=test_case,
140138
__check_log__=check_log_case, **kwargs):
141-
142139
if (__check_log__ is not None
143140
and callable(__check_log__)
144141
and not is_envvar_true(PYAZURE_WEBHOST_DEBUG)):

tests/unittests/test_http_functions.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,22 @@ def check_log_async_logging(self, host_out: typing.List[str]):
9393
self.assertIn('hello info', host_out)
9494
self.assertIn('and another error', host_out)
9595

96+
# We should see "Function is async"
97+
is_async = any([ho.startswith("Function is async") for ho in host_out])
98+
self.assertTrue(is_async)
99+
96100
def test_sync_logging(self):
97101
# Test that logging doesn't *break* things.
98102
r = self.webhost.request('GET', 'sync_logging')
99103
self.assertEqual(r.status_code, 200)
100104
self.assertEqual(r.text, 'OK-sync')
101105

102106
def check_log_sync_logging(self, host_out: typing.List[str]):
103-
self.assertIn('a gracefully handled error')
107+
self.assertIn('a gracefully handled error', host_out)
108+
109+
# We should see a warning "Function is sync"
110+
is_sync = any([ho.startswith("Function is sync") for ho in host_out])
111+
self.assertTrue(is_sync)
104112

105113
def test_return_context(self):
106114
r = self.webhost.request('GET', 'return_context')

tests/unittests/test_mock_http_functions.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ async def test_call_sync_function_check_logs(self):
2020
self.assertEqual(r.response.result.status,
2121
protos.StatusResult.Success)
2222

23-
self.assertEqual(len(r.logs), 1)
23+
user_logs = [l for l in r.logs if l.category == 'my function']
24+
self.assertEqual(len(user_logs), 1)
2425

25-
log = r.logs[0]
26+
log = user_logs[0]
2627
self.assertEqual(log.invocation_id, invoke_id)
2728
self.assertTrue(log.message.startswith(
2829
'a gracefully handled error'))
@@ -45,15 +46,18 @@ async def test_call_async_function_check_logs(self):
4546
self.assertEqual(r.response.result.status,
4647
protos.StatusResult.Success)
4748

48-
self.assertEqual(len(r.logs), 2)
49+
user_logs = [l for l in r.logs if l.category == 'my function']
50+
self.assertEqual(len(user_logs), 2)
4951

50-
self.assertEqual(r.logs[0].invocation_id, invoke_id)
51-
self.assertEqual(r.logs[0].message, 'hello info')
52-
self.assertEqual(r.logs[0].level, protos.RpcLog.Information)
52+
first_msg = user_logs[0]
53+
self.assertEqual(first_msg.invocation_id, invoke_id)
54+
self.assertEqual(first_msg.message, 'hello info')
55+
self.assertEqual(first_msg.level, protos.RpcLog.Information)
5356

54-
self.assertEqual(r.logs[1].invocation_id, invoke_id)
55-
self.assertTrue(r.logs[1].message.startswith('and another error'))
56-
self.assertEqual(r.logs[1].level, protos.RpcLog.Error)
57+
second_msg = user_logs[1]
58+
self.assertEqual(second_msg.invocation_id, invoke_id)
59+
self.assertTrue(second_msg.message.startswith('and another error'))
60+
self.assertEqual(second_msg.level, protos.RpcLog.Error)
5761

5862
self.assertEqual(r.response.return_value.string, 'OK-async')
5963

0 commit comments

Comments
 (0)