Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for debug logs in executed functions. #745

Merged
merged 4 commits into from
Sep 25, 2020
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ celerybeat-schedule

# virtualenv (.venv/.venv36/.venv37/.venv38)
.venv*
venv/
venv*/
ENV/
py3env/

Expand Down
2 changes: 1 addition & 1 deletion azure_functions_worker/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async def dispatch_forever(self):
# established, should use it for system and user logs
logging_handler = AsyncLoggingHandler()
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.setLevel(logging.DEBUG)
Hazhzeng marked this conversation as resolved.
Show resolved Hide resolved
root_logger.addHandler(logging_handler)
logger.info('Switched to gRPC logging.')
logging_handler.flush()
Expand Down
26 changes: 17 additions & 9 deletions azure_functions_worker/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,28 @@

import argparse

from ._thirdparty import aio_compat
from . import dispatcher
from . import logging
from ._thirdparty import aio_compat
from .logging import error_logger, logger


def parse_args():
parser = argparse.ArgumentParser(
description='Python Azure Functions Worker')
parser.add_argument('--host')
parser.add_argument('--port', type=int)
parser.add_argument('--workerId', dest='worker_id')
parser.add_argument('--requestId', dest='request_id')
parser.add_argument('--host',
help="host address")
parser.add_argument('--port', type=int,
help='id for the requests')
parser.add_argument('--workerId', dest='worker_id',
help='id for the worker')
parser.add_argument('--requestId', dest='request_id',
help='log destination: stdout, stderr, '
'syslog, or a file path')
parser.add_argument('--log-level', type=str, default='INFO',
choices=['TRACE', 'INFO', 'WARNING', 'ERROR'],)
choices=['TRACE', 'INFO', 'WARNING', 'ERROR'],
help="log level: 'TRACE', 'INFO', 'WARNING', "
"or 'ERROR'")
parser.add_argument('--log-to', type=str, default=None,
Hazhzeng marked this conversation as resolved.
Show resolved Hide resolved
help='log destination: stdout, stderr, '
'syslog, or a file path')
Expand All @@ -45,8 +52,9 @@ def main():


async def start_async(host, port, worker_id, request_id):
disp = await dispatcher.Dispatcher.connect(
host, port, worker_id, request_id,
connect_timeout=5.0)
disp = await dispatcher.Dispatcher.connect(host=host, port=port,
worker_id=worker_id,
request_id=request_id,
connect_timeout=5.0)

