From 01d2d8d4fb08e717733dc14556be17b1c79b67c5 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 16 Jan 2023 14:39:31 -0800 Subject: [PATCH] feat: Azure Functions (#3071) Support for tracing/monitoring Azure Functions. Supported triggers/bindings: HTTP (spec'd), Timer (not spec'd). Spec: https://github.com/elastic/apm/pull/716 Closes: #3015 Co-authored-by: Brandon Morelli --- .eslintrc.json | 1 + CHANGELOG.asciidoc | 19 + NOTICE.md | 34 + docs/azure-functions.asciidoc | 155 +++ docs/images/azure-functions-configuration.png | Bin 0 -> 106252 bytes docs/set-up.asciidoc | 4 +- docs/supported-technologies.asciidoc | 1 + examples/an-azure-function-app/.gitignore | 49 + examples/an-azure-function-app/.npmrc | 1 + .../an-azure-function-app/Bye/function.json | 19 + examples/an-azure-function-app/Bye/index.js | 11 + .../an-azure-function-app/Hi/function.json | 19 + examples/an-azure-function-app/Hi/index.js | 28 + examples/an-azure-function-app/README.md | 59 ++ examples/an-azure-function-app/host.json | 15 + examples/an-azure-function-app/initapm.js | 3 + .../an-azure-function-app/local.settings.json | 11 + examples/an-azure-function-app/package.json | 13 + lib/config.js | 61 +- lib/instrumentation/azure-functions.js | 450 +++++++++ lib/instrumentation/index.js | 13 +- lib/ritm.js | 224 +++++ package-lock.json | 951 +++++++++++++++++- package.json | 11 +- test/_utils.js | 23 + .../azure-functions/azure-functions.test.js | 524 ++++++++++ .../fixtures/AJsAzureFnApp/.gitignore | 50 + .../AJsAzureFnApp/HttpFn1/function.json | 19 + .../fixtures/AJsAzureFnApp/HttpFn1/index.js | 14 + .../HttpFnBindingsRes/function.json | 19 + .../AJsAzureFnApp/HttpFnBindingsRes/index.js | 13 + .../HttpFnContextDone/function.json | 19 + .../AJsAzureFnApp/HttpFnContextDone/index.js | 18 + .../HttpFnDistTraceA/function.json | 19 + .../AJsAzureFnApp/HttpFnDistTraceA/index.js | 49 + .../HttpFnDistTraceB/function.json | 19 + .../AJsAzureFnApp/HttpFnDistTraceB/index.js | 12 + .../AJsAzureFnApp/HttpFnError/function.json | 19 + .../AJsAzureFnApp/HttpFnError/index.js | 9 + .../HttpFnReturnContext/function.json | 19 + .../HttpFnReturnContext/index.js | 18 + .../HttpFnReturnObject/function.json | 19 + .../AJsAzureFnApp/HttpFnReturnObject/index.js | 12 + .../HttpFnReturnResponseData/function.json | 19 + .../HttpFnReturnResponseData/index.js | 13 + .../HttpFnReturnString/function.json | 19 + .../AJsAzureFnApp/HttpFnReturnString/index.js | 12 + .../HttpFnRouteTemplate/function.json | 20 + .../HttpFnRouteTemplate/index.js | 12 + .../fixtures/AJsAzureFnApp/README.md | 12 + .../fixtures/AJsAzureFnApp/host.json | 15 + .../fixtures/AJsAzureFnApp/initapm.js | 10 + .../AJsAzureFnApp/local.settings.json | 16 + .../fixtures/AJsAzureFnApp/package-lock.json | 12 + .../fixtures/AJsAzureFnApp/package.json | 12 + .../instrumentation/modules/next/next.test.js | 25 +- 56 files changed, 3156 insertions(+), 87 deletions(-) create mode 100644 docs/azure-functions.asciidoc create mode 100644 docs/images/azure-functions-configuration.png create mode 100644 examples/an-azure-function-app/.gitignore create mode 100644 examples/an-azure-function-app/.npmrc create mode 100644 examples/an-azure-function-app/Bye/function.json create mode 100644 examples/an-azure-function-app/Bye/index.js create mode 100644 examples/an-azure-function-app/Hi/function.json create mode 100644 examples/an-azure-function-app/Hi/index.js create mode 100644 examples/an-azure-function-app/README.md create mode 100644 examples/an-azure-function-app/host.json create mode 100644 examples/an-azure-function-app/initapm.js create mode 100644 examples/an-azure-function-app/local.settings.json create mode 100644 examples/an-azure-function-app/package.json create mode 100644 lib/instrumentation/azure-functions.js create mode 100644 lib/ritm.js create mode 100644 test/instrumentation/azure-functions/azure-functions.test.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/.gitignore create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/function.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/index.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/README.md create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/host.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/initapm.js create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/local.settings.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package-lock.json create mode 100644 test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package.json diff --git a/.eslintrc.json b/.eslintrc.json index a4a2847bb4..c38fc5a95d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -18,6 +18,7 @@ "/examples/esbuild/dist", "/examples/typescript/dist", "/examples/nextjs", + "/examples/an-azure-function-app", "/lib/opentelemetry-bridge/opentelemetry-core-mini", "/test/babel/out.js", "/test/lambda/fixtures/esbuild-bundled-handler/hello.js", diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2aa4d7a5b0..7734b95fd5 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,25 @@ Notes: === Node.js Agent version 3.x +==== Unreleased + +[float] +===== Breaking changes + +[float] +===== Features + +* Support for tracing/monitoring https://learn.microsoft.com/en-us/azure/azure-functions/[Azure Functions]. + See the <> document. + ({pull}3071[#3071], https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md[spec]) + +[float] +===== Bug fixes + +[float] +===== Chores + + [[release-notes-3.41.1]] ==== 3.41.1 2022/12/21 diff --git a/NOTICE.md b/NOTICE.md index c16910c523..a9a0dd1766 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -424,3 +424,37 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` + + +## require-in-the-middle + +- **path:** [lib/ritm.js](lib/ritm.js) +- **author:** Thomas Watson Steen +- **project url:** https://github.com/elastic/require-in-the-middle +- **original file:** https://github.com/elastic/require-in-the-middle/blob/v5.2.0/index.js +- **license:** MIT License (MIT), http://opensource.org/licenses/MIT + +``` +The MIT License (MIT) + +Copyright (c) 2016-2019 Thomas Watson Steen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + diff --git a/docs/azure-functions.asciidoc b/docs/azure-functions.asciidoc new file mode 100644 index 0000000000..08aa0884e2 --- /dev/null +++ b/docs/azure-functions.asciidoc @@ -0,0 +1,155 @@ +:framework: Azure Functions + +[[azure-functions]] + +ifdef::env-github[] +NOTE: For the best reading experience, +please view this documentation at https://www.elastic.co/guide/en/apm/agent/nodejs/current/azure-functions.html[elastic.co] +endif::[] + +=== Monitoring Node.js Azure Functions + +The Node.js APM Agent can trace function invocations in an https://learn.microsoft.com/en-us/azure/azure-functions/[Azure Functions] app. + + +[float] +[[azure-functions-prerequisites]] +==== Prerequisites + +You need an APM Server to send APM data to. Follow the +{apm-guide-ref}/apm-quick-start.html[APM Quick start] if you have not set one up +yet. You will need your *APM server URL* and an APM server *secret token* (or +*API key*) for configuring the APM agent below. + +You will also need an Azure Function app to monitor. If you do not have an +existing one, you can follow https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-node#create-supporting-azure-resources-for-your-function[this Azure guide] +to create one. + +[IMPORTANT] +==== +If you use `func init --javascript ...` as suggested in this Azure guide, +then it is recommended that you *uninstall* the `azure-functions-core-tools` +dependency by running `npm uninstall azure-functions-core-tools` and +https://github.com/Azure/azure-functions-core-tools#installing[install it separately]. +Having `azure-functions-core-tools` as a "devDependency" in your package.json +will result in unreasonably large deployments that will be very slow to publish +and will run your Azure Function app VM out of disk space. +==== + +You can also take a look at and use this https://github.com/elastic/apm-agent-nodejs/tree/main/examples/an-azure-function-app/[Azure Functions example app with Elastic APM already integrated]. + +[float] +[[azure-functions-setup]] +==== Step 1: Add the APM agent dependency + +Add the `elastic-apm-node` module as a dependency of your application: + +[source,bash] +---- +npm install elastic-apm-node --save # or 'yarn add elastic-apm-node' +---- + + +[float] +==== Step 2: Start the APM agent + +For the APM agent to instrument Azure Functions, it needs to be started when the +Azure host starts its Node.js worker processes. The best way to do so is by +using an app-level entry point (support for this was added for Node.js Azure +Functions https://github.com/Azure/azure-functions-nodejs-worker/issues/537[here]). + +1. Create a module to start the APM agent. For example, a file at the root of your repository named "initapm.js": ++ +[source,javascript] +---- +// initapm.js +require('elastic-apm-node').start({ + <1> +}) +---- +<1> Optional <> can be added here. + +2. Add a "main" entry to your package.json pointing to the app init file. ++ +[source,json] +---- +... + "main": "initapm.js", +... +---- ++ +If your application already has a "main" init file, you can instead add the +`require('elastic-apm-node').start()` to top of that file. + + +[float] +==== Step 3: Configure the APM agent + +The APM agent can be <> with options to the +`.start()` method or with environment variables. Using environment variables +allows one to use https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-use-azure-function-app-settings?tabs=portal#settings[application settings in the Azure Portal] which allows hiding values and updating settings +without needing to re-deploy code. + +Open _Configuration > Application settings_ for your Function App in the Azure Portal +and set: + +[source,yaml] +---- +ELASTIC_APM_SERVER_URL: +ELASTIC_APM_SECRET_TOKEN: +---- + +For example: + +image::./images/azure-functions-configuration.png[Configuring the APM Agent in the Azure Portal] + +For local testing via `func start` you can set these environment variables in +your terminal, or in the "local.settings.json" file. See the +<> for full details on supported +configuration variables. + + +[float] +==== Step 4: (Re-)deploy your Azure Function app + +[source,bash] +---- +func azure functionapp publish +---- + +Now, when you invoke your Azure Functions, you should see your application +show up as a Service in the APM app in Kibana and see APM transactions for +function invocations. Tracing data is forwarded to APM server after a period +of time, so allow a minute or so for data to appear. + + +[float] +[[azure-functions-limitations]] +==== Limitations + +This instrumentation does not send an APM transaction or error to APM server when +a handler has an `uncaughtException` or `unhandledRejection`. +The Azure Functions Node.js reference https://learn.microsoft.com/en-ca/azure/azure-functions/functions-reference-node#use-async-and-await[has a section] with best practices for avoiding these cases. + +Azure Functions instrumentation currently does _not_ collect system metrics in +the background because of a concern with unintentionally increasing Azure +Functions costs (for Consumption plans). + + +[float] +[[azure-functions-filter-sensitive-information]] +==== Filter sensitive information + +include::./shared-set-up.asciidoc[tag=filter-sensitive-info] + +[float] +[[azure-functions-compatibility]] +==== Compatibility + +include::./shared-set-up.asciidoc[tag=compatibility-link] + +[float] +[[azure-functions-troubleshooting]] +==== Troubleshooting + +include::./shared-set-up.asciidoc[tag=troubleshooting-link] diff --git a/docs/images/azure-functions-configuration.png b/docs/images/azure-functions-configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..68a3ee59cbcb439bae3db34ffc0c3513757bb113 GIT binary patch literal 106252 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 unw6WInCv5gYOy0c*qI9JWEo> +* <> * <> * <> * <> @@ -14,7 +15,6 @@ To get you off the ground, we've prepared guides for setting up the Agent with a * <> * <> * <> -* <> // end::web-frameworks-list[] Alternatively, you can <>. @@ -30,6 +30,8 @@ Other useful documentation includes: include::./lambda.asciidoc[] +include::./azure-functions.asciidoc[] + include::./express.asciidoc[] include::./fastify.asciidoc[] diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 26c846f508..954f9a00c2 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -88,6 +88,7 @@ These are the frameworks that we officially support: |======================================================================= | Framework | Version | Note | <> | N/A | +| <> | ~4 | See https://learn.microsoft.com/en-ca/azure/azure-functions/set-runtime-version[the guide on Azure Functions runtime versions]. | <> | ^4.0.0 | | <> | >=1.0.0 | See also https://www.fastify.io/docs/latest/Reference/LTS/[Fastify's own LTS documentation] | <> | >=17.9.0 <22.0.0 | diff --git a/examples/an-azure-function-app/.gitignore b/examples/an-azure-function-app/.gitignore new file mode 100644 index 0000000000..34ef47a99a --- /dev/null +++ b/examples/an-azure-function-app/.gitignore @@ -0,0 +1,49 @@ +.vscode/ + +bin +obj +csx +.vs +edge +Publish + +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json + +node_modules +dist + +# Local python packages +.python_packages/ + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Azurite artifacts +__blobstorage__ +__queuestorage__ +__azurite_db*__.json diff --git a/examples/an-azure-function-app/.npmrc b/examples/an-azure-function-app/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/examples/an-azure-function-app/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/examples/an-azure-function-app/Bye/function.json b/examples/an-azure-function-app/Bye/function.json new file mode 100644 index 0000000000..91052aaf8a --- /dev/null +++ b/examples/an-azure-function-app/Bye/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/examples/an-azure-function-app/Bye/index.js b/examples/an-azure-function-app/Bye/index.js new file mode 100644 index 0000000000..f0d0364ce0 --- /dev/null +++ b/examples/an-azure-function-app/Bye/index.js @@ -0,0 +1,11 @@ +module.exports = async function (context, _req) { + const body = JSON.stringify({ good: 'bye' }) + context.res = { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + }, + body + } +} diff --git a/examples/an-azure-function-app/Hi/function.json b/examples/an-azure-function-app/Hi/function.json new file mode 100644 index 0000000000..91052aaf8a --- /dev/null +++ b/examples/an-azure-function-app/Hi/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/examples/an-azure-function-app/Hi/index.js b/examples/an-azure-function-app/Hi/index.js new file mode 100644 index 0000000000..2a50aa6ac0 --- /dev/null +++ b/examples/an-azure-function-app/Hi/index.js @@ -0,0 +1,28 @@ +const http = require('http') +const https = require('https') + +module.exports = async function (context, req) { + return new Promise((resolve, reject) => { + // Call the 'Bye' Function in this same Function App... + const url = new URL(req.url) + url.pathname = '/api/Bye' + const proto = (url.protocol === 'https:' ? https : http) + proto.get(url, res => { + res.resume() + res.on('error', reject) + res.on('end', () => { + // ... then respond. + const body = JSON.stringify({ hi: 'there' }) + context.res = { + status: 200, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body) + }, + body + } + resolve() + }) + }) + }) +} diff --git a/examples/an-azure-function-app/README.md b/examples/an-azure-function-app/README.md new file mode 100644 index 0000000000..42bb872bc4 --- /dev/null +++ b/examples/an-azure-function-app/README.md @@ -0,0 +1,59 @@ +This directory holds a very small Azure Function App implemented in Node.js +and setup to be traced by the Elastic APM agent. The App has two "functions": + +1. `Hi` - an HTTP-triggered function that will call the `Bye` function, then + respond with `{"hi":"there"}`. +2. `Bye` - an HTTP-triggered function that will respond with `{"good":"bye"}`. + + +# Testing locally + +1. Have an APM server to send tracing data to. If you don't have one, + [start here](https://www.elastic.co/guide/en/apm/guide/current/apm-quick-start.html). + +2. Install the [Azure Functions Core Tools](https://github.com/Azure/azure-functions-core-tools), + which provide a `func` CLI tool for running Azure Functions locally for + development, and for publishing an Function App to Azure. One way to + install is via: + + npm install -g azure-functions-core-tools@4 + + It is recommended that you **not** install it in the local `./node_modules` + folder, because its large install size will get in the way of publishing to + Azure. + +3. Set environment variable to configure the APM agent, for example: + + ``` + export ELASTIC_APM_SERVER_URL=https://... + export ELASTIC_APM_SECRET_TOKEN=... + ``` + +4. `npm start` + +5. In a separate terminal, call the Azure Function via: + + ``` + curl -i http://localhost:7071/api/Hello + ``` + + +# Testing on Azure + +1. To run this Azure Function App on Azure itself you will need to have an Azure + account and create some supporting resources. + See [this Azure guide](https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-node#create-supporting-azure-resources-for-your-function). + +2. Deploy the function app via `func azure functionapp publish `. + +3. Configure the `ELASTIC_APM_SERVER_URL` and `ELASTIC_APM_SECRET_TOKEN` environment + variables in the "Configuration" settings page of the Azure Portal. + +4. Call your functions: + + ``` + curl -i https://.azurewebsites.net/api/hi + ``` + +The result (after a minute for data to propagate) should be a `` service +in the Kibana APM app with traces of all function invocations. diff --git a/examples/an-azure-function-app/host.json b/examples/an-azure-function-app/host.json new file mode 100644 index 0000000000..fd4bee790b --- /dev/null +++ b/examples/an-azure-function-app/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} \ No newline at end of file diff --git a/examples/an-azure-function-app/initapm.js b/examples/an-azure-function-app/initapm.js new file mode 100644 index 0000000000..482b0b1be4 --- /dev/null +++ b/examples/an-azure-function-app/initapm.js @@ -0,0 +1,3 @@ +require('elastic-apm-node').start({ + // ... +}) diff --git a/examples/an-azure-function-app/local.settings.json b/examples/an-azure-function-app/local.settings.json new file mode 100644 index 0000000000..9dcd935ef0 --- /dev/null +++ b/examples/an-azure-function-app/local.settings.json @@ -0,0 +1,11 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsStorage": "", + + "REGION_NAME": "test-region-name", + "WEBSITE_SITE_NAME": "an-azure-function-app", + "WEBSITE_INSTANCE_ID": "test-website-instance-id" + } +} diff --git a/examples/an-azure-function-app/package.json b/examples/an-azure-function-app/package.json new file mode 100644 index 0000000000..275269bd46 --- /dev/null +++ b/examples/an-azure-function-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "an-azure-function-app", + "version": "1.0.0", + "description": "An example Azure Function app showing Elastic APM integration for tracing/monitoring", + "private": true, + "main": "initapm.js", + "scripts": { + "start": "func start" + }, + "dependencies": { + "elastic-apm-node": "^3.42.0" + } +} diff --git a/lib/config.js b/lib/config.js index d242323021..c116cfb940 100644 --- a/lib/config.js +++ b/lib/config.js @@ -20,6 +20,7 @@ const { WildcardMatcher } = require('./wildcard-matcher') const { CloudMetadata } = require('./cloud-metadata') const { NoopTransport } = require('./noop-transport') const { isLambdaExecutionEnvironment } = require('./lambda') +const { isAzureFunctionsEnvironment, getAzureFunctionsExtraMetadata } = require('./instrumentation/azure-functions') let confFile = loadConfigFile() @@ -410,24 +411,41 @@ class Config { this.logger.error('serviceName "%s" is invalid: %s', this.serviceName, err.message) this.serviceName = null } - } else if (isLambda) { - this.serviceName = process.env.AWS_LAMBDA_FUNCTION_NAME } else { - // Zero-conf support: use package.json#name, else - // `unknown-${service.agent.name}-service`. - try { - this.serviceName = serviceNameFromPackageJson() - } catch (err) { - this.logger.warn(err.message) + if (isLambda) { + this.serviceName = process.env.AWS_LAMBDA_FUNCTION_NAME + } else if (isAzureFunctionsEnvironment && process.env.WEBSITE_SITE_NAME) { + this.serviceName = process.env.WEBSITE_SITE_NAME + } + if (this.serviceName) { + try { + validateServiceName(this.serviceName) + } catch (err) { + this.logger.warn('"%s" is not a valid serviceName: %s', this.serviceName, err.message) + this.serviceName = null + } } if (!this.serviceName) { - this.serviceName = 'unknown-nodejs-service' + // Zero-conf support: use package.json#name, else + // `unknown-${service.agent.name}-service`. + try { + this.serviceName = serviceNameFromPackageJson() + } catch (err) { + this.logger.warn(err.message) + } + if (!this.serviceName) { + this.serviceName = 'unknown-nodejs-service' + } } } if (this.serviceVersion) { // pass } else if (isLambda) { this.serviceVersion = process.env.AWS_LAMBDA_FUNCTION_VERSION + } else if (isAzureFunctionsEnvironment && process.env.WEBSITE_SITE_NAME) { + // Leave this empty. There isn't a meaningful service version field + // in Azure Functions envvars, and falling back to package.json ends up + // finding the version of the "azure-functions-core-tools" package. } else { // Zero-conf support: use package.json#version, if possible. try { @@ -439,8 +457,8 @@ class Config { normalize(this, this.logger) - if (isLambda) { - // Override some config in AWS Lambda environment. + if (isLambda || isAzureFunctionsEnvironment) { + // Override some config in AWS Lambda or Azure Functions environments. this.metricsInterval = 0 this.cloudProvider = 'none' this.centralConfig = false @@ -611,7 +629,7 @@ function findPkgInfo () { startDir = path.dirname(process.argv[1]) } if (!startDir) { - process.cwd() + startDir = process.cwd() } pkgInfoCache = { startDir, @@ -1238,12 +1256,12 @@ function getBaseClientConfig (conf, agent) { // https://www.elastic.co/guide/en/ecs/current/ecs-event.html#field-event-module clientLogger = agent.logger.child({ 'event.module': 'apmclient' }) } + const isLambda = isLambdaExecutionEnvironment() const clientConfig = { agentName: 'nodejs', agentVersion: version, serviceName: conf.serviceName, - serviceNodeName: conf.serviceNodeName, serviceVersion: conf.serviceVersion, frameworkName: conf.frameworkName, frameworkVersion: conf.frameworkVersion, @@ -1286,12 +1304,25 @@ function getBaseClientConfig (conf, agent) { kubernetesPodUID: conf.kubernetesPodUID } - // Metadata handling. - if (isLambdaExecutionEnvironment()) { + // `service_node_name` is ignored in Lambda and Azure Functions envs. + if (conf.serviceNodeName) { + if (isLambda) { + agent.logger.warn({ serviceNodeName: conf.serviceNodeName }, 'ignoring "serviceNodeName" config setting in Lambda environment') + } else if (isAzureFunctionsEnvironment) { + agent.logger.warn({ serviceNodeName: conf.serviceNodeName }, 'ignoring "serviceNodeName" config setting in Azure Functions environment') + } else { + clientConfig.serviceNodeName = conf.serviceNodeName + } + } + + // Extra metadata handling. + if (isLambda) { // Tell the Client to wait for a subsequent `.setExtraMetadata()` call // before allowing intake requests. This will be called by `apm.lambda()` // on first Lambda function invocation. clientConfig.expectExtraMetadata = true + } else if (isAzureFunctionsEnvironment) { + clientConfig.extraMetadata = getAzureFunctionsExtraMetadata() } else if (conf.cloudProvider !== 'none') { clientConfig.cloudMetadataFetcher = new CloudMetadata(conf.cloudProvider, conf.logger, conf.serviceName) } diff --git a/lib/instrumentation/azure-functions.js b/lib/instrumentation/azure-functions.js new file mode 100644 index 0000000000..e7f2a9b26b --- /dev/null +++ b/lib/instrumentation/azure-functions.js @@ -0,0 +1,450 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +'use strict' + +// Instrumentation of Azure Functions. +// Spec: https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md +// +// This instrumentation is started if the `FUNCTIONS_WORKER_RUNTIME` envvar +// indicates we are in an Azure Functions environment. This is different from +// most instrumentations that hook into user code `require()`ing a particular +// module. +// +// The azure-functions-nodejs-worker repo holds the "nodejsWorker.js" process +// code in which user Functions are executed. That repo monkey-patches +// `Module.prototype.require` to inject a virtual `@azure/functions-core` +// module which exposes a hooks mechanism for invocation start and end. See +// https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/setupCoreModule.ts#L20-L54 +// and `registerHook` usage below. + +const fs = require('fs') +const path = require('path') + +const constants = require('../constants') + +let isInstrumented = false +let hookDisposables = [] // This holds the `Disposable` objects with which to remove previously registered @azure/functions-core hooks. + +// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md#deriving-cold-starts +let isFirstRun = true + +// The trigger types for which we support special handling. +const TRIGGER_OTHER = 1 // +const TRIGGER_HTTP = 2 // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook +const TRIGGER_TIMER = 3 // https://learn.microsoft.com/en-ca/azure/azure-functions/functions-bindings-timer + +const TRANS_TYPE_FROM_TRIGGER_TYPE = { + [TRIGGER_OTHER]: 'request', + [TRIGGER_HTTP]: 'request', + // Note: `transaction.type = "timer"` is not in the shared APM agent spec yet. + [TRIGGER_TIMER]: 'timer' +} +// See APM spec and OTel `faas.trigger` at +// https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/faas/ +const FAAS_TRIGGER_TYPE_FROM_TRIGGER_TYPE = { + [TRIGGER_OTHER]: 'other', + [TRIGGER_HTTP]: 'http', + // Note: `faas.trigger = "timer"` is not in the shared APM agent spec yet. + [TRIGGER_TIMER]: 'timer' +} + +const gHttpRouteFromFuncDir = new Map() +const DEFAULT_ROUTE_PREFIX = 'api' +let gRoutePrefix = null + +// Mimic a subset of `FunctionInfo` from Azure code +// https://github.com/Azure/azure-functions-nodejs-library/blob/v3.5.0/src/FunctionInfo.ts +// to help with handling. +// ...plus some additional functionality for `httpRoute` and `routePrefix`. +class FunctionInfo { + constructor (bindingDefinitions, executionContext, log) { + // Example `bindingDefinitions`: + // [{"name":"req","type":"httpTrigger","direction":"in"}, + // {"name":"res","type":"http","direction":"out"}] + this.triggerType = TRIGGER_OTHER + this.httpOutputName = '' + this.hasHttpTrigger = false + this.hasReturnBinding = false + this.outputBindingNames = [] + for (const bd of bindingDefinitions) { + if (bd.direction !== 'in') { + if (bd.type && bd.type.toLowerCase() === 'http') { + this.httpOutputName = bd.name + } + this.outputBindingNames.push(bd.name) + if (bd.name === '$return') { + this.hasReturnBinding = true + } + } + if (bd.type) { + const typeLc = bd.type.toLowerCase() + switch (typeLc) { + case 'httptrigger': // "type": "httpTrigger" + this.triggerType = TRIGGER_HTTP + break + case 'timertrigger': + this.triggerType = TRIGGER_TIMER + break + } + } + } + + // If this is an HTTP triggered-function, then get its route template and + // route prefix. + // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger#customize-the-http-endpoint + // A possible custom "route" is not included in the given context, so we + // attempt to load the "function.json" file. A possible custom route prefix + // is in "host.json". + this.httpRoute = null + this.routePrefix = null + if (this.triggerType === TRIGGER_HTTP) { + const funcDir = executionContext.functionDirectory + if (!funcDir) { + this.httpRoute = executionContext.functionName + } else if (gHttpRouteFromFuncDir.has(funcDir)) { + this.httpRoute = gHttpRouteFromFuncDir.get(funcDir) + } else { + try { + const fj = JSON.parse(fs.readFileSync(path.join(funcDir, 'function.json'))) + for (let i = 0; i < fj.bindings.length; i++) { + const binding = fj.bindings[i] + if (binding.direction === 'in' && binding.type && binding.type.toLowerCase() === 'httptrigger') { + if (binding.route !== undefined) { + this.httpRoute = binding.route + } else { + this.httpRoute = executionContext.functionName + } + gHttpRouteFromFuncDir.set(funcDir, this.httpRoute) + } + } + log.trace({ funcDir, httpRoute: this.httpRoute }, 'azure-functions: loaded route') + } catch (httpRouteErr) { + log.debug('azure-functions: could not determine httpRoute for function %s: %s', executionContext.functionName, httpRouteErr.message) + this.httpRoute = executionContext.functionName + } + } + + if (gRoutePrefix) { + this.routePrefix = gRoutePrefix + } else if (!funcDir) { + this.routePrefix = gRoutePrefix = DEFAULT_ROUTE_PREFIX + } else { + try { + const hj = JSON.parse(fs.readFileSync(path.join(path.dirname(funcDir), 'host.json'))) + if (hj && + hj.extensions && + hj.extensions.http && + hj.extensions.http.routePrefix !== undefined) { + const rawRoutePrefix = hj.extensions.http.routePrefix + this.routePrefix = gRoutePrefix = normRoutePrefix(rawRoutePrefix) + log.trace({ hj, routePrefix: this.routePrefix, rawRoutePrefix }, 'azure-functions: loaded route prefix') + } else { + this.routePrefix = gRoutePrefix = DEFAULT_ROUTE_PREFIX + } + } catch (routePrefixErr) { + log.debug('azure-functions: could not determine routePrefix: %s', routePrefixErr.message) + this.routePrefix = gRoutePrefix = DEFAULT_ROUTE_PREFIX + } + } + } + } +} + +// Normalize a routePrefix to *not* have a leading slash. +// +// Given routePrefix='/foo' and functionName='MyFn', Microsoft.AspNetCore.Routing +// will create a route `//foo/MyFn`. Actual HTTP requests to `GET /foo/MyFn`, +// `GET //foo/MyFn`, and any number of leading slashes will work. So let's +// settle on the more typical single leading slash. +function normRoutePrefix (routePrefix) { + return routePrefix.startsWith('/') ? routePrefix.slice(1) : routePrefix +} + +/** + * Set transaction data for HTTP triggers from the Lambda function result. + */ +function setTransDataFromHttpTriggerResult (trans, hookCtx) { + if (hookCtx.error) { + trans.setOutcome(constants.OUTCOME_FAILURE) + trans.result = 'HTTP 5xx' + trans.res = { + statusCode: 500 + } + return + } + + // Attempt to get what the Azure Functions system will use for the HTTP response + // data. This is a pain because Azure Functions supports a number of different + // ways the user can return a response. Part of the handling for this is: + // https://github.com/Azure/azure-functions-nodejs-library/blob/v3.5.0/src/InvocationModel.ts#L77-L144 + const funcInfo = hookCtx.hookData.funcInfo + const result = hookCtx.result + const context = hookCtx.invocationContext + let httpRes + if (funcInfo.hasReturnBinding) { + httpRes = hookCtx.result + } else { + if (result && typeof result === 'object' && result[funcInfo.httpOutputName] !== undefined) { + httpRes = result[funcInfo.httpOutputName] + } else if (context.bindings && context.bindings[funcInfo.httpOutputName] !== undefined) { + httpRes = context.bindings[funcInfo.httpOutputName] + } else if (context.res !== undefined) { + httpRes = context.res + } + } + + // Azure Functions requires that the HTTP output response value be an 'object', + // otherwise it errors out the response (statusCode=500) and logs an error: + // Stack: Error: The HTTP response must be an 'object' type that can include properties such as 'body', 'status', and 'headers'. Learn more: https://go.microsoft.com/fwlink/?linkid=2112563 + if (typeof httpRes !== 'object') { + trans.setOutcome(constants.OUTCOME_FAILURE) + trans.result = 'HTTP 5xx' + trans.res = { + statusCode: 500 + } + return + } + + let statusCode = Number(httpRes.status) + if (!Number.isInteger(statusCode)) { + // While https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger + // suggests the default may be "HTTP 204 No Content", my observation is that + // 200 is the actual default. + statusCode = 200 + } + + if (statusCode < 500) { + trans.setOutcome(constants.OUTCOME_SUCCESS) + } else { + trans.setOutcome(constants.OUTCOME_FAILURE) + } + trans.result = 'HTTP ' + statusCode.toString()[0] + 'xx' + trans.res = { + statusCode, + body: httpRes.body + } + if (httpRes.headers && typeof httpRes.headers === 'object') { + trans.res.headers = httpRes.headers + } +} + +// The Azure account id is also called the "subscription GUID". +// https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings#app-environment +function getAzureAccountId () { + return process.env.WEBSITE_OWNER_NAME && process.env.WEBSITE_OWNER_NAME.split('+', 1)[0] +} + +// ---- exports + +const isAzureFunctionsEnvironment = !!process.env.FUNCTIONS_WORKER_RUNTIME + +// Gather APM metadata for this Azure Function instance per +// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-azure-functions.md#metadata +function getAzureFunctionsExtraMetadata () { + const metadata = { + service: { + framework: { + // Passing this service.framework.name to Client#setExtraMetadata() + // ensures that it "wins" over a framework name from + // `agent.setFramework()`, because in the client `_extraMetadata` + // wins over `_conf.metadata`. + name: 'Azure Functions', + version: process.env.FUNCTIONS_EXTENSION_VERSION + }, + runtime: { + name: process.env.FUNCTIONS_WORKER_RUNTIME + }, + node: { + configured_name: process.env.WEBSITE_INSTANCE_ID + } + }, + // https://github.com/elastic/apm/blob/main/specs/agents/metadata.md#azure-functions + cloud: { + provider: 'azure', + region: process.env.REGION_NAME, + service: { + name: 'functions' + } + } + } + const accountId = getAzureAccountId() + if (accountId) { + metadata.cloud.account = { id: accountId } + } + if (process.env.WEBSITE_SITE_NAME) { + metadata.cloud.instance = { name: process.env.WEBSITE_SITE_NAME } + } + if (process.env.WEBSITE_RESOURCE_GROUP) { + metadata.cloud.project = { name: process.env.WEBSITE_RESOURCE_GROUP } + } + return metadata +} + +function instrument (agent) { + if (isInstrumented) { + return + } + isInstrumented = true + + const ins = agent._instrumentation + const log = agent.logger + let d + + let core + try { + core = require('@azure/functions-core') + } catch (err) { + log.warn({ err }, 'could not import "@azure/functions-core": skipping Azure Functions instrumentation') + return + } + + // Note: We *could* hook into 'appTerminate' to attempt a quick flush of the + // current intake request. However, I have not seen a need for it yet. + // d = core.registerHook('appTerminate', async (hookCtx) => { + // log.trace('azure-functions: appTerminate') + // // flush here ... + // }) + // hookDisposables.push(d) + + // See examples at https://github.com/Azure/azure-functions-nodejs-worker/issues/522 + d = core.registerHook('preInvocation', (hookCtx) => { + if (!hookCtx.invocationContext) { + // Doesn't look like `require('@azure/functions-core').PreInvocationContext`. Abort. + return + } + + const context = hookCtx.invocationContext + const invocationId = context.invocationId + log.trace({ invocationId }, 'azure-functions: preInvocation') + + const isColdStart = isFirstRun + if (isFirstRun) { + isFirstRun = false + } + + const funcInfo = hookCtx.hookData.funcInfo = new FunctionInfo( + context.bindingDefinitions, context.executionContext, log) + const triggerType = funcInfo.triggerType + + // Handle trace-context. + // Note: We ignore the `context.traceContext`. By default it is W3C + // trace-context that continues the given traceparent in headers. However, + // we do not injest that span, so would get a broken distributed trace if + // we included it. + let traceparent + let tracestate + if (triggerType === TRIGGER_HTTP && context.req && context.req.headers) { + traceparent = context.req.headers.traceparent || context.req.headers['elastic-apm-traceparent'] + tracestate = context.req.headers.tracestate + } + + const trans = hookCtx.hookData.trans = ins.startTransaction( + // This is the default name. Trigger-specific values are added below. + context.executionContext.functionName, + TRANS_TYPE_FROM_TRIGGER_TYPE[triggerType], + { + childOf: traceparent, + tracestate + } + ) + + // Expected env vars are documented at: + // https://learn.microsoft.com/en-us/azure/app-service/reference-app-settings + const accountId = getAzureAccountId() + const resourceGroup = process.env.WEBSITE_RESOURCE_GROUP + const fnAppName = process.env.WEBSITE_SITE_NAME + const fnName = context.executionContext.functionName + const faasData = { + trigger: { + type: FAAS_TRIGGER_TYPE_FROM_TRIGGER_TYPE[triggerType] + }, + execution: invocationId, + coldstart: isColdStart + } + if (accountId && resourceGroup && fnAppName) { + faasData.id = `/subscriptions/${accountId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${fnAppName}/functions/${fnName}` + } + if (fnAppName && fnName) { + faasData.name = `${fnAppName}/${fnName}` + } + trans.setFaas(faasData) + + if (triggerType === TRIGGER_HTTP) { + // The request object is the first item in `hookCtx.inputs`. See: + // https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/eventHandlers/InvocationHandler.ts#L127 + const req = hookCtx.inputs[0] + if (req) { + trans.req = req // Used for setting `trans.context.request` by `getContextFromRequest()`. + if (agent._conf.usePathAsTransactionName && req.url) { + trans.setDefaultName(`${req.method} ${new URL(req.url).pathname}`) + } else { + const route = (funcInfo.routePrefix + ? `/${funcInfo.routePrefix}/${funcInfo.httpRoute}` + : `/${funcInfo.httpRoute}`) + trans.setDefaultName(`${req.method} ${route}`) + } + } + } + }) + hookDisposables.push(d) + + d = core.registerHook('postInvocation', (hookCtx) => { + if (!hookCtx.invocationContext) { + // Doesn't look like `require('@azure/functions-core').PreInvocationContext`. Abort. + return + } + const invocationId = hookCtx.invocationContext.invocationId + log.trace({ invocationId }, 'azure-functions: postInvocation') + + const trans = hookCtx.hookData.trans + if (!trans) { + return + } + + const funcInfo = hookCtx.hookData.funcInfo + if (funcInfo.triggerType === TRIGGER_HTTP) { + setTransDataFromHttpTriggerResult(trans, hookCtx) + } else if (hookCtx.error) { + trans.result = constants.RESULT_FAILURE + trans.setOutcome(constants.OUTCOME_FAILURE) + } else { + trans.result = constants.RESULT_SUCCESS + trans.setOutcome(constants.OUTCOME_SUCCESS) + } + + if (hookCtx.error) { + // Capture the error before trans.end() so it associates with the + // current trans. `skipOutcome` to avoid setting outcome on a possible + // currentSpan, because this error applies to the transaction, not any + // sub-span. + agent.captureError(hookCtx.error, { skipOutcome: true }) + } + + trans.end() + }) + hookDisposables.push(d) +} + +function uninstrument () { + if (!isInstrumented) { + return + } + isInstrumented = false + + // Unregister `core.registerHook()` calls from above. + hookDisposables.forEach(d => { + d.dispose() + }) + hookDisposables = [] +} + +module.exports = { + isAzureFunctionsEnvironment, + getAzureFunctionsExtraMetadata, + instrument, + uninstrument +} diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index c3c871b7b7..09631a6c94 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -9,7 +9,7 @@ var fs = require('fs') var path = require('path') -var hook = require('require-in-the-middle') +const Hook = require('../ritm') const semver = require('semver') const config = require('../config') @@ -23,6 +23,7 @@ const { } = require('./run-context') const { getLambdaHandlerInfo } = require('../lambda') const undiciInstr = require('./modules/undici') +const azureFunctionsInstr = require('./azure-functions') const nodeSupportsAsyncLocalStorage = semver.satisfies(process.versions.node, '>=14.5 || ^12.19.0') // Node v16.5.0 added fetch support (behind `--experimental-fetch` until @@ -226,6 +227,11 @@ Instrumentation.prototype.start = function (runContextClass) { this._log.debug('instrumenting fetch') undiciInstr.instrumentUndici(this._agent) } + + if (azureFunctionsInstr.isAzureFunctionsEnvironment) { + this._log.debug('instrumenting azure-functions') + azureFunctionsInstr.instrument(this._agent) + } } // Stop active instrumentation and reset global state *as much as possible*. @@ -251,6 +257,9 @@ Instrumentation.prototype.stop = function () { if (nodeHasInstrumentableFetch) { undiciInstr.uninstrumentUndici() } + if (azureFunctionsInstr.isAzureFunctionsEnvironment) { + azureFunctionsInstr.uninstrument() + } } // Reset internal state for (relatively) clean re-use of this Instrumentation. @@ -282,7 +291,7 @@ Instrumentation.prototype._startHook = function () { this._agent.logger.debug('adding hook to Node.js module loader') - this._hook = hook(this._patches.keys, function (exports, name, basedir) { + this._hook = new Hook(this._patches.keys, function (exports, name, basedir) { const enabled = self._isModuleEnabled(name) var pkg, version diff --git a/lib/ritm.js b/lib/ritm.js new file mode 100644 index 0000000000..009f3fd328 --- /dev/null +++ b/lib/ritm.js @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +'use strict' + +/** + * This file is extracted from the 'require-in-the-middle' project copyright by + * Thomas Watson Steen. It has been modified to be used in the current + * context. + * + * Project: https://github.com/elastic/require-in-the-middle. + * License: MIT, http://opensource.org/licenses/MIT + */ + +const path = require('path') +const Module = require('module') +const resolve = require('resolve') +const debug = require('debug')('require-in-the-middle') +const moduleDetailsFromPath = require('module-details-from-path') + +module.exports = Hook + +/** + * Is the given module a "core" module? + * https://nodejs.org/api/modules.html#core-modules + * + * @type {(moduleName: string) => boolean} + */ +let isCore +if (Module.isBuiltin) { // as of node v18.6.0 + isCore = Module.isBuiltin +} else { + isCore = moduleName => { + // Prefer `resolve.core` lookup to `resolve.isCore(moduleName)` because the + // latter is doing version range matches for every call. + return !!resolve.core[moduleName] + } +} + +// 'foo/bar.js' or 'foo/bar/index.js' => 'foo/bar' +const normalize = /([/\\]index)?(\.js)?$/ + +function Hook (modules, options, onrequire) { + if ((this instanceof Hook) === false) return new Hook(modules, options, onrequire) + if (typeof modules === 'function') { + onrequire = modules + modules = null + options = null + } else if (typeof options === 'function') { + onrequire = options + options = null + } + + if (typeof Module._resolveFilename !== 'function') { + console.error('Error: Expected Module._resolveFilename to be a function (was: %s) - aborting!', typeof Module._resolveFilename) + console.error('Please report this error as an issue related to Node.js %s at %s', process.version, require('../package.json').bugs.url) + return + } + + this.cache = new Map() + this._unhooked = false + this._origRequire = Module.prototype.require + + const self = this + const patching = new Set() + const internals = options ? options.internals === true : false + const hasWhitelist = Array.isArray(modules) + + debug('registering require hook') + + this._require = Module.prototype.require = function (id) { + if (self._unhooked === true) { + // if the patched require function could not be removed because + // someone else patched it after it was patched here, we just + // abort and pass the request onwards to the original require + debug('ignoring require call - module is soft-unhooked') + return self._origRequire.apply(this, arguments) + } + + const core = isCore(id) + let filename // the string used for caching + if (core) { + filename = id + // If this is a builtin module that can be identified both as 'foo' and + // 'node:foo', then prefer 'foo' as the caching key. + if (id.startsWith('node:')) { + const idWithoutPrefix = id.slice(5) + if (isCore(idWithoutPrefix)) { + filename = idWithoutPrefix + } + } + } else { + try { + filename = Module._resolveFilename(id, this) + } catch (resolveErr) { + // If someone *else* monkey-patches before this monkey-patch, then that + // code might expect `require(someId)` to get through so it can be + // handled, even if `someId` cannot be resolved to a filename. In this + // case, instead of throwing we defer to the underlying `require`. + // + // For example the Azure Functions Node.js worker module does this, + // where `@azure/functions-core` resolves to an internal object. + // https://github.com/Azure/azure-functions-nodejs-worker/blob/v3.5.2/src/setupCoreModule.ts#L46-L54 + debug(`Module._resolveFilename(${id}) threw "${resolveErr.message}", calling original Module.require`) + return self._origRequire.apply(this, arguments) + } + } + + let moduleName, basedir + + debug('processing %s module require(\'%s\'): %s', core === true ? 'core' : 'non-core', id, filename) + + // return known patched modules immediately + if (self.cache.has(filename) === true) { + debug('returning already patched cached module: %s', filename) + return self.cache.get(filename) + } + + // Check if this module has a patcher in-progress already. + // Otherwise, mark this module as patching in-progress. + const isPatching = patching.has(filename) + if (isPatching === false) { + patching.add(filename) + } + + const exports = self._origRequire.apply(this, arguments) + + // If it's already patched, just return it as-is. + if (isPatching === true) { + debug('module is in the process of being patched already - ignoring: %s', filename) + return exports + } + + // The module has already been loaded, + // so the patching mark can be cleaned up. + patching.delete(filename) + + if (core === true) { + if (hasWhitelist === true && modules.includes(filename) === false) { + debug('ignoring core module not on whitelist: %s', filename) + return exports // abort if module name isn't on whitelist + } + moduleName = filename + } else if (hasWhitelist === true && modules.includes(filename)) { + // whitelist includes the absolute path to the file including extension + const parsedPath = path.parse(filename) + moduleName = parsedPath.name + basedir = parsedPath.dir + } else { + const stat = moduleDetailsFromPath(filename) + if (!stat) { + debug('could not parse filename: %s', filename) + return exports // abort if filename could not be parsed + } + moduleName = stat.name + basedir = stat.basedir + + const fullModuleName = resolveModuleName(stat) + + debug('resolved filename to module: %s (id: %s, resolved: %s, basedir: %s)', moduleName, id, fullModuleName, basedir) + + // Ex: require('foo/lib/../bar.js') + // moduleName = 'foo' + // fullModuleName = 'foo/bar' + if (hasWhitelist === true && modules.includes(moduleName) === false) { + if (modules.includes(fullModuleName) === false) return exports // abort if module name isn't on whitelist + + // if we get to this point, it means that we're requiring a whitelisted sub-module + moduleName = fullModuleName + } else { + // figure out if this is the main module file, or a file inside the module + let res + try { + res = resolve.sync(moduleName, { basedir }) + } catch (e) { + debug('could not resolve module: %s', moduleName) + return exports // abort if module could not be resolved (e.g. no main in package.json and no index.js file) + } + + if (res !== filename) { + // this is a module-internal file + if (internals === true) { + // use the module-relative path to the file, prefixed by original module name + moduleName = moduleName + path.sep + path.relative(basedir, filename) + debug('preparing to process require of internal file: %s', moduleName) + } else { + debug('ignoring require of non-main module file: %s', res) + return exports // abort if not main module file + } + } + } + } + + // only call onrequire the first time a module is loaded + if (self.cache.has(filename) === false) { + // ensure that the cache entry is assigned a value before calling + // onrequire, in case calling onrequire requires the same module. + self.cache.set(filename, exports) + debug('calling require hook: %s', moduleName) + self.cache.set(filename, onrequire(exports, moduleName, basedir)) + } + + debug('returning module: %s', moduleName) + return self.cache.get(filename) + } +} + +Hook.prototype.unhook = function () { + this._unhooked = true + if (this._require === Module.prototype.require) { + Module.prototype.require = this._origRequire + debug('unhook successful') + } else { + debug('unhook unsuccessful') + } +} + +function resolveModuleName (stat) { + const normalizedPath = path.sep !== '/' ? stat.path.split(path.sep).join('/') : stat.path + return path.posix.join(stat.name, normalizedPath).replace(normalize, '') +} diff --git a/package-lock.json b/package-lock.json index 97476f2738..3e55ec0e01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "basic-auth": "^2.0.1", "cookie": "^0.5.0", "core-util-is": "^1.0.2", - "elastic-apm-http-client": "11.0.4", + "debug": "^4.1.1", + "elastic-apm-http-client": "11.1.0", "end-of-stream": "^1.4.4", "error-callsites": "^2.0.4", "error-stack-parser": "^2.0.6", @@ -27,13 +28,14 @@ "is-native": "^1.0.1", "lru-cache": "^6.0.0", "measured-reporting": "^1.51.1", + "module-details-from-path": "^1.0.3", "monitor-event-loop-delay": "^1.0.0", "object-filter-sequence": "^1.0.0", "object-identity-map": "^1.0.2", "original-url": "^1.2.3", "pino": "^6.11.2", "relative-microtime": "^2.0.0", - "require-in-the-middle": "^5.2.0", + "resolve": "^1.22.1", "semver": "^6.3.0", "set-cookie-serde": "^1.0.0", "shallow-clone-shim": "^2.0.0", @@ -57,6 +59,7 @@ "apollo-server-core": "^3.0.0", "apollo-server-express": "^3.0.0", "aws-sdk": "^2.622.0", + "azure-functions-core-tools": "^4.0.4915", "backport": "^5.1.2", "benchmark": "^2.1.4", "bluebird": "^3.7.2", @@ -97,7 +100,6 @@ "memcached": "^2.2.2", "mimic-response": "^2.1.0", "mkdirp": "^0.5.1", - "module-details-from-path": "^1.0.3", "mongodb": "^4.2.1", "mongodb-core": "^3.2.7", "mysql": "^2.18.1", @@ -117,6 +119,7 @@ "tedious": "^15.1.0", "test-all-versions": "^4.1.1", "thunky": "^1.1.0", + "tree-kill": "^1.2.2", "typescript": "^4.7.4", "undici": "^5.8.0", "vasync": "^2.2.0", @@ -4526,21 +4529,497 @@ "node": "*" } }, - "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/azure-functions-core-tools": { + "version": "4.0.4915", + "resolved": "https://registry.npmjs.org/azure-functions-core-tools/-/azure-functions-core-tools-4.0.4915.tgz", + "integrity": "sha512-z+dQHEfnOScDDlihOlTXn7hmvxjaqtOdEcTEmIRNR8N4nP9zqY8RHfKp3Z86pmh5PfXODnN+buCpJJM/iux9CA==", + "dev": true, + "hasInstallScript": true, + "hasShrinkwrap": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "chalk": "3.0.0", + "https-proxy-agent": "5.0.0", + "progress": "2.0.3", + "rimraf": "3.0.2", + "unzipper": "0.10.10" + }, + "bin": { + "azfun": "lib/main.js", + "azurefunctions": "lib/main.js", + "func": "lib/main.js" + }, + "engines": { + "node": ">=6.9.1" + } + }, + "node_modules/azure-functions-core-tools/node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/azure-functions-core-tools/node_modules/ansi-styles": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", + "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", + "dev": true, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/azure-functions-core-tools/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/azure-functions-core-tools/node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/azure-functions-core-tools/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/azure-functions-core-tools/node_modules/buffer-indexof-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", + "integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/azure-functions-core-tools/node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/azure-functions-core-tools/node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/azure-functions-core-tools/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/azure-functions-core-tools/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/azure-functions-core-tools/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/azure-functions-core-tools/node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/azure-functions-core-tools/node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/azure-functions-core-tools/node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/azure-functions-core-tools/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/azure-functions-core-tools/node_modules/graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/azure-functions-core-tools/node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/azure-functions-core-tools/node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/azure-functions-core-tools/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/azure-functions-core-tools/node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/azure-functions-core-tools/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/azure-functions-core-tools/node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/azure-functions-core-tools/node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/azure-functions-core-tools/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/azure-functions-core-tools/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/azure-functions-core-tools/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/azure-functions-core-tools/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/azure-functions-core-tools/node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/azure-functions-core-tools/node_modules/unzipper": { + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", + "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" } }, + "node_modules/azure-functions-core-tools/node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/azure-functions-core-tools/node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", @@ -6153,9 +6632,9 @@ "dev": true }, "node_modules/elastic-apm-http-client": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/elastic-apm-http-client/-/elastic-apm-http-client-11.0.4.tgz", - "integrity": "sha512-449Qj/STi9hgnIk2KQ7719E7lpM3/i4Afs7NUhSOX8wV3sxn/+ItIHx9kKJthzhDDezxIfQcH83v83AF67GspQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/elastic-apm-http-client/-/elastic-apm-http-client-11.1.0.tgz", + "integrity": "sha512-jQQ0G68Z+UKdNlVywuQ+kz52AnNusQSuZP1CWNJS4z1Wg8mBazKNkQUHf3JbqfWCfw8BycofwgKp3sd+awueQg==", "dependencies": { "agentkeepalive": "^4.2.1", "breadth-filter": "^2.0.0", @@ -12756,19 +13235,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-in-the-middle": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", - "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", - "dependencies": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -14443,6 +14909,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -18876,6 +19351,400 @@ "follow-redirects": "^1.14.0" } }, + "azure-functions-core-tools": { + "version": "4.0.4915", + "resolved": "https://registry.npmjs.org/azure-functions-core-tools/-/azure-functions-core-tools-4.0.4915.tgz", + "integrity": "sha512-z+dQHEfnOScDDlihOlTXn7hmvxjaqtOdEcTEmIRNR8N4nP9zqY8RHfKp3Z86pmh5PfXODnN+buCpJJM/iux9CA==", + "dev": true, + "requires": { + "chalk": "3.0.0", + "https-proxy-agent": "5.0.0", + "progress": "2.0.3", + "rimraf": "3.0.2", + "unzipper": "0.10.10" + }, + "dependencies": { + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "agent-base": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.0.tgz", + "integrity": "sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ansi-styles": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz", + "integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "dev": true + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "dev": true, + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer-indexof-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", + "integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=", + "dev": true + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "dev": true + }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "dev": true, + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "mkdirp": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", + "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "dev": true + }, + "unzipper": { + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.10.tgz", + "integrity": "sha512-wEgtqtrnJ/9zIBsQb8UIxOhAH1eTHfi7D/xvmrUoMEePeI6u24nq1wigazbIFtHt6ANYXdEVTvc8XYNlTurs7A==", + "dev": true, + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, "babel-plugin-polyfill-corejs2": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", @@ -20118,9 +20987,9 @@ "dev": true }, "elastic-apm-http-client": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/elastic-apm-http-client/-/elastic-apm-http-client-11.0.4.tgz", - "integrity": "sha512-449Qj/STi9hgnIk2KQ7719E7lpM3/i4Afs7NUhSOX8wV3sxn/+ItIHx9kKJthzhDDezxIfQcH83v83AF67GspQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/elastic-apm-http-client/-/elastic-apm-http-client-11.1.0.tgz", + "integrity": "sha512-jQQ0G68Z+UKdNlVywuQ+kz52AnNusQSuZP1CWNJS4z1Wg8mBazKNkQUHf3JbqfWCfw8BycofwgKp3sd+awueQg==", "requires": { "agentkeepalive": "^4.2.1", "breadth-filter": "^2.0.0", @@ -25264,16 +26133,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-in-the-middle": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", - "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", - "requires": { - "debug": "^4.1.1", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.1" - } - }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -26620,6 +27479,12 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==" }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", diff --git a/package.json b/package.json index 1476c14bfd..20294051ac 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "lint:yaml-files": "./dev-utils/lint-yaml-files.sh # requires node >=10", "coverage": "COVERAGE=true ./test/script/run_tests.sh", "test": "./test/script/run_tests.sh", - "test:deps": "dependency-check index.js start.js start-next.js 'lib/**/*.js' 'test/**/*.js' '!test/instrumentation/modules/next/a-nextjs-app' --no-dev -i async_hooks -i perf_hooks -i parseurl -i node:http", + "test:deps": "dependency-check index.js start.js start-next.js 'lib/**/*.js' 'test/**/*.js' '!test/instrumentation/modules/next/a-nextjs-app' --no-dev -i async_hooks -i perf_hooks -i parseurl -i node:http -i @azure/functions-core", "test:tav": "tav --quiet && (cd test/instrumentation/modules/next/a-nextjs-app && tav --quiet)", "test:docs": "./test/script/docker/run_docs.sh", "test:types": "tsc --project test/types/tsconfig.json && tsc --project test/types/transpile/tsconfig.json && node test/types/transpile/index.js && tsc --project test/types/transpile-default/tsconfig.json && node test/types/transpile-default/index.js", @@ -94,7 +94,8 @@ "basic-auth": "^2.0.1", "cookie": "^0.5.0", "core-util-is": "^1.0.2", - "elastic-apm-http-client": "11.0.4", + "debug": "^4.1.1", + "elastic-apm-http-client": "11.1.0", "end-of-stream": "^1.4.4", "error-callsites": "^2.0.4", "error-stack-parser": "^2.0.6", @@ -104,13 +105,14 @@ "is-native": "^1.0.1", "lru-cache": "^6.0.0", "measured-reporting": "^1.51.1", + "module-details-from-path": "^1.0.3", "monitor-event-loop-delay": "^1.0.0", "object-filter-sequence": "^1.0.0", "object-identity-map": "^1.0.2", "original-url": "^1.2.3", "pino": "^6.11.2", "relative-microtime": "^2.0.0", - "require-in-the-middle": "^5.2.0", + "resolve": "^1.22.1", "semver": "^6.3.0", "set-cookie-serde": "^1.0.0", "shallow-clone-shim": "^2.0.0", @@ -134,6 +136,7 @@ "apollo-server-core": "^3.0.0", "apollo-server-express": "^3.0.0", "aws-sdk": "^2.622.0", + "azure-functions-core-tools": "^4.0.4915", "backport": "^5.1.2", "benchmark": "^2.1.4", "bluebird": "^3.7.2", @@ -174,7 +177,6 @@ "memcached": "^2.2.2", "mimic-response": "^2.1.0", "mkdirp": "^0.5.1", - "module-details-from-path": "^1.0.3", "mongodb": "^4.2.1", "mongodb-core": "^3.2.7", "mysql": "^2.18.1", @@ -194,6 +196,7 @@ "tedious": "^15.1.0", "test-all-versions": "^4.1.1", "thunky": "^1.1.0", + "tree-kill": "^1.2.2", "typescript": "^4.7.4", "undici": "^5.8.0", "vasync": "^2.2.0", diff --git a/test/_utils.js b/test/_utils.js index 9493c48bd9..8b9085f7d1 100644 --- a/test/_utils.js +++ b/test/_utils.js @@ -6,6 +6,8 @@ 'use strict' +// A dumping ground for testing utility functions. + const fs = require('fs') const moduleDetailsFromPath = require('module-details-from-path') @@ -68,8 +70,29 @@ function safeGetPackageVersion (packageName) { } } +// Match ANSI escapes (from https://stackoverflow.com/a/29497680/14444044). +const ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g /* eslint-disable-line no-control-regex */ + +/** + * Format the given data for passing to `t.comment()`. + * + * - t.comment() wipes leading whitespace. Prefix lines with '|' to avoid + * that, and to visually group a multi-line write. + * - Drop ANSI escape characters, because those include control chars that + * are illegal in XML. When we convert TAP output to JUnit XML for + * Jenkins, then Jenkins complains about invalid XML. `FORCE_COLOR=0` + * can be used to disable ANSI escapes in `next dev`'s usage of chalk, + * but not in its coloured exception output. + */ +function formatForTComment (data) { + return data.toString('utf8') + .replace(ANSI_RE, '') + .trimRight().replace(/\r?\n/g, '\n|') + '\n' +} + module.exports = { dottedLookup, findObjInArray, + formatForTComment, safeGetPackageVersion } diff --git a/test/instrumentation/azure-functions/azure-functions.test.js b/test/instrumentation/azure-functions/azure-functions.test.js new file mode 100644 index 0000000000..1c3c6933fa --- /dev/null +++ b/test/instrumentation/azure-functions/azure-functions.test.js @@ -0,0 +1,524 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +'use strict' + +const assert = require('assert') +const { spawn } = require('child_process') +const http = require('http') +const os = require('os') +const path = require('path') + +const semver = require('semver') +const tape = require('tape') +const treekill = require('tree-kill') + +const { MockAPMServer } = require('../../_mock_apm_server') +const { formatForTComment } = require('../../_utils') + +if (!semver.satisfies(process.version, '>=14 <19')) { + console.log(`# SKIP Azure Functions runtime ~4 does not support node ${process.version} (https://aka.ms/functions-node-versions)`) + process.exit() +} + +/** + * Wait for the test "func start" to be ready. + * + * This polls the admin endpoint until + * it gets a 200 response, assuming the server is ready by then. + * It times out after ~60s -- so long because startup on Windows CI has been + * found to take a long time (it is downloading 250MB+ in "ExtensionBundle"s). + * + * @param {Test} t - This is only used to `t.comment(...)` with progress. + * @param {Function} cb - Calls `cb(err)` if there was a timeout, `cb()` on + * success. + */ +function waitForServerReady (t, cb) { + let sentinel = 30 + const INTERVAL_MS = 2000 + + const pollForServerReady = () => { + const req = http.get( + 'http://127.0.0.1:7071/admin/functions', + { + agent: false, + timeout: 500 + }, + res => { + res.resume() + res.on('end', () => { + if (res.statusCode !== 200) { + scheduleNextPoll(`statusCode=${res.statusCode}`) + } else { + cb() + } + }) + } + ) + req.on('error', err => { + scheduleNextPoll(err.message) + }) + } + + const scheduleNextPoll = (msg) => { + t.comment(`[sentinel=${sentinel} ${new Date().toISOString()}] wait another 2s for server ready: ${msg}`) + sentinel-- + if (sentinel <= 0) { + cb(new Error('timed out')) + } else { + setTimeout(pollForServerReady, INTERVAL_MS) + } + } + + pollForServerReady() +} + +async function makeTestRequest (t, testReq) { + return new Promise((resolve, reject) => { + const reqOpts = testReq.reqOpts + const url = `http://127.0.0.1:7071${reqOpts.path}` + t.comment(`makeTestRequest: "${testReq.testName}" (${reqOpts.method} ${url})`) + const req = http.request( + url, + { + method: reqOpts.method + }, + res => { + const chunks = [] + res.on('data', chunk => { chunks.push(chunk) }) + res.on('end', () => { + const body = Buffer.concat(chunks) + if (testReq.expectedRes.statusCode) { + t.equal(res.statusCode, testReq.expectedRes.statusCode, `res.statusCode === ${testReq.expectedRes.statusCode}`) + } + if (testReq.expectedRes.headers) { + for (const [k, v] of Object.entries(testReq.expectedRes.headers)) { + if (v instanceof RegExp) { + t.ok(v.test(res.headers[k]), `res.headers[${JSON.stringify(k)}] =~ ${v}`) + } else { + t.equal(res.headers[k], v, `res.headers[${JSON.stringify(k)}] === ${JSON.stringify(v)}`) + } + } + } + if (testReq.expectedRes.body) { + if (testReq.expectedRes.body instanceof RegExp) { + t.ok(testReq.expectedRes.body.test(body), `body =~ ${testReq.expectedRes.body}`) + } else if (typeof testReq.expectedRes.body === 'string') { + t.equal(body.toString(), testReq.expectedRes.body, 'body') + } else { + t.fail(`unsupported type for TEST_REQUESTS[].expectedRes.body: ${typeof testReq.expectedRes.body}`) + } + } + resolve() + }) + } + ) + req.on('error', reject) + req.end() + }) +} + +function getEventField (e, fieldName) { + return (e.transaction || e.error || e.span)[fieldName] +} + +/** + * Assert that the given `apmEvents` (events that the mock APM server received) + * match all the expected APM events in `TEST_REQUESTS`. + */ +function checkExpectedApmEvents (t, apmEvents) { + // metadata + if (apmEvents.length > 0) { + const metadata = apmEvents.shift().metadata + t.ok(metadata, 'metadata is first event') + t.equal(metadata.service.name, 'AJsAzureFnApp', 'metadata.service.name') + t.equal(metadata.service.framework.name, 'Azure Functions', 'metadata.service.framework.name') + t.equal(metadata.service.framework.version, '~4', 'metadata.service.framework.version') + t.equal(metadata.service.runtime.name, 'node', 'metadata.service.runtime.name') + t.equal(metadata.service.node.configured_name, 'test-website-instance-id', 'metadata.service.node.configured_name') + t.equal(metadata.cloud.account.id, '2491fc8e-f7c1-4020-b9c6-78509919fd16', 'metadata.cloud.account.id') + t.equal(metadata.cloud.instance.name, 'AJsAzureFnApp', 'metadata.cloud.instance.name') + t.equal(metadata.cloud.project.name, 'my-resource-group', 'metadata.cloud.project.name') + t.equal(metadata.cloud.provider, 'azure', 'metadata.cloud.provider') + t.equal(metadata.cloud.region, 'test-region-name', 'metadata.cloud.region') + t.equal(metadata.cloud.service.name, 'functions', 'metadata.cloud.service.name') + } + + // Filter out any metadata from separate requests, and metricsets which we + // aren't testing. + apmEvents = apmEvents + .filter(e => !e.metadata) + .filter(e => !e.metricset) + + // Sort all the remaining APM events and check expectations from TEST_REQUESTS. + apmEvents = apmEvents + .sort((a, b) => { + return getEventField(a, 'timestamp') < getEventField(b, 'timestamp') ? -1 : 1 + }) + TEST_REQUESTS.forEach(testReq => { + t.comment(`check APM events for "${testReq.testName}"`) + // Collect all events for this transaction's trace_id, and pass that to + // the `checkApmEvents` function for this request. + let apmEventsForReq = [] + if (apmEvents.length > 0) { + assert(apmEvents[0].transaction, `next APM event is a transaction: ${JSON.stringify(apmEvents[0])}`) + const traceId = apmEvents[0].transaction.trace_id + apmEventsForReq = apmEvents.filter(e => getEventField(e, 'trace_id') === traceId) + apmEvents = apmEvents.filter(e => getEventField(e, 'trace_id') !== traceId) + } + testReq.checkApmEvents(t, apmEventsForReq) + }) + + t.equal(apmEvents.length, 0, 'no additional unexpected APM server events: ' + JSON.stringify(apmEvents)) +} + +// ---- tests + +const UUID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i + +var TEST_REQUESTS = [ + { + testName: 'HttpFn1', + reqOpts: { method: 'GET', path: '/api/HttpFn1' }, + expectedRes: { + statusCode: 200, // the Azure Functions default + headers: { myheadername: 'MyHeaderValue' }, + body: 'HttpFn1 body' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFn1', 'transaction.name') + t.equal(trans.type, 'request', 'transaction.type') + t.equal(trans.outcome, 'success', 'transaction.outcome') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.faas.name, 'AJsAzureFnApp/HttpFn1', 'transaction.faas.name') + t.equal(trans.faas.id, + '/subscriptions/2491fc8e-f7c1-4020-b9c6-78509919fd16/resourceGroups/my-resource-group/providers/Microsoft.Web/sites/AJsAzureFnApp/functions/HttpFn1', + 'transaction.faas.id') + t.equal(trans.faas.trigger.type, 'http', 'transaction.faas.trigger.type') + t.ok(UUID_RE.test(trans.faas.execution), 'transaction.faas.execution ' + trans.faas.execution) + t.equal(trans.faas.coldstart, true, 'transaction.faas.coldstart') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.request.url.full, 'http://127.0.0.1:7071/api/HttpFn1', 'transaction.context.request.url.full') + t.ok(trans.context.request.headers, 'transaction.context.request.headers') + t.equal(trans.context.response.status_code, 200, 'transaction.context.response.status_code') + t.equal(trans.context.response.headers.MyHeaderName, 'MyHeaderValue', 'transaction.context.response.headers.MyHeaderName') + } + }, + // Only a test a subset of fields to not be redundant with previous cases. + { + testName: 'HttpFnError throws an error', + reqOpts: { method: 'GET', path: '/api/HttpFnError' }, + expectedRes: { + statusCode: 500 + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 2) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnError', 'transaction.name') + t.equal(trans.outcome, 'failure', 'transaction.outcome') + t.equal(trans.result, 'HTTP 5xx', 'transaction.result') + t.equal(trans.faas.name, 'AJsAzureFnApp/HttpFnError', 'transaction.faas.name') + t.equal(trans.faas.coldstart, false, 'transaction.faas.coldstart') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 500, 'transaction.context.response.status_code') + + const error = apmEventsForReq[1].error + t.equal(error.parent_id, trans.id, 'error.parent_id') + t.deepEqual(error.transaction, + { name: trans.name, type: trans.type, sampled: trans.sampled }, + 'error.transaction') + t.equal(error.exception.message, 'thrown error in HttpFnError', 'error.exception.message') + t.equal(error.exception.type, 'Error', 'error.exception.type') + t.equal(error.exception.handled, true, 'error.exception.handled') + const topFrame = error.exception.stacktrace[0] + t.equal(topFrame.filename, path.join('HttpFnError', 'index.js'), 'topFrame.filename') + t.equal(topFrame.lineno, 8, 'topFrame.lineno') + t.equal(topFrame.function, 'ThrowErrorHandler', 'topFrame.function') + } + }, + { + testName: 'HttpFnBindingsRes', + reqOpts: { method: 'GET', path: '/api/HttpFnBindingsRes' }, + expectedRes: { + statusCode: 202, + body: 'HttpFnBindingsRes body' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnBindingsRes', 'transaction.name') + t.equal(trans.outcome, 'success', 'transaction.outcome') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 202, 'transaction.context.response.status_code') + } + }, + { + testName: 'HttpFnContextDone', + reqOpts: { method: 'GET', path: '/api/HttpFnContextDone' }, + expectedRes: { + statusCode: 202, + body: 'HttpFnContextDone body' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnContextDone', 'transaction.name') + t.equal(trans.outcome, 'success', 'transaction.outcome') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 202, 'transaction.context.response.status_code') + } + }, + { + testName: 'HttpFnReturnContext', + reqOpts: { method: 'GET', path: '/api/HttpFnReturnContext' }, + expectedRes: { + statusCode: 202, + body: 'HttpFnReturnContext body' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnReturnContext', 'transaction.name') + t.equal(trans.outcome, 'success', 'transaction.outcome') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 202, 'transaction.context.response.status_code') + } + }, + { + testName: 'HttpFnReturnResponseData', + reqOpts: { method: 'GET', path: '/api/HttpFnReturnResponseData' }, + expectedRes: { + statusCode: 202, + body: 'HttpFnReturnResponseData body' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnReturnResponseData', 'transaction.name') + t.equal(trans.outcome, 'success', 'transaction.outcome') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 202, 'transaction.context.response.status_code') + } + }, + { + testName: 'HttpFnReturnObject', + reqOpts: { method: 'GET', path: '/api/HttpFnReturnObject' }, + expectedRes: { + statusCode: 200 + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnReturnObject', 'transaction.name') + t.equal(trans.outcome, 'success', 'transaction.outcome') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 200, 'transaction.context.response.status_code') + } + }, + { + testName: 'HttpFnReturnString', + reqOpts: { method: 'GET', path: '/api/HttpFnReturnString' }, + expectedRes: { + statusCode: 500 + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFnReturnString', 'transaction.name') + t.equal(trans.outcome, 'failure', 'transaction.outcome') + t.equal(trans.result, 'HTTP 5xx', 'transaction.result') + t.equal(trans.context.request.method, 'GET', 'transaction.context.request.method') + t.equal(trans.context.response.status_code, 500, 'transaction.context.response.status_code') + } + }, + { + testName: 'GET httpfn1 (lower-case in URL path)', + reqOpts: { method: 'GET', path: '/api/httpfn1' }, + expectedRes: { + statusCode: 200, // the Azure Functions default + headers: { myheadername: 'MyHeaderValue' }, + body: 'HttpFn1 body' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/HttpFn1', 'transaction.name') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.faas.name, 'AJsAzureFnApp/HttpFn1', 'transaction.faas.name') + t.equal(trans.faas.id, + '/subscriptions/2491fc8e-f7c1-4020-b9c6-78509919fd16/resourceGroups/my-resource-group/providers/Microsoft.Web/sites/AJsAzureFnApp/functions/HttpFn1', + 'transaction.faas.id') + t.equal(trans.context.request.url.full, 'http://127.0.0.1:7071/api/httpfn1', 'transaction.context.request.url.full') + } + }, + { + // https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger#customize-the-http-endpoint + testName: 'HttpFnRouteTemplate', + reqOpts: { method: 'GET', path: '/api/products/electronics/42' }, + expectedRes: { + statusCode: 202, + body: 'HttpFnRouteTemplate body: category=electronics id=42' + }, + checkApmEvents: (t, apmEventsForReq) => { + t.equal(apmEventsForReq.length, 1) + const trans = apmEventsForReq[0].transaction + t.equal(trans.name, 'GET /api/products/{category:alpha}/{id:int?}', 'transaction.name') + t.equal(trans.result, 'HTTP 2xx', 'transaction.result') + t.equal(trans.faas.name, 'AJsAzureFnApp/HttpFnRouteTemplate', 'transaction.faas.name') + t.equal(trans.faas.id, + '/subscriptions/2491fc8e-f7c1-4020-b9c6-78509919fd16/resourceGroups/my-resource-group/providers/Microsoft.Web/sites/AJsAzureFnApp/functions/HttpFnRouteTemplate', + 'transaction.faas.id') + t.equal(trans.context.request.url.full, 'http://127.0.0.1:7071/api/products/electronics/42', 'transaction.context.request.url.full') + } + }, + { + testName: 'HttpFnDistTrace', + reqOpts: { method: 'GET', path: '/api/HttpFnDistTraceA' }, + expectedRes: { + statusCode: 200, + body: 'HttpFnDistTraceA body' + }, + checkApmEvents: (t, apmEventsForReq) => { + // Expect: + // trans "GET HttpFnDistTraceA" + // `- span "spanA" + // `- span "GET $HOST:$PORT" + // `- trans "GET HttpFnDistTraceB" + t.equal(apmEventsForReq.length, 4) + const t1 = apmEventsForReq[0].transaction + t.equal(t1.name, 'GET /api/HttpFnDistTraceA', 't1.name') + t.equal(t1.faas.name, 'AJsAzureFnApp/HttpFnDistTraceA', 't1.faas.name') + const s1 = apmEventsForReq[1].span + t.equal(s1.name, 'spanA', 's1.name') + t.equal(s1.parent_id, t1.id, 's1 is a child of t1') + const s2 = apmEventsForReq[2].span + t.equal(s2.name, `GET ${s2.context.service.target.name}`, 's2.name') + t.equal(s2.type, 'external', 's2.type') + t.equal(s2.parent_id, s1.id, 's2 is a child of s1') + const t2 = apmEventsForReq[3].transaction + t.equal(t2.name, 'GET /api/HttpFnDistTraceB', 't2.name') + t.equal(t2.faas.name, 'AJsAzureFnApp/HttpFnDistTraceB', 't2.faas.name') + t.equal(t2.parent_id, s2.id, 't2 is a child of s2') + t.equal(t2.context.request.headers.traceparent, `00-${t1.trace_id}-${s2.id}-01`, 't2 traceparent header') + t.equal(t2.context.request.headers.tracestate, 'es=s:1', 't2 tracestate header') + } + } +] +// TEST_REQUESTS = TEST_REQUESTS.filter(r => ~r.testName.indexOf('HttpFn1')) // Use this for dev work. + +tape.test('azure functions', function (suite) { + let apmServer + let apmServerUrl + + suite.test('setup', function (t) { + apmServer = new MockAPMServer() + apmServer.start(function (serverUrl) { + apmServerUrl = serverUrl + t.comment('mock APM apmServerUrl: ' + apmServerUrl) + t.end() + }) + }) + + let fnAppProc + const funcExe = path.resolve(__dirname, '../../../node_modules/.bin/func') + ( + os.platform() === 'win32' ? '.cmd' : '') + const fnAppDir = path.join(__dirname, 'fixtures', 'AJsAzureFnApp') + suite.test('setup: "func start" for AJsAzureFnApp fixture', t => { + fnAppProc = spawn( + funcExe, + ['start'], + { + cwd: fnAppDir, + env: Object.assign({}, process.env, { + ELASTIC_APM_SERVER_URL: apmServerUrl, + ELASTIC_APM_API_REQUEST_TIME: '2s' + }) + } + ) + fnAppProc.on('error', err => { + t.error(err, 'no error from "func start"') + }) + fnAppProc.stdout.on('data', data => { + t.comment(`["func start" stdout] ${formatForTComment(data)}`) + }) + fnAppProc.stderr.on('data', data => { + t.comment(`["func start" stderr] ${formatForTComment(data)}`) + }) + + // Allow some time for an early fail of `func start`, e.g. if there is + // already a user of port 7071... + const onEarlyClose = code => { + t.fail(`"func start" failed early: code=${code}`) + fnAppProc = null + clearTimeout(earlyCloseTimer) + t.end() + } + fnAppProc.on('close', onEarlyClose) + const earlyCloseTimer = setTimeout(() => { + fnAppProc.removeListener('close', onEarlyClose) + + // ... then wait for the server to be ready. + waitForServerReady(t, waitErr => { + if (waitErr) { + t.fail(`error waiting for "func start" to be ready: ${waitErr.message}`) + treekill(fnAppProc.pid, 'SIGKILL') + fnAppProc = null + } else { + t.comment('"func start" is ready') + } + t.end() + }) + }, 1000) + }) + + suite.test('make requests', async t => { + if (!fnAppProc) { + t.skip('there is no fnAppProc') + t.end() + return + } + + apmServer.clear() + for (let i = 0; i < TEST_REQUESTS.length; i++) { + await makeTestRequest(t, TEST_REQUESTS[i]) + } + + t.end() + }) + + suite.test('check all APM events', t => { + if (!fnAppProc) { + t.skip('there is no fnAppProc') + t.end() + return + } + + // To ensure we get all the trace data from the instrumented function app + // server, we wait 2x the `apiRequestTime` (set above) before stopping it. + fnAppProc.on('close', _code => { + checkExpectedApmEvents(t, apmServer.events) + t.end() + }) + t.comment('wait 4s for trace data to be sent before closing "func start"') + setTimeout(() => { + treekill(fnAppProc.pid, 'SIGKILL') + }, 4000) // 2x ELASTIC_APM_API_REQUEST_TIME set above + }) + + suite.test('teardown', function (t) { + apmServer.close() + t.end() + }) + + suite.end() +}) diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/.gitignore b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/.gitignore new file mode 100644 index 0000000000..9ee7cd6ffe --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/.gitignore @@ -0,0 +1,50 @@ +bin +obj +csx +.vs +edge +Publish + +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json +# Explicitly allow local.settings.json for local testing. +# local.settings.json + +node_modules +dist +.vscode/ + +# Local python packages +.python_packages/ + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Azurite artifacts +__blobstorage__ +__queuestorage__ +__azurite_db*__.json diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/function.json new file mode 100644 index 0000000000..c49c51116f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/index.js new file mode 100644 index 0000000000..ee95a2cc3e --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFn1/index.js @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function (context, _req) { + context.res = { + headers: { + MyHeaderName: 'MyHeaderValue' + }, + body: 'HttpFn1 body' + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/function.json new file mode 100644 index 0000000000..c49c51116f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/index.js new file mode 100644 index 0000000000..62e990d22d --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnBindingsRes/index.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function (context) { + // Using this wins over possible `context.res` usage. + context.bindings.res = { + status: 202, + body: 'HttpFnBindingsRes body' + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/function.json new file mode 100644 index 0000000000..c49c51116f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/index.js new file mode 100644 index 0000000000..1f73b19379 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnContextDone/index.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = function (context) { + // This is an old (deprecated?) Azure Functions way to signal completion for + // a non-async function handler. + context.done( + null, { + res: { + status: 202, + body: 'HttpFnContextDone body' + } + } + ) +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/function.json new file mode 100644 index 0000000000..c49c51116f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/index.js new file mode 100644 index 0000000000..e187c08b44 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceA/index.js @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +const apm = require('../../../../../../') // elastic-apm-node + +const http = require('http') +const https = require('https') + +async function callHttpFnDistTrace (req, suffix) { + const u = new URL(req.url) + u.pathname = u.pathname.replace(/.$/, suffix) + const url = u.toString() + const proto = u.protocol === 'https:' ? https : http + return new Promise((resolve, reject) => { + const clientReq = proto.request(url, function (clientRes) { + const chunks = [] + clientRes.on('data', function (chunk) { + chunks.push(chunk) + }) + clientRes.on('end', function () { + const body = chunks.join('') + resolve({ + statusCode: clientRes.statusCode, + headers: clientRes.headers, + body: body + }) + }) + clientRes.on('error', reject) + }) + clientReq.on('error', reject) + clientReq.end() + }) +} + +module.exports = async function (context, req) { + const span = apm.startSpan('spanA') + await callHttpFnDistTrace(req, 'B') + if (span) { + span.end() + } + + context.res = { + status: 200, + body: 'HttpFnDistTraceA body' + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/function.json new file mode 100644 index 0000000000..c49c51116f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/index.js new file mode 100644 index 0000000000..36e60f2d31 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnDistTraceB/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function (context, req) { + context.res = { + status: 200, + body: 'HttpFnDistTraceB body' + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/function.json new file mode 100644 index 0000000000..91052aaf8a --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} \ No newline at end of file diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/index.js new file mode 100644 index 0000000000..1ef8771a4f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnError/index.js @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function ThrowErrorHandler (context, req) { + throw new Error('thrown error in HttpFnError') +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/function.json new file mode 100644 index 0000000000..c49c51116f --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/index.js new file mode 100644 index 0000000000..7ce21945f9 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnContext/index.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function () { + // If returning an object with a field that matches the type=http "out" + // binding, then this return value is used and wins over `context.res` and + // `context.bindings.*` usage. + // Note that this does *not* use a '$return' binding in function.json! + return { + res: { + status: 202, + body: 'HttpFnReturnContext body' + } + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/function.json new file mode 100644 index 0000000000..b1b83fe548 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/index.js new file mode 100644 index 0000000000..be107d605b --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnObject/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function () { + // Using a '$return' binding, so only the return value is used (not + // `context.res` or `context.bindings.*`). Any object is fine, but if it + // provides none of the fields for an HTTP response, then the default is used. + return { foo: 'bar' } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/function.json new file mode 100644 index 0000000000..b1b83fe548 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/index.js new file mode 100644 index 0000000000..84dd25a567 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnResponseData/index.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function () { + // Using a '$return' binding, one can return the response data directly. + return { + status: 202, + body: 'HttpFnReturnResponseData body' + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/function.json new file mode 100644 index 0000000000..b1b83fe548 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/function.json @@ -0,0 +1,19 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/index.js new file mode 100644 index 0000000000..6fd5400790 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnReturnString/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function () { + // This uses a '$return' binding, so the retval is used. + // Any HTTP response from an Azure Function is meant to be an *object*. If + // not, then Azure returns a 500 response with an empty body. + return 'this is return value string' +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/function.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/function.json new file mode 100644 index 0000000000..2a3aebd5c1 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/function.json @@ -0,0 +1,20 @@ +{ + "bindings": [ + { + "authLevel": "Anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ], + "route": "products/{category:alpha}/{id:int?}" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/index.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/index.js new file mode 100644 index 0000000000..44bd24c3f9 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/HttpFnRouteTemplate/index.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +module.exports = async function (context, _req) { + context.res = { + status: 202, + body: `HttpFnRouteTemplate body: category=${context.req.params.category} id=${context.req.params.id}` + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/README.md b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/README.md new file mode 100644 index 0000000000..e9264594af --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/README.md @@ -0,0 +1,12 @@ +A Node.js JavaScript Azure function app to be used for testing of +elastic-apm-node. + +# Notes on how this was created + +- `func init AJsAzureFnApp` +- Remove "azure-functions-core-tools" devDep and move to top-level to share + between possibly many fixtures. +- An HTTP-triggered function: `func new --name HttpFn1 --template "HTTP trigger" --authlevel "anonymous"` +- ... + + diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/host.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/host.json new file mode 100644 index 0000000000..519fe11b51 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/initapm.js b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/initapm.js new file mode 100644 index 0000000000..af573b87ec --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/initapm.js @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and other contributors where applicable. + * Licensed under the BSD 2-Clause License; you may not use this file except in + * compliance with the BSD 2-Clause License. + */ + +// For the normal use case an "initapm.js" would look like: +// module.exports = require('elastic-apm-node').start(/* { ... } */) + +module.exports = require('../../../../../').start() diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/local.settings.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/local.settings.json new file mode 100644 index 0000000000..51058f07a5 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/local.settings.json @@ -0,0 +1,16 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "node", + "AzureWebJobsStorage": "", + "WEBSITE_RUN_FROM_PACKAGE": "1", + "WEBSITE_NODE_DEFAULT_VERSION": "~16", + "FUNCTIONS_EXTENSION_VERSION": "~4", + + "WEBSITE_SITE_NAME": "AJsAzureFnApp", + "WEBSITE_OWNER_NAME": "2491fc8e-f7c1-4020-b9c6-78509919fd16+my-resource-group-ARegionShortNamewebspace", + "WEBSITE_RESOURCE_GROUP": "my-resource-group", + "WEBSITE_INSTANCE_ID": "test-website-instance-id", + "REGION_NAME": "test-region-name" + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package-lock.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package-lock.json new file mode 100644 index 0000000000..99c7ea5e3e --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "a-js-azure-fn", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "1.0.0", + "devDependencies": {} + } + } +} diff --git a/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package.json b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package.json new file mode 100644 index 0000000000..e8a076fab3 --- /dev/null +++ b/test/instrumentation/azure-functions/fixtures/AJsAzureFnApp/package.json @@ -0,0 +1,12 @@ +{ + "name": "", + "version": "1.0.0", + "description": "", + "main": "initapm.js", + "scripts": { + "start": "func start", + "test": "echo \"No tests yet...\"" + }, + "dependencies": {}, + "devDependencies": {} +} diff --git a/test/instrumentation/modules/next/next.test.js b/test/instrumentation/modules/next/next.test.js index aa1c325609..cc43cfa442 100644 --- a/test/instrumentation/modules/next/next.test.js +++ b/test/instrumentation/modules/next/next.test.js @@ -31,6 +31,7 @@ const semver = require('semver') const tape = require('tape') const { MockAPMServer } = require('../../../_mock_apm_server') +const { formatForTComment } = require('../../../_utils') if (os.platform() === 'win32') { // Limitation: currently don't support testing on Windows. @@ -56,9 +57,6 @@ if (process.env.ELASTIC_APM_CONTEXT_MANAGER === 'patch') { const testAppDir = path.join(__dirname, 'a-nextjs-app') -// Match ANSI escapes (from https://stackoverflow.com/a/29497680/14444044). -const ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g /* eslint-disable-line no-control-regex */ - let apmServer let nextJsVersion // Determined after `npm ci` is run. let serverUrl @@ -391,23 +389,6 @@ if (DEV_TEST_FILTER) { // ---- utility functions -/** - * Format the given data for passing to `t.comment()`. - * - * - t.comment() wipes leading whitespace. Prefix lines with '|' to avoid - * that, and to visually group a multi-line write. - * - Drop ANSI escape characters, because those include control chars that - * are illegal in XML. When we convert TAP output to JUnit XML for - * Jenkins, then Jenkins complains about invalid XML. `FORCE_COLOR=0` - * can be used to disable ANSI escapes in `next dev`'s usage of chalk, - * but not in its coloured exception output. - */ -function formatForTComment (data) { - return data.toString('utf8') - .replace(ANSI_RE, '') - .trimRight().replace(/\n/g, '\n|') + '\n' -} - /** * Wait for the test a-nextjs-app server to be ready. * @@ -738,7 +719,7 @@ tape.test('-- prod server tests --', suite => { }) setTimeout(() => { nextServerProc.kill('SIGTERM') - }, 4000) // 2x ELASTIC_APM_API_REQUEST_SIZE set above + }, 4000) // 2x ELASTIC_APM_API_REQUEST_TIME set above }) suite.end() @@ -834,7 +815,7 @@ tape.test('-- dev server tests --', suite => { }) setTimeout(() => { nextServerProc.kill('SIGTERM') - }, 4000) // 2x ELASTIC_APM_API_REQUEST_SIZE set above + }, 4000) // 2x ELASTIC_APM_API_REQUEST_TIME set above }) suite.end()