Skip to content

Commit d9551c2

Browse files
committed
Enable dependency logging filtering v2
Introducing a new flag named PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES which customers can specify list of function app modules(like function_app, or blueprint modules) to receive logs from. The feature is only supported in python new programming model. Any modules(like third party libraries logs)'s logs will not be logged and captured.
1 parent 052b416 commit d9551c2

File tree

8 files changed

+260
-8
lines changed

8 files changed

+260
-8
lines changed

azure_functions_worker/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
PYTHON_ISOLATE_WORKER_DEPENDENCIES = "PYTHON_ISOLATE_WORKER_DEPENDENCIES"
2323
PYTHON_ENABLE_WORKER_EXTENSIONS = "PYTHON_ENABLE_WORKER_EXTENSIONS"
2424
PYTHON_ENABLE_DEBUG_LOGGING = "PYTHON_ENABLE_DEBUG_LOGGING"
25+
PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES = \
26+
"PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES"
2527
FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED = \
2628
"FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED"
2729
"""

azure_functions_worker/dispatcher.py

+18-7
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
2525
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
2626
PYTHON_THREADPOOL_THREAD_COUNT_MIN,
27-
PYTHON_ENABLE_DEBUG_LOGGING, SCRIPT_FILE_NAME)
27+
PYTHON_ENABLE_DEBUG_LOGGING, SCRIPT_FILE_NAME,
28+
PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES)
2829
from .extension import ExtensionManager
29-
from .logging import disable_console_logging, enable_console_logging
30+
from .logging import disable_console_logging, enable_console_logging, \
31+
enable_dependency_log_filtering
3032
from .logging import enable_debug_logging_recommendation
3133
from .logging import (logger, error_logger, is_system_log_category,
3234
CONSOLE_LOG_PREFIX, format_exception)
@@ -98,7 +100,8 @@ def get_sync_tp_workers_set(self):
98100
3.9 scenarios (as we'll start passing only None by default), and we
99101
need to get that information.
100102
101-
Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__._max_workers
103+
Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__
104+
._max_workers
102105
"""
103106
return self._sync_call_tp._max_workers
104107

@@ -176,7 +179,8 @@ def stop(self) -> None:
176179

177180
self._stop_sync_call_tp()
178181

179-
def on_logging(self, record: logging.LogRecord, formatted_msg: str) -> None:
182+
def on_logging(self, record: logging.LogRecord,
183+
formatted_msg: str) -> None:
180184
if record.levelno >= logging.CRITICAL:
181185
log_level = protos.RpcLog.Critical
182186
elif record.levelno >= logging.ERROR:
@@ -312,6 +316,12 @@ async def _handle__functions_metadata_request(self, request):
312316
status=protos.StatusResult.Success)))
313317

314318
try:
319+
# Filter function app dependency logs by root logger if
320+
# logging filtering setting is specified
321+
if os.getenv(PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES) is not None:
322+
enable_dependency_log_filtering(
323+
os.environ[PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES])
324+
315325
fx_metadata_results = self.index_functions(function_path)
316326

317327
return protos.StreamingMessage(
@@ -367,7 +377,6 @@ async def _handle__function_load_request(self, request):
367377
function_name,
368378
func_request.metadata.directory
369379
)
370-
371380
logger.info('Successfully processed FunctionLoadRequest, '
372381
'request ID: %s, '
373382
'function ID: %s,'
@@ -436,7 +445,8 @@ async def _handle__invocation_request(self, request):
436445
pytype=pb_type_info.pytype,
437446
shmem_mgr=self._shmem_mgr)
438447

439-
fi_context = self._get_context(invoc_request, fi.name, fi.directory)
448+
fi_context = self._get_context(invoc_request, fi.name,
449+
fi.directory)
440450
if fi.requires_context:
441451
args['context'] = fi_context
442452

