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

Provide function execution hooks #522

Closed
ejizba opened this issue Jan 28, 2022 · 13 comments · Fixed by #548
Closed

Provide function execution hooks #522

ejizba opened this issue Jan 28, 2022 · 13 comments · Fixed by #548
Assignees
Labels
enhancement P1 v4 model 🚀 Related to the new V4 programming model
Milestone

Comments

@ejizba
Copy link
Contributor

ejizba commented Jan 28, 2022

Sample Usage

This has been updated to reflect the final design

import { registerHook } from '@azure/functions-core';

registerHook('preInvocation', (context: PreInvocationContext) => {
    // can completely change the inputs passed to a user's function
    context.inputs = ['hello'];
    // can write to `context.hookData` and read the value in a postInvocationHook
    context.hookData.value = 1;
    // can write to the _invocation_ context passed to a user's function
    context.invocationContext.dataForInvoc = 2;
});

const disposable = registerHook('postInvocation', (context: PostInvocationContext) => {
    // can read data from `context.hookData` set in the preInvocationHook
    if (context.hookData.value === 1) {
    }
    // can modify the error thrown by the function
    if (context.error) {
        context.error = new Error(`Add a prefix to error text: ${context.error.message}`);
    }
    // can completely change the result of a function
    context.result = 'world';
});

disposable.dispose(); // Can stop the hook from executing in the future

NOTE: If you want to use hooks with esm, you have to import registerHook slightly differently:

import { createRequire } from "module";
const require = createRequire(import.meta.url);
const { registerHook } = require('@azure/functions-core');

// The rest works the same as above
registerHook('preInvocation', async (context) => {

});

Background

Basically, users should be able to register a hook that runs a piece of code at various points in time (i.e. pre invocation and post invocation) at both the function level and app level (meaning you register it once and it runs for all functions).

The main scenario is to simplify users integrating with the Application Insights sdk for Node.js. Users can already do it, but it's a bit complicated (see docs here). If we were to add pre and post invocation hooks, most of that logic could be shifted to the sdk itself instead of the user's code. Some work was done in this PR, but the hooks were never actually exposed or usable.

Also, users could leverage these hooks for any reason (other than app insights) so IMO we need to make sure this is an "official" api. The Python team did this by adding worker extensions which we may or may not want to model after. NOTE: It would be much easier for us to support app-level hooks after the new programming model changes.

@anthonychu
Copy link
Member

I propose adding support for an optional file containing startup code because there's nowhere else to put startup code.

There should be a way to specify the name of the startup file. Options include:

  • host.json - It has a schema that our team owns, we can add a section to it for the Node.js worker
  • package.json - It's common to add custom sections to package.json. For instance:
    "azureFunctions": {
      "startup": "startup.js"
    }

Startup file should support both common.js (startup.js) or ES modules (startup.mjs).

Example of what this file might look like:

module.exports = async function startup(app) {
  app.registerPreinvocationHook(async (context) => {
    // do stuff with context
    // supports both sync and promise-returning functions
  });

  app.registerPostinvocationHook(async (context) => {
    // do stuff with context
    // supports both sync and promise-returning functions
  });

  app.registerStartupHook(async () => {
    // do stuff on worker startup
    // technically not needed, can just put code in this file
    // supports both sync and promise-returning functions
  });

  app.registerShutdownHook(async () => {
    // do stuff on worker shutdown
    // supports both sync and promise-returning functions
  });
}

registerPreinvocationHook and registerPostinvocationHook can include an optional first argument to filter which functions that the hook will be run on.

// single function
app.registerPreinvocationHook("myfunction", (context) => {});

// multiple functions
app.registerPreinvocationHook(["myfunction1", "myfunction2"], (context) => {});

// complex filtering
app.registerPreinvocationHook((context) => {
  // filter logic here
});

For each type of hook, hooks are run in the order in which they were registered.

How this might work for App Insights or any other package that want to register hooks:

const appInsightsHooks = require('app-insights-functions-hooks');
module.exports = async function startup(app) {
  appInsightsHooks.register(app);
};

@phillipharding
Copy link

This sounds like a great feature, in terms of where to register the startup file in host.json or package.json?

Just my 2-pence....

The NodeJs function apps I build are all built using a webpack pipeline resulting in a deployable codebase without a package.json - in part to remove the cold-start latency caused by the functions runtime 'npm installing' package dependencies

For this reason, I'd prefer to see the startup configuration in host.json, also the host.json file describes much about the operating characteristics of a functions app instance, this to me feels the right place for it.

@ejizba
Copy link
Contributor Author

ejizba commented Feb 10, 2022

Preface: From a technical perspective, we need the function app directory to read the host.json or package.json. That is not provided by the host today, but is provided with the new worker indexing changes as a part of #480, which is still behind a feature flag. If I ignore that limitation temporarily, here are my suggestions:

package.json

If we're going to add to package.json (my preference), IMO we should use the "main" field that already exists (docs here) as opposed to a custom field for Azure Functions.

In response to @phillipharding, the functions runtime does not run npm install during cold start. It will run npm install at deployment time, but only if SCM_DO_BUILD_DURING_DEPLOYMENT is set to true in your app settings. In fact, we recommend users include the "package.json" to improve cold start as discussed in #449.

startup.js

