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 0000000000..68a3ee59cb Binary files /dev/null and b/docs/images/azure-functions-configuration.png differ diff --git a/docs/set-up.asciidoc b/docs/set-up.asciidoc index 4f3a377cff..6cdafb41af 100644 --- a/docs/set-up.asciidoc +++ b/docs/set-up.asciidoc @@ -7,6 +7,7 @@ To get you off the ground, we've prepared guides for setting up the Agent with a // Updates made here will be applied elsewhere as well. // tag::web-frameworks-list[] * <> +* <> * <> * <> * <> @@ -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()