await disp.dispatch_forever()
18 changes: 12 additions & 6 deletions azure_functions_worker/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def wrapper(self, *args, __meth__=test_case,
# Trim off host output timestamps
host_output = getattr(self, 'host_out', '')
output_lines = host_output.splitlines()
ts_re = r"^\[\d+\/\d+\/\d+ \d+\:\d+\:\d+ (A|P)M\]"
ts_re = r"^\[\d+\/\d+\/\d+ \d+\:\d+\:\d+.*(A|P)*M*\]"
output = list(map(
lambda s: re.sub(ts_re, '', s).strip(),
output_lines))
Expand All @@ -171,6 +171,11 @@ class WebHostTestCase(unittest.TestCase, metaclass=WebHostTestCaseMeta):
In addition to automatically starting up a WebHost instance,
this test case class logs WebHost stdout/stderr in case
a unit test fails.

You can write two sets of test - test_* and check_log_* tests.

test_ABC - Unittest
check_log_ABC - Check logs generated during the execution of test_ABC.
"""
host_stdout_logger = logging.getLogger('webhosttests')

Expand Down Expand Up @@ -728,7 +733,7 @@ def call(*args, **kwargs):
return decorate


def _remove_path(path):
def remove_path(path):
if path.is_symlink():
path.unlink()
elif path.is_dir():
Expand All @@ -738,7 +743,7 @@ def _remove_path(path):


def _symlink_dir(src, dst):
_remove_path(dst)
remove_path(dst)

if ON_WINDOWS:
shutil.copytree(str(src), str(dst))
Expand All @@ -751,8 +756,9 @@ def _setup_func_app(app_root):
ping_func = app_root / 'ping'
host_json = app_root / 'host.json'

with open(host_json, 'w') as f:
f.write(HOST_JSON_TEMPLATE)
if not os.path.isfile(host_json):
with open(host_json, 'w') as f:
f.write(HOST_JSON_TEMPLATE)

_symlink_dir(TESTS_ROOT / 'common' / 'ping', ping_func)
_symlink_dir(EXTENSIONS_PATH, extensions)
Expand All @@ -764,7 +770,7 @@ def _teardown_func_app(app_root):
host_json = app_root / 'host.json'

for path in (extensions, ping_func, host_json):
_remove_path(path)
remove_path(path)


def _main():
Expand Down
15 changes: 15 additions & 0 deletions tests/unittests/http_functions/debug_logging/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
13 changes: 13 additions & 0 deletions tests/unittests/http_functions/debug_logging/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import logging

import azure.functions


def main(req: azure.functions.HttpRequest):
logging.info('logging info', exc_info=True)
logging.warning('logging warning', exc_info=True)
logging.debug('logging debug', exc_info=True)
logging.error('logging error', exc_info=True)
return 'OK-debug'
15 changes: 15 additions & 0 deletions tests/unittests/http_functions/debug_user_logging/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
16 changes: 16 additions & 0 deletions tests/unittests/http_functions/debug_user_logging/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import logging

import azure.functions


logger = logging.getLogger('my function')


def main(req: azure.functions.HttpRequest):
logger.info('logging info', exc_info=True)
logger.warning('logging warning', exc_info=True)
logger.debug('logging debug', exc_info=True)
logger.error('logging error', exc_info=True)
return 'OK-user-debug'
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
13 changes: 13 additions & 0 deletions tests/unittests/log_filtering_functions/debug_logging/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import logging

import azure.functions


def main(req: azure.functions.HttpRequest):
logging.info('logging info', exc_info=True)
logging.warning('logging warning', exc_info=True)
logging.debug('logging debug', exc_info=True)
logging.error('logging error', exc_info=True)
return 'OK-debug'
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"scriptFile": "main.py",
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
import logging

import azure.functions


logger = logging.getLogger('my function')


def main(req: azure.functions.HttpRequest):
logger.info('logging info', exc_info=True)
logger.warning('logging warning', exc_info=True)
logger.debug('logging debug', exc_info=True)
logger.error('logging error', exc_info=True)
return 'OK-user-debug'
22 changes: 22 additions & 0 deletions tests/unittests/test_http_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ def check_log_async_logging(self, host_out: typing.List[str]):
self.assertIn('hello info', host_out)
self.assertIn('and another error', host_out)

def test_debug_logging(self):
r = self.webhost.request('GET', 'debug_logging')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'OK-debug')

def check_log_debug_logging(self, host_out: typing.List[str]):
self.assertIn('logging info', host_out)
self.assertIn('logging warning', host_out)
self.assertIn('logging debug', host_out)
self.assertIn('logging error', host_out)

def test_debug_with_user_logging(self):
r = self.webhost.request('GET', 'debug_user_logging')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'OK-user-debug')

def check_log_debug_with_user_logging(self, host_out: typing.List[str]):
self.assertIn('logging info', host_out)
self.assertIn('logging warning', host_out)
self.assertIn('logging debug', host_out)
self.assertIn('logging error', host_out)

def test_sync_logging(self):
# Test that logging doesn't *break* things.
r = self.webhost.request('GET', 'sync_logging')
Expand Down
64 changes: 64 additions & 0 deletions tests/unittests/test_log_filtering_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import typing

from azure_functions_worker import testutils
from azure_functions_worker.testutils import TESTS_ROOT, remove_path

HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO = """\
{
"version": "2.0",
"logging": {
"logLevel": {
"default": "Information"
}
},
"functionTimeout": "00:05:00"
}
"""


class TestLogFilteringFunctions(testutils.WebHostTestCase):

@classmethod
def setUpClass(cls):
host_json = TESTS_ROOT / cls.get_script_dir() / 'host.json'

with open(host_json, 'w+') as f:
f.write(HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO)

super(TestLogFilteringFunctions, cls).setUpClass()

@classmethod
def tearDownClass(cls):
host_json = TESTS_ROOT / cls.get_script_dir() / 'host.json'
remove_path(host_json)

super(TestLogFilteringFunctions, cls).tearDownClass()

@classmethod
def get_script_dir(cls):
return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions'

def test_debug_logging(self):
r = self.webhost.request('GET', 'debug_logging')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'OK-debug')

def check_log_debug_logging(self, host_out: typing.List[str]):
self.assertIn('logging info', host_out)
self.assertIn('logging warning', host_out)
self.assertIn('logging error', host_out)
self.assertNotIn('logging debug', host_out)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, this explicitly test the scenario on the host logging control side. Thanks for making the change


def test_debug_with_user_logging(self):
r = self.webhost.request('GET', 'debug_user_logging')
self.assertEqual(r.status_code, 200)
self.assertEqual(r.text, 'OK-user-debug')

def check_log_debug_with_user_logging(self, host_out: typing.List[str]):
self.assertIn('logging info', host_out)
self.assertIn('logging warning', host_out)
self.assertIn('logging error', host_out)
self.assertNotIn('logging debug', host_out)