-
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
Conversation
/azp run |
No pipelines are associated with this pull request. |
This is a good idea, but wondering if there should be a discussion in this repo about how this is implemented. (if the issue exists, please link to it, would love to take a look) Specifically interested in examples of how these callbacks are registered, and whether there was any discussions on pros/cons compared to a middleware/pipeline model. We also had asks for this in other languages, so it feels like there might be a broader discussion here to ensure the right scenarios are covered and see if there's a possibility to create a consistent approach. |
@anthonychu @markwolff is implementing this on the Node side and we are essentially doing the same thing. What would you recommend for next steps if we want to get this feature in ASAP? |
@lzchen OK so this is internal to the workers and it's specific to supporting App Insights correlation and is not something that's exposed to the customer? If that's the case, I don't have any concerns. |
Hi @lzchen, thanks for making the PR, overall it looks good. |
@Hazhzeng |
@vrdmr - Well spotted The customer writing the Azure Functions code shouldn't need to import the azure_functions_worker, but the library writing the extension will need to do it. I would expect the appinsight will publish a separate library and wrapping the opencensus (e.g. azure-functions-appinsight) and place the initialization code into it. So that the customer only need to import azure-functions-appinsight. But this still introduces an issue since Linux Consumption SKU prioritizes the customer sys.path over the Azure Functions Worker sys.path. This has a potential to trigger the backward-compatibility issue like #729 since azure-functions-appinsight will also introduce an installation of azure_functions_worker -> grpcio, grpcio-tools -> protobuf. I think to resolve this issue from the root, we do need to fork the grpcio (Apache-2.0), grpcio-tools (Apache-2.0) and protobuf (license) library from google. I consider other alternatives but none of them actually solve the issue.
@lzchen - Sorry, I should've caught this much earlier when planning the change. We need to solve this potential backward-compatibility issue before releasing this change, will reset my review. |
a4a3be3
to
698c524
Compare
Any updates on this? |
Hey @vrdmr, I think we should continue merge this PR since we introduces PYTHON_ISOLATE_WORKER_DEPENDENCIES flag. |
I'm a bit confused by this. From the description and the usage sample, my impression is that this is indeed a feature exposed to the customer. Whether we document/advertise this or not, we are going to expose the I'm not saying we shouldn't be doing this. But we need to be aware of this, and also consider consistency with other languages. Also, are we going to document this? |
An e2e test could be created to light up the scenario of connecting custom dependencies via Opencensus with other services yes. I agree this can be added after this PR. |
Good question. I'm not sure about the functions worker guidelines for not exposing methods to the user (in Azure monitor, we simply make the methods protected and it implies to users that methods are not exposed in our API). Would this be sufficient enough? It is difficult (especially a language like Python) to completely prevent users from using this.
This feature has already been implemented in nodejs, however the methods are marked as private.
If this functionality is not being exposed to customers, do we have to document this? |
# 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 comment
The 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 function.json
would have been a better approach for this? To make the contract clearer for all the languages?
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 azure_functions_worker
would be added to the requirements.txt
and then using the extensions view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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: def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
as well as extracting the context from the headers.
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 (callback
) in the Opencensus code upon initiation of the AzureExporter
. This means that Opencensus will take a dependency on azure-functions-worker, attach a callback function with the hook on initiation and users will only have to manually set the tracer to be the current tracer in their functions app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 register_before_invocation_request
and register_after_invocation_request
? From the description and the sample, my impression is that this is going to be up to the function app code (which makes this a feature available to the customer). If you don't intend to make app authors do that, how is this supposed to happen? Are you going to clone and modify the worker (that would explain everything)? Are we expecting another PR that will actually invoke these from the worker code?
from azure_functions_worker import extensions | ||
|
||
|
||
class TestExtensions(TestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a function test in the test_http_functions
file with this approach, to get a more complete test.
|
||
|
||
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Please make BEFORE_INVOCATION_REQUEST_CALLBACKS
and AFTER_INVOCATION_REQUEST_CALLBACKS
as private variables itself rather than raw strings here.
Also - Any specific reason for the long names here? Would this be better?
BEFORE_INVOCATION_CALLBACKS
AFTER_INVOCATION_CALLBACKS
_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 comment
The 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
"""
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 comment
The 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.
@lzchen Please help me understand this, I might be missing some context. I'm still not clear on how it is not exposed to customers. I thought the sample code in the description demonstrated intended usage. So, after this PR, any Python function app can take advantage of this feature by invoking In the nodejs implementation you are referring to, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change the names of the functions and other variables to - Using pre
and post
instead of before
and after
respectively. Pre- and Post- make it clearer of what they are supposed to do.
get_pre_invocation_request_callbacks
get_post_invocation_request_callbacks
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 comment
The 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 comment
The 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 comment
The 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.
try: | ||
callback(context) | ||
except Exception as ex: | ||
logger.warning( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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 azure_functions_worker
prefix.
import logging
logger = logging.getLogger('SomeExtension')
logger.info('xxx')
@AnatoliB The intention was to simply light up our Azure monitor scenario and make things simpler for functions customers to obtain connected traces without having to manually add too much code. It is not really within the scope of this PR to try to add new functionality. Speaking to @Hazhzeng it was just a nice addition to have to allow users to be able to do this and I believed it to be a simple addition. If you think that exposing this to customers adds too much functionality, and if the customers are okay with manually instrumenting their code in their functions app then that could be brought into the discussion. |
@lzchen I like the feature, I believe it is useful, and the implementation looks good to me. But I keep reading two things that look mutually-exclusive to me:
I think we need to choose one, it cannot be both :-) |
@AnatoliB |
@anthonychu Since we agreed that this is actually a customer-facing feature, do you want to review it from this perspective? UPDATE: We've discussed this offline, and I realized that the Node.js implementation is different. As opposed to this PR, it does not expose a customer-facing feature, so keeping callbacks consistent is pointless, and the question below is irrelevant.
type InvocationRequestBefore = (context: Context, userFn: Function) => Function;
type InvocationRequestAfter = (context: Context) => void;
|
Closing in favor of #815 |
Description
Fixes #
From talking to @Hazhzeng , we want to be able to execute custom logic in a customer's function, especially when it comes to reading/modifying the context before the request is made. This PR adds hooks for before and after the handling of the invocation request.
An example using OpenCensus AzureExporter which exports telemetry to App Insights. This is in a normal HTTPTrigger function.
In the above code, the context is passed through these hooks and made available to the callback functions. Within the callbacks, OpenCensus SDK treats the context as the parent context, creates a new tracer with this context as the parent, and sets this tracer to be the one that is used currently (
execution_context.set_opencensus_tracer(tracer)
). Then any custom dependencies that will be tracked using Opencensus SDK (likeresponse = requests.get(url='http://example.com')
, will be connected with the current context within the functions environment.PR information
Quality of Code and Contribution Guidelines