From 3adbadfc303ae8ced9cc9a905d06582b58cccf4b Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 18 Sep 2023 19:37:32 +0200 Subject: [PATCH] feat: add @aws-sdk/client-dynamodb instrumentation (#3530) --- .ci/scripts/test.sh | 2 +- .ci/tav.json | 1 + .tav.yml | 23 +- CHANGELOG.asciidoc | 3 + dev-utils/aws-sdk-s3-client-tav-versions.sh | 23 -- docs/supported-technologies.asciidoc | 1 + .../modules/@aws-sdk/client-dynamodb.js | 132 +++++++ .../modules/@smithy/smithy-client.js | 12 + package-lock.json | 180 +++++++++ package.json | 1 + test/_utils.js | 23 ++ .../modules/@aws-sdk/client-dynamodb.test.js | 365 ++++++++++++++++++ .../@aws-sdk/fixtures/use-client-dynamodb.js | 244 ++++++++++++ .../@aws-sdk/fixtures/use-client-dynamodb.mjs | 57 +++ .../@aws-sdk/fixtures/use-client-s3.js | 26 +- .../modules/fastify/fastify.test.js | 3 +- .../modules/http/http-esm.test.js | 9 +- test/testconsts.js | 2 + 18 files changed, 1048 insertions(+), 59 deletions(-) delete mode 100755 dev-utils/aws-sdk-s3-client-tav-versions.sh create mode 100644 lib/instrumentation/modules/@aws-sdk/client-dynamodb.js create mode 100644 test/instrumentation/modules/@aws-sdk/client-dynamodb.test.js create mode 100644 test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.js create mode 100644 test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.mjs diff --git a/.ci/scripts/test.sh b/.ci/scripts/test.sh index d98c85b375..233f70462b 100755 --- a/.ci/scripts/test.sh +++ b/.ci/scripts/test.sh @@ -213,7 +213,7 @@ elif [[ -n "${TAV_MODULE}" ]]; then memcached) DOCKER_COMPOSE_FILE=docker-compose-memcached.yml ;; - aws-sdk|@aws-sdk/client-s3) + aws-sdk|@aws-sdk/client-s3|@aws-sdk/client-dynamodb) DOCKER_COMPOSE_FILE=docker-compose-localstack.yml ;; *) diff --git a/.ci/tav.json b/.ci/tav.json index 28a96829b7..065c47cc34 100644 --- a/.ci/tav.json +++ b/.ci/tav.json @@ -6,6 +6,7 @@ ], "modules": [ { "name": "@apollo/server", "minVersion": 14 }, + { "name": "@aws-sdk/client-dynamodb", "minVersion": 14 }, { "name": "@aws-sdk/client-s3", "minVersion": 14 }, { "name": "@elastic/elasticsearch", "minVersion": 10 }, { "name": "@hapi/hapi", "minVersion": 8 }, diff --git a/.tav.yml b/.tav.yml index 1e7bc61271..84610bf7fc 100644 --- a/.tav.yml +++ b/.tav.yml @@ -517,18 +517,27 @@ aws-sdk: - node test/instrumentation/modules/aws-sdk/sqs.test.js - node test/instrumentation/modules/aws-sdk/dynamodb.test.js +# For all AWS-SDK clients want this version range: +# versions: '>=3.15.0 <4' +# However, @awk-sdk/client-* releases *very* frequently (almost every day) and there +# is no need to test *all* those releases. Instead we statically list a subset +# of versions to test. '@aws-sdk/client-s3': - # We want this version range: - # versions: '>=3.15.0 <4' - # However, @awk-sdk/client-s3 releases *very* frequently (almost every day) and there - # is no need to test *all* those releases. Instead we statically list a subset - # of versions to test. - # # Maintenance note: This should be updated periodically using: # node ./dev-utils/tav-versions.js @aws-sdk/client-s3 ">=3.15.0 <4" # + # Test v3.15.0, every N=47 of 241 releases, and current latest. + versions: '3.15.0 || 3.56.0 || 3.159.0 || 3.236.0 || 3.319.0 || 3.400.0 || 3.412.0 || >3.412.0 <4' + commands: + - node test/instrumentation/modules/@aws-sdk/client-s3.test.js + node: '>=14' + +'@aws-sdk/client-dynamodb': + # Maintenance note: This should be updated periodically using: + # node ./dev-utils/tav-versions.js @aws-sdk/client-dynamodb ">=3.15.0 <4" + # # Test v3.15.0, every N=44 of 225 releases, and current latest. - versions: '3.15.0 || 3.54.0 || 3.141.0 || 3.218.0 || 3.300.0 || 3.373.0 || 3.379.1 || >3.379.1 <4' + versions: '3.15.0 || 3.55.0 || 3.165.0 || 3.234.0 || 3.312.0 || 3.398.0 || 3.410.0 || >3.410.0 <4' commands: - node test/instrumentation/modules/@aws-sdk/client-s3.test.js node: '>=14' diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index b31a54fdca..7ccdf5d942 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -45,6 +45,9 @@ See the <> guide. * Update <> support to `@opentelemetry/api` version 1.6.0. {pull}3622[#3622] +* Add support for `@aws-sdk/client-dynamodb`, one of the AWS SDK v3 clients. + ({issues}2958[#2958]) + [float] ===== Bug fixes diff --git a/dev-utils/aws-sdk-s3-client-tav-versions.sh b/dev-utils/aws-sdk-s3-client-tav-versions.sh deleted file mode 100755 index 5111c6b9f2..0000000000 --- a/dev-utils/aws-sdk-s3-client-tav-versions.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/sh -# -# Calculate and emit the "versions:" block of ".tav.yml" for aws-sdk. -# This will include: -# - the first supported release -# - the latest current release -# - and ~5 releases in between - -npm info -j @aws-sdk/client-s3 | node -e ' - var semver = require("semver"); - var chunks = []; - process.stdin - .resume() - .on("data", (chunk) => { chunks.push(chunk) }) - .on("end", () => { - var input = JSON.parse(chunks.join("")); - var vers = input.versions.filter(v => semver.satisfies(v, ">=3.15.0 <4")); - var modulus = Math.floor((vers.length - 2) / 5); - console.log(" # Test v3.15.0, every N=%d of %d releases, and current latest.", modulus, vers.length); - vers = vers.filter((v, idx, arr) => idx % modulus === 0 || idx === arr.length - 1); - console.log(" versions: '\''%s || >%s <4'\''", vers.join(" || "), vers[vers.length-1]) - }) -' diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 35f4b030bf..ab2c01203c 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -115,6 +115,7 @@ The Node.js agent will automatically instrument the following modules to give yo |Module |Version |Note |https://www.npmjs.com/package/aws-sdk[aws-sdk] |>=2.858.0 <3 |Will instrument SQS send/receive/delete messages, all S3 methods, all DynamoDB methods, and the SNS publish method |https://www.npmjs.com/package/@aws-sdk/client-s3[@aws-sdk/client-s3] |>=3.15.0 <4 |Will instrument all S3 methods +|https://www.npmjs.com/package/@aws-sdk/client-dynamodb[@aws-sdk/client-dynamodb] |>=3.15.0 <4 |Will instrument all DynamoDB methods |https://www.npmjs.com/package/cassandra-driver[cassandra-driver] |>=3.0.0 <5 |Will instrument all queries |https://www.npmjs.com/package/elasticsearch[elasticsearch] |>=8.0.0 |Will instrument all queries |https://www.npmjs.com/package/@elastic/elasticsearch[@elastic/elasticsearch] |>=7.0.0 <9.0.0 |Will instrument all queries diff --git a/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js b/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js new file mode 100644 index 0000000000..b26394a27a --- /dev/null +++ b/lib/instrumentation/modules/@aws-sdk/client-dynamodb.js @@ -0,0 +1,132 @@ +/* + * 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 constants = require('../../../constants'); +const NAME = 'DynamoDB'; +const TYPE = 'db'; +const SUBTYPE = 'dynamodb'; +const elasticAPMStash = Symbol('elasticAPMStash'); + +/** + * Returns middlewares to instrument an S3Client instance + * + * @param {import('@aws-sdk/client-dynamodb').DynamoDBClient} client + * @param {any} agent + * @returns {import('../@smithy/smithy-client').AWSMiddlewareEntry[]} + */ +function dynamoDBMiddlewareFactory(client, agent) { + return [ + { + middleware: (next, context) => async (args) => { + // Ensure there is a span from the wrapped `client.send()`. + const span = agent._instrumentation.currSpan(); + if (!span || !(span.type === TYPE && span.subtype === SUBTYPE)) { + return await next(args); + } + + const input = args.input; + const table = input && input.TableName; + // The given span comes with the operation name and we need to + // add the table if applies + if (table) { + span.name += ' ' + table; + } + + let err; + let result; + let response; + let statusCode; + try { + result = await next(args); + response = result && result.response; + statusCode = response && response.statusCode; + } catch (ex) { + // Save the error for use in `finally` below, but re-throw it to + // not impact code flow. + err = ex; + + // This code path happens with a GetObject conditional request + // that returns a 304 Not Modified. + statusCode = err && err.$metadata && err.$metadata.httpStatusCode; + throw ex; + } finally { + if (statusCode) { + span._setOutcomeFromHttpStatusCode(statusCode); + } else { + span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE); + } + if (err && (!statusCode || statusCode >= 400)) { + agent.captureError(err, { skipOutcome: true }); + } + + const config = client.config; + const region = await config.region(); + + // Set the db context + const dbContext = { type: SUBTYPE }; // dynamodb + if (region) { + dbContext.instance = region; + } + if (input && input.KeyConditionExpression) { + dbContext.statement = input.KeyConditionExpression; + } + span.setDbContext(dbContext); + + // Set destination context + const destContext = {}; + if (context[elasticAPMStash]) { + destContext.address = context[elasticAPMStash].hostname; + destContext.port = context[elasticAPMStash].port; + } + if (region) { + destContext.service = { resource: `dynamodb/${region}` }; + destContext.cloud = { region }; + } + span._setDestinationContext(destContext); + + // TODO: add OTel attributes when spec is merged + // https://github.com/elastic/apm/pull/817 + + span.end(); + } + + return result; + }, + options: { step: 'initialize', priority: 'high', name: 'elasticAPMSpan' }, + }, + { + middleware: (next, context) => async (args) => { + const req = args.request; + let port = req.port; + + // Resolve port for HTTP(S) protocols + if (port === undefined) { + if (req.protocol === 'https:') { + port = 443; + } else if (req.protocol === 'http:') { + port = 80; + } + } + + context[elasticAPMStash] = { + hostname: req.hostname, + port, + }; + return next(args); + }, + options: { step: 'finalizeRequest', name: 'elasticAPMHTTPInfo' }, + }, + ]; +} + +module.exports = { + DYNAMODB_NAME: NAME, + DYNAMODB_TYPE: TYPE, + DYNAMODB_SUBTYPE: SUBTYPE, + dynamoDBMiddlewareFactory, +}; diff --git a/lib/instrumentation/modules/@smithy/smithy-client.js b/lib/instrumentation/modules/@smithy/smithy-client.js index d1d9106f6c..45261b801b 100644 --- a/lib/instrumentation/modules/@smithy/smithy-client.js +++ b/lib/instrumentation/modules/@smithy/smithy-client.js @@ -15,6 +15,12 @@ const { S3_SUBTYPE, s3MiddlewareFactory, } = require('../@aws-sdk/client-s3'); +const { + DYNAMODB_NAME, + DYNAMODB_TYPE, + DYNAMODB_SUBTYPE, + dynamoDBMiddlewareFactory, +} = require('../@aws-sdk/client-dynamodb'); /** * We do alias them to a local type @@ -56,6 +62,12 @@ const clientsConfig = { SUBTYPE: S3_SUBTYPE, factory: s3MiddlewareFactory, }, + DynamoDBClient: { + NAME: DYNAMODB_NAME, + TYPE: DYNAMODB_TYPE, + SUBTYPE: DYNAMODB_SUBTYPE, + factory: dynamoDBMiddlewareFactory, + }, }; module.exports = function (mod, agent, { name, version, enabled }) { diff --git a/package-lock.json b/package-lock.json index ba2a7f0a72..1cd812cccc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ }, "devDependencies": { "@apollo/server": "^4.2.2", + "@aws-sdk/client-dynamodb": "^3.379.1", "@aws-sdk/client-s3": "^3.379.1", "@aws-sdk/s3-request-presigner": "^3.379.1", "@babel/cli": "^7.8.4", @@ -792,6 +793,56 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.379.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.379.1.tgz", + "integrity": "sha512-6a1nDXkWfMgXvjHNR4rqN+ujqJDKa2WRNC+8DBKfcumsRb/f8JLz8q+K7jOOEz3i0gsaXao1tyxe+lM5Y0NfeQ==", + "dev": true, + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.379.1", + "@aws-sdk/credential-provider-node": "3.379.1", + "@aws-sdk/middleware-endpoint-discovery": "3.379.1", + "@aws-sdk/middleware-host-header": "3.379.1", + "@aws-sdk/middleware-logger": "3.378.0", + "@aws-sdk/middleware-recursion-detection": "3.378.0", + "@aws-sdk/middleware-signing": "3.379.1", + "@aws-sdk/middleware-user-agent": "3.379.1", + "@aws-sdk/types": "3.378.0", + "@aws-sdk/util-endpoints": "3.378.0", + "@aws-sdk/util-user-agent-browser": "3.378.0", + "@aws-sdk/util-user-agent-node": "3.378.0", + "@smithy/config-resolver": "^2.0.1", + "@smithy/fetch-http-handler": "^2.0.1", + "@smithy/hash-node": "^2.0.1", + "@smithy/invalid-dependency": "^2.0.1", + "@smithy/middleware-content-length": "^2.0.1", + "@smithy/middleware-endpoint": "^2.0.1", + "@smithy/middleware-retry": "^2.0.1", + "@smithy/middleware-serde": "^2.0.1", + "@smithy/middleware-stack": "^2.0.0", + "@smithy/node-config-provider": "^2.0.1", + "@smithy/node-http-handler": "^2.0.1", + "@smithy/protocol-http": "^2.0.1", + "@smithy/smithy-client": "^2.0.1", + "@smithy/types": "^2.0.2", + "@smithy/url-parser": "^2.0.1", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.0.0", + "@smithy/util-defaults-mode-browser": "^2.0.1", + "@smithy/util-defaults-mode-node": "^2.0.1", + "@smithy/util-retry": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "@smithy/util-waiter": "^2.0.1", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.379.1", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.379.1.tgz", @@ -1100,6 +1151,19 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.310.0.tgz", + "integrity": "sha512-y3wipforet41EDTI0vnzxILqwAGll1KfI5qcdX9pXF/WF1f+3frcOtPiWtQEZQpy4czRogKm3BHo70QBYAZxlQ==", + "dev": true, + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.378.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.378.0.tgz", @@ -1117,6 +1181,22 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.379.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.379.1.tgz", + "integrity": "sha512-HpFF3Nb9csmg/j/trs4OhrQvthKFVz9lKkarGzxwYzaMqZ/xqFyPScJlZ41VgIkP+iP48IZVxAzLL/rsmsi/jA==", + "dev": true, + "dependencies": { + "@aws-sdk/endpoint-cache": "3.310.0", + "@aws-sdk/types": "3.378.0", + "@smithy/protocol-http": "^2.0.1", + "@smithy/types": "^2.0.2", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.378.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.378.0.tgz", @@ -13236,6 +13316,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "dev": true, + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/module-details-from-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", @@ -14086,6 +14175,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "dev": true + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -18862,6 +18957,53 @@ } } }, + "@aws-sdk/client-dynamodb": { + "version": "3.379.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.379.1.tgz", + "integrity": "sha512-6a1nDXkWfMgXvjHNR4rqN+ujqJDKa2WRNC+8DBKfcumsRb/f8JLz8q+K7jOOEz3i0gsaXao1tyxe+lM5Y0NfeQ==", + "dev": true, + "requires": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.379.1", + "@aws-sdk/credential-provider-node": "3.379.1", + "@aws-sdk/middleware-endpoint-discovery": "3.379.1", + "@aws-sdk/middleware-host-header": "3.379.1", + "@aws-sdk/middleware-logger": "3.378.0", + "@aws-sdk/middleware-recursion-detection": "3.378.0", + "@aws-sdk/middleware-signing": "3.379.1", + "@aws-sdk/middleware-user-agent": "3.379.1", + "@aws-sdk/types": "3.378.0", + "@aws-sdk/util-endpoints": "3.378.0", + "@aws-sdk/util-user-agent-browser": "3.378.0", + "@aws-sdk/util-user-agent-node": "3.378.0", + "@smithy/config-resolver": "^2.0.1", + "@smithy/fetch-http-handler": "^2.0.1", + "@smithy/hash-node": "^2.0.1", + "@smithy/invalid-dependency": "^2.0.1", + "@smithy/middleware-content-length": "^2.0.1", + "@smithy/middleware-endpoint": "^2.0.1", + "@smithy/middleware-retry": "^2.0.1", + "@smithy/middleware-serde": "^2.0.1", + "@smithy/middleware-stack": "^2.0.0", + "@smithy/node-config-provider": "^2.0.1", + "@smithy/node-http-handler": "^2.0.1", + "@smithy/protocol-http": "^2.0.1", + "@smithy/smithy-client": "^2.0.1", + "@smithy/types": "^2.0.2", + "@smithy/url-parser": "^2.0.1", + "@smithy/util-base64": "^2.0.0", + "@smithy/util-body-length-browser": "^2.0.0", + "@smithy/util-body-length-node": "^2.0.0", + "@smithy/util-defaults-mode-browser": "^2.0.1", + "@smithy/util-defaults-mode-node": "^2.0.1", + "@smithy/util-retry": "^2.0.0", + "@smithy/util-utf8": "^2.0.0", + "@smithy/util-waiter": "^2.0.1", + "tslib": "^2.5.0", + "uuid": "^8.3.2" + } + }, "@aws-sdk/client-s3": { "version": "3.379.1", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.379.1.tgz", @@ -19140,6 +19282,16 @@ "tslib": "^2.5.0" } }, + "@aws-sdk/endpoint-cache": { + "version": "3.310.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.310.0.tgz", + "integrity": "sha512-y3wipforet41EDTI0vnzxILqwAGll1KfI5qcdX9pXF/WF1f+3frcOtPiWtQEZQpy4czRogKm3BHo70QBYAZxlQ==", + "dev": true, + "requires": { + "mnemonist": "0.38.3", + "tslib": "^2.5.0" + } + }, "@aws-sdk/middleware-bucket-endpoint": { "version": "3.378.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.378.0.tgz", @@ -19154,6 +19306,19 @@ "tslib": "^2.5.0" } }, + "@aws-sdk/middleware-endpoint-discovery": { + "version": "3.379.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.379.1.tgz", + "integrity": "sha512-HpFF3Nb9csmg/j/trs4OhrQvthKFVz9lKkarGzxwYzaMqZ/xqFyPScJlZ41VgIkP+iP48IZVxAzLL/rsmsi/jA==", + "dev": true, + "requires": { + "@aws-sdk/endpoint-cache": "3.310.0", + "@aws-sdk/types": "3.378.0", + "@smithy/protocol-http": "^2.0.1", + "@smithy/types": "^2.0.2", + "tslib": "^2.5.0" + } + }, "@aws-sdk/middleware-expect-continue": { "version": "3.378.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.378.0.tgz", @@ -28551,6 +28716,15 @@ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true }, + "mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "dev": true, + "requires": { + "obliterator": "^1.6.1" + } + }, "module-details-from-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", @@ -29205,6 +29379,12 @@ "es-abstract": "^1.20.4" } }, + "obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "dev": true + }, "obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", diff --git a/package.json b/package.json index 8f36e49425..fe7218d18d 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ }, "devDependencies": { "@apollo/server": "^4.2.2", + "@aws-sdk/client-dynamodb": "^3.379.1", "@aws-sdk/client-s3": "^3.379.1", "@aws-sdk/s3-request-presigner": "^3.379.1", "@babel/cli": "^7.8.4", diff --git a/test/_utils.js b/test/_utils.js index 485fd2997a..97d563e64b 100644 --- a/test/_utils.js +++ b/test/_utils.js @@ -146,6 +146,28 @@ function sortApmEvents(events) { }); } +/** + * Slurp everything from the given ReadableStream and return the content, + * converted to a string. + */ +async function slurpStream(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on('error', (err) => { + reject(err); + }); + stream.on('readable', function () { + let chunk; + while ((chunk = this.read()) !== null) { + chunks.push(chunk); + } + }); + stream.on('end', () => { + resolve(Buffer.concat(chunks).toString('utf8')); + }); + }); +} + function quoteArg(a) { if (a.includes("'")) { return "'" + a.replace("'", "'\\''") + "'"; @@ -358,6 +380,7 @@ module.exports = { findObjsInArray, formatForTComment, safeGetPackageVersion, + slurpStream, sortApmEvents, runTestFixtures, }; diff --git a/test/instrumentation/modules/@aws-sdk/client-dynamodb.test.js b/test/instrumentation/modules/@aws-sdk/client-dynamodb.test.js new file mode 100644 index 0000000000..67272be3b3 --- /dev/null +++ b/test/instrumentation/modules/@aws-sdk/client-dynamodb.test.js @@ -0,0 +1,365 @@ +/* + * 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'; + +// Test DynamoDB instrumentation of the '@aws-sdk/client-dynamodb' module. +// +// Note that this uses localstack for testing, which mimicks the DynamoDB API but +// isn't identical. Some known limitations: +// https://docs.localstack.cloud/user-guide/aws/dynamodb/ + +if (process.env.GITHUB_ACTIONS === 'true' && process.platform === 'win32') { + console.log('# SKIP: GH Actions do not support docker services on Windows'); + process.exit(0); +} + +const test = require('tape'); + +const { validateSpan } = require('../../../_validate_schema'); +const { runTestFixtures, sortApmEvents } = require('../../../_utils'); +const { NODE_VER_RANGE_IITM_GE14 } = require('../../../testconsts'); +const NODE_VER_RANGE_AWS_SDK = '>=14.0.0'; +const AWS_REGION = 'us-east-2'; +const LOCALSTACK_HOST = process.env.LOCALSTACK_HOST || 'localhost'; +const endpoint = 'http://' + LOCALSTACK_HOST + ':4566'; + +const testFixtures = [ + { + name: 'simple DynamoDB V3 usage scenario', + script: 'fixtures/use-client-dynamodb.js', + cwd: __dirname, + timeout: 20000, // sanity guard on the test hanging + maxBuffer: 10 * 1024 * 1024, // This is big, but I don't ever want this to be a failure reason. + env: { + AWS_ACCESS_KEY_ID: 'fake', + AWS_SECRET_ACCESS_KEY: 'fake', + TEST_TABLE_NAME: 'elasticapmtest-table-3', + TEST_ENDPOINT: endpoint, + TEST_REGION: AWS_REGION, + }, + versionRanges: { node: NODE_VER_RANGE_AWS_SDK }, + verbose: false, + checkApmServer: (t, apmServer) => { + t.ok(apmServer.events[0].metadata, 'metadata'); + const events = sortApmEvents(apmServer.events); + + // First the transaction. + t.ok(events[0].transaction, 'got the transaction'); + const tx = events.shift().transaction; + const errors = events.filter((e) => e.error).map((e) => e.error); + + // Compare some common fields across all spans. + const spans = events.filter((e) => e.span).map((e) => e.span); + spans.forEach((s) => { + const errs = validateSpan(s); + t.equal(errs, null, 'span is valid (per apm-server intake schema)'); + }); + t.equal( + spans.filter((s) => s.trace_id === tx.trace_id).length, + spans.length, + 'all spans have the same trace_id', + ); + t.equal( + spans.filter((s) => s.transaction_id === tx.id).length, + spans.length, + 'all spans have the same transaction_id', + ); + t.equal( + spans.filter((s) => s.sync === false).length, + spans.length, + 'all spans have sync=false', + ); + t.equal( + spans.filter((s) => s.sample_rate === 1).length, + spans.length, + 'all spans have sample_rate=1', + ); + + const failingSpanId = spans[5].id; // index of non existing table error + spans.forEach((s) => { + // Remove variable and common fields to facilitate t.deepEqual below. + delete s.id; + delete s.transaction_id; + delete s.parent_id; + delete s.trace_id; + delete s.timestamp; + delete s.duration; + delete s.sync; + delete s.sample_rate; + }); + + // Work through each of the pipeline functions (lisTables, createTable, ...) in the script: + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB ListTables', + type: 'db', + subtype: 'dynamodb', + action: 'ListTables', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + type: 'dynamodb', + }, + }, + outcome: 'success', + }, + 'listTables produced expected span', + ); + + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB CreateTable elasticapmtest-table-3', + type: 'db', + subtype: 'dynamodb', + action: 'CreateTable', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + type: 'dynamodb', + }, + }, + outcome: 'success', + }, + 'createTable produced expected span', + ); + + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB PutItem elasticapmtest-table-3', + type: 'db', + subtype: 'dynamodb', + action: 'PutItem', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + type: 'dynamodb', + }, + }, + outcome: 'success', + }, + 'putItem produced expected span', + ); + + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB Query elasticapmtest-table-3', + type: 'db', + subtype: 'dynamodb', + action: 'Query', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + statement: 'RECORD_ID = :foo', + type: 'dynamodb', + }, + }, + outcome: 'success', + }, + 'query produced expected span', + ); + + t.deepEqual( + spans.shift(), + { + name: 'get-signed-url', + type: 'custom', + subtype: null, + action: null, + outcome: 'success', + }, + 'custom span for getSignedUrl call', + ); + + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB Query elasticapmtest-table-3-unexistent', + type: 'db', + subtype: 'dynamodb', + action: 'Query', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + statement: 'RECORD_ID = :foo', + type: 'dynamodb', + }, + }, + outcome: 'failure', + }, + 'failing query produced expected span', + ); + + t.equal(errors.length, 1, 'got 1 error'); + t.equal( + errors[0].parent_id, + failingSpanId, + 'error is a child of the failing span', + ); + t.equal(errors[0].transaction_id, tx.id, 'error.transaction_id'); + t.equal( + errors[0].exception.type, + 'ResourceNotFoundException', + 'error.exception.type', + ); + + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB DeleteItem elasticapmtest-table-3', + type: 'db', + subtype: 'dynamodb', + action: 'DeleteItem', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + type: 'dynamodb', + }, + }, + outcome: 'success', + }, + 'deleteItem produced expected span', + ); + + t.deepEqual( + spans.shift(), + { + name: 'DynamoDB DeleteTable elasticapmtest-table-3', + type: 'db', + subtype: 'dynamodb', + action: 'DeleteTable', + context: { + service: { target: { type: 'dynamodb', name: AWS_REGION } }, + destination: { + address: LOCALSTACK_HOST, + port: 4566, + cloud: { region: 'us-east-2' }, + service: { + type: '', + name: '', + resource: `dynamodb/${AWS_REGION}`, + }, + }, + db: { + instance: AWS_REGION, + type: 'dynamodb', + }, + }, + outcome: 'success', + }, + 'deleteTable produced expected span', + ); + + t.equal(spans.length, 0, 'all spans accounted for'); + }, + }, + { + name: '@aws-sdk/client-dynamodb ESM', + script: 'fixtures/use-client-dynamodb.mjs', + cwd: __dirname, + env: { + NODE_OPTIONS: + '--experimental-loader=../../../../loader.mjs --require=../../../../start.js', + NODE_NO_WARNINGS: '1', + AWS_ACCESS_KEY_ID: 'fake', + AWS_SECRET_ACCESS_KEY: 'fake', + TEST_ENDPOINT: endpoint, + TEST_REGION: 'us-east-2', + }, + versionRanges: { + node: NODE_VER_RANGE_IITM_GE14, + }, + verbose: true, + checkApmServer: (t, apmServer) => { + t.ok(apmServer.events[0].metadata, 'metadata'); + const events = sortApmEvents(apmServer.events); + + t.ok(events[0].transaction, 'got the transaction'); + const tx = events.shift().transaction; + + const span = events.shift().span; + t.equal(span.parent_id, tx.id, 'span.parent_id'); + t.equal(span.name, 'DynamoDB ListTables', 'span.name'); + + t.equal(events.length, 0, 'all events accounted for'); + }, + }, +]; + +test('@aws-sdk/client-dynamodb fixtures', (suite) => { + runTestFixtures(suite, testFixtures); + suite.end(); +}); diff --git a/test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.js b/test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.js new file mode 100644 index 0000000000..38f231eb95 --- /dev/null +++ b/test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.js @@ -0,0 +1,244 @@ +/* + * 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'; + +// Run a single scenario of using the DynamoDB client (callback style) with APM +// enabled. This is used to test that the expected APM events are generated. +// It writes log.info (in ecs-logging format, see +// https://github.com/trentm/go-ecslog#install) for each S3 client API call. +// +// This script can also be used for manual testing of APM instrumentation of DynamoDB +// against a real DynamoDB account. This can be useful because tests are done against +// https://github.com/localstack/localstack that *simulates* DynamoDB with imperfect +// fidelity. +// +// Auth note: By default this uses the AWS profile/configuration from the +// environment. If you do not have that configured (i.e. do not have +// "~/.aws/...") files, then you can still use localstack via setting: +// unset AWS_PROFILE +// export AWS_ACCESS_KEY_ID=fake +// export AWS_SECRET_ACCESS_KEY=fake +// See also: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html +// +// Usage: +// # Run against the default configured AWS profile, creating a new bucket +// # and deleting it afterwards. +// node use-client-dynamodb.js | ecslog +// +// # Testing against localstack. +// docker run --rm -it -p 4566:4566 localstack/localstack +// TEST_ENDPOINT=http://localhost:4566 node use-client-dynamodb.js | ecslog +// +// # Use TEST_TABLE_NAME to re-use an existing table (and not delete it). +// # For safety the table name must start with "elasticapmtest-table-". +// TEST_TABLE_NAME=elasticapmtest-table-3 node use-client-dynamodb.js | ecslog +// +// Output from a sample run is here: +// https://gist.github.com/david-luna/2b785a3197891505902fa85ee8ff3e3d + +const apm = require('../../../../..').start({ + serviceName: 'use-client-dynamodb', + captureExceptions: false, + centralConfig: false, + metricsInterval: 0, + cloudProvider: 'none', + stackTraceLimit: 4, // get it smaller for reviewing output + logLevel: 'info', +}); + +// const crypto = require('crypto') +const assert = require('assert'); +const { + DynamoDBClient, + ListTablesCommand, + CreateTableCommand, + PutItemCommand, + QueryCommand, + DeleteItemCommand, + DeleteTableCommand, +} = require('@aws-sdk/client-dynamodb'); +const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); + +const TEST_TABLE_NAME_PREFIX = 'elasticapmtest-table-'; + +// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/ +async function useClientDynamoDB(dynamoDBClient, tableName) { + const region = await dynamoDBClient.config.region(); + const log = apm.logger.child({ + 'event.module': 'app', + endpoint: dynamoDBClient.config.endpoint, + region, + }); + + let command; + let data; + + command = new ListTablesCommand(); + data = await dynamoDBClient.send(command); + assert( + apm.currentSpan === null, + 'DynamoDB span (or its HTTP span) should not be currentSpan after awaiting the task', + ); + log.info({ data }, 'query'); + + const tableIsPreexisting = data.TableNames.some((t) => t === tableName); + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/CreateTableCommand/ + if (!tableIsPreexisting) { + command = new CreateTableCommand({ + TableName: tableName, + AttributeDefinitions: [ + { + AttributeName: 'RECORD_ID', + AttributeType: 'S', + }, + ], + KeySchema: [ + { + AttributeName: 'RECORD_ID', + KeyType: 'HASH', + }, + ], + ProvisionedThroughput: { + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + }, + }); + data = await dynamoDBClient.send(command); + log.info({ data }, 'createTable'); + } + + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/PutItemCommand/ + command = new PutItemCommand({ + TableName: tableName, + Item: { + RECORD_ID: { S: '001' }, + }, + }); + data = await dynamoDBClient.send(command); + assert( + apm.currentSpan === null, + 'DynamoDB span (or its HTTP span) should not be currentSpan after awaiting the task', + ); + log.info({ data }, 'putItem'); + + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/QueryCommand/ + command = new QueryCommand({ + TableName: tableName, + KeyConditionExpression: 'RECORD_ID = :foo', + ExpressionAttributeValues: { + ':foo': { S: '001' }, + }, + }); + data = await dynamoDBClient.send(command); + assert( + apm.currentSpan === null, + 'DynamoDB span (or its HTTP span) should not be currentSpan after awaiting the task', + ); + log.info({ data }, 'query'); + + // Get a signed URL. + // This is interesting to test, because `getSignedUrl` uses the command + // `middlewareStack` -- including our added middleware -- **without** calling + // `dynamoDBClient.send()`. The test here is to ensure this doesn't break. + const customSpan = apm.startSpan('get-signed-url'); + const signedUrl = await getSignedUrl( + dynamoDBClient, + new QueryCommand({ + TableName: tableName, + KeyConditionExpression: 'RECORD_ID = :foo', + ExpressionAttributeValues: { ':foo': { S: '001' } }, + }), + { expiresIn: 3600 }, + ); + log.info({ signedUrl }, 'getSignedUrl'); + customSpan.end(); + + command = new QueryCommand({ + TableName: tableName + '-unexistent', + KeyConditionExpression: 'RECORD_ID = :foo', + ExpressionAttributeValues: { + ':foo': { S: '001' }, + }, + }); + try { + data = await dynamoDBClient.send(command); + throw new Error('expected ResourceNotFoundException error for query'); + } catch (err) { + log.info({ err }, 'query with error'); + const statusCode = err && err.$metadata && err.$metadata.httpStatusCode; + if (statusCode !== 400) { + throw err; + } + } + + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/DeleteItemCommand/ + command = new DeleteItemCommand({ + TableName: tableName, + Key: { + RECORD_ID: { S: '001' }, + }, + }); + data = await dynamoDBClient.send(command); + log.info({ data }, 'deleteItem'); + + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/DeleteTableCommand/ + command = new DeleteTableCommand({ TableName: tableName }); + data = await dynamoDBClient.send(command); + log.info({ data }, 'deleteTable'); +} + +// Return a timestamp of the form YYYYMMDDHHMMSS, which can be used in an DynamoDB +// table name name: +// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html +function getTimestamp() { + return new Date() + .toISOString() + .split('.')[0] + .replace(/[^0-9]/g, ''); +} + +// ---- mainline + +function main() { + const region = process.env.TEST_REGION || 'us-east-2'; + const endpoint = process.env.TEST_ENDPOINT || null; + const tableName = + process.env.TEST_TABLE_NAME || TEST_TABLE_NAME_PREFIX + getTimestamp(); + + // Guard against any table name being used because we will be creating and + // deleting records in it, and potentially *deleting* the table. + if (!tableName.startsWith(TEST_TABLE_NAME_PREFIX)) { + throw new Error( + `cannot use table name "${tableName}", it must start with ${TEST_TABLE_NAME_PREFIX}`, + ); + } + + const dynamoDBClient = new DynamoDBClient({ + region, + endpoint, + }); + + // Ensure an APM transaction so spans can happen. + const tx = apm.startTransaction('manual'); + + useClientDynamoDB(dynamoDBClient, tableName).then( + function () { + tx.end(); + dynamoDBClient.destroy(); + process.exitCode = 0; + }, + function (err) { + apm.logger.error(err, 'useClientDynamoDB rejected'); + tx.setOutcome('failure'); + tx.end(); + dynamoDBClient.destroy(); + process.exitCode = 1; + }, + ); +} + +main(); diff --git a/test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.mjs b/test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.mjs new file mode 100644 index 0000000000..6d8641ffee --- /dev/null +++ b/test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.mjs @@ -0,0 +1,57 @@ +/* + * 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'; + +// A small subset of "./use-client-dynamodb.js". Mainly this is to test that +// instrumentation of @aws-sdk/client-dynamodb in an ES module works. +// See "./use-client-dynamodb.js" for more details. +// +// Usage: +// node --experimental-loader=./loader.mjs --require=./start.js test/instrumentation/modules/@aws-sdk/fixtures/use-client-dynamodb.mjs + +import apm from '../../../../../index.js'; // 'elastic-apm-node' +import { DynamoDBClient, ListTablesCommand } from '@aws-sdk/client-dynamodb'; + +import assert from 'assert'; + +// ---- support functions + +async function useClientDynamoDB(dynamoDBClient, tableName) { + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/ListTablesCommand/ + const command = new ListTablesCommand(); + const data = await dynamoDBClient.send(command); + assert( + apm.currentSpan === null, + 'DynamoDB span (or its HTTP span) should not be currentSpan after awaiting the task', + ); + console.log('ListTables found %d tables', data.TableNames.length); +} + +// ---- mainline + +function main() { + const region = process.env.TEST_REGION || 'us-east-2'; + const endpoint = process.env.TEST_ENDPOINT || null; + const dynamodbClient = new DynamoDBClient({ region, endpoint }); + + const tx = apm.startTransaction('manual'); + useClientDynamoDB(dynamodbClient).then( + function () { + tx.end(); + dynamodbClient.destroy(); + process.exitCode = 0; + }, + function () { + tx.setOutcome('failure'); + tx.end(); + dynamodbClient.destroy(); + process.exitCode = 1; + }, + ); +} + +main(); diff --git a/test/instrumentation/modules/@aws-sdk/fixtures/use-client-s3.js b/test/instrumentation/modules/@aws-sdk/fixtures/use-client-s3.js index cea3447723..0def4d6c5d 100644 --- a/test/instrumentation/modules/@aws-sdk/fixtures/use-client-s3.js +++ b/test/instrumentation/modules/@aws-sdk/fixtures/use-client-s3.js @@ -65,31 +65,9 @@ const { } = require('@aws-sdk/client-s3'); const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); -const TEST_BUCKET_NAME_PREFIX = 'elasticapmtest-bucket-'; - -// ---- support functions +const { slurpStream } = require('../../../../_utils'); -/** - * Slurp everything from the given ReadableStream and return the content, - * converted to a string. - */ -async function slurpStream(stream) { - return new Promise((resolve, reject) => { - const chunks = []; - stream.on('error', (err) => { - reject(err); - }); - stream.on('readable', function () { - let chunk; - while ((chunk = this.read()) !== null) { - chunks.push(chunk); - } - }); - stream.on('end', () => { - resolve(Buffer.concat(chunks).toString('utf8')); - }); - }); -} +const TEST_BUCKET_NAME_PREFIX = 'elasticapmtest-bucket-'; // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/index.html async function useClientS3(s3Client, bucketName) { diff --git a/test/instrumentation/modules/fastify/fastify.test.js b/test/instrumentation/modules/fastify/fastify.test.js index 2587bfda53..43af147f56 100644 --- a/test/instrumentation/modules/fastify/fastify.test.js +++ b/test/instrumentation/modules/fastify/fastify.test.js @@ -15,6 +15,7 @@ if (isFastifyIncompat) { const test = require('tape'); const { runTestFixtures } = require('../../../_utils'); +const { NODE_VER_RANGE_IITM_GE14 } = require('../../../testconsts'); const testFixtures = [ { @@ -61,7 +62,7 @@ const testFixtures = [ ELASTIC_APM_CAPTURE_BODY: 'all', }, versionRanges: { - node: '^14.13.1 || ^16.0.0 || ^18.1.0 <20', // NODE_VER_RANGE_IITM minus node v12 because top-level `await` is used + node: NODE_VER_RANGE_IITM_GE14, // IITM and `import fastify from 'fastify'` fail without https://github.com/fastify/fastify/pull/2590 // I would have thought the only failure would be with a named import, // so I don't completely understand the issue. diff --git a/test/instrumentation/modules/http/http-esm.test.js b/test/instrumentation/modules/http/http-esm.test.js index 546219a909..538960852f 100644 --- a/test/instrumentation/modules/http/http-esm.test.js +++ b/test/instrumentation/modules/http/http-esm.test.js @@ -9,7 +9,10 @@ const test = require('tape'); const { runTestFixtures } = require('../../../_utils'); -const { NODE_VER_RANGE_IITM } = require('../../../testconsts'); +const { + NODE_VER_RANGE_IITM, + NODE_VER_RANGE_IITM_GE14, +} = require('../../../testconsts'); const testFixtures = [ { @@ -108,7 +111,7 @@ const testFixtures = [ ELASTIC_APM_USE_PATH_AS_TRANSACTION_NAME: 'true', }, versionRanges: { - node: '^14.13.1 || ^16.0.0 || ^18.1.0 <20', // NODE_VER_RANGE_IITM minus node v12 because top-level `await` is used + node: NODE_VER_RANGE_IITM_GE14, }, verbose: false, checkApmServer: (t, apmServer) => { @@ -134,7 +137,7 @@ const testFixtures = [ NODE_NO_WARNINGS: '1', // skip warnings about --experimental-loader }, versionRanges: { - node: '^14.13.1 || ^16.0.0 || ^18.1.0 <20', // NODE_VER_RANGE_IITM minus node v12 because top-level `await` is used + node: NODE_VER_RANGE_IITM_GE14, }, verbose: true, }, diff --git a/test/testconsts.js b/test/testconsts.js index 4ed121b274..badf8d2c5d 100644 --- a/test/testconsts.js +++ b/test/testconsts.js @@ -16,7 +16,9 @@ // - Current node v20 does not work with IITM // https://github.com/DataDog/import-in-the-middle/pull/27 const NODE_VER_RANGE_IITM = '^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.1.0 <20'; +const NODE_VER_RANGE_IITM_GE14 = '^14.13.1 || ^16.0.0 || ^18.1.0 <20'; // NODE_VER_RANGE_IITM minus node v12 module.exports = { NODE_VER_RANGE_IITM, + NODE_VER_RANGE_IITM_GE14, };