@@ -631,7 +641,8 @@ async def _handle__close_shared_memory_resources_request(self, request):
631641
@staticmethod
632642
def _get_context(invoc_request: protos.InvocationRequest, name: str,
633643
directory: str) -> bindings.Context:
634-
""" For more information refer: https://aka.ms/azfunc-invocation-context
644+
""" For more information refer:
645+
https://aka.ms/azfunc-invocation-context
635646
"""
636647
trace_context = bindings.TraceContext(
637648
invoc_request.trace_context.trace_parent,

azure_functions_worker/logging.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
SYSTEM_LOG_PREFIX = "azure_functions_worker"
1313
SDK_LOG_PREFIX = "azure.functions"
1414

15-
1615
logger: logging.Logger = logging.getLogger('azure_functions_worker')
1716
error_logger: logging.Logger = (
1817
logging.getLogger('azure_functions_worker_errors'))
@@ -21,6 +20,14 @@
2120
error_handler: Optional[logging.Handler] = None
2221

2322

23+
def enable_dependency_log_filtering(module_names, *args) -> None:
24+
class DependencyLoggingFilter(logging.Filter):
25+
def filter(self, record):
26+
return record.module in module_names
27+
28+
logging.getLogger().addFilter(DependencyLoggingFilter())
29+
30+
2431
def format_exception(exception: Exception) -> str:
2532
msg = str(exception) + "\n"
2633
if (sys.version_info.major, sys.version_info.minor) < (3, 10):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import logging
2+
import azure.functions as func
3+
import external_lib
4+
5+
bp = func.Blueprint()
6+
7+
8+
@bp.route(route="test_blueprint_logging")
9+
def test_blueprint_logging(req: func.HttpRequest) -> func.HttpResponse:
10+
logging.info('logging from blueprint')
11+
external_lib.ExternalLib()
12+
return func.HttpResponse('ok')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import logging
2+
3+
4+
class ExternalLib:
5+
def __init__(self):
6+
logging.info('logging from external library')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import logging
2+
3+
4+
class ExternalLib:
5+
def __init__(self):
6+
logging.info('logging from external library')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import logging
5+
6+
import azure.functions as func
7+
import external_lib
8+
from blueprint.blueprint import bp
9+
10+
app = func.FunctionApp()
11+
app.register_functions(bp)
12+
13+
14+
@app.route(route="default_template")
15+
def default_template(req: func.HttpRequest) -> func.HttpResponse:
16+
logging.info('Python HTTP trigger function processed a request.')
17+
external_lib.ExternalLib()
18+
19+
return func.HttpResponse('ok')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import typing
4+
import os
5+
from unittest.mock import patch
6+
from tests.utils import testutils
7+
from azure_functions_worker.constants import \
8+
PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES
9+
10+
HOST_JSON_TEMPLATE_WITH_LOGLEVEL_INFO = """\
11+
{
12+
"version": "2.0",
13+
"logging": {
14+
"logLevel": {
15+
"default": "Information"
16+
}
17+
}
18+
}
19+
"""
20+
21+
22+
class TestDependencyLoggingEnabledFunctions(testutils.WebHostTestCase):
23+
"""
24+
Tests for cx dependency logging filtering enabled case.
25+
"""
26+
27+
@classmethod
28+
def setUpClass(cls):
29+
os_environ = os.environ.copy()
30+
# Turn on feature flag
31+
os_environ[
32+
PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES] = 'function_app'
33+
cls._patch_environ = patch.dict('os.environ', os_environ)
34+
cls._patch_environ.start()
35+
super().setUpClass()
36+
37+
@classmethod
38+
def tearDownClass(cls):
39+
super().tearDownClass()
40+
cls._patch_environ.stop()
41+
42+
@classmethod
43+
def get_script_dir(cls):
44+
return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' / \
45+
'dependency_logging_filter'
46+
47+
def test_dependency_logging_filter_enabled(self):
48+
"""
49+
Verify when cx dependency logging filter is enabled, cx function
50+
dependencies logs are not captured.
51+
"""
52+
r = self.webhost.request('GET', 'default_template')
53+
self.assertEqual(r.status_code, 200)
54+
self.assertEqual(r.text, 'ok')
55+
56+
def check_log_dependency_logging_filter_enabled(self,
57+
host_out: typing.List[
58+
str]):
59+
self.assertIn('Python HTTP trigger function processed a request.',
60+
host_out)
61+
self.assertNotIn('logging from external library', host_out)
62+
63+
64+
class TestDependencyLoggingDisabledFunctions(testutils.WebHostTestCase):
65+
"""
66+
Tests for cx dependency logging filtering disabled case.
67+
"""
68+
69+
@classmethod
70+
def setUpClass(cls):
71+
os_environ = os.environ.copy()
72+
# Turn off feature flag
73+
cls._patch_environ = patch.dict('os.environ', os_environ)
74+
cls._patch_environ.start()
75+
super().setUpClass()
76+
77+
@classmethod
78+
def tearDownClass(cls):
79+
super().tearDownClass()
80+
cls._patch_environ.stop()
81+
82+
@classmethod
83+
def get_script_dir(cls):
84+
return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' / \
85+
'dependency_logging_filter'
86+
87+
def test_dependency_logging_filter_disabled(self):
88+
"""
89+
Verify when cx dependency logging filter is disabled, dependencies logs
90+
are captured.
91+
"""
92+
r = self.webhost.request('GET', 'default_template')
93+
self.assertEqual(r.status_code, 200)
94+
self.assertEqual(r.text, 'ok')
95+
96+
def check_log_dependency_logging_filter_disabled(self,
97+
host_out: typing.List[
98+
str]):
99+
self.assertIn('Python HTTP trigger function processed a request.',
100+
host_out)
101+
self.assertIn('logging from external library', host_out)
102+
103+
104+
class TestDependencyLoggingEnabledBluePrintFunctions(
105+
testutils.WebHostTestCase):
106+
"""
107+
Tests for cx dependency logging filtering with blueprint func enabled case.
108+
"""
109+
110+
@classmethod
111+
def setUpClass(cls):
112+
os_environ = os.environ.copy()
113+
# Turn off feature flag
114+
os_environ[
115+
PYTHON_ENABLE_DEPENDENCY_LOG_FILTERING_MODULES] = 'function_app,' \
116+
'blueprint'
117+
cls._patch_environ = patch.dict('os.environ', os_environ)
118+
cls._patch_environ.start()
119+
super().setUpClass()
120+
121+
@classmethod
122+
def tearDownClass(cls):
123+
super().tearDownClass()
124+
cls._patch_environ.stop()
125+
126+
@classmethod
127+
def get_script_dir(cls):
128+
return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' / \
129+
'dependency_logging_filter'
130+
131+
def test_dependency_logging_filter_blueprint_enabled(self):
132+
"""
133+
Verify when cx dependency logging filter with blueprint func is
134+
enabled, dependencies logs
135+
are filtered out.
136+
"""
137+
r = self.webhost.request('GET', 'test_blueprint_logging')
138+
self.assertEqual(r.status_code, 200)
139+
self.assertEqual(r.text, 'ok')
140+
141+
def check_log_dependency_logging_filter_blueprint_enabled(self,
142+
host_out:
143+
typing.List[
144+
str]):
145+
self.assertIn('logging from blueprint',
146+
host_out)
147+
self.assertNotIn('logging from external library', host_out)
148+
149+
150+
class TestDependencyLoggingDisabledBluePrintFunctions(
151+
testutils.WebHostTestCase):
152+
"""
153+
Tests for cx dependency logging filtering with blueprint func disabled
154+
case.
155+
"""
156+
157+
@classmethod
158+
def setUpClass(cls):
159+
os_environ = os.environ.copy()
160+
cls._patch_environ = patch.dict('os.environ', os_environ)
161+
cls._patch_environ.start()
162+
super().setUpClass()
163+
164+
@classmethod
165+
def tearDownClass(cls):
166+
super().tearDownClass()
167+
cls._patch_environ.stop()
168+
169+
@classmethod
170+
def get_script_dir(cls):
171+
return testutils.UNIT_TESTS_FOLDER / 'log_filtering_functions' / \
172+
'dependency_logging_filter'
173+
174+
def test_dependency_logging_filter_disabled_blueprint(self):
175+
"""
176+
Verify when cx dependency logging filter with blueprint functions is
177+
disabled, dependencies logs are captured.
178+
"""
179+
r = self.webhost.request('GET', 'test_blueprint_logging')
180+
self.assertEqual(r.status_code, 200)
181+
self.assertEqual(r.text, 'ok')
182+
183+
def check_log_dependency_logging_filter_disabled_blueprint(self,
184+
host_out:
185+
typing.List[
186+
str]):
187+
self.assertIn('logging from blueprint',
188+
host_out)
189+
self.assertIn('logging from external library', host_out)

0 commit comments

Comments
 (0)