-
Notifications
You must be signed in to change notification settings - Fork 107
feature: Add invocation request hooks #757
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
Changes from all commits
87aab9a
4dc4d69
b74bca0
cef0f76
7d468e5
c21d866
ccc8299
c4a6a56
d49405c
9e4761f
b96e614
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,8 @@ | |
PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT, | ||
PYTHON_THREADPOOL_THREAD_COUNT_MAX, | ||
PYTHON_THREADPOOL_THREAD_COUNT_MIN) | ||
from .extensions import (get_before_invocation_request_callbacks, | ||
get_after_invocation_request_callbacks) | ||
from .logging import disable_console_logging, enable_console_logging | ||
from .logging import error_logger, is_system_log_category, logger | ||
from .utils.common import get_app_setting | ||
|
@@ -359,9 +361,19 @@ async def _handle__invocation_request(self, req): | |
trigger_metadata=trigger_metadata, | ||
pytype=pb_type_info.pytype) | ||
|
||
context = bindings.Context( | ||
fi.name, fi.directory, invocation_id, trace_context) | ||
|
||
# Execute before invocation callbacks | ||
for callback in get_before_invocation_request_callbacks(): | ||
try: | ||
callback(context) | ||
except Exception as ex: | ||
logger.warning( | ||
"Before invocation callback failed with: %s.", ex) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Specifically - is this a USER log or System log? I think its system and won't be shown to the user. @Hazhzeng could you please confirm. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logger is a system log. Yes, it won't be shown to user. |
||
|
||
if fi.requires_context: | ||
args['context'] = bindings.Context( | ||
fi.name, fi.directory, invocation_id, trace_context) | ||
args['context'] = context | ||
|
||
if fi.output_types: | ||
for name in fi.output_types: | ||
|
@@ -402,6 +414,14 @@ async def _handle__invocation_request(self, req): | |
fi.return_type.binding_name, call_result, | ||
pytype=fi.return_type.pytype) | ||
|
||
# Execute after invocation callbacks | ||
for callback in get_after_invocation_request_callbacks(): | ||
try: | ||
callback(context) | ||
except Exception as ex: | ||
logger.warning( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah same, this is system log. For extension to expose the log to users, they need to define a new logger without the import logging
logger = logging.getLogger('SomeExtension')
logger.info('xxx') |
||
"After invocation callback failed with: %s.", ex) | ||
|
||
# Actively flush customer print() function to console | ||
sys.stdout.flush() | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. | ||
|
||
_EXTENSIONS_CONTEXT = dict() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Hazhzeng @lzchen - I don't know the internal details or discussions of the feature, but wouldn't a My issue (not strong though) is still with this design choice - to import this - as this isn't the convention and this would start new precedence. from azure_functions_worker.extensions import register_before_invocation_request, register_after_invocation_request Can you explain an example app - I am assuming There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the scenario in which we are trying to light up is that incoming requests to the user's function won't be correlated with dependency calls made within the user's function when collecting telemetry via Opencensus. Currently, our workaround is to tell users to connect the telemetry manually by setting the used tracer (Opencensus uses a tracer object to enable distributed tracing), into the passed in context that is received in the function header: In order to not have to have the user call this manually in their functions app, we have discussed implementing the ability to attach function hooks in the functions worker, which can run code before/after running a user's function. With this, we can run the "connecting traces" logic right before the user's function is run as shown in the example in the PR description. The example above simply shows how this can be done in code. It is not what we expect the customers to do because we will be planning on adding this "hook" logic ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lzchen After this PR is merged, what and how is going to invoke |
||
|
||
|
||
def register_before_invocation_request(callback): | ||
if _EXTENSIONS_CONTEXT.get("BEFORE_INVOCATION_REQUEST_CALLBACKS"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please make Also - Any specific reason for the long names here? Would this be better?
|
||
_EXTENSIONS_CONTEXT.get( | ||
"BEFORE_INVOCATION_REQUEST_CALLBACKS").append(callback) | ||
else: | ||
_EXTENSIONS_CONTEXT["BEFORE_INVOCATION_REQUEST_CALLBACKS"] = [callback] | ||
|
||
|
||
def register_after_invocation_request(callback): | ||
if _EXTENSIONS_CONTEXT.get("AFTER_INVOCATION_REQUEST_CALLBACKS"): | ||
_EXTENSIONS_CONTEXT.get( | ||
"AFTER_INVOCATION_REQUEST_CALLBACKS").append(callback) | ||
else: | ||
_EXTENSIONS_CONTEXT["AFTER_INVOCATION_REQUEST_CALLBACKS"] = [callback] | ||
|
||
|
||
def clear_before_invocation_request_callbacks(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For all the functions in here, please add py-doc strings. def function():
"""Doc here
""" |
||
_EXTENSIONS_CONTEXT.pop("BEFORE_INVOCATION_REQUEST_CALLBACKS", None) | ||
|
||
|
||
def clear_after_invocation_request_callbacks(): | ||
_EXTENSIONS_CONTEXT.pop("AFTER_INVOCATION_REQUEST_CALLBACKS", None) | ||
|
||
|
||
def get_before_invocation_request_callbacks(): | ||
return _EXTENSIONS_CONTEXT.get("BEFORE_INVOCATION_REQUEST_CALLBACKS", []) | ||
|
||
|
||
def get_after_invocation_request_callbacks(): | ||
return _EXTENSIONS_CONTEXT.get("AFTER_INVOCATION_REQUEST_CALLBACKS", []) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. | ||
|
||
from unittest import TestCase, mock | ||
|
||
from azure_functions_worker import extensions | ||
|
||
|
||
class TestExtensions(TestCase): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a function test in the |
||
|
||
def tearDown(self): | ||
extensions._EXTENSIONS_CONTEXT.clear() | ||
|
||
def test_register_before_invocation_request(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above for PyDocs. Please add a single line for each test to describe what the test is doing. |
||
mock_cb = mock.Mock() | ||
mock_cb2 = mock.Mock() | ||
extensions.register_before_invocation_request(mock_cb) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["BEFORE_INVOCATION_REQUEST_CALLBACKS"][0], | ||
mock_cb, | ||
) | ||
extensions.register_before_invocation_request(mock_cb2) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["BEFORE_INVOCATION_REQUEST_CALLBACKS"][1], | ||
mock_cb2, | ||
) | ||
|
||
def test_register_after_invocation_request(self): | ||
mock_cb = mock.Mock() | ||
mock_cb2 = mock.Mock() | ||
extensions.register_after_invocation_request(mock_cb) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["AFTER_INVOCATION_REQUEST_CALLBACKS"][0], | ||
mock_cb, | ||
) | ||
extensions.register_after_invocation_request(mock_cb2) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["AFTER_INVOCATION_REQUEST_CALLBACKS"][1], | ||
mock_cb2, | ||
) | ||
|
||
def test_clear_before_invocation_request_callbacks(self): | ||
mock_cb = mock.Mock() | ||
extensions.register_before_invocation_request(mock_cb) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["BEFORE_INVOCATION_REQUEST_CALLBACKS"][0], | ||
mock_cb, | ||
) | ||
extensions.clear_before_invocation_request_callbacks() | ||
self.assertIsNone( | ||
extensions._EXTENSIONS_CONTEXT. | ||
get("BEFORE_INVOCATION_REQUEST_CALLBACKS"), | ||
) | ||
|
||
def test_clear_after_invocation_request_callbacks(self): | ||
mock_cb = mock.Mock() | ||
extensions.register_after_invocation_request(mock_cb) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["AFTER_INVOCATION_REQUEST_CALLBACKS"][0], | ||
mock_cb, | ||
) | ||
extensions.clear_after_invocation_request_callbacks() | ||
self.assertIsNone( | ||
extensions._EXTENSIONS_CONTEXT. | ||
get("AFTER_INVOCATION_REQUEST_CALLBACKS"), | ||
) | ||
|
||
def test_get_before_invocation_request_callbacks(self): | ||
mock_cb = mock.Mock() | ||
extensions.register_before_invocation_request(mock_cb) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["BEFORE_INVOCATION_REQUEST_CALLBACKS"][0], | ||
mock_cb, | ||
) | ||
self.assertEqual( | ||
extensions.get_before_invocation_request_callbacks()[0], | ||
mock_cb | ||
) | ||
|
||
def test_get_after_invocation_request_callbacks(self): | ||
mock_cb = mock.Mock() | ||
extensions.register_after_invocation_request(mock_cb) | ||
self.assertEqual( | ||
extensions._EXTENSIONS_CONTEXT | ||
["AFTER_INVOCATION_REQUEST_CALLBACKS"][0], | ||
mock_cb, | ||
) | ||
self.assertEqual( | ||
extensions.get_after_invocation_request_callbacks()[0], | ||
mock_cb | ||
) |
Uh oh!
There was an error while loading. Please reload this page.