I would suggest this file should be more flexible as opposed to just used for startup. First, I would name it "main.js" or "index.js" which I believe are the most common entrypoint names for Node.js apps. Second, I would suggest exporting the "startup" function as part of an object, not as the main export. This allows us to add other functions to the main export in the future (for example "shutdown"):

async function startup(app) {}
async function shutdown(app) {}
module.exports = { startup, shutdown }

Azure Functions API

My last suggestion is to provide an official "module" or "api" from the Node.js worker as opposed to just this "app" object at startup. An issue with the app object is that you have to store it and pass it around if you want to use it in other files and it can implicitly only be used at startup. I would recommend we provide a "built-in" module (similar to how Node.js provides "http" or "path") where it's just there by default without having to install it in your project.

import * as app from "@azure/function-app"; // Name TBD. A few others I considered were "@azure/functions" (matching the existing types package) and "@azure/functions-worker"

app.addHook('preInvocation', async (context: Context) => {
});

Another benefit of this model is I believe it would be easier to do with or without a startup function. Users could add the hook in any one of their function files in the existing programming model, and then just move that to a startup file in the new programming model.

@anthonychu
Copy link
Member

Another point to ensure is considered... because what's proposed so far are separate pre and post invocation hooks, the only way for pre and post hooks to communicate is via adding stuff to the context object. I think there should be clear guidance where in the context object this data should be placed.

@aaronpowell
Copy link

As an overall concept - this is something that I think would be really useful and I can see several use cases which it would fit well for, one of this is the OpenAPI extension that I created.

I agree with @anthonychu on having four life cycle events to hook into, this offers the most flexibility (and having a filter on the preInvocation would be ideal).

From a design point, 100% vote to package.json using main as this avoids introducing a new model and sticking with what is more standard in the Node ecosystem, and having it as an importable object (using the Azure Functions API model from @ejizba) can make it easier to write TypeScript support for and have external packages (such as AppInsights or my OpenAPI one) integrate with.

Here's another aside though - could you use the startupHook to dynamically register a new Function endpoint? I'd find this useful for OpenAPI as presently you have to create your own endpoint which returns the swagger.json but if you could have it dynamically register the endpoint from the package, the developer experience is simpler. Then you have a really powerful middleware pipeline available.

@ejizba
Copy link
Contributor Author

ejizba commented Feb 11, 2022

could you use the startupHook to dynamically register a new Function endpoint?

I think that's out of scope for this issue, but very much should be possible with #480

@aaronpowell
Copy link

could you use the startupHook to dynamically register a new Function endpoint?

I think that's out of scope for this issue, but very much should be possible with #480

Agreed - I'm just thinking through how a design would look and how it can feed into future planning

@phillipharding
Copy link

functions runtime does not run npm install during cold start. It will run npm install at deployment time, but only if SCM_DO_BUILD_DURING_DEPLOYMENT is set to true in your app settings. In fact, we recommend users include the "package.json" to improve cold start as discussed in #449.

@ejizba thanks for the info and correction 🙂

@ejizba
Copy link
Contributor Author

ejizba commented Feb 17, 2022

In the interest of enabling distributed tracing faster, I've split the above proposals into several separate issues. We should be able to make progress on them before the new programming model, so I think that'll still satisfy folks.

In terms of this issue, I think the goals are just:

  • preInvocation and postInvocation hooks
  • code can be put in any file (aka a function file for now, but moved to an app-level file later)

@ChuckJonas
Copy link

since this is closed, does that mean it's possible to use these hooks today? Is there any documentation?

@ejizba
Copy link
Contributor Author

ejizba commented Sep 28, 2022

Yes it is technically possible and we have sample usage at the top of this issue. That being said, we are working on an easier way to use the hooks tracked by Azure/azure-functions-nodejs-library#7 and that is the one we plan to fully document. If you have no urgency, I would suggest waiting until that issue is done

@ChuckJonas
Copy link

@azure/functions-core does not seem to be a valid npm package...

Assuming this would be a path forward for fixing distributed tracing, this is pretty high priority...

Whats ETA on the #7 release?

Also, do you happen to know if modifying the function context.traceContext in one of these functions would impact the binding of AppInsights to context.log?

My use case is to fix the fact that service bus Diagonistic-Id is not set as the trace parent on Service Bus Triggered functions.

registerHook('preInvocation', (context: PreInvocationContext) => {
  const sbTraceParent = context.bindingData.applicationProperties['diagnostic-Id'];
  if (sbTraceParent) {
    context.traceContext.traceparent = sbTraceParent;
  }
});

@ejizba
Copy link
Contributor Author

ejizba commented Sep 30, 2022

You are correct, @azure/functions-core is not an npm package. It is a "built-in" module provided by the Functions runtime and is only available within an Azure context (aka running in Azure or using the func cli locally). This is similar to how Node.js provides built-in "path" or "fs" modules only available in a Node.js context. (As a side note, we plan to publish a @types/azure_functions-core package eventually - tracked by #567).

The point of #7 is to essentially hide all of the confusion about "built-in" modules from users. We have a real npm package @azure/functions which will essentially wrap the hooks api from @azure/functions-core and expose it as its own hooks api. We hope to complete the work in October, but it relies on some changes in Azure that'll likely take another 2ish months to roll out.

I don't have the answers to your app insights questions offhand. I would suggest you file a new issue and we will investigate. Comments on closed issues are somewhat easy for us to lose track of

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement P1 v4 model 🚀 Related to the new V4 programming model
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants