diff --git a/NEWS.md b/NEWS.md index 27b4dab4e5..ed7138b8b9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,60 +1,107 @@ -### v12.6.0 (2024-10-30) - -#### Features - -* Added ARN and provider to Lambda segments ([#2674](https://github.com/newrelic/node-newrelic/pull/2674)) ([a23294c](https://github.com/newrelic/node-newrelic/commit/a23294c2d2cc665de5df1a0f3c9099dedbfbb896)) -* Added IAST configurations for scan scheduling and restrictions ([#2645](https://github.com/newrelic/node-newrelic/pull/2645)) ([13a627a](https://github.com/newrelic/node-newrelic/commit/13a627a1a1529dd8f8c93d0b9f582457c019a63e)) - -#### Documentation - -* Updated compatibility report ([#2673](https://github.com/newrelic/node-newrelic/pull/2673)) ([e4d0503](https://github.com/newrelic/node-newrelic/commit/e4d0503934f7de45d3cdb6dbb4640a66cf8d8421)) - -#### Miscellaneous chores - -* Replaced static openssl cert usage with in-process cert ([#2671](https://github.com/newrelic/node-newrelic/pull/2671)) ([72872f4](https://github.com/newrelic/node-newrelic/commit/72872f4313fd9bc6d2b358a735dc76b9cda1a489)) - -### v12.5.2 (2024-10-23) - -#### Features - -* Updated shim/when instrumentation to use tracer to run in context ([#2642](https://github.com/newrelic/node-newrelic/pull/2642)) ([1a80ad5](https://github.com/newrelic/node-newrelic/commit/1a80ad56a6d502182a0db368f40443467f7943df)) - -#### Bug fixes - -* Fixed amqplib instrumentation via ESM ([#2668](https://github.com/newrelic/node-newrelic/pull/2668)) ([a36deba](https://github.com/newrelic/node-newrelic/commit/a36deba7ba7b46c4947fcb83db0a4f97bd4c0bf1)) - -#### Documentation - -* Remove SECURITY.md ([#2633](https://github.com/newrelic/node-newrelic/pull/2633)) ([41002cd](https://github.com/newrelic/node-newrelic/commit/41002cd1c423c378bfbe024ebe7dae03d02d2949)) -* Updated compatibility report ([#2670](https://github.com/newrelic/node-newrelic/pull/2670)) ([281c0fa](https://github.com/newrelic/node-newrelic/commit/281c0fa3be096a0ef3eef25b0f51df7ae8bf50bf)) -* Updated match custom-assertion jsdoc ([#2636](https://github.com/newrelic/node-newrelic/pull/2636)) ([c37abe5](https://github.com/newrelic/node-newrelic/commit/c37abe5eb4528493bb3950e376bf780d6cd29023)) - -#### Miscellaneous chores - -* Upgraded `import-in-the-middle` to work around a bug introduced in 1.11.1 ([#2618](https://github.com/newrelic/node-newrelic/pull/2618)) ([9ad476a](https://github.com/newrelic/node-newrelic/commit/9ad476a765eee472f510239b4265d10f0a50c666)) - -#### Tests - -* Migrated `aws-sdk-v2` and `aws-sdk-v3` tests to `node:test` ([#2620](https://github.com/newrelic/node-newrelic/pull/2620)) ([e0dc015](https://github.com/newrelic/node-newrelic/commit/e0dc01571087c3d53434d2d21d77206592137b66)) -* Migrated `test/unit/shim` to `node:test` ([#2599](https://github.com/newrelic/node-newrelic/pull/2599)) ([8d1acff](https://github.com/newrelic/node-newrelic/commit/8d1acffabf29ba6e0b5e20a675b87a81f893fc0a)) -* Migrated `test/versioned/amqplib` to `node:test` ([#2612](https://github.com/newrelic/node-newrelic/pull/2612)) ([7bdada6](https://github.com/newrelic/node-newrelic/commit/7bdada678c18997c268cf19c56262b870bae5673)) -* Migrated `test/versioned/express` to `node:test` ([#2609](https://github.com/newrelic/node-newrelic/pull/2609)) ([bd2f1a5](https://github.com/newrelic/node-newrelic/commit/bd2f1a53f8e03810e3c0aa5d042b6b256ec7346b)) -* Migrated bluebird versioned tests to `node:test` ([#2635](https://github.com/newrelic/node-newrelic/pull/2635)) ([6e28fad](https://github.com/newrelic/node-newrelic/commit/6e28fad70390ffaf4df7ccbc96c88f79bb5d2fe2)) -* Migrated last group of unit tests to `node:test` ([#2624](https://github.com/newrelic/node-newrelic/pull/2624)) ([386f546](https://github.com/newrelic/node-newrelic/commit/386f54682128d0dda8ad073a57cd45109c927fe1)) -* Migrated unit tests to `node:test` ([#2623](https://github.com/newrelic/node-newrelic/pull/2623)) ([86231b7](https://github.com/newrelic/node-newrelic/commit/86231b7dec5bc5807ae26a88a7b8f2ff1535d9c4)) -* Updated tests that relied on `tspl` by awating the `plan.completed` instead of calling `end` to avoid flaky tests ([#2610](https://github.com/newrelic/node-newrelic/pull/2610)) ([935ac14](https://github.com/newrelic/node-newrelic/commit/935ac14dbff7d11e797d290fb24a0d791ac9a61a)) -* Updated tests that used the context manager directly and instead use the tracer to access the segment context ([#2643](https://github.com/newrelic/node-newrelic/pull/2643)) ([b917b3e](https://github.com/newrelic/node-newrelic/commit/b917b3ea9416eaf64bf365f6f46a0d4eafdfc437)) -* Updated the mininum version of pg-native in pg-esm tests to align with the pg tests ([#2616](https://github.com/newrelic/node-newrelic/pull/2616)) ([16be714](https://github.com/newrelic/node-newrelic/commit/16be71404dcea903f3f7b7d4d238cc0a416d7b79)) -* Migrated `bunyan`, `pino`, and `winston` tests to `node:test` ([#2634](https://github.com/newrelic/node-newrelic/pull/2634)) ([69c1ab8](https://github.com/newrelic/node-newrelic/commit/69c1ab8951f8cd405986e879399dff716f839a78)) -* Migrated `fastify` tests to `node:test` ([#2632](https://github.com/newrelic/node-newrelic/pull/2632)) ([b522477](https://github.com/newrelic/node-newrelic/commit/b522477168c2049b12bcfd39ae485f9e5374f724)) -* Migrated block of unit tests to `node:test` ([#2607](https://github.com/newrelic/node-newrelic/pull/2607)) ([e33807b](https://github.com/newrelic/node-newrelic/commit/e33807b817852bb7cdc93c9b171250df17a3b867)) -* Migrated block of unit tests to `node:test` ([#2604](https://github.com/newrelic/node-newrelic/pull/2604)) ([cd90ce1](https://github.com/newrelic/node-newrelic/commit/cd90ce11908edc4376a704153f44d4f3ddfb6866)) -* Migrated block of unit tests to `node:test` ([#2593](https://github.com/newrelic/node-newrelic/pull/2593)) ([6d4d49e](https://github.com/newrelic/node-newrelic/commit/6d4d49e075d8c4c687d4730b65aa39177e384ce5)) - -#### Continuous integration - -* Added delay to site extension publishing to wait for NPM ([#2665](https://github.com/newrelic/node-newrelic/pull/2665)) ([e412020](https://github.com/newrelic/node-newrelic/commit/e412020865bab187d8c7d274cdc6973946286a1f)) - +### v12.7.0 (2024-11-11) + +#### Features + +* Added `cloud.resource_id` attribute to dynamo spans ([#2701](https://github.com/newrelic/node-newrelic/pull/2701)) ([904f41b](https://github.com/newrelic/node-newrelic/commit/904f41b26637394a24aa13f31ff94b100ae6d090)) +* Enhance Proxy Request Handling to Display Actual External URLs ([#2698](https://github.com/newrelic/node-newrelic/pull/2698)) ([3ef7bbe](https://github.com/newrelic/node-newrelic/commit/3ef7bbe595860234c021a02235e6fd0615da5f69)) + * Thanks for the contribution @mstarzec386 + +#### Documentation + +* Updated compatibility report ([#2712](https://github.com/newrelic/node-newrelic/pull/2712)) ([82f0e98](https://github.com/newrelic/node-newrelic/commit/82f0e9806c88d14cba2e0cdf47593e036107bd7d)) ([#2699](https://github.com/newrelic/node-newrelic/pull/2699)) ([4432c42](https://github.com/newrelic/node-newrelic/commit/4432c4215d68cc79333ee3828f1ecd55476c63d8)) + +#### Miscellaneous chores + +* Added a benchmark script for our sql parser ([#2708](https://github.com/newrelic/node-newrelic/pull/2708)) ([9b6de68](https://github.com/newrelic/node-newrelic/commit/9b6de6852747230b87a9873faffba6e5b39669f3)) +* Updated express-esm, generic-pool, grpc, & grpc-esm tests to node:test ([#2702](https://github.com/newrelic/node-newrelic/pull/2702)) ([a229bbf](https://github.com/newrelic/node-newrelic/commit/a229bbf0dd92c43fb2da077d8dce831b84c0c972)) + +#### Tests + +* Migrated `mysql` and `mysql2` versioned tests to `node:test` ([#2711](https://github.com/newrelic/node-newrelic/pull/2711)) ([fc767e0](https://github.com/newrelic/node-newrelic/commit/fc767e08d8b546d14c53c07bc2cfe65f3fb55368)) + +### v12.6.1 (2024-11-07) + +#### Features + +* added `cloud.aws.account_id` to default config ([#2691](https://github.com/newrelic/node-newrelic/pull/2691)) ([0ccee8e](https://github.com/newrelic/node-newrelic/commit/0ccee8e471b5568a36a5ef755f83f0da513548c8)) + +#### Bug fixes + +* Fixed issue parsing docker container id ([#2705](https://github.com/newrelic/node-newrelic/pull/2705)) ([0c897ab](https://github.com/newrelic/node-newrelic/commit/0c897ab7bea32daf0afbf75e2349f2fad008cc92)) + +#### Documentation + +* Updated compatibility report ([#2679](https://github.com/newrelic/node-newrelic/pull/2679)) ([3c19cdf](https://github.com/newrelic/node-newrelic/commit/3c19cdfed751dfa20ebba471a6cdd320f0610d95)) + +#### Miscellaneous chores + +* Moved recorders to `lib/metrics/recorders` ([#2666](https://github.com/newrelic/node-newrelic/pull/2666)) ([d8dfe84](https://github.com/newrelic/node-newrelic/commit/d8dfe843aebf275bda6d5c857cfe263039bc1a83)) + +#### Tests + +* Fixed file extensions for aws-sdk v2 and v3 versioned tests to reflect they have been migrated to `node:test` ([#2687](https://github.com/newrelic/node-newrelic/pull/2687)) ([4ec09ba](https://github.com/newrelic/node-newrelic/commit/4ec09ba34a825fa9decdc2d854eec6d24ee37ac4)) +* Migrated elasticsearch and esm-package versioned tests to `node:test` ([#2680](https://github.com/newrelic/node-newrelic/pull/2680)) ([0e0c2b2](https://github.com/newrelic/node-newrelic/commit/0e0c2b2f15e2179def4e67741fc988b7d16248d7)) +* Updated cls and connect tests to node:test ([#2676](https://github.com/newrelic/node-newrelic/pull/2676)) ([1e74434](https://github.com/newrelic/node-newrelic/commit/1e74434efd21c13199ad12af837129d251136c76)) +* Reorganized custom assertions and improved test reporter ([#2700](https://github.com/newrelic/node-newrelic/pull/2700)) ([9e98b18](https://github.com/newrelic/node-newrelic/commit/9e98b18b0f2768df9f75348975bebe904418a4a2)) +* Updated cassandra-driver tests to node:test ([#2678](https://github.com/newrelic/node-newrelic/pull/2678)) ([bd4f7ff](https://github.com/newrelic/node-newrelic/commit/bd4f7ff9df2cd1057e21a6ba3e28aac9fe02ecc6)) + +### v12.6.0 (2024-10-30) + +#### Features + +* Added ARN and provider to Lambda segments ([#2674](https://github.com/newrelic/node-newrelic/pull/2674)) ([a23294c](https://github.com/newrelic/node-newrelic/commit/a23294c2d2cc665de5df1a0f3c9099dedbfbb896)) +* Added IAST configurations for scan scheduling and restrictions ([#2645](https://github.com/newrelic/node-newrelic/pull/2645)) ([13a627a](https://github.com/newrelic/node-newrelic/commit/13a627a1a1529dd8f8c93d0b9f582457c019a63e)) + +#### Documentation + +* Updated compatibility report ([#2673](https://github.com/newrelic/node-newrelic/pull/2673)) ([e4d0503](https://github.com/newrelic/node-newrelic/commit/e4d0503934f7de45d3cdb6dbb4640a66cf8d8421)) + +#### Miscellaneous chores + +* Replaced static openssl cert usage with in-process cert ([#2671](https://github.com/newrelic/node-newrelic/pull/2671)) ([72872f4](https://github.com/newrelic/node-newrelic/commit/72872f4313fd9bc6d2b358a735dc76b9cda1a489)) + +### v12.5.2 (2024-10-23) + +#### Features + +* Updated shim/when instrumentation to use tracer to run in context ([#2642](https://github.com/newrelic/node-newrelic/pull/2642)) ([1a80ad5](https://github.com/newrelic/node-newrelic/commit/1a80ad56a6d502182a0db368f40443467f7943df)) + +#### Bug fixes + +* Fixed amqplib instrumentation via ESM ([#2668](https://github.com/newrelic/node-newrelic/pull/2668)) ([a36deba](https://github.com/newrelic/node-newrelic/commit/a36deba7ba7b46c4947fcb83db0a4f97bd4c0bf1)) + +#### Documentation + +* Remove SECURITY.md ([#2633](https://github.com/newrelic/node-newrelic/pull/2633)) ([41002cd](https://github.com/newrelic/node-newrelic/commit/41002cd1c423c378bfbe024ebe7dae03d02d2949)) +* Updated compatibility report ([#2670](https://github.com/newrelic/node-newrelic/pull/2670)) ([281c0fa](https://github.com/newrelic/node-newrelic/commit/281c0fa3be096a0ef3eef25b0f51df7ae8bf50bf)) +* Updated match custom-assertion jsdoc ([#2636](https://github.com/newrelic/node-newrelic/pull/2636)) ([c37abe5](https://github.com/newrelic/node-newrelic/commit/c37abe5eb4528493bb3950e376bf780d6cd29023)) + +#### Miscellaneous chores + +* Upgraded `import-in-the-middle` to work around a bug introduced in 1.11.1 ([#2618](https://github.com/newrelic/node-newrelic/pull/2618)) ([9ad476a](https://github.com/newrelic/node-newrelic/commit/9ad476a765eee472f510239b4265d10f0a50c666)) + +#### Tests + +* Migrated `aws-sdk-v2` and `aws-sdk-v3` tests to `node:test` ([#2620](https://github.com/newrelic/node-newrelic/pull/2620)) ([e0dc015](https://github.com/newrelic/node-newrelic/commit/e0dc01571087c3d53434d2d21d77206592137b66)) +* Migrated `test/unit/shim` to `node:test` ([#2599](https://github.com/newrelic/node-newrelic/pull/2599)) ([8d1acff](https://github.com/newrelic/node-newrelic/commit/8d1acffabf29ba6e0b5e20a675b87a81f893fc0a)) +* Migrated `test/versioned/amqplib` to `node:test` ([#2612](https://github.com/newrelic/node-newrelic/pull/2612)) ([7bdada6](https://github.com/newrelic/node-newrelic/commit/7bdada678c18997c268cf19c56262b870bae5673)) +* Migrated `test/versioned/express` to `node:test` ([#2609](https://github.com/newrelic/node-newrelic/pull/2609)) ([bd2f1a5](https://github.com/newrelic/node-newrelic/commit/bd2f1a53f8e03810e3c0aa5d042b6b256ec7346b)) +* Migrated bluebird versioned tests to `node:test` ([#2635](https://github.com/newrelic/node-newrelic/pull/2635)) ([6e28fad](https://github.com/newrelic/node-newrelic/commit/6e28fad70390ffaf4df7ccbc96c88f79bb5d2fe2)) +* Migrated last group of unit tests to `node:test` ([#2624](https://github.com/newrelic/node-newrelic/pull/2624)) ([386f546](https://github.com/newrelic/node-newrelic/commit/386f54682128d0dda8ad073a57cd45109c927fe1)) +* Migrated unit tests to `node:test` ([#2623](https://github.com/newrelic/node-newrelic/pull/2623)) ([86231b7](https://github.com/newrelic/node-newrelic/commit/86231b7dec5bc5807ae26a88a7b8f2ff1535d9c4)) +* Updated tests that relied on `tspl` by awating the `plan.completed` instead of calling `end` to avoid flaky tests ([#2610](https://github.com/newrelic/node-newrelic/pull/2610)) ([935ac14](https://github.com/newrelic/node-newrelic/commit/935ac14dbff7d11e797d290fb24a0d791ac9a61a)) +* Updated tests that used the context manager directly and instead use the tracer to access the segment context ([#2643](https://github.com/newrelic/node-newrelic/pull/2643)) ([b917b3e](https://github.com/newrelic/node-newrelic/commit/b917b3ea9416eaf64bf365f6f46a0d4eafdfc437)) +* Updated the mininum version of pg-native in pg-esm tests to align with the pg tests ([#2616](https://github.com/newrelic/node-newrelic/pull/2616)) ([16be714](https://github.com/newrelic/node-newrelic/commit/16be71404dcea903f3f7b7d4d238cc0a416d7b79)) +* Migrated `bunyan`, `pino`, and `winston` tests to `node:test` ([#2634](https://github.com/newrelic/node-newrelic/pull/2634)) ([69c1ab8](https://github.com/newrelic/node-newrelic/commit/69c1ab8951f8cd405986e879399dff716f839a78)) +* Migrated `fastify` tests to `node:test` ([#2632](https://github.com/newrelic/node-newrelic/pull/2632)) ([b522477](https://github.com/newrelic/node-newrelic/commit/b522477168c2049b12bcfd39ae485f9e5374f724)) +* Migrated block of unit tests to `node:test` ([#2607](https://github.com/newrelic/node-newrelic/pull/2607)) ([e33807b](https://github.com/newrelic/node-newrelic/commit/e33807b817852bb7cdc93c9b171250df17a3b867)) +* Migrated block of unit tests to `node:test` ([#2604](https://github.com/newrelic/node-newrelic/pull/2604)) ([cd90ce1](https://github.com/newrelic/node-newrelic/commit/cd90ce11908edc4376a704153f44d4f3ddfb6866)) +* Migrated block of unit tests to `node:test` ([#2593](https://github.com/newrelic/node-newrelic/pull/2593)) ([6d4d49e](https://github.com/newrelic/node-newrelic/commit/6d4d49e075d8c4c687d4730b65aa39177e384ce5)) + +#### Continuous integration + +* Added delay to site extension publishing to wait for NPM ([#2665](https://github.com/newrelic/node-newrelic/pull/2665)) ([e412020](https://github.com/newrelic/node-newrelic/commit/e412020865bab187d8c7d274cdc6973946286a1f)) + ### v12.5.1 (2024-09-23) #### Bug fixes diff --git a/bin/run-bench.js b/bin/run-bench.js old mode 100644 new mode 100755 index bc2f0e7dda..82127faae5 --- a/bin/run-bench.js +++ b/bin/run-bench.js @@ -1,3 +1,4 @@ +#!/usr/bin/env node /* * Copyright 2022 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 diff --git a/changelog.json b/changelog.json index d529610c94..4ed7434c98 100644 --- a/changelog.json +++ b/changelog.json @@ -1,6 +1,29 @@ { "repository": "newrelic/node-newrelic", "entries": [ + { + "version": "12.7.0", + "changes": { + "security": [], + "bugfixes": [], + "features": [ + "Added `cloud.resource_id` attribute to dynamo spans", + "Enhance Proxy Request Handling to Display Actual External URLs" + ] + } + }, + { + "version": "12.6.1", + "changes": { + "security": [], + "bugfixes": [ + "Fixed issue parsing docker container id" + ], + "features": [ + "added `cloud.aws.account_id` to default config" + ] + } + }, { "version": "12.6.0", "changes": { @@ -657,4 +680,4 @@ } } ] -} +} \ No newline at end of file diff --git a/compatibility.md b/compatibility.md index abed2583b6..97d60ba758 100644 --- a/compatibility.md +++ b/compatibility.md @@ -13,30 +13,30 @@ version. | --- | --- | --- | --- | | `@apollo/gateway` | 2.3.0 | 2.9.3 | `@newrelic/apollo-server-plugin@1.0.0` | | `@apollo/server` | 4.0.0 | 4.11.2 | `@newrelic/apollo-server-plugin@2.1.0` | -| `@aws-sdk/client-bedrock-runtime` | 3.474.0 | 3.682.0 | 11.13.0 | -| `@aws-sdk/client-dynamodb` | 3.0.0 | 3.682.0 | 8.7.1 | -| `@aws-sdk/client-sns` | 3.0.0 | 3.682.0 | 8.7.1 | -| `@aws-sdk/client-sqs` | 3.0.0 | 3.682.0 | 8.7.1 | -| `@aws-sdk/lib-dynamodb` | 3.377.0 | 3.682.0 | 8.7.1 | +| `@aws-sdk/client-bedrock-runtime` | 3.474.0 | 3.687.0 | 11.13.0 | +| `@aws-sdk/client-dynamodb` | 3.0.0 | 3.687.0 | 8.7.1 | +| `@aws-sdk/client-sns` | 3.0.0 | 3.687.0 | 8.7.1 | +| `@aws-sdk/client-sqs` | 3.0.0 | 3.687.0 | 8.7.1 | +| `@aws-sdk/lib-dynamodb` | 3.377.0 | 3.687.0 | 8.7.1 | | `@aws-sdk/smithy-client` | 3.47.0 | 3.374.0 | 8.7.1 | | `@elastic/elasticsearch` | 7.16.0 | 8.15.1 | 11.9.0 | | `@grpc/grpc-js` | 1.4.0 | 1.12.2 | 8.17.0 | | `@hapi/hapi` | 20.1.2 | 21.3.12 | 9.0.0 | | `@koa/router` | 11.0.2 | 13.1.0 | 3.2.0 | -| `@langchain/core` | 0.1.17 | 0.3.16 | 11.13.0 | -| `@nestjs/cli` | 9.0.0 | 10.4.5 | 10.1.0 | -| `@prisma/client` | 5.0.0 | 5.21.1 | 11.0.0 | +| `@langchain/core` | 0.1.17 | 0.3.17 | 11.13.0 | +| `@nestjs/cli` | 9.0.0 | 10.4.7 | 10.1.0 | +| `@prisma/client` | 5.0.0 | 5.22.0 | 11.0.0 | | `@smithy/smithy-client` | 2.0.0 | 3.4.2 | 11.0.0 | | `amqplib` | 0.5.0 | 0.10.4 | 2.0.0 | | `apollo-server` | 3.0.0 | 3.13.0 | `@newrelic/apollo-server-plugin@1.0.0` | | `apollo-server-express` | 3.0.0 | 3.13.0 | `@newrelic/apollo-server-plugin@1.0.0` | -| `aws-sdk` | 2.2.48 | 2.1691.0 | 6.2.0 | +| `aws-sdk` | 2.2.48 | 2.1692.0 | 6.2.0 | | `bluebird` | 2.0.0 | 3.7.2 | 1.27.0 | | `bunyan` | 1.8.12 | 1.8.15 | 9.3.0 | | `cassandra-driver` | 3.4.0 | 4.7.2 | 1.7.1 | | `connect` | 3.0.0 | 3.7.0 | 2.6.0 | | `express` | 4.6.0 | 4.21.1 | 2.6.0 | -| `fastify` | 2.0.0 | 5.0.0 | 8.5.0 | +| `fastify` | 2.0.0 | 5.1.0 | 8.5.0 | | `generic-pool` | 3.0.0 | 3.9.0 | 0.9.0 | | `ioredis` | 4.0.0 | 5.4.1 | 1.26.2 | | `kafkajs` | 2.0.0 | 2.2.4 | 11.19.0 | @@ -46,9 +46,9 @@ version. | `memcached` | 2.2.0 | 2.2.2 | 1.26.2 | | `mongodb` | 4.1.4 | 6.10.0 | 1.32.0 | | `mysql` | 2.2.0 | 2.18.1 | 1.32.0 | -| `mysql2` | 2.0.0 | 3.11.3 | 1.32.0 | -| `next` | 13.4.19 | 15.0.2 | 12.0.0 | -| `openai` | 4.0.0 | 4.69.0 | 11.13.0 | +| `mysql2` | 2.0.0 | 3.11.4 | 1.32.0 | +| `next` | 13.4.19 | 15.0.3 | 12.0.0 | +| `openai` | 4.0.0 | 4.71.1 | 11.13.0 | | `pg` | 8.2.0 | 8.13.1 | 9.0.0 | | `pg-native` | 3.0.0 | 3.2.0 | 9.0.0 | | `pino` | 7.0.0 | 9.5.0 | 8.11.0 | @@ -58,7 +58,7 @@ version. | `superagent` | 3.0.0 | 10.1.1 | 4.9.0 | | `undici` | 5.0.0 | 6.20.1 | 11.1.0 | | `when` | 3.7.0 | 3.7.8 | 1.26.2 | -| `winston` | 3.0.0 | 3.15.0 | 8.11.0 | +| `winston` | 3.0.0 | 3.17.0 | 8.11.0 | *When package is not specified, support is within the `newrelic` package. diff --git a/lib/config/default.js b/lib/config/default.js index d9a95ad35e..95ca5f3f79 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -413,6 +413,17 @@ defaultConfig.definition = () => ({ default: false } }, + cloud: { + aws: { + /** + * The AWS account ID for the AWS account associated with this app. + */ + account_id: { + formatter: int, + default: null + } + } + }, /** * Options regarding collecting system information. Used for system * utilization based pricing scheme. diff --git a/lib/db/parsed-statement.js b/lib/db/parsed-statement.js index c509e22ab2..ac876fe977 100644 --- a/lib/db/parsed-statement.js +++ b/lib/db/parsed-statement.js @@ -5,9 +5,6 @@ 'use strict' -const { DB, ALL } = require('../metrics/names') -const { DESTINATIONS } = require('../config/attribute-filter') - function ParsedStatement(type, operation, collection, raw) { this.type = type this.operation = operation @@ -21,55 +18,4 @@ function ParsedStatement(type, operation, collection, raw) { } } -ParsedStatement.prototype.recordMetrics = function recordMetrics(segment, scope) { - const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction - const type = transaction.isWeb() ? DB.WEB : DB.OTHER - const thisTypeSlash = this.type + '/' - const operation = DB.OPERATION + '/' + thisTypeSlash + this.operation - - // Note, an operation metric should _always_ be created even if the action was - // a statement. This is part of the spec. - - // Rollups - transaction.measure(operation, null, duration, exclusive) - transaction.measure(DB.PREFIX + type, null, duration, exclusive) - transaction.measure(DB.PREFIX + thisTypeSlash + type, null, duration, exclusive) - transaction.measure(DB.PREFIX + thisTypeSlash + ALL, null, duration, exclusive) - transaction.measure(DB.ALL, null, duration, exclusive) - - // If we can parse the SQL statement, create a 'statement' metric, and use it - // as the scoped metric for transaction breakdowns. Otherwise, skip the - // 'statement' metric and use the 'operation' metric as the scoped metric for - // transaction breakdowns. - let collection - if (this.collection) { - collection = DB.STATEMENT + '/' + thisTypeSlash + this.collection + '/' + this.operation - transaction.measure(collection, null, duration, exclusive) - if (scope) { - transaction.measure(collection, scope, duration, exclusive) - } - } else if (scope) { - transaction.measure(operation, scope, duration, exclusive) - } - - // This recorder is side-effectful Because we are depending on the recorder - // setting the transaction name, recorders must always be run before generating - // the final transaction trace - segment.name = collection || operation - - // Datastore instance metrics. - const attributes = segment.attributes.get(DESTINATIONS.TRANS_SEGMENT) - if (attributes.host && attributes.port_path_or_id) { - const instanceName = - DB.INSTANCE + '/' + thisTypeSlash + attributes.host + '/' + attributes.port_path_or_id - transaction.measure(instanceName, null, duration, exclusive) - } - - if (this.raw) { - transaction.agent.queries.add(segment, this.type.toLowerCase(), this.raw, this.trace) - } -} - module.exports = ParsedStatement diff --git a/lib/instrumentation/aws-sdk/v3/dynamodb.js b/lib/instrumentation/aws-sdk/v3/dynamodb.js index 0d51ed0675..0ddaceb417 100644 --- a/lib/instrumentation/aws-sdk/v3/dynamodb.js +++ b/lib/instrumentation/aws-sdk/v3/dynamodb.js @@ -72,21 +72,66 @@ function dynamoMiddleware(shim, config, next, context) { } } -const dynamoMiddlewareConfig = { - middleware: dynamoMiddleware, - init(shim) { - shim.setDatastore(shim.DYNAMODB) - return true - }, - type: InstrumentationDescriptor.TYPE_DATASTORE, - config: { - name: 'NewRelicDynamoMiddleware', - step: 'initialize', - priority: 'high', - override: true +/** + * Wraps the deserialize middleware step to add the + * cloud.resource_id segment attributes for the AWS command + * + * @param {Shim} shim + * @param {Object} config AWS command configuration + * @param {function} next next function in middleware chain + * @returns {function} + */ +function resourceIdMiddlerware(shim, config, next) { + return async function wrappedResourceIdMiddlerware(args) { + let region + try { + region = await config.region() + const segment = shim.getSegment() + + const accountId = shim.agent.config.cloud.aws.account_id + + if (accountId) { + const attributes = segment.getAttributes() + segment.addAttribute( + 'cloud.resource_id', + `arn:aws:dynamodb:${region}:${accountId}:table/${attributes.collection}` + ) + } + } catch (err) { + shim.logger.debug(err, 'Failed to add AWS cloud resource id to segment') + } + + return next(args) } } +const dynamoMiddlewareConfig = [ + { + middleware: dynamoMiddleware, + init(shim) { + shim.setDatastore(shim.DYNAMODB) + return true + }, + type: InstrumentationDescriptor.TYPE_DATASTORE, + config: { + name: 'NewRelicDynamoMiddleware', + step: 'initialize', + priority: 'high', + override: true + } + }, + { + middleware: resourceIdMiddlerware, + type: InstrumentationDescriptor.TYPE_GENERIC, + config: { + name: 'NewRelicCloudResource', + step: 'deserialize', + priority: 'low', + override: true + } + } +] + module.exports = { dynamoMiddlewareConfig } diff --git a/lib/instrumentation/aws-sdk/v3/smithy-client.js b/lib/instrumentation/aws-sdk/v3/smithy-client.js index 62a2ff175c..a3ce0c3288 100644 --- a/lib/instrumentation/aws-sdk/v3/smithy-client.js +++ b/lib/instrumentation/aws-sdk/v3/smithy-client.js @@ -17,8 +17,8 @@ const middlewareByClient = { BedrockRuntime: [...middlewareConfig, bedrockMiddlewareConfig], SNS: [...middlewareConfig, snsMiddlewareConfig], SQS: [...middlewareConfig, sqsMiddlewareConfig], - DynamoDB: [...middlewareConfig, dynamoMiddlewareConfig], - DynamoDBDocument: [...middlewareConfig, dynamoMiddlewareConfig] + DynamoDB: [...middlewareConfig, ...dynamoMiddlewareConfig], + DynamoDBDocument: [...middlewareConfig, ...dynamoMiddlewareConfig] } module.exports = function instrumentSmithyClient(shim, smithyClientExport) { diff --git a/lib/instrumentation/core/http-outbound.js b/lib/instrumentation/core/http-outbound.js index c1d6d2d52b..fb3d2c4918 100644 --- a/lib/instrumentation/core/http-outbound.js +++ b/lib/instrumentation/core/http-outbound.js @@ -15,6 +15,7 @@ const copy = require('../../util/copy') const symbols = require('../../symbols') const http = require('http') const synthetics = require('../../synthetics') +const { URL } = require('node:url') const NAMES = require('../../metrics/names') const DEFAULT_HOST = 'localhost' @@ -93,6 +94,27 @@ function extractHostPort(opts) { return { host, hostname, port } } +/** + * Extracts the host, hostname, and port from HTTP request options when using a proxy + * + * @param {object} opts HTTP request options + * @returns {object|null} { host, hostname, port } if proxy request is detected, or null otherwise. + */ +function extractHostPortViaProxy(opts) { + const pathname = opts.pathname || opts.path + + if (pathname && (pathname.startsWith('https://') || pathname.startsWith('http://'))) { + const url = new URL(pathname) + return { + host: url.host, + hostname: url.hostname, + port: url.port || url.protocol === 'https:' ? '443' : '80' + } + } + + return null +} + /** * Instruments an outbound HTTP request. * @@ -103,7 +125,9 @@ function extractHostPort(opts) { */ module.exports = function instrumentOutbound(agent, opts, makeRequest) { opts = parseOpts(opts) - const { host, hostname, port } = extractHostPort(opts) + + const viaProxy = extractHostPortViaProxy(opts) + const { host, hostname, port } = viaProxy ? viaProxy : extractHostPort(opts) if (!hostname || port < 1) { logger.warn('Invalid host name (%s) or port (%s) for outbound request.', hostname, port) diff --git a/lib/instrumentation/express.js b/lib/instrumentation/express.js index ef1676487b..f245834218 100644 --- a/lib/instrumentation/express.js +++ b/lib/instrumentation/express.js @@ -127,7 +127,7 @@ function wrapResponse(shim, response) { if (!shim.isFunction(cb)) { ++cbIdx cb = function defaultRenderCB(err, str) { - // https://github.com/expressjs/express/blob/4.x/lib/response.js#L961-L962 + // https://github.com/expressjs/express/blob/4.x/lib/response.js#L961-L962 if (err) { return res.req.next(err) } diff --git a/lib/llm-events/aws-bedrock/bedrock-command.js b/lib/llm-events/aws-bedrock/bedrock-command.js index 937b48c2c0..58ebc14a10 100644 --- a/lib/llm-events/aws-bedrock/bedrock-command.js +++ b/lib/llm-events/aws-bedrock/bedrock-command.js @@ -85,7 +85,11 @@ class BedrockCommand { result = this.#body.prompt } else if (this.isClaude3() === true) { result = this.#body?.messages?.reduce((acc, curr) => { - acc += curr?.content ?? '' + if (typeof curr?.content === 'string') { + acc += curr?.content + } else if (Object.keys(curr?.content).length) { + acc += curr?.content.text ?? '' + } return acc }, '') } diff --git a/lib/metrics/recorders/database-operation.js b/lib/metrics/recorders/database-operation.js new file mode 100644 index 0000000000..11e4c6ea46 --- /dev/null +++ b/lib/metrics/recorders/database-operation.js @@ -0,0 +1,62 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const metrics = require('../names') + +/** + * Records all the metrics required for database operations. + * + * - `recordOperationMetrics(segment [, scope])` + * + * @private + * @this DatastoreShim + * @implements {MetricFunction} + * @param {TraceSegment} segment - The segment being recorded. + * @param {string} [scope] - The scope of the segment. + * @see DatastoreShim#recordOperation + * @see MetricFunction + */ +function recordOperationMetrics(segment, scope) { + if (!segment) { + return + } + + const duration = segment.getDurationInMillis() + const exclusive = segment.getExclusiveDurationInMillis() + const transaction = segment.transaction + const type = transaction.isWeb() ? 'allWeb' : 'allOther' + const operation = segment.name + + if (scope) { + transaction.measure(operation, scope, duration, exclusive) + } + + transaction.measure(operation, null, duration, exclusive) + transaction.measure(metrics.DB.PREFIX + type, null, duration, exclusive) + transaction.measure(metrics.DB.ALL, null, duration, exclusive) + transaction.measure(this._metrics.ALL, null, duration, exclusive) + transaction.measure( + metrics.DB.PREFIX + this._metrics.PREFIX + '/' + type, + null, + duration, + exclusive + ) + + const attributes = segment.getAttributes() + if (attributes.host && attributes.port_path_or_id) { + const instanceName = [ + metrics.DB.INSTANCE, + this._metrics.PREFIX, + attributes.host, + attributes.port_path_or_id + ].join('/') + + transaction.measure(instanceName, null, duration, exclusive) + } +} + +module.exports = recordOperationMetrics diff --git a/lib/metrics/recorders/database.js b/lib/metrics/recorders/database.js new file mode 100644 index 0000000000..7d4e172ec6 --- /dev/null +++ b/lib/metrics/recorders/database.js @@ -0,0 +1,68 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const { DB, ALL } = require('../names') +const { DESTINATIONS } = require('../../config/attribute-filter') + +/** + * @this ParsedStatement + * @param {TraceSegment} segment - The segment being recorded. + * @param {string} [scope] - The scope of the segment. + */ + +function recordQueryMetrics(segment, scope) { + const duration = segment.getDurationInMillis() + const exclusive = segment.getExclusiveDurationInMillis() + const transaction = segment.transaction + const type = transaction.isWeb() ? DB.WEB : DB.OTHER + const thisTypeSlash = this.type + '/' + const operation = DB.OPERATION + '/' + thisTypeSlash + this.operation + + // Note, an operation metric should _always_ be created even if the action was + // a statement. This is part of the spec. + + // Rollups + transaction.measure(operation, null, duration, exclusive) + transaction.measure(DB.PREFIX + type, null, duration, exclusive) + transaction.measure(DB.PREFIX + thisTypeSlash + type, null, duration, exclusive) + transaction.measure(DB.PREFIX + thisTypeSlash + ALL, null, duration, exclusive) + transaction.measure(DB.ALL, null, duration, exclusive) + + // If we can parse the SQL statement, create a 'statement' metric, and use it + // as the scoped metric for transaction breakdowns. Otherwise, skip the + // 'statement' metric and use the 'operation' metric as the scoped metric for + // transaction breakdowns. + let collection + if (this.collection) { + collection = DB.STATEMENT + '/' + thisTypeSlash + this.collection + '/' + this.operation + transaction.measure(collection, null, duration, exclusive) + if (scope) { + transaction.measure(collection, scope, duration, exclusive) + } + } else if (scope) { + transaction.measure(operation, scope, duration, exclusive) + } + + // This recorder is side-effectful Because we are depending on the recorder + // setting the transaction name, recorders must always be run before generating + // the final transaction trace + segment.name = collection || operation + + // Datastore instance metrics. + const attributes = segment.attributes.get(DESTINATIONS.TRANS_SEGMENT) + if (attributes.host && attributes.port_path_or_id) { + const instanceName = + DB.INSTANCE + '/' + thisTypeSlash + attributes.host + '/' + attributes.port_path_or_id + transaction.measure(instanceName, null, duration, exclusive) + } + + if (this.raw) { + segment.transaction.agent.queries.add(segment, this.type.toLowerCase(), this.raw, this.trace) + } +} + +module.exports = recordQueryMetrics diff --git a/lib/metrics/recorders/middleware.js b/lib/metrics/recorders/middleware.js new file mode 100644 index 0000000000..61693cfb88 --- /dev/null +++ b/lib/metrics/recorders/middleware.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * Creates a recorder for middleware metrics. + * + * @private + * @param {object} _shim instance of shim + * @param {string} metricName name of metric + * @returns {Function} recorder for middleware + */ +function makeMiddlewareRecorder(_shim, metricName) { + return function middlewareMetricRecorder(segment, scope) { + const duration = segment.getDurationInMillis() + const exclusive = segment.getExclusiveDurationInMillis() + const transaction = segment.transaction + + if (scope) { + transaction.measure(metricName, scope, duration, exclusive) + } + transaction.measure(metricName, null, duration, exclusive) + } +} + +module.exports = makeMiddlewareRecorder diff --git a/lib/shim/datastore-shim.js b/lib/shim/datastore-shim.js index 9c1b85bfcc..70d103ee0e 100644 --- a/lib/shim/datastore-shim.js +++ b/lib/shim/datastore-shim.js @@ -15,7 +15,9 @@ const Shim = require('./shim') const urltils = require('../util/urltils') const util = require('util') const specs = require('./specs') +const recordOperationMetrics = require('../../lib/metrics/recorders/database-operation') const { DatastoreParameters } = specs.params +const recordQueryMetrics = require('../../lib/metrics/recorders/database') /** * An enumeration of well-known datastores so that new instrumentations can use @@ -284,7 +286,7 @@ function recordOperation(nodule, properties, opSpec) { if (!segDesc?.name?.startsWith(shim._metrics.OPERATION)) { segDesc.name = shim._metrics.OPERATION + segDesc.name } - segDesc.recorder = _recordOperationMetrics.bind(shim) + segDesc.recorder = recordOperationMetrics.bind(shim) } return segDesc @@ -567,7 +569,7 @@ function _recordQuery(suffix, nodule, properties, querySpec) { const name = (parsed.collection || 'other') + '/' + parsed.operation + suffix shim.logger.trace('Found and parsed query %s -> %s', parsed.type, name) segDesc.name = shim._metrics.STATEMENT + name - segDesc.recorder = _recordQueryMetrics.bind(null, parsed) + segDesc.recorder = recordQueryMetrics.bind(parsed) } return segDesc @@ -598,72 +600,6 @@ function _getSpec({ spec, shim, fn, fnName, args }) { return dsSpec } -/** - * Records all query metrics when a segment is active - * - * @private - * @param {ParsedStatement} parsed instance of ParsedStatement - * @param {TraceSegment} segment active segment - * @param {string} scope scope of metrics if it exists - */ -function _recordQueryMetrics(parsed, segment, scope) { - if (segment) { - parsed.recordMetrics(segment, scope) - } -} - -/** - * Records all the metrics required for database operations. - * - * - `_recordOperationMetrics(segment [, scope])` - * - * @private - * @this DatastoreShim - * @implements {MetricFunction} - * @param {TraceSegment} segment - The segment being recorded. - * @param {string} [scope] - The scope of the segment. - * @see DatastoreShim#recordOperation - * @see MetricFunction - */ -function _recordOperationMetrics(segment, scope) { - if (!segment) { - return - } - - const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction - const type = transaction.isWeb() ? 'allWeb' : 'allOther' - const operation = segment.name - - if (scope) { - transaction.measure(operation, scope, duration, exclusive) - } - - transaction.measure(operation, null, duration, exclusive) - transaction.measure(metrics.DB.PREFIX + type, null, duration, exclusive) - transaction.measure(metrics.DB.ALL, null, duration, exclusive) - transaction.measure(this._metrics.ALL, null, duration, exclusive) - transaction.measure( - metrics.DB.PREFIX + this._metrics.PREFIX + '/' + type, - null, - duration, - exclusive - ) - - const attributes = segment.getAttributes() - if (attributes.host && attributes.port_path_or_id) { - const instanceName = [ - metrics.DB.INSTANCE, - this._metrics.PREFIX, - attributes.host, - attributes.port_path_or_id - ].join('/') - - transaction.measure(instanceName, null, duration, exclusive) - } -} - /** * Extracts the query string from the arguments according to the given spec. * diff --git a/lib/shim/webframework-shim/middleware.js b/lib/shim/webframework-shim/middleware.js index d76f4cab86..ed7cde37e7 100644 --- a/lib/shim/webframework-shim/middleware.js +++ b/lib/shim/webframework-shim/middleware.js @@ -13,6 +13,7 @@ const { } = require('./common') const { assignCLMSymbol } = require('../../util/code-level-metrics') const { RecorderSpec } = require('../specs') +const makeMiddlewareRecorder = require('../../metrics/recorders/middleware') const MIDDLEWARE_TYPE_DETAILS = { APPLICATION: { name: 'Mounted App: ', path: true, record: false }, @@ -88,7 +89,7 @@ function constructRecorder({ txInfo, typeDetails, shim, metricName }) { let recorder = null if (typeDetails.record) { const stackPath = txInfo.transaction.nameState.getPath() || '' - recorder = _makeMiddlewareRecorder(shim, metricName + '/' + stackPath) + recorder = makeMiddlewareRecorder(shim, metricName + '/' + stackPath) } return recorder } @@ -325,27 +326,6 @@ module.exports._recordMiddleware = function _recordMiddleware(shim, middleware, ) } -/** - * Creates a recorder for middleware metrics. - * - * @private - * @param {object} _shim instance of shim - * @param {string} metricName name of metric - * @returns {Function} recorder for middleware - */ -function _makeMiddlewareRecorder(_shim, metricName) { - return function middlewareMetricRecorder(segment, scope) { - const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction - - if (scope) { - transaction.measure(metricName, scope, duration, exclusive) - } - transaction.measure(metricName, null, duration, exclusive) - } -} - /** * Wrap the `next` middleware function and push on our name state if we find it. We only want to * push the name state if there is a next so that we can safely remove it diff --git a/lib/utilization/docker-info.js b/lib/utilization/docker-info.js index d830b95998..043f67bf0c 100644 --- a/lib/utilization/docker-info.js +++ b/lib/utilization/docker-info.js @@ -116,11 +116,27 @@ function fetchDockerVendorInfo(agent, callback, logger = log) { logger.debug( `${CGROUPS_V2_PATH} not found, trying to parse container id from ${CGROUPS_V1_PATH}` ) - findCGroupsV1(callback) + findCGroupsV1(callback, logger) return } - parseCGroupsV2(data, callback) + parseCGroupsV2( + data, + (_, v2Data) => { + if (v2Data !== null) { + // We found a valid Docker identifier in the v2 file, so we are going + // to prioritize it. + return callback(null, v2Data) + } + + // For some reason, we have a /proc/self/mountinfo but it does not have + // any Docker information in it (that we have detected). So we will + // fall back to trying the cgroups v1 file. + logger.debug(`Attempting to fall back to cgroups v1 parsing.`) + findCGroupsV1(callback, logger) + }, + logger + ) }) } @@ -136,6 +152,7 @@ function parseCGroupsV2(data, callback, logger = log) { const containerLine = new RegExp('/docker/containers/([0-9a-f]{64})/') const line = containerLine.exec(data) if (line) { + logger.debug(`Found docker id from cgroups v2: ${line[1]}`) callback(null, { id: line[1] }) } else { logger.debug(`Found ${CGROUPS_V2_PATH} but failed to parse Docker container id.`) @@ -169,7 +186,8 @@ function findCGroupsV1(callback, logger = log) { }) if (id) { - vendorInfo = { id: id } + vendorInfo = { id } + logger.debug(`Found docker id from cgroups v1: ${id}`) callback(null, vendorInfo) } else { logger.debug('No matching cpu group found.') diff --git a/package.json b/package.json index 97b4256870..b04804200e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "newrelic", - "version": "12.6.0", + "version": "12.7.0", "author": "New Relic Node.js agent team ", "license": "Apache-2.0", "contributors": [ diff --git a/test/benchmark/Readme.md b/test/benchmark/Readme.md new file mode 100644 index 0000000000..b87843c0f0 --- /dev/null +++ b/test/benchmark/Readme.md @@ -0,0 +1,18 @@ +## Running Benchmarks + +The easiest way to run all benchmarks is by using the npm script: + +```sh +> npm run bench +``` + +If you need to run a single benchmark suite, for example the sql parser +benchmarks, it is easiest to run and view the output by: + +```sh +> ./bin/run-bench.js lib/db/query-parsers/sql.bench.js && \ + cat benchmark_results/$(ls -1rt benchmark_results | tail -n 1) +``` + +Notice that we do not specify the leading "test/benchmark/" when providing +the benchmark file we want to run. diff --git a/test/benchmark/lib/db/query-parsers/sql.bench.js b/test/benchmark/lib/db/query-parsers/sql.bench.js new file mode 100644 index 0000000000..2f162fdee5 --- /dev/null +++ b/test/benchmark/lib/db/query-parsers/sql.bench.js @@ -0,0 +1,78 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const parseSql = require('../../../../../lib/db/query-parsers/sql') +const benchmark = require('../../../../lib/benchmark') +const suite = benchmark.createBenchmark({ name: 'parseSql', runs: 200_000 }) + +const tests = [ + { + name: 'leading-multi-line-comment-single-line', + fn: leadingMultiLineCommentSingleLine + }, + { + name: 'leading-multi-line-comment-multiple-lines', + fn: leadingMultiLineCommentMultipleLines + }, + { + name: 'single-embedded-multi-line-comment', + fn: singleEmbeddedMultiLineComment + }, + { + name: 'multiple-embedded-multi-line-comments', + fn: multipleEmbeddedMultiLineComments + }, + { + name: 'select-statement', + fn: selectStatement + }, + { + name: 'update-statement', + fn: updateStatement + }, + { + name: 'delete-statement', + fn: deleteStatement + } +] + +for (const test of tests) { + suite.add(test) +} +suite.run() + +function leadingMultiLineCommentSingleLine() { + parseSql(`/* insert into bar some stuff */ insert into foo (col1)`) +} + +function leadingMultiLineCommentMultipleLines() { + parseSql(`/*insert into bar some stuff*/ + insert into foo (col1) values('bar') + `) +} + +function singleEmbeddedMultiLineComment() { + parseSql(`insert /* insert into bar */ into foo`) +} + +function multipleEmbeddedMultiLineComments() { + parseSql(`insert /* comments! */ into /* insert into bar some stuff */ foo /* MOAR */ (col1)`) +} + +function selectStatement() { + parseSql( + `with foobar (col1) as cte select * from foo as a join on cte using (col1) where a.bar = 'baz'` + ) +} + +function updateStatement() { + parseSql(`update foo set bar = 'baz' where col1 = 1`) +} + +function deleteStatement() { + parseSql(`delete from foo where bar = 'baz'`) +} diff --git a/test/integration/instrumentation/http-outbound.tap.js b/test/integration/instrumentation/http-outbound.tap.js index da629ebf8f..4efe2b146e 100644 --- a/test/integration/instrumentation/http-outbound.tap.js +++ b/test/integration/instrumentation/http-outbound.tap.js @@ -111,6 +111,45 @@ tap.test('external requests', function (t) { } }) + t.test('should recognize requests via proxy correctly', function (t) { + const proxyUrl = 'https://www.google.com/proxy/path' + const proxyServer = http.createServer(function onRequest(req, res) { + t.equal(req.url, proxyUrl) + req.resume() + res.end('ok') + }) + t.teardown(() => proxyServer.close()) + + proxyServer.listen(0) + + helper.runInTransaction(agent, function inTransaction() { + const opts = { + host: 'localhost', + port: proxyServer.address().port, + path: proxyUrl, + protocol: 'http:' + } + + const req = http.get(opts, function onResponse(res) { + res.resume() + res.once('end', function () { + const segment = agent.tracer.getTransaction().trace.root.children[0] + t.equal( + segment.name, + `External/www.google.com/proxy/path`, + 'should name segment as an external service' + ) + t.end() + }) + }) + + req.on('error', function onError(err) { + t.fail('Request should not error: ' + err.message) + t.end() + }) + }) + }) + t.test('should not duplicate the external segment', function (t) { const https = require('https') diff --git a/test/integration/utilization/docker-info.tap.js b/test/integration/utilization/docker-info.tap.js index 4370556131..248ace74d4 100644 --- a/test/integration/utilization/docker-info.tap.js +++ b/test/integration/utilization/docker-info.tap.js @@ -78,5 +78,9 @@ function mockProcRead(data, v2) { common.readProc.onCall(1).yields(null, data) } else { common.readProc.onCall(0).yields(null, data) + // The empty.txt test fails if we don't have the next line present. This + // is due to solving NR-332492 (falling back to v1 if a v2 file is present + // but does not contain a parseable docker container id). + common.readProc.onCall(1).yields(null, data) } } diff --git a/test/lib/custom-assertions.js b/test/lib/custom-assertions.js deleted file mode 100644 index 8e8792be09..0000000000 --- a/test/lib/custom-assertions.js +++ /dev/null @@ -1,394 +0,0 @@ -/* - * Copyright 2023 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' -const assert = require('node:assert') -const { isSimpleObject } = require('../../lib/util/objects') - -function assertExactClmAttrs(segmentStub, expectedAttrs) { - const attrs = segmentStub.addAttribute.args - const attrsObj = attrs.reduce((obj, [key, value]) => { - obj[key] = value - return obj - }, {}) - assert.deepEqual(attrsObj, expectedAttrs, 'CLM attrs should match') -} - -/** - * Asserts the appropriate Code Level Metrics attributes on a segment - * - * @param {object} params - * @param {object} params.segments list of segments to assert { segment, filepath, name } - * @param {boolean} params.enabled if CLM is enabled or not - * @param {boolean} params.skipFull flag to skip asserting `code.lineno` and `code.column` - */ -function assertCLMAttrs({ segments, enabled: clmEnabled, skipFull = false }) { - segments.forEach((segment) => { - const attrs = segment.segment.getAttributes() - if (clmEnabled) { - assert.equal(attrs['code.function'], segment.name, 'should have appropriate code.function') - if (segment.filepath instanceof RegExp) { - assert.match( - attrs['code.filepath'], - segment.filepath, - 'should have appropriate code.filepath' - ) - } else { - assert.ok( - attrs['code.filepath'].endsWith(segment.filepath), - 'should have appropriate code.filepath' - ) - } - - if (!skipFull) { - assert.equal(typeof attrs['code.lineno'], 'number', 'lineno should be a number') - assert.equal(typeof attrs['code.column'], 'number', 'column should be a number') - } - } else { - assert.ok(!attrs['code.function'], 'function should not exist') - assert.ok(!attrs['code.filepath'], 'filepath should not exist') - assert.ok(!attrs['code.lineno'], 'lineno should not exist') - assert.ok(!attrs['code.column'], 'column should not exist') - } - }) -} - -/** - * assertion to test if a property is non-writable - * - * @param {Object} params - * @param {Object} params.obj obj to assign value - * @param {string} params.key key to assign value - * @param {string} params.value expected value of obj[key] - */ -function isNonWritable({ obj, key, value }) { - assert.throws(function () { - obj[key] = 'testNonWritable test value' - }, new RegExp("(read only property '" + key + "'|Cannot set property " + key + ')')) - - if (value) { - assert.strictEqual(obj[key], value) - } else { - assert.notStrictEqual( - obj[key], - 'testNonWritable test value', - 'should not set value when non-writable' - ) - } -} - -/** - * Verifies the expected length of children segments and that every - * id matches between a segment array and the children - * - * @param {Object} parent trace - * @param {Array} segments list of expected segments - */ -function compareSegments(parent, segments) { - assert.ok(parent.children.length, segments.length, 'should be the same amount of children') - segments.forEach((segment, index) => { - assert.equal(parent.children[index].id, segment.id, 'should have same ids') - }) -} - -/** - * @param {TraceSegment} parent Parent segment - * @param {Array} expected Array of strings that represent segment names. - * If an item in the array is another array, it - * represents children of the previous item. - * @param {boolean} options.exact If true, then the expected segments must match - * exactly, including their position and children on all - * levels. When false, then only check that each child - * exists. - * @param {array} options.exclude Array of segment names that should be excluded from - * validation. This is useful, for example, when a - * segment may or may not be created by code that is not - * directly under test. Only used when `exact` is true. - * @param {object} [options] Set of optional parameters. - * @param {object} [options.assert] The assertion library to use. Defaults to - * `node:assert`. - */ -function assertSegments(parent, expected, options, { assert = require('node:assert') } = {}) { - let child - let childCount = 0 - - // rather default to what is more likely to fail than have a false test - let exact = true - if (options && options.exact === false) { - exact = options.exact - } else if (options === false) { - exact = false - } - - function getChildren(_parent) { - return _parent.children.filter(function (item) { - if (exact && options && options.exclude) { - return options.exclude.indexOf(item.name) === -1 - } - return true - }) - } - - const children = getChildren(parent) - if (exact) { - for (let i = 0; i < expected.length; ++i) { - const sequenceItem = expected[i] - - if (typeof sequenceItem === 'string') { - child = children[childCount++] - assert.equal( - child ? child.name : undefined, - sequenceItem, - 'segment "' + - parent.name + - '" should have child "' + - sequenceItem + - '" in position ' + - childCount - ) - - // If the next expected item is not array, then check that the current - // child has no children - if (!Array.isArray(expected[i + 1])) { - assert.ok( - getChildren(child).length === 0, - 'segment "' + child.name + '" should not have any children' - ) - } - } else if (typeof sequenceItem === 'object') { - assertSegments(child, sequenceItem, options) - } - } - - // check if correct number of children was found - assert.equal(children.length, childCount) - } else { - for (let i = 0; i < expected.length; i++) { - const sequenceItem = expected[i] - - if (typeof sequenceItem === 'string') { - // find corresponding child in parent - for (let j = 0; j < parent.children.length; j++) { - if (parent.children[j].name === sequenceItem) { - child = parent.children[j] - } - } - assert.ok(child, 'segment "' + parent.name + '" should have child "' + sequenceItem + '"') - if (typeof expected[i + 1] === 'object') { - assertSegments(child, expected[i + 1], exact) - } - } - } - } -} - -const TYPE_MAPPINGS = { - String: 'string', - Number: 'number' -} - -/** - * Like `tap.prototype.match`. Verifies that `actual` satisfies the shape - * provided by `expected`. This does actual assertions with `node:assert` - * - * There is limited support for type matching - * - * @example - * match(obj, { - * key: String, - * number: Number - * }) - * - * @example - * const input = { - * foo: /^foo.+bar$/, - * bar: [1, 2, '3'] - * } - * match(input, { - * foo: 'foo is bar', - * bar: [1, 2, '3'] - * }) - * match(input, { - * foo: 'foo is bar', - * bar: [1, 2, '3', 4] - * }) - * - * @param {string|object} actual The entity to verify. - * @param {string|object} expected What the entity should match against. - * @param {object} [options] Set of optional parameters. - * @param {object} [options.assert] The assertion library to use. Defaults to - * `node:assert`. - * - */ -function match(actual, expected, { assert = require('node:assert') } = {}) { - // match substring - if (typeof actual === 'string' && typeof expected === 'string') { - assert.ok(actual.indexOf(expected) > -1) - return - } - - for (const key in expected) { - if (key in actual) { - if (typeof expected[key] === 'function') { - const type = expected[key] - assert.ok(typeof actual[key] === TYPE_MAPPINGS[type.name]) - } else if (expected[key] instanceof RegExp) { - assert.ok(expected[key].test(actual[key])) - } else if (typeof expected[key] === 'object' && expected[key] !== null) { - match(actual[key], expected[key]) - } else { - assert.equal(actual[key], expected[key]) - } - } - } -} - -/** - * @param {Metrics} metrics metrics under test - * @param {Array} expected Array of metric data where metric data is in this form: - * [ - * { - * “name”:”name of metric”, - * “scope”:”scope of metric”, - * }, - * [count, - * total time, - * exclusive time, - * min time, - * max time, - * sum of squares] - * ] - * @param {boolean} exclusive When true, found and expected metric lengths should match - * @param {boolean} assertValues When true, metric values must match expected - * @param {object} [options] Set of optional parameters. - * @param {object} [options.assert] The assertion library to use. Defaults to - * `node:assert`. - */ -function assertMetrics( - metrics, - expected, - exclusive, - assertValues, - { assert = require('node:assert') } = {} -) { - // Assertions about arguments because maybe something returned undefined - // unexpectedly and is passed in, or a return type changed. This will - // hopefully help catch that and make it obvious. - assert.ok(isSimpleObject(metrics), 'first argument required to be an Metrics object') - assert.ok(Array.isArray(expected), 'second argument required to be an array of metrics') - assert.ok(typeof exclusive === 'boolean', 'third argument required to be a boolean if provided') - - if (assertValues === undefined) { - assertValues = true - } - - for (let i = 0, len = expected.length; i < len; i++) { - const expectedMetric = expected[i] - const metric = metrics.getMetric(expectedMetric[0].name, expectedMetric[0].scope) - assert.ok(metric, `should find ${expectedMetric[0].name}`) - if (assertValues) { - assert.deepEqual(metric.toJSON(), expectedMetric[1]) - } - } - - if (exclusive) { - const metricsList = metrics.toJSON() - assert.equal(metricsList.length, expected.length) - } -} - -/** - * @param {Transaction} transaction Nodejs agent transaction - * @param {Array} expected Array of metric data where metric data is in this form: - * [ - * { - * “name”:”name of metric”, - * “scope”:”scope of metric”, - * }, - * [count, - * total time, - * exclusive time, - * min time, - * max time, - * sum of squares] - * ] - * @param {boolean} exact When true, found and expected metric lengths should match - */ -function assertMetricValues(transaction, expected, exact) { - const metrics = transaction.metrics - - for (let i = 0; i < expected.length; ++i) { - let expectedMetric = Object.assign({}, expected[i]) - let name = null - let scope = null - - if (typeof expectedMetric === 'string') { - name = expectedMetric - expectedMetric = {} - } else { - name = expectedMetric[0].name - scope = expectedMetric[0].scope - } - - const metric = metrics.getMetric(name, scope) - assert.ok(metric, 'should have expected metric name') - - assert.deepStrictEqual(metric.toJSON(), expectedMetric[1], 'metric values should match') - } - - if (exact) { - const metricsJSON = metrics.toJSON() - assert.equal(metricsJSON.length, expected.length, 'metrics length should match') - } -} - -/** - * Asserts the wrapped callback is wrapped and the unwrapped version is the original. - * It also verifies it does not throw an error - * - * @param {object} shim shim lib - * @param {Function} original callback - */ -function checkWrappedCb(shim, cb) { - // The wrapped callback is always the last argument - const wrappedCB = arguments[arguments.length - 1] - assert.notStrictEqual(wrappedCB, cb) - assert.ok(shim.isWrapped(wrappedCB)) - assert.equal(shim.unwrap(wrappedCB), cb) - - assert.doesNotThrow(function () { - wrappedCB() - }) -} - -/** - * Helper that verifies the original callback - * and wrapped callback are the same - * - * @param {object} shim shim lib - * @param {Function} original callback - */ -function checkNotWrappedCb(shim, cb) { - // The callback is always the last argument - const wrappedCB = arguments[arguments.length - 1] - assert.equal(wrappedCB, cb) - assert.equal(shim.isWrapped(wrappedCB), false) - assert.doesNotThrow(function () { - wrappedCB() - }) -} - -module.exports = { - assertCLMAttrs, - assertExactClmAttrs, - assertMetrics, - assertMetricValues, - assertSegments, - checkWrappedCb, - checkNotWrappedCb, - compareSegments, - isNonWritable, - match -} diff --git a/test/lib/custom-assertions/Readme.md b/test/lib/custom-assertions/Readme.md new file mode 100644 index 0000000000..ed6f143b96 --- /dev/null +++ b/test/lib/custom-assertions/Readme.md @@ -0,0 +1,3 @@ +This module is a collection of helper functions to assert expectations +in tests. Each file represents a single assertion function, and _must_ export +a named function. diff --git a/test/lib/custom-assertions/assert-clm-attrs.js b/test/lib/custom-assertions/assert-clm-attrs.js new file mode 100644 index 0000000000..8fafce425c --- /dev/null +++ b/test/lib/custom-assertions/assert-clm-attrs.js @@ -0,0 +1,50 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * Asserts the appropriate Code Level Metrics attributes on a segment + * + * @param {object} params + * @param {object} params.segments list of segments to assert { segment, filepath, name } + * @param {boolean} params.enabled if CLM is enabled or not + * @param {boolean} params.skipFull flag to skip asserting `code.lineno` and `code.column` + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function assertCLMAttrs( + { segments, enabled: clmEnabled, skipFull = false }, + { assert = require('node:assert') } = {} +) { + segments.forEach((segment) => { + const attrs = segment.segment.getAttributes() + if (clmEnabled) { + assert.equal(attrs['code.function'], segment.name, 'should have appropriate code.function') + if (segment.filepath instanceof RegExp) { + assert.match( + attrs['code.filepath'], + segment.filepath, + 'should have appropriate code.filepath' + ) + } else { + assert.ok( + attrs['code.filepath'].endsWith(segment.filepath), + 'should have appropriate code.filepath' + ) + } + + if (!skipFull) { + assert.equal(typeof attrs['code.lineno'], 'number', 'lineno should be a number') + assert.equal(typeof attrs['code.column'], 'number', 'column should be a number') + } + } else { + assert.ok(!attrs['code.function'], 'function should not exist') + assert.ok(!attrs['code.filepath'], 'filepath should not exist') + assert.ok(!attrs['code.lineno'], 'lineno should not exist') + assert.ok(!attrs['code.column'], 'column should not exist') + } + }) +} diff --git a/test/lib/custom-assertions/assert-exact-clm-attrs.js b/test/lib/custom-assertions/assert-exact-clm-attrs.js new file mode 100644 index 0000000000..487c7c089e --- /dev/null +++ b/test/lib/custom-assertions/assert-exact-clm-attrs.js @@ -0,0 +1,19 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = function assertExactClmAttrs( + segmentStub, + expectedAttrs, + { assert = require('node:assert') } = {} +) { + const attrs = segmentStub.addAttribute.args + const attrsObj = attrs.reduce((obj, [key, value]) => { + obj[key] = value + return obj + }, {}) + assert.deepEqual(attrsObj, expectedAttrs, 'CLM attrs should match') +} diff --git a/test/lib/assert-metrics.js b/test/lib/custom-assertions/assert-metric-values.js similarity index 87% rename from test/lib/assert-metrics.js rename to test/lib/custom-assertions/assert-metric-values.js index 261db99fbc..e4e69cea1c 100644 --- a/test/lib/assert-metrics.js +++ b/test/lib/custom-assertions/assert-metric-values.js @@ -5,12 +5,6 @@ 'use strict' -module.exports = { - assertMetricValues -} - -const assert = require('node:assert') - /** * @param {Transaction} transaction Nodejs agent transaction * @param {Array} expected Array of metric data where metric data is in this form: @@ -27,8 +21,15 @@ const assert = require('node:assert') * sum of squares] * ] * @param {boolean} exact When true, found and expected metric lengths should match + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. */ -function assertMetricValues(transaction, expected, exact) { +module.exports = function assertMetricValues( + transaction, + expected, + exact, + { assert = require('node:assert') } = {} +) { const metrics = transaction.metrics for (let i = 0; i < expected.length; ++i) { diff --git a/test/lib/custom-assertions/assert-metrics.js b/test/lib/custom-assertions/assert-metrics.js new file mode 100644 index 0000000000..7bfafb2b0e --- /dev/null +++ b/test/lib/custom-assertions/assert-metrics.js @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const { isSimpleObject } = require('../../../lib/util/objects') + +/** + * @param {Metrics} metrics metrics under test + * @param {Array} expected Array of metric data where metric data is in this form: + * [ + * { + * “name”:”name of metric”, + * “scope”:”scope of metric”, + * }, + * [count, + * total time, + * exclusive time, + * min time, + * max time, + * sum of squares] + * ] + * @param {boolean} exclusive When true, found and expected metric lengths should match + * @param {boolean} assertValues When true, metric values must match expected + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function assertMetrics( + metrics, + expected, + exclusive, + assertValues, + { assert = require('node:assert') } = {} +) { + // Assertions about arguments because maybe something returned undefined + // unexpectedly and is passed in, or a return type changed. This will + // hopefully help catch that and make it obvious. + assert.ok(isSimpleObject(metrics), 'first argument required to be an Metrics object') + assert.ok(Array.isArray(expected), 'second argument required to be an array of metrics') + assert.ok(typeof exclusive === 'boolean', 'third argument required to be a boolean if provided') + + if (assertValues === undefined) { + assertValues = true + } + + for (let i = 0, len = expected.length; i < len; i++) { + const expectedMetric = expected[i] + const metric = metrics.getMetric(expectedMetric[0].name, expectedMetric[0].scope) + assert.ok(metric, `should find ${expectedMetric[0].name}`) + if (assertValues) { + assert.deepEqual(metric.toJSON(), expectedMetric[1]) + } + } + + if (exclusive) { + const metricsList = metrics.toJSON() + assert.equal(metricsList.length, expected.length) + } +} diff --git a/test/lib/custom-assertions/assert-segments.js b/test/lib/custom-assertions/assert-segments.js new file mode 100644 index 0000000000..2004c3371a --- /dev/null +++ b/test/lib/custom-assertions/assert-segments.js @@ -0,0 +1,101 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * @param {TraceSegment} parent Parent segment + * @param {Array} expected Array of strings that represent segment names. + * If an item in the array is another array, it + * represents children of the previous item. + * @param {boolean} options.exact If true, then the expected segments must match + * exactly, including their position and children on all + * levels. When false, then only check that each child + * exists. + * @param {array} options.exclude Array of segment names that should be excluded from + * validation. This is useful, for example, when a + * segment may or may not be created by code that is not + * directly under test. Only used when `exact` is true. + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function assertSegments( + parent, + expected, + options, + { assert = require('node:assert') } = {} +) { + let child + let childCount = 0 + + // rather default to what is more likely to fail than have a false test + let exact = true + if (options && options.exact === false) { + exact = options.exact + } else if (options === false) { + exact = false + } + + function getChildren(_parent) { + return _parent.children.filter(function (item) { + if (exact && options && options.exclude) { + return options.exclude.indexOf(item.name) === -1 + } + return true + }) + } + + const children = getChildren(parent) + if (exact) { + for (let i = 0; i < expected.length; ++i) { + const sequenceItem = expected[i] + + if (typeof sequenceItem === 'string') { + child = children[childCount++] + assert.equal( + child ? child.name : undefined, + sequenceItem, + 'segment "' + + parent.name + + '" should have child "' + + sequenceItem + + '" in position ' + + childCount + ) + + // If the next expected item is not array, then check that the current + // child has no children + if (!Array.isArray(expected[i + 1])) { + assert.ok( + getChildren(child).length === 0, + 'segment "' + child.name + '" should not have any children' + ) + } + } else if (typeof sequenceItem === 'object') { + assertSegments(child, sequenceItem, options, { assert }) + } + } + + // check if correct number of children was found + assert.equal(children.length, childCount) + } else { + for (let i = 0; i < expected.length; i++) { + const sequenceItem = expected[i] + + if (typeof sequenceItem === 'string') { + // find corresponding child in parent + for (let j = 0; j < parent.children.length; j++) { + if (parent.children[j].name === sequenceItem) { + child = parent.children[j] + } + } + assert.ok(child, 'segment "' + parent.name + '" should have child "' + sequenceItem + '"') + if (typeof expected[i + 1] === 'object') { + assertSegments(child, expected[i + 1], { exact }, { assert }) + } + } + } + } +} diff --git a/test/lib/custom-assertions/check-not-wrapped-cb.js b/test/lib/custom-assertions/check-not-wrapped-cb.js new file mode 100644 index 0000000000..af4e75dbcd --- /dev/null +++ b/test/lib/custom-assertions/check-not-wrapped-cb.js @@ -0,0 +1,25 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * Helper that verifies the original callback + * and wrapped callback are the same + * + * @param {object} shim Shimmer instance. + * @param {Function} cb The callback to check. + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function checkNotWrappedCb(shim, cb, { assert = require('node:assert') } = {}) { + // The callback is always the last argument + const wrappedCB = arguments[arguments.length - 1] + assert.equal(wrappedCB, cb) + assert.equal(shim.isWrapped(wrappedCB), false) + assert.doesNotThrow(function () { + wrappedCB() + }) +} diff --git a/test/lib/custom-assertions/check-wrapped-cb.js b/test/lib/custom-assertions/check-wrapped-cb.js new file mode 100644 index 0000000000..10beb04f82 --- /dev/null +++ b/test/lib/custom-assertions/check-wrapped-cb.js @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * Asserts the wrapped callback is wrapped and the unwrapped version is the original. + * It also verifies it does not throw an error + * + * @param {object} shim Shimmer instance. + * @param {Function} cb The callback to check. + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function checkWrappedCb(shim, cb, { assert = require('node:assert') } = {}) { + // The wrapped callback is always the last argument + const wrappedCB = arguments[arguments.length - 1] + assert.notStrictEqual(wrappedCB, cb) + assert.ok(shim.isWrapped(wrappedCB)) + assert.equal(shim.unwrap(wrappedCB), cb) + + assert.doesNotThrow(function () { + wrappedCB() + }) +} diff --git a/test/lib/custom-assertions/compare-segments.js b/test/lib/custom-assertions/compare-segments.js new file mode 100644 index 0000000000..fde765b060 --- /dev/null +++ b/test/lib/custom-assertions/compare-segments.js @@ -0,0 +1,26 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * Verifies the expected length of children segments and that every + * id matches between a segment array and the children + * + * @param {Object} parent trace + * @param {Array} segments list of expected segments + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function compareSegments( + parent, + segments, + { assert = require('node:assert') } = {} +) { + assert.ok(parent.children.length, segments.length, 'should be the same amount of children') + segments.forEach((segment, index) => { + assert.equal(parent.children[index].id, segment.id, 'should have same ids') + }) +} diff --git a/test/lib/custom-assertions/index.js b/test/lib/custom-assertions/index.js new file mode 100644 index 0000000000..910959e195 --- /dev/null +++ b/test/lib/custom-assertions/index.js @@ -0,0 +1,26 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const fs = require('node:fs') + +module.exports = {} + +const entries = fs.readdirSync(__dirname, { withFileTypes: true, encoding: 'utf8' }) +for (const entry of entries) { + if (entry.isFile() === false || entry.name === 'index.js' || entry.name === 'Readme.md') { + continue + } + + try { + const fn = require(`./${entry.name}`) + module.exports[fn.name] = fn + } catch (error) { + /* eslint-disable-next-line */ + console.log(`could not load ${entry.name}: ${error.message}`) + throw error + } +} diff --git a/test/lib/custom-assertions/is-non-writable.js b/test/lib/custom-assertions/is-non-writable.js new file mode 100644 index 0000000000..8469d1c1a4 --- /dev/null +++ b/test/lib/custom-assertions/is-non-writable.js @@ -0,0 +1,35 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * assertion to test if a property is non-writable + * + * @param {Object} params + * @param {Object} params.obj obj to assign value + * @param {string} params.key key to assign value + * @param {string} params.value expected value of obj[key] + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function isNonWritable( + { obj, key, value }, + { assert = require('node:assert') } = {} +) { + assert.throws(function () { + obj[key] = 'testNonWritable test value' + }, new RegExp("(read only property '" + key + "'|Cannot set property " + key + ')')) + + if (value) { + assert.strictEqual(obj[key], value) + } else { + assert.notStrictEqual( + obj[key], + 'testNonWritable test value', + 'should not set value when non-writable' + ) + } +} diff --git a/test/lib/custom-assertions/match.js b/test/lib/custom-assertions/match.js new file mode 100644 index 0000000000..27b2f8ef20 --- /dev/null +++ b/test/lib/custom-assertions/match.js @@ -0,0 +1,65 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const TYPE_MAPPINGS = { + String: 'string', + Number: 'number' +} + +/** + * Like `tap.prototype.match`. Verifies that `actual` satisfies the shape + * provided by `expected`. This does actual assertions with `node:assert` + * + * There is limited support for type matching + * + * @example + * match(obj, { + * key: String, + * number: Number + * }) + * + * @example + * const input = { + * foo: /^foo.+bar$/, + * bar: [1, 2, '3'] + * } + * match(input, { + * foo: 'foo is bar', + * bar: [1, 2, '3'] + * }) + * match(input, { + * foo: 'foo is bar', + * bar: [1, 2, '3', 4] + * }) + * + * @param {string|object} actual The entity to verify. + * @param {string|object} expected What the entity should match against. + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + */ +module.exports = function match(actual, expected, { assert = require('node:assert') } = {}) { + // match substring + if (typeof actual === 'string' && typeof expected === 'string') { + assert.ok(actual.indexOf(expected) > -1) + return + } + + for (const key in expected) { + if (key in actual) { + if (typeof expected[key] === 'function') { + const type = expected[key] + assert.ok(typeof actual[key] === TYPE_MAPPINGS[type.name]) + } else if (expected[key] instanceof RegExp) { + assert.ok(expected[key].test(actual[key])) + } else if (typeof expected[key] === 'object' && expected[key] !== null) { + match(actual[key], expected[key], { assert }) + } else { + assert.equal(actual[key], expected[key]) + } + } + } +} diff --git a/test/lib/custom-assertions/not-has.js b/test/lib/custom-assertions/not-has.js new file mode 100644 index 0000000000..58dff1311b --- /dev/null +++ b/test/lib/custom-assertions/not-has.js @@ -0,0 +1,31 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const get = require('../../../lib/util/get') + +/** + * Asserts that the `found` object does not contain a property as defined + * by `doNotWant`. + * + * @param {object} params Input parameters + * @param {object} found The object to test for absence. + * @param {string} doNotWant Dot separated path to a field that should not + * have a value. + * @param {string} [msg] Assertion message to include. + * @param {object} [deps] Injected dependencies. + * @param {object} [deps.assert] Assertion library to use. + * + * @throws {Error} When the `found` object contains a value at the specified + * `doNotWant` path. + */ +module.exports = function notHas( + { found, doNotWant, msg }, + { assert = require('node:assert') } = {} +) { + const result = get(found, doNotWant) + assert.equal(result, undefined, msg) +} diff --git a/test/lib/test-reporter.mjs b/test/lib/test-reporter.mjs index e55f0d0ff7..df3b7e3228 100644 --- a/test/lib/test-reporter.mjs +++ b/test/lib/test-reporter.mjs @@ -14,11 +14,42 @@ const OUTPUT_MODE = process.env.OUTPUT_MODE?.toLowerCase() ?? 'simple' const isSilent = OUTPUT_MODE === 'quiet' || OUTPUT_MODE === 'silent' +function colorize(type, text) { + if (type === 'pass') { + const blackText = `\x1b[30m${text}` + const boldblackText = `\x1b[1m${blackText}` + // Green background with black text + return `\x1b[42m${boldblackText}\x1b[0m` + } + + if (type === 'fail') { + const whiteText = `\x1b[37m${text}` + const boldWhiteText = `\x1b[1m${whiteText}` + // Red background with white text + return `\x1b[41m${boldWhiteText}\x1b[0m` + } + + return text +} + async function* reporter(source) { const passed = new Set() const failed = new Set() + // We'll use the queued map to deal with `enqueue` and `dequeue` events. + // This lets us keep track of individual tests (each "test()" in a test file + // counts as one test). We can probably refactor this out once we are set + // Node >=20 as a baseline, because it has a `test:completed` event. + const queued = new Map() + + // We only want to report for the overall test file having passed or failed. + // Since we don't have Node >= 20 events available, we have to fudge it + // ourselves. + const reported = new Set() + for await (const event of source) { + const file = event.data.file + // Once v18 has been dropped, we might want to revisit the output of // cases. The `event` object is supposed to provide things like // the failing line number and column, along with the failing test name. @@ -32,20 +63,44 @@ async function* reporter(source) { // // See https://nodejs.org/api/test.html#event-testfail. switch (event.type) { + case 'test:enqueue': { + if (queued.has(file) === false) { + queued.set(file, new Set()) + } + const tests = queued.get(file) + tests.add(event.data.line) + break + } + + case 'test:dequeue': { + queued.get(file).delete(event.data.line) + break + } + case 'test:pass': { - passed.add(event.data.file) + passed.add(file) if (isSilent === true) { yield '' - } else { - yield `passed: ${event.data.file}\n` + break } + if (queued.get(file).size > 0 || reported.has(file) === true) { + break + } + + reported.add(file) + yield `${colorize('pass', 'passed')}: ${file}\n` break } case 'test:fail': { - failed.add(event.data.file) - yield `failed: ${event.data.file}\n` + if (queued.get(file).size > 0 || reported.has(file) === true) { + break + } + + reported.add(file) + failed.add(file || event.data.name) + yield `${colorize('fail', 'failed')}: ${file}\n` break } @@ -56,11 +111,12 @@ async function* reporter(source) { } if (failed.size > 0) { - yield `\n\nFailed tests:\n` + yield `\n\n${colorize('fail', 'Failed tests:')}\n` for (const file of failed) { yield `${file}\n` } } + yield `\n\nPassed: ${passed.size}\nFailed: ${failed.size}\nTotal: ${passed.size + failed.size}\n` } diff --git a/test/unit/collector/remote-method.test.js b/test/unit/collector/remote-method.test.js index 1bd1087b1f..f9fc6d0850 100644 --- a/test/unit/collector/remote-method.test.js +++ b/test/unit/collector/remote-method.test.js @@ -15,7 +15,7 @@ const proxyquire = require('proxyquire') const helper = require('../../lib/agent_helper') const Config = require('../../../lib/config') const Collector = require('../../lib/test-collector') -const { assertMetricValues } = require('../../lib/assert-metrics') +const { assertMetricValues } = require('../../lib/custom-assertions') const RemoteMethod = require('../../../lib/collector/remote-method') const NAMES = require('../../../lib/metrics/names') diff --git a/test/unit/config/config-defaults.test.js b/test/unit/config/config-defaults.test.js index f06252d4b8..ba8b24c9b4 100644 --- a/test/unit/config/config-defaults.test.js +++ b/test/unit/config/config-defaults.test.js @@ -37,6 +37,10 @@ test('with default properties', async (t) => { assert.equal(configuration.license_key, '') }) + await t.test('should have no cloud aws account id', () => { + assert.equal(configuration.cloud.aws.account_id, null) + }) + await t.test('should connect to the collector at collector.newrelic.com', () => { assert.equal(configuration.host, 'collector.newrelic.com') }) diff --git a/test/unit/config/config-env.test.js b/test/unit/config/config-env.test.js index 934cadeee7..9b8e873e9e 100644 --- a/test/unit/config/config-env.test.js +++ b/test/unit/config/config-env.test.js @@ -592,6 +592,13 @@ test('when overriding configuration values via environment variables', async (t) }) }) + await t.test('should pick up cloud aws account_id', (t, end) => { + idempotentEnv({ NEW_RELIC_CLOUD_AWS_ACCOUNT_ID: '123456789123' }, (tc) => { + assert.equal(tc.cloud.aws.account_id, 123456789123) + end() + }) + }) + await t.test('should reject disabling ssl', (t, end) => { idempotentEnv({ NEW_RELIC_USE_SSL: false }, (tc) => { assert.equal(tc.ssl, true) diff --git a/test/unit/llm-events/aws-bedrock/bedrock-command.test.js b/test/unit/llm-events/aws-bedrock/bedrock-command.test.js index 639d11ef54..8097a4fad5 100644 --- a/test/unit/llm-events/aws-bedrock/bedrock-command.test.js +++ b/test/unit/llm-events/aws-bedrock/bedrock-command.test.js @@ -24,6 +24,13 @@ const claude = { } } +const claude35 = { + modelId: 'anthropic.claude-3-5-sonnet-20240620-v1:0', + body: { + messages: [{ role: 'user', content: { type: 'text', text: 'who are you' } }] + } +} + const claude3 = { modelId: 'anthropic.claude-3-haiku-20240307-v1:0', body: { @@ -182,6 +189,42 @@ test('claude3 complete command works', async (t) => { assert.equal(cmd.temperature, payload.body.temperature) }) +test('claude35 minimal command works with claude 3 api', async (t) => { + t.nr.updatePayload(structuredClone(claude3)) + const cmd = new BedrockCommand(t.nr.input) + assert.equal(cmd.isClaude3(), true) + assert.equal(cmd.maxTokens, undefined) + assert.equal(cmd.modelId, claude3.modelId) + assert.equal(cmd.modelType, 'completion') + assert.equal(cmd.prompt, claude3.body.messages[0].content) + assert.equal(cmd.temperature, undefined) +}) + +test('claude35 minimal command works', async (t) => { + t.nr.updatePayload(structuredClone(claude35)) + const cmd = new BedrockCommand(t.nr.input) + assert.equal(cmd.isClaude3(), true) + assert.equal(cmd.maxTokens, undefined) + assert.equal(cmd.modelId, claude35.modelId) + assert.equal(cmd.modelType, 'completion') + assert.equal(cmd.prompt, claude35.body.messages[0].content.text) + assert.equal(cmd.temperature, undefined) +}) + +test('claude35 complete command works', async (t) => { + const payload = structuredClone(claude35) + payload.body.max_tokens = 25 + payload.body.temperature = 0.5 + t.nr.updatePayload(payload) + const cmd = new BedrockCommand(t.nr.input) + assert.equal(cmd.isClaude3(), true) + assert.equal(cmd.maxTokens, 25) + assert.equal(cmd.modelId, payload.modelId) + assert.equal(cmd.modelType, 'completion') + assert.equal(cmd.prompt, payload.body.messages[0].content.text) + assert.equal(cmd.temperature, payload.body.temperature) +}) + test('cohere minimal command works', async (t) => { t.nr.updatePayload(structuredClone(cohere)) const cmd = new BedrockCommand(t.nr.input) diff --git a/test/unit/parsed-statement.test.js b/test/unit/metrics-recorder/database-metrics-recorder.test.js similarity index 94% rename from test/unit/parsed-statement.test.js rename to test/unit/metrics-recorder/database-metrics-recorder.test.js index 33b9569699..6625c3cf6d 100644 --- a/test/unit/parsed-statement.test.js +++ b/test/unit/metrics-recorder/database-metrics-recorder.test.js @@ -8,11 +8,12 @@ const test = require('node:test') const assert = require('node:assert') -const helper = require('../lib/agent_helper') -const { match } = require('../lib/custom-assertions') +const helper = require('../../lib/agent_helper') +const { match } = require('../../lib/custom-assertions') -const Transaction = require('../../lib/transaction') -const ParsedStatement = require('../../lib/db/parsed-statement') +const Transaction = require('../../../lib/transaction') +const ParsedStatement = require('../../../lib/db/parsed-statement') +const recordQueryMetrics = require('../../../lib/metrics/recorders/database') function checkMetric(metrics, name, scope) { match(metrics.getMetric(name, scope), { total: 0.333 }) @@ -32,7 +33,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - ps.recordMetrics(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST') transaction.end() ctx.nr.metrics = transaction.metrics @@ -100,7 +101,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - ps.recordMetrics(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST') transaction.end() ctx.nr.metrics = transaction.metrics @@ -165,7 +166,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - ps.recordMetrics(segment, null) + recordQueryMetrics.bind(ps)(segment, null) transaction.end() ctx.nr.metrics = transaction.metrics @@ -228,7 +229,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - ps.recordMetrics(segment, null) + recordQueryMetrics.bind(ps)(segment, null) transaction.end() ctx.nr.metrics = transaction.metrics @@ -297,13 +298,13 @@ test('recording slow queries', async (t) => { ctx.nr.segment = segment segment.setDurationInMillis(503) - ps.recordMetrics(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST') const ps2 = new ParsedStatement('MySql', 'select', 'foo', 'select * from foo where b=2') const segment2 = transaction.trace.add('test') segment2.setDurationInMillis(501) - ps2.recordMetrics(segment2, 'TEST') + recordQueryMetrics.bind(ps2)(segment2, 'TEST') transaction.end() }) @@ -358,13 +359,13 @@ test('recording slow queries', async (t) => { ctx.nr.segment = segment segment.setDurationInMillis(503) - ps.recordMetrics(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST') const ps2 = new ParsedStatement('MySql', 'select', null, 'select * from foo where b=2') const segment2 = transaction.trace.add('test') segment2.setDurationInMillis(501) - ps2.recordMetrics(segment2, 'TEST') + recordQueryMetrics.bind(ps2)(segment2, 'TEST') transaction.end() }) @@ -427,13 +428,13 @@ test('recording slow queries', async (t) => { ctx.nr.segment = segment segment.setDurationInMillis(503) - ps.recordMetrics(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST') const ps2 = new ParsedStatement('MySql', 'select', null, null) const segment2 = transaction.trace.add('test') segment2.setDurationInMillis(501) - ps2.recordMetrics(segment2, 'TEST') + recordQueryMetrics.bind(ps2)(segment2, 'TEST') transaction.end() }) diff --git a/test/unit/utilization/docker-info.test.js b/test/unit/utilization/docker-info.test.js index 8eac18a3f0..21c6687e92 100644 --- a/test/unit/utilization/docker-info.test.js +++ b/test/unit/utilization/docker-info.test.js @@ -31,9 +31,11 @@ test.beforeEach(async (ctx) => { utilCommon.readProc = (path, cb) => { cb(null, 'docker-1') } + ctx.nr.utilCommon = utilCommon - const { getBootId } = require('../../../lib/utilization/docker-info') + const { getBootId, getVendorInfo } = require('../../../lib/utilization/docker-info') ctx.nr.getBootId = getBootId + ctx.nr.getVendorInfo = getVendorInfo ctx.nr.agent = helper.loadMockedAgent() ctx.nr.agent.config.utilization = { @@ -100,3 +102,31 @@ test('data on success', (t, end) => { end() } }) + +test('falls back to v1 correctly', (t, end) => { + const { agent, logger, getVendorInfo, utilCommon } = t.nr + let invocation = 0 + + utilCommon.readProc = (path, callback) => { + if (invocation === 0) { + invocation += 1 + return callback(null, 'invalid cgroups v2 file') + } + callback(null, '4:cpu:/docker/f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee') + } + + getVendorInfo(agent, gotInfo, logger) + + function gotInfo(error, info) { + assert.ifError(error) + assert.deepStrictEqual(info, { + id: 'f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee' + }) + assert.deepStrictEqual(t.nr.logs, [ + 'Found /proc/self/mountinfo but failed to parse Docker container id.', + 'Attempting to fall back to cgroups v1 parsing.', + 'Found docker id from cgroups v1: f37a7e4d17017e7bf774656b19ca4360c6cdc4951c86700a464101d0d9ce97ee' + ]) + end() + } +}) diff --git a/test/versioned/aws-sdk-v3/client-dynamodb.test.js b/test/versioned/aws-sdk-v3/client-dynamodb.test.js index 804a380d64..c6ead32433 100644 --- a/test/versioned/aws-sdk-v3/client-dynamodb.test.js +++ b/test/versioned/aws-sdk-v3/client-dynamodb.test.js @@ -24,7 +24,14 @@ test('DynamoDB', async (t) => { }) ctx.nr.server = server - ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.agent = helper.instrumentMockedAgent({ + cloud: { + aws: { + account_id: 123456789123 + } + } + }) + const Shim = require('../../../lib/shim/datastore-shim') ctx.nr.setDatastoreSpy = sinon.spy(Shim.prototype, 'setDatastore') const lib = require('@aws-sdk/client-dynamodb') @@ -98,6 +105,25 @@ test('DynamoDB', async (t) => { finish({ commands, tx, setDatastoreSpy }) }) }) + + await t.test('cloud.resource_id attribute not set when account_id is not set', async (t) => { + const { agent, commands, client } = t.nr + agent.config.cloud.aws.account_id = null + + await helper.runInTransaction(agent, async (tx) => { + for (const command of commands) { + await client.send(command) + } + tx.end() + const root = tx.trace.root + const segments = common.checkAWSAttributes(root, common.DATASTORE_PATTERN) + + segments.forEach((segment) => { + const attrs = segment.attributes.get(common.SEGMENT_DESTINATION) + assert.equal(attrs['cloud.resource_id'], null) + }) + }) + }) }) function createCommands({ lib, tableName }) { @@ -181,6 +207,7 @@ function finish({ commands, tx, setDatastoreSpy }) { ) const attrs = segment.attributes.get(common.SEGMENT_DESTINATION) attrs.port_path_or_id = parseInt(attrs.port_path_or_id, 10) + const accountId = tx.agent.config.cloud.aws.account_id match(attrs, { 'host': String, @@ -190,7 +217,8 @@ function finish({ commands, tx, setDatastoreSpy }) { 'aws.operation': command.constructor.name, 'aws.requestId': String, 'aws.region': 'us-east-1', - 'aws.service': /dynamodb|DynamoDB/ + 'aws.service': /dynamodb|DynamoDB/, + 'cloud.resource_id': `arn:aws:dynamodb:${attrs['aws.region']}:${accountId}:table/${attrs.collection}` }) }) diff --git a/test/versioned/aws-sdk-v3/lib-dynamodb.test.js b/test/versioned/aws-sdk-v3/lib-dynamodb.test.js index 71ce035056..032dbc5736 100644 --- a/test/versioned/aws-sdk-v3/lib-dynamodb.test.js +++ b/test/versioned/aws-sdk-v3/lib-dynamodb.test.js @@ -21,7 +21,13 @@ test('DynamoDB', async (t) => { }) ctx.nr.server = server - ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.agent = helper.instrumentMockedAgent({ + cloud: { + aws: { + account_id: 123456789123 + } + } + }) const lib = require('@aws-sdk/lib-dynamodb') ctx.nr.DynamoDBDocument = lib.DynamoDBDocument ctx.nr.DynamoDBDocumentClient = lib.DynamoDBDocumentClient @@ -137,6 +143,8 @@ function finish(end, tests, tx) { const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) assert.equal(externalSegments.length, 0, 'should not have any External segments') + const accountId = tx.agent.config.cloud.aws.account_id + segments.forEach((segment, i) => { const operation = tests[i].operation assert.equal( @@ -154,7 +162,8 @@ function finish(end, tests, tx) { 'aws.operation': operation, 'aws.requestId': String, 'aws.region': 'us-east-1', - 'aws.service': 'DynamoDB' + 'aws.service': 'DynamoDB', + 'cloud.resource_id': `arn:aws:dynamodb:${attrs['aws.region']}:${accountId}:table/${attrs.collection}` }) }) diff --git a/test/versioned/cassandra-driver/package.json b/test/versioned/cassandra-driver/package.json index f2a010858a..617ce4a1fa 100644 --- a/test/versioned/cassandra-driver/package.json +++ b/test/versioned/cassandra-driver/package.json @@ -12,9 +12,8 @@ "cassandra-driver": ">=3.4.0" }, "files": [ - "query.tap.js" + "query.test.js" ] } - ], - "dependencies": {} + ] } diff --git a/test/versioned/cassandra-driver/query.tap.js b/test/versioned/cassandra-driver/query.tap.js deleted file mode 100644 index b150e6b904..0000000000 --- a/test/versioned/cassandra-driver/query.tap.js +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const test = require('tap').test -const params = require('../../lib/params') -const helper = require('../../lib/agent_helper') - -const agent = helper.instrumentMockedAgent() -const cassandra = require('cassandra-driver') -const { findSegment } = require('../../lib/metrics_helper') - -// constants for keyspace and table creation -const KS = 'test' -const FAM = 'testFamily' -const PK = 'pk_column' -const COL = 'test_column' - -const client = new cassandra.Client({ - contactPoints: [params.cassandra_host], - protocolOptions: params.cassandra_port, - keyspace: KS, - localDataCenter: 'datacenter1' -}) - -const colValArr = ['Jim', 'Bob', 'Joe'] -const pkValArr = [111, 222, 333] -let insQuery = 'INSERT INTO ' + KS + '.' + FAM + ' (' + PK + ',' + COL -insQuery += ') VALUES(?, ?);' - -const insArr = [ - { query: insQuery, params: [pkValArr[0], colValArr[0]] }, - { query: insQuery, params: [pkValArr[1], colValArr[1]] }, - { query: insQuery, params: [pkValArr[2], colValArr[2]] } -] - -const hints = [ - ['int', 'varchar'], - ['int', 'varchar'], - ['int', 'varchar'] -] - -let selQuery = 'SELECT * FROM ' + KS + '.' + FAM + ' WHERE ' -selQuery += PK + ' = 111;' - -/** - * Deletion of testing keyspace if already exists, - * then recreation of a testable keyspace and table - * - * - * @param Callback function to set off running the tests - */ - -async function cassSetup() { - const setupClient = new cassandra.Client({ - contactPoints: [params.cassandra_host], - protocolOptions: params.cassandra_port, - localDataCenter: 'datacenter1' - }) - - function runCommand(cmd) { - return new Promise((resolve, reject) => { - setupClient.execute(cmd, function (err) { - if (err) { - reject(err) - } - - resolve() - }) - }) - } - - const ksDrop = 'DROP KEYSPACE IF EXISTS ' + KS + ';' - await runCommand(ksDrop) - - let ksCreate = 'CREATE KEYSPACE ' + KS + ' WITH replication = ' - ksCreate += "{'class': 'SimpleStrategy', 'replication_factor': 1};" - - await runCommand(ksCreate) - - let famCreate = 'CREATE TABLE ' + KS + '.' + FAM + ' (' + PK + ' int PRIMARY KEY, ' - famCreate += COL + ' varchar );' - - await runCommand(famCreate) - - setupClient.shutdown() -} - -test('Cassandra instrumentation', { timeout: 5000 }, async function testInstrumentation(t) { - t.before(async function () { - await cassSetup() - }) - - t.teardown(function tearDown() { - helper.unloadAgent(agent) - client.shutdown() - }) - - t.afterEach(() => { - agent.queries.clear() - agent.metrics.clear() - }) - - t.test('executeBatch - callback style', function (t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play') - helper.runInTransaction(agent, function transactionInScope(tx) { - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should be visible') - t.equal(tx, transaction, 'We got the same transaction') - - client.batch(insArr, { hints: hints }, function done(error, ok) { - if (error) { - t.error(error) - return t.end() - } - - t.ok(agent.getTransaction(), 'transaction should still be visible') - t.ok(ok, 'everything should be peachy after setting') - - client.execute(selQuery, function (error, value) { - if (error) { - return t.error(error) - } - - t.ok(agent.getTransaction(), 'transaction should still still be visible') - t.equal(value.rows[0][COL], colValArr[0], 'Cassandra client should still work') - - t.equal( - transaction.trace.root.children.length, - 1, - 'there should be only one child of the root' - ) - verifyTrace(t, transaction.trace, KS + '.' + FAM) - transaction.end() - checkMetric(t) - t.end() - }) - }) - }) - }) - - t.test('executeBatch - promise style', function (t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play') - helper.runInTransaction(agent, function transactionInScope(tx) { - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should be visible') - t.equal(tx, transaction, 'We got the same transaction') - - client.batch(insArr, { hints: hints }).then(function () { - client - .execute(selQuery) - .then((result) => { - t.ok(agent.getTransaction(), 'transaction should still still be visible') - t.equal(result.rows[0][COL], colValArr[0], 'Cassandra client should still work') - - t.equal( - transaction.trace.root.children.length, - 2, - 'there should be two children of the root' - ) - verifyTrace(t, transaction.trace, KS + '.' + FAM) - transaction.end() - checkMetric(t) - }) - .catch((error) => { - t.error(error) - }) - .finally(() => { - t.end() - }) - }) - }) - }) - - t.test('executeBatch - slow query', function (t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play') - helper.runInTransaction(agent, function transactionInScope(tx) { - // enable slow queries - agent.config.transaction_tracer.explain_threshold = 1 - agent.config.transaction_tracer.record_sql = 'raw' - agent.config.slow_sql.enabled = true - - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should be visible') - t.equal(tx, transaction, 'We got the same transaction') - - client.batch(insArr, { hints: hints }, function done(error, ok) { - if (error) { - t.error(error) - return t.end() - } - - const slowQuery = 'SELECT * FROM ' + KS + '.' + FAM - t.ok(agent.getTransaction(), 'transaction should still be visible') - t.ok(ok, 'everything should be peachy after setting') - - client.execute(slowQuery, function (error) { - if (error) { - return t.error(error) - } - - verifyTrace(t, transaction.trace, KS + '.' + FAM) - transaction.end() - t.ok(agent.queries.samples.size > 0, 'there should be a slow query') - checkMetric(t) - t.end() - }) - }) - }) - }) - - function checkMetric(t, scoped) { - const agentMetrics = agent.metrics._metrics - - const expected = { - 'Datastore/operation/Cassandra/insert': 1, - 'Datastore/allWeb': 2, - 'Datastore/Cassandra/allWeb': 2, - 'Datastore/Cassandra/all': 2, - 'Datastore/all': 2, - 'Datastore/statement/Cassandra/test.testFamily/insert': 1, - 'Datastore/operation/Cassandra/select': 1, - 'Datastore/statement/Cassandra/test.testFamily/select': 1 - } - - for (const expectedMetric in expected) { - if (expected.hasOwnProperty(expectedMetric)) { - const count = expected[expectedMetric] - - const metric = agentMetrics[scoped ? 'scoped' : 'unscoped'][expectedMetric] - t.ok(metric, 'metric "' + expectedMetric + '" should exist') - if (!metric) { - return - } - - t.equal(metric.callCount, count, 'should be called ' + count + ' times') - t.ok(metric.total, 'should have set total') - t.ok(metric.totalExclusive, 'should have set totalExclusive') - t.ok(metric.min, 'should have set min') - t.ok(metric.max, 'should have set max') - t.ok(metric.sumOfSquares, 'should have set sumOfSquares') - } - } - } - - function verifyTrace(t, trace, table) { - t.ok(trace, 'trace should exist') - t.ok(trace.root, 'root element should exist') - - const setSegment = findSegment( - trace.root, - 'Datastore/statement/Cassandra/' + table + '/insert/batch' - ) - - t.ok(setSegment, 'trace segment for insert should exist') - - if (setSegment) { - verifyTraceSegment(t, setSegment, 'insert/batch') - - t.ok( - setSegment.children.length >= 2, - 'set should have at least a dns lookup and callback/promise child' - ) - - const getSegment = findSegment( - trace.root, - 'Datastore/statement/Cassandra/' + table + '/select' - ) - t.ok(getSegment, 'trace segment for select should exist') - - if (getSegment) { - verifyTraceSegment(t, getSegment, 'select') - - t.ok(getSegment.children.length >= 1, 'get should have a callback/promise segment') - t.ok(getSegment.timer.hrDuration, 'trace segment should have ended') - } - } - } - - function verifyTraceSegment(t, segment, queryType) { - t.equal( - segment.name, - 'Datastore/statement/Cassandra/' + KS + '.' + FAM + '/' + queryType, - 'should register the execute' - ) - - const segmentAttributes = segment.getAttributes() - t.equal(segmentAttributes.product, 'Cassandra', 'should set product attribute') - t.equal(segmentAttributes.port_path_or_id, '9042', 'should set port attribute') - t.equal(segmentAttributes.database_name, 'test', 'should set database_name attribute') - t.equal(segmentAttributes.host, agent.config.getHostnameSafe(), 'should set host attribute') - } -}) diff --git a/test/versioned/cassandra-driver/query.test.js b/test/versioned/cassandra-driver/query.test.js new file mode 100644 index 0000000000..688809d8b9 --- /dev/null +++ b/test/versioned/cassandra-driver/query.test.js @@ -0,0 +1,273 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { findSegment } = require('../../lib/metrics_helper') +const params = require('../../lib/params') +const helper = require('../../lib/agent_helper') + +// constants for keyspace and table creation +const KS = 'test' +const FAM = 'testFamily' +const PK = 'pk_column' +const COL = 'test_column' + +const colValArr = ['Jim', 'Bob', 'Joe'] +const pkValArr = [111, 222, 333] +const insQuery = `INSERT INTO ${KS}.${FAM} (${PK}, ${COL}) VALUES(?, ?)` + +const insArr = [ + { query: insQuery, params: [pkValArr[0], colValArr[0]] }, + { query: insQuery, params: [pkValArr[1], colValArr[1]] }, + { query: insQuery, params: [pkValArr[2], colValArr[2]] } +] + +const hints = [ + ['int', 'varchar'], + ['int', 'varchar'], + ['int', 'varchar'] +] + +const selQuery = `SELECT * FROM ${KS}.${FAM} WHERE ${PK} = 111;` + +async function cassSetup(cassandra) { + const setupClient = new cassandra.Client({ + contactPoints: [params.cassandra_host], + protocolOptions: params.cassandra_port, + localDataCenter: 'datacenter1' + }) + + function runCommand(cmd) { + return new Promise((resolve, reject) => { + setupClient.execute(cmd, function (err) { + if (err) { + return reject(err) + } + + resolve() + }) + }) + } + + const ksDrop = `DROP KEYSPACE IF EXISTS ${KS};` + await runCommand(ksDrop) + + const ksCreate = `CREATE KEYSPACE ${KS} WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};` + await runCommand(ksCreate) + + const famCreate = `CREATE TABLE ${KS}.${FAM} (${PK} int PRIMARY KEY, ${COL} varchar);` + await runCommand(famCreate) + + setupClient.shutdown() +} + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const cassandra = require('cassandra-driver') + await cassSetup(cassandra) + + ctx.nr.client = new cassandra.Client({ + contactPoints: [params.cassandra_host], + protocolOptions: params.cassandra_port, + keyspace: KS, + localDataCenter: 'datacenter1' + }) +}) + +test.afterEach((ctx) => { + ctx.nr.agent.queries.clear() + ctx.nr.agent.metrics.clear() + helper.unloadAgent(ctx.nr.agent) + ctx.nr.client.shutdown() + removeModules(['cassandra-driver']) +}) + +test('executeBatch - callback style', (t, end) => { + const { agent, client } = t.nr + assert.equal(agent.getTransaction(), undefined, 'no transaction should be in play') + helper.runInTransaction(agent, (tx) => { + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should be visible') + assert.equal(tx, transaction, 'we got the same transaction') + + client.batch(insArr, { hints: hints }, (error, ok) => { + assert.ifError(error, 'should not get an error') + + assert.ok(agent.getTransaction(), 'transaction should still be visible') + assert.ok(ok, 'everything should be peachy after setting') + + client.execute(selQuery, (error, value) => { + assert.ifError(error, 'should not get an error') + + assert.ok(agent.getTransaction(), 'transaction should still be visible') + assert.equal(value.rows[0][COL], colValArr[0], 'cassandra client should still work') + + assert.equal( + transaction.trace.root.children.length, + 1, + 'there should be only one child of the root' + ) + verifyTrace(agent, transaction.trace, `${KS}.${FAM}`) + transaction.end() + checkMetric(agent) + + end() + }) + }) + }) +}) + +test('executeBatch - promise style', (t, end) => { + const { agent, client } = t.nr + assert.equal(agent.getTransaction(), undefined, 'no transaction should be in play') + helper.runInTransaction(agent, (tx) => { + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should be visible') + assert.equal(tx, transaction, 'we got the same transaction') + + client + .batch(insArr, { hints: hints }) + .then(() => { + client + .execute(selQuery) + .then((result) => { + assert.ok(agent.getTransaction(), 'transaction should still be visible') + assert.equal(result.rows[0][COL], colValArr[0], 'cassandra client should still work') + + assert.equal( + transaction.trace.root.children.length, + 2, + 'there should be two children of the root' + ) + verifyTrace(agent, transaction.trace, `${KS}.${FAM}`) + transaction.end() + checkMetric(agent) + }) + .catch((error) => assert.ifError(error)) + .finally(end) + }) + .catch((error) => assert.ifError(error)) + }) +}) + +test('executeBatch - slow query', (t, end) => { + const { agent, client } = t.nr + assert.equal(agent.getTransaction(), undefined, 'no transaction should be in play') + helper.runInTransaction(agent, (tx) => { + // enable slow queries + agent.config.transaction_tracer.explain_threshold = 1 + agent.config.transaction_tracer.record_sql = 'raw' + agent.config.slow_sql.enabled = true + + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should be visible') + assert.equal(tx, transaction, 'We got the same transaction') + + client.batch(insArr, { hints: hints }, (error, ok) => { + assert.ifError(error, 'should not get an error') + + const slowQuery = `SELECT * FROM ${KS}.${FAM}` + assert.ok(agent.getTransaction(), 'transaction should still be visible') + assert.ok(ok, 'everything should be peachy after setting') + + client.execute(slowQuery, (error) => { + assert.ifError(error, 'should not get an error') + + verifyTrace(agent, transaction.trace, `${KS}.${FAM}`) + transaction.end() + assert.ok(agent.queries.samples.size > 0, 'there should be a slow query') + checkMetric(agent) + + end() + }) + }) + }) +}) + +function checkMetric(agent, scoped) { + const agentMetrics = agent.metrics._metrics + + const expected = { + 'Datastore/operation/Cassandra/insert': 1, + 'Datastore/allWeb': 2, + 'Datastore/Cassandra/allWeb': 2, + 'Datastore/Cassandra/all': 2, + 'Datastore/all': 2, + 'Datastore/statement/Cassandra/test.testFamily/insert': 1, + 'Datastore/operation/Cassandra/select': 1, + 'Datastore/statement/Cassandra/test.testFamily/select': 1 + } + + for (const expectedMetric in expected) { + if (expected.hasOwnProperty(expectedMetric)) { + const count = expected[expectedMetric] + + const metric = agentMetrics[scoped ? 'scoped' : 'unscoped'][expectedMetric] + assert.ok(metric, 'metric "' + expectedMetric + '" should exist') + if (!metric) { + return + } + + assert.equal(metric.callCount, count, 'should be called ' + count + ' times') + assert.ok(metric.total, 'should have set total') + assert.ok(metric.totalExclusive, 'should have set totalExclusive') + assert.ok(metric.min, 'should have set min') + assert.ok(metric.max, 'should have set max') + assert.ok(metric.sumOfSquares, 'should have set sumOfSquares') + } + } +} + +function verifyTrace(agent, trace, table) { + assert.ok(trace, 'trace should exist') + assert.ok(trace.root, 'root element should exist') + + const setSegment = findSegment( + trace.root, + 'Datastore/statement/Cassandra/' + table + '/insert/batch' + ) + + assert.ok(setSegment, 'trace segment for insert should exist') + + if (setSegment) { + verifyTraceSegment(agent, setSegment, 'insert/batch') + + assert.ok( + setSegment.children.length >= 2, + 'set should have at least a dns lookup and callback/promise child' + ) + + const getSegment = findSegment(trace.root, 'Datastore/statement/Cassandra/' + table + '/select') + assert.ok(getSegment, 'trace segment for select should exist') + + if (getSegment) { + verifyTraceSegment(agent, getSegment, 'select') + + assert.ok(getSegment.children.length >= 1, 'get should have a callback/promise segment') + assert.ok(getSegment.timer.hrDuration, 'trace segment should have ended') + } + } +} + +function verifyTraceSegment(agent, segment, queryType) { + assert.equal( + segment.name, + 'Datastore/statement/Cassandra/' + KS + '.' + FAM + '/' + queryType, + 'should register the execute' + ) + + const segmentAttributes = segment.getAttributes() + assert.equal(segmentAttributes.product, 'Cassandra', 'should set product attribute') + assert.equal(segmentAttributes.port_path_or_id, '9042', 'should set port attribute') + assert.equal(segmentAttributes.database_name, 'test', 'should set database_name attribute') + assert.equal(segmentAttributes.host, agent.config.getHostnameSafe(), 'should set host attribute') +} diff --git a/test/versioned/elastic/elasticsearch.tap.js b/test/versioned/elastic/elasticsearch.test.js similarity index 52% rename from test/versioned/elastic/elasticsearch.tap.js rename to test/versioned/elastic/elasticsearch.test.js index 3f33fe60cc..02253b0aa1 100644 --- a/test/versioned/elastic/elasticsearch.tap.js +++ b/test/versioned/elastic/elasticsearch.test.js @@ -4,9 +4,8 @@ */ 'use strict' - -const tap = require('tap') -const test = tap.test +const test = require('node:test') +const assert = require('node:assert') const helper = require('../../lib/agent_helper') const params = require('../../lib/params') const urltils = require('../../../lib/util/urltils') @@ -15,7 +14,6 @@ const { readFile } = require('fs/promises') const semver = require('semver') const DB_INDEX = `test-${randomString()}` const DB_INDEX_2 = `test2-${randomString()}` -const DB_INDEX_3 = `test3-${randomString()}` const SEARCHTERM_1 = randomString() function randomString() { @@ -50,67 +48,64 @@ function setMsearch(body, version) { } } -test('Elasticsearch instrumentation', (t) => { - t.autoend() - - let METRIC_HOST_NAME = null - let HOST_ID = null - - let agent - let client - let pkgVersion - - t.before(async () => { +test('Elasticsearch instrumentation', async (t) => { + t.beforeEach(async (ctx) => { // Determine version. ElasticSearch v7 did not export package, so we have to read the file // instead of requiring it, as we can with 8+. const pkg = await readFile(`${__dirname}/node_modules/@elastic/elasticsearch/package.json`) - ;({ version: pkgVersion } = JSON.parse(pkg.toString())) + const { version: pkgVersion } = JSON.parse(pkg.toString()) - agent = helper.instrumentMockedAgent() + const agent = helper.instrumentMockedAgent() - METRIC_HOST_NAME = urltils.isLocalhost(params.elastic_host) + const METRIC_HOST_NAME = urltils.isLocalhost(params.elastic_host) ? agent.config.getHostnameSafe() : params.elastic_host - HOST_ID = METRIC_HOST_NAME + '/' + params.elastic_port + const HOST_ID = METRIC_HOST_NAME + '/' + params.elastic_port // need to capture attributes agent.config.attributes.enabled = true const { Client } = require('@elastic/elasticsearch') - client = new Client({ + const client = new Client({ node: `http://${params.elastic_host}:${params.elastic_port}` }) + ctx.nr = { + agent, + client, + pkgVersion, + METRIC_HOST_NAME, + HOST_ID + } + return Promise.all([ client.indices.create({ index: DB_INDEX }), client.indices.create({ index: DB_INDEX_2 }) ]) }) - t.afterEach(() => { - agent.queries.clear() - }) - - t.teardown(() => { - agent && helper.unloadAgent(agent) + t.afterEach((ctx) => { + const { agent, client } = ctx.nr + helper.unloadAgent(agent) return Promise.all([ client.indices.delete({ index: DB_INDEX }), client.indices.delete({ index: DB_INDEX_2 }) ]) }) - t.test('should be able to record creating an index', async (t) => { + await t.test('should be able to record creating an index', async (t) => { + const { agent, client } = t.nr const index = `test-index-${randomString()}` - t.teardown(async () => { + t.after(async () => { await client.indices.delete({ index }) }) await helper.runInTransaction(agent, async function transactionInScope(transaction) { - t.ok(transaction, 'transaction should be visible') + assert.ok(transaction, 'transaction should be visible') await client.indices.create({ index }) const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.equal( + assert.equal( firstChild.name, `Datastore/statement/ElasticSearch/${index}/index.create`, 'should record index PUT as create' @@ -118,36 +113,15 @@ test('Elasticsearch instrumentation', (t) => { }) }) - t.test('should record bulk operations', async (t) => { + await t.test('should record bulk operations', async (t) => { + const { agent, client, pkgVersion } = t.nr await helper.runInTransaction(agent, async function transactionInScope(transaction) { - const operations = [ - { index: { _index: DB_INDEX } }, - { title: 'First Bulk Doc', body: 'Content of first bulk document' }, - { index: { _index: DB_INDEX } }, - { title: 'Second Bulk Doc', body: 'Content of second bulk document.' }, - { index: { _index: DB_INDEX } }, - { title: 'Third Bulk Doc', body: 'Content of third bulk document.' }, - { index: { _index: DB_INDEX } }, - { title: 'Fourth Bulk Doc', body: 'Content of fourth bulk document.' }, - { index: { _index: DB_INDEX_2 } }, - { title: 'Fifth Bulk Doc', body: 'Content of fifth bulk document' }, - { index: { _index: DB_INDEX_2 } }, - { - title: 'Sixth Bulk Doc', - body: `Content of sixth bulk document. Has search term: ${SEARCHTERM_1}` - }, - { index: { _index: DB_INDEX_2 } }, - { title: 'Seventh Bulk Doc', body: 'Content of seventh bulk document.' }, - { index: { _index: DB_INDEX_2 } }, - { title: 'Eighth Bulk Doc', body: 'Content of eighth bulk document.' } - ] - - await client.bulk(setBulkBody(operations, pkgVersion)) - t.ok(transaction, 'transaction should still be visible after bulk create') + await bulkInsert({ client, pkgVersion }) + assert.ok(transaction, 'transaction should still be visible after bulk create') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.equal( + assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/any/bulk.create', 'should record bulk operation' @@ -155,24 +129,10 @@ test('Elasticsearch instrumentation', (t) => { }) }) - t.test('should record bulk operations triggered by client helpers', async (t) => { + await t.test('should record bulk operations triggered by client helpers', async (t) => { + const { agent, client } = t.nr await helper.runInTransaction(agent, async function transactionInScope(transaction) { - const operations = [ - { title: 'Ninth Bulk Doc from helpers', body: 'Content of ninth bulk document' }, - { title: 'Tenth Bulk Doc from helpers', body: 'Content of tenth bulk document.' }, - { title: 'Eleventh Bulk Doc from helpers', body: 'Content of eleventh bulk document.' }, - { title: 'Twelfth Bulk Doc from helpers', body: 'Content of twelfth bulk document.' }, - { - title: 'Thirteenth Bulk Doc from helpers', - body: 'Content of thirteenth bulk document' - }, - { - title: 'Fourteenth Bulk Doc from helpers', - body: 'Content of fourteenth bulk document.' - }, - { title: 'Fifteenth Bulk Doc from helpers', body: 'Content of fifteenth bulk document.' }, - { title: 'Sixteenth Bulk Doc from helpers', body: 'Content of sixteenth bulk document.' } - ] + const operations = getBulkData() await client.helpers.bulk({ datasource: operations, onDocument() { @@ -182,13 +142,13 @@ test('Elasticsearch instrumentation', (t) => { }, refreshOnCompletion: true }) - t.ok(transaction, 'transaction should still be visible after bulk create') + assert.ok(transaction, 'transaction should still be visible after bulk create') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - t.ok(trace?.root?.children?.[1], 'trace, trace root, and second child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[1], 'trace, trace root, and second child should exist') // helper interface results in a first child of timers.setTimeout, with the second child related to the operation const secondChild = trace.root.children[1] - t.equal( + assert.equal( secondChild.name, 'Datastore/statement/ElasticSearch/any/bulk.create', 'should record bulk operation' @@ -196,7 +156,8 @@ test('Elasticsearch instrumentation', (t) => { }) }) - t.test('should record search with query string', async function (t) { + await t.test('should record search with query string', async function (t) { + const { agent, client, METRIC_HOST_NAME } = t.nr // enable slow queries agent.config.transaction_tracer.explain_threshold = 0 agent.config.transaction_tracer.record_sql = 'raw' @@ -204,24 +165,24 @@ test('Elasticsearch instrumentation', (t) => { await helper.runInTransaction(agent, async function transactionInScope(transaction) { const expectedQuery = { q: SEARCHTERM_1 } const search = await client.search({ index: DB_INDEX_2, ...expectedQuery }) - t.ok(search, 'search should return a result') - t.ok(transaction, 'transaction should still be visible after search') + assert.ok(search, 'search should return a result') + assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.match( + assert.equal( firstChild.name, `Datastore/statement/ElasticSearch/${DB_INDEX_2}/search`, 'querystring search should be recorded as a search' ) const attrs = firstChild.getAttributes() - t.match(attrs.product, 'ElasticSearch') - t.match(attrs.host, METRIC_HOST_NAME) + assert.equal(attrs.product, 'ElasticSearch') + assert.equal(attrs.host, METRIC_HOST_NAME) transaction.end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') for (const query of agent.queries.samples.values()) { - t.ok(query.total > 0, 'the samples should have positive duration') - t.match( + assert.ok(query.total > 0, 'the samples should have positive duration') + assert.equal( query.trace.query, JSON.stringify(expectedQuery), 'expected query string should have been used' @@ -229,7 +190,8 @@ test('Elasticsearch instrumentation', (t) => { } }) }) - t.test('should record search with request body', async function (t) { + await t.test('should record search with request body', async function (t) { + const { agent, client, pkgVersion, METRIC_HOST_NAME } = t.nr // enable slow queries agent.config.transaction_tracer.explain_threshold = 0 agent.config.transaction_tracer.record_sql = 'raw' @@ -239,27 +201,27 @@ test('Elasticsearch instrumentation', (t) => { const expectedQuery = { query: { match: { body: 'document' } } } const requestBody = setRequestBody(expectedQuery, pkgVersion) const search = await client.search({ index: DB_INDEX, ...requestBody }) - t.ok(search, 'search should return a result') - t.ok(transaction, 'transaction should still be visible after search') + assert.ok(search, 'search should return a result') + assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.match( + assert.equal( firstChild.name, `Datastore/statement/ElasticSearch/${DB_INDEX}/search`, 'search index is specified, so name shows it' ) const attrs = firstChild.getAttributes() - t.equal(attrs.product, 'ElasticSearch') - t.equal(attrs.host, METRIC_HOST_NAME) - t.equal(attrs.port_path_or_id, `${params.elastic_port}`) + assert.equal(attrs.product, 'ElasticSearch') + assert.equal(attrs.host, METRIC_HOST_NAME) + assert.equal(attrs.port_path_or_id, `${params.elastic_port}`) // TODO: update once instrumentation is properly setting database name - t.equal(attrs.database_name, 'unknown') + assert.equal(attrs.database_name, 'unknown') transaction.end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') for (const query of agent.queries.samples.values()) { - t.ok(query.total > 0, 'the samples should have positive duration') - t.match( + assert.ok(query.total > 0, 'the samples should have positive duration') + assert.equal( query.trace.query, JSON.stringify({ ...expectedQuery }), 'expected query body should have been recorded' @@ -268,7 +230,8 @@ test('Elasticsearch instrumentation', (t) => { }) }) - t.test('should record search across indices', async function (t) { + await t.test('should record search across indices', async function (t) { + const { agent, client, pkgVersion, METRIC_HOST_NAME } = t.nr // enable slow queries agent.config.transaction_tracer.explain_threshold = 0 agent.config.transaction_tracer.record_sql = 'raw' @@ -277,24 +240,24 @@ test('Elasticsearch instrumentation', (t) => { const expectedQuery = { query: { match: { body: 'document' } } } const requestBody = setRequestBody(expectedQuery, pkgVersion) const search = await client.search({ ...requestBody }) - t.ok(search, 'search should return a result') - t.ok(transaction, 'transaction should still be visible after search') + assert.ok(search, 'search should return a result') + assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.match( + assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/any/search', 'child name on all indices should show search' ) const attrs = firstChild.getAttributes() - t.match(attrs.product, 'ElasticSearch') - t.match(attrs.host, METRIC_HOST_NAME) + assert.equal(attrs.product, 'ElasticSearch') + assert.equal(attrs.host, METRIC_HOST_NAME) transaction.end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') for (const query of agent.queries.samples.values()) { - t.ok(query.total > 0, 'the samples should have positive duration') - t.match( + assert.ok(query.total > 0, 'the samples should have positive duration') + assert.equal( query.trace.query, JSON.stringify({ ...expectedQuery }), 'expected query body should have been recorded' @@ -302,10 +265,12 @@ test('Elasticsearch instrumentation', (t) => { } }) }) - t.test('should record msearch', async function (t) { + await t.test('should record msearch', async function (t) { + const { agent, client, METRIC_HOST_NAME, pkgVersion } = t.nr agent.config.transaction_tracer.explain_threshold = 0 agent.config.transaction_tracer.record_sql = 'raw' agent.config.slow_sql.enabled = true + await bulkInsert({ client, pkgVersion }) await helper.runInTransaction(agent, async function transactionInScope(transaction) { const expectedQuery = [ {}, // cross-index searches have can have an empty metadata section @@ -321,27 +286,27 @@ test('Elasticsearch instrumentation', (t) => { results = search?.body?.responses } - t.ok(results, 'msearch should return results') - t.equal(results?.length, 2, 'there should be two responses--one per search') - t.equal(results?.[0]?.hits?.hits?.length, 1, 'first search should return one result') - t.equal(results?.[1]?.hits?.hits?.length, 10, 'second search should return ten results') - t.ok(transaction, 'transaction should still be visible after search') + assert.ok(results, 'msearch should return results') + assert.equal(results?.length, 2, 'there should be two responses--one per search') + assert.equal(results?.[0]?.hits?.hits?.length, 1, 'first search should return one result') + assert.equal(results?.[1]?.hits?.hits?.length, 8, 'second search should return ten results') + assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.match( + assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/any/msearch.create', 'child name should show msearch' ) const attrs = firstChild.getAttributes() - t.match(attrs.product, 'ElasticSearch') - t.match(attrs.host, METRIC_HOST_NAME) + assert.equal(attrs.product, 'ElasticSearch') + assert.equal(attrs.host, METRIC_HOST_NAME) transaction.end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') for (const query of agent.queries.samples.values()) { - t.ok(query.total > 0, 'the samples should have positive duration') - t.match( + assert.ok(query.total > 0, 'the samples should have positive duration') + assert.equal( query.trace.query, JSON.stringify(expectedQuery), 'expected msearch query should have been recorded' @@ -350,10 +315,12 @@ test('Elasticsearch instrumentation', (t) => { }) }) - t.test('should record msearch via helpers', async function (t) { + await t.test('should record msearch via helpers', async function (t) { + const { agent, client, pkgVersion } = t.nr agent.config.transaction_tracer.explain_threshold = 0 agent.config.transaction_tracer.record_sql = 'raw' agent.config.slow_sql.enabled = true + await bulkInsert({ client, pkgVersion }) await helper.runInTransaction(agent, async function transactionInScope(transaction) { const m = client.helpers.msearch() const searchA = await m.search({}, { query: { match: { body: SEARCHTERM_1 } } }) @@ -361,31 +328,31 @@ test('Elasticsearch instrumentation', (t) => { const resultsA = searchA?.body?.hits const resultsB = searchB?.body?.hits - t.ok(resultsA, 'msearch for sixth should return results') - t.ok(resultsB, 'msearch for bulk should return results') - t.equal(resultsA?.hits?.length, 1, 'first search should return one result') - t.equal(resultsB?.hits?.length, 10, 'second search should return ten results') - t.ok(transaction, 'transaction should still be visible after search') + assert.ok(resultsA, 'msearch for sixth should return results') + assert.ok(resultsB, 'msearch for bulk should return results') + assert.equal(resultsA?.hits?.length, 1, 'first search should return one result') + assert.equal(resultsB?.hits?.length, 8, 'second search should return ten results') + assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - t.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') + assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') const firstChild = trace.root.children[0] - t.match( + assert.equal( firstChild.name, 'timers.setTimeout', 'helpers, for some reason, generates a setTimeout metric first' ) transaction.end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') for (const query of agent.queries.samples.values()) { // which query gets captured in helper.msearch is non-deterministic - t.ok(query.total > 0, 'the samples should have positive duration') + assert.ok(query.total > 0, 'the samples should have positive duration') } }) }) - t.test('should create correct metrics', async function (t) { + await t.test('should create correct metrics', async function (t) { + const { agent, client, pkgVersion, HOST_ID } = t.nr const id = `key-${randomString()}` - t.plan(28) await helper.runInTransaction(agent, async function transactionInScope(transaction) { const documentProp = setRequestBody( { @@ -427,12 +394,12 @@ test('Elasticsearch instrumentation', (t) => { 'Datastore/statement/ElasticSearch/any/search': 1 } expected['Datastore/instance/ElasticSearch/' + HOST_ID] = 5 - checkMetrics(t, unscoped, expected) + checkMetrics(unscoped, expected) }) }) - t.test('should not add instance attributes/metrics when disabled', async function (t) { - t.plan(4) + await t.test('should not add instance attributes/metrics when disabled', async function (t) { + const { agent, client, pkgVersion, HOST_ID } = t.nr // disable agent.config.datastore_tracer.instance_reporting.enabled = false @@ -457,140 +424,81 @@ test('Elasticsearch instrumentation', (t) => { const createSegment = transaction.trace.root.children[0] const attributes = createSegment.getAttributes() - t.equal(attributes.host, undefined, 'should not have host attribute') - t.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') - t.equal(attributes.database_name, undefined, 'should not have db name attribute') + assert.equal(attributes.host, undefined, 'should not have host attribute') + assert.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') + assert.equal(attributes.database_name, undefined, 'should not have db name attribute') transaction.end() const unscoped = transaction.metrics.unscoped - t.equal( + assert.equal( unscoped['Datastore/instance/ElasticSearch/' + HOST_ID], undefined, 'should not have instance metric' ) }) }) - t.test('edge cases', async (t) => { + await t.test('edge cases', async (t) => { + const { agent, client } = t.nr await helper.runInTransaction(agent, async function transactionInScope(transaction) { try { await client.indices.create({ index: '_search' }) } catch (e) { - t.ok(e, 'should not be able to create an index named _search') + assert.ok(e, 'should not be able to create an index named _search') } const firstChild = transaction?.trace?.root?.children[0] - t.equal( + assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/_search/index.create', 'should record the attempted index creation without altering the index name' ) }) }) - t.test('index existence check should not error', async (t) => { + await t.test('index existence check should not error', async (t) => { + const { agent, client } = t.nr await helper.runInTransaction(agent, async function transactionInScope() { try { await client.indices.exists({ index: DB_INDEX }) } catch (e) { - t.notOk(e, 'should be able to check for index existence') + assert.ok(!e, 'should be able to check for index existence') } }) }) }) -test('Elasticsearch uninstrumented behavior, to check helpers', { skip: false }, (t) => { - t.autoend() - - let client - // eslint-disable-next-line no-unused-vars - let pkgVersion - - t.before(async () => { - // Determine version. ElasticSearch v7 did not export package, so we have to read the file - // instead of requiring it, as we can with 8+. - const pkg = await readFile(`${__dirname}/node_modules/@elastic/elasticsearch/package.json`) - ;({ version: pkgVersion } = JSON.parse(pkg.toString())) - - const { Client } = require('@elastic/elasticsearch') - client = new Client({ - node: `http://${params.elastic_host}:${params.elastic_port}` +function getBulkData(includeIndex) { + let operations = [ + { title: 'First Bulk Doc', body: 'Content of first bulk document' }, + { title: 'Second Bulk Doc', body: 'Content of second bulk document.' }, + { title: 'Third Bulk Doc', body: 'Content of third bulk document.' }, + { title: 'Fourth Bulk Doc', body: 'Content of fourth bulk document.' }, + { title: 'Fifth Bulk Doc', body: 'Content of fifth bulk document' }, + { + title: 'Sixth Bulk Doc', + body: `Content of sixth bulk document. Has search term: ${SEARCHTERM_1}` + }, + { title: 'Seventh Bulk Doc', body: 'Content of seventh bulk document.' }, + { title: 'Eighth Bulk Doc', body: 'Content of eighth bulk document.' } + ] + + if (includeIndex) { + operations = operations.flatMap((doc, i) => { + return [{ index: { _index: i < 4 ? DB_INDEX : DB_INDEX_2 } }, doc] }) + } - return Promise.all([client.indices.create({ index: DB_INDEX_3 })]) - }) - - t.teardown(() => { - return Promise.all([client.indices.delete({ index: DB_INDEX_3 })]) - }) + return operations +} - t.test('should record bulk operations triggered by client helpers', async (t) => { - const operations = [ - { - title: 'Uninstrumented First Bulk Doc from helpers', - body: 'Content of uninstrumented first bulk document' - }, - { - title: 'Uninstrumented Second Bulk Doc from helpers', - body: 'Content of uninstrumented second bulk document.' - }, - { - title: 'Uninstrumented Third Bulk Doc from helpers', - body: 'Content of uninstrumented third bulk document.' - }, - { - title: 'Uninstrumented Fourth Bulk Doc from helpers', - body: 'Content of uninstrumented fourth bulk document.' - }, - { - title: 'Uninstrumented Fifth Bulk Doc from helpers', - body: 'Content of uninstrumented fifth bulk document' - }, - { - title: 'Uninstrumented Sixth Bulk Doc from helpers', - body: 'Content of uninstrumented sixth bulk document.' - }, - { - title: 'Uninstrumented Seventh Bulk Doc from helpers', - body: 'Content of uninstrumented seventh bulk document.' - }, - { - title: 'Uninstrumented Eighth Bulk Doc from helpers', - body: 'Content of uninstrumented eighth bulk document.' - } - ] - const result = await client.helpers.bulk({ - datasource: operations, - onDocument() { - return { - index: { _index: DB_INDEX_3 } - } - } - // refreshOnCompletion: true - }) // setBulkBody(operations, pkgVersion) - t.ok(result, 'We should have been able to create bulk entries without error') - t.equal(result.total, 8, 'We should have been inserted eight records') - }) - t.test('should be able to check bulk insert with msearch via helpers', async function (t) { - const m = client.helpers.msearch() - const searchA = await m.search({ index: DB_INDEX_3 }, { query: { match: { body: 'sixth' } } }) - const searchB = await m.search( - { index: DB_INDEX_3 }, - { query: { match: { body: 'uninstrumented' } } } - ) - const resultsA = searchA?.body?.hits - const resultsB = searchB?.body?.hits - - t.ok(resultsA, 'msearch should return a response for A') - t.ok(resultsB, 'msearch should return results for B') - // some versions of helper msearch seem not to return results for the first search. - // t.equal(resultsA?.hits?.length, 1, 'first search should return one result') - t.equal(resultsB?.hits?.length, 8, 'second search should return eight results') - }) -}) +async function bulkInsert({ client, pkgVersion }) { + const operations = getBulkData(true) + await client.bulk(setBulkBody(operations, pkgVersion)) +} -function checkMetrics(t, metrics, expected) { +function checkMetrics(metrics, expected) { Object.keys(expected).forEach(function (name) { - t.ok(metrics[name], 'should have metric ' + name) + assert.ok(metrics[name], 'should have metric ' + name) if (metrics[name]) { - t.equal( + assert.equal( metrics[name].callCount, expected[name], 'should have ' + expected[name] + ' calls for ' + name diff --git a/test/versioned/elastic/elasticsearchNoop.tap.js b/test/versioned/elastic/elasticsearchNoop.tap.js deleted file mode 100644 index 5899aee370..0000000000 --- a/test/versioned/elastic/elasticsearchNoop.tap.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const test = tap.test -const helper = require('../../lib/agent_helper') -const params = require('../../lib/params') -const crypto = require('crypto') -const DB_INDEX = `test-${randomString()}` - -function randomString() { - return crypto.randomBytes(5).toString('hex') -} - -test('Elasticsearch instrumentation', (t) => { - t.autoend() - - let agent - let client - - t.before(async () => { - agent = helper.instrumentMockedAgent() - - // need to capture attributes - agent.config.attributes.enabled = true - const { Client } = require('@elastic/elasticsearch') - - client = new Client({ - node: `http://${params.elastic_host}:${params.elastic_port}` - }) - }) - - t.afterEach(() => { - agent.queries.clear() - }) - - t.teardown(() => { - agent && helper.unloadAgent(agent) - }) - - t.test('unsupported version should noop db tracing, but record web transaction', async (t) => { - await helper.runInTransaction(agent, async function transactionInScope(transaction) { - try { - await client.indices.create({ index: DB_INDEX }) - } catch (e) { - t.notOk(e, 'should not error') - } - const firstChild = transaction?.trace?.root?.children[0] - t.equal( - firstChild.name, - `External/localhost:9200/${DB_INDEX}`, - 'should record index creation as an external transaction' - ) - await client.indices.delete({ index: DB_INDEX }) - }) - }) -}) diff --git a/test/versioned/elastic/elasticsearchNoop.test.js b/test/versioned/elastic/elasticsearchNoop.test.js new file mode 100644 index 0000000000..1ffd758f6b --- /dev/null +++ b/test/versioned/elastic/elasticsearchNoop.test.js @@ -0,0 +1,59 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const test = require('node:test') +const assert = require('node:assert') +const helper = require('../../lib/agent_helper') +const params = require('../../lib/params') +const crypto = require('crypto') +const DB_INDEX = `test-${randomString()}` + +function randomString() { + return crypto.randomBytes(5).toString('hex') +} + +test('Elasticsearch instrumentation', async (t) => { + t.beforeEach(async (ctx) => { + const agent = helper.instrumentMockedAgent() + + // need to capture attributes + agent.config.attributes.enabled = true + const { Client } = require('@elastic/elasticsearch') + + const client = new Client({ + node: `http://${params.elastic_host}:${params.elastic_port}` + }) + ctx.nr = { + agent, + client + } + }) + + t.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + }) + + await t.test( + 'unsupported version should noop db tracing, but record web transaction', + async (t) => { + const { agent, client } = t.nr + await helper.runInTransaction(agent, async function transactionInScope(transaction) { + try { + await client.indices.create({ index: DB_INDEX }) + } catch (e) { + assert.ok(!e, 'should not error') + } + const firstChild = transaction?.trace?.root?.children[0] + assert.equal( + firstChild.name, + `External/localhost:9200/${DB_INDEX}`, + 'should record index creation as an external transaction' + ) + await client.indices.delete({ index: DB_INDEX }) + }) + } + ) +}) diff --git a/test/versioned/elastic/package.json b/test/versioned/elastic/package.json index 876c188bc8..de9c213f7f 100644 --- a/test/versioned/elastic/package.json +++ b/test/versioned/elastic/package.json @@ -17,7 +17,7 @@ "@elastic/elasticsearch": "7.13.0" }, "files": [ - "elasticsearchNoop.tap.js" + "elasticsearchNoop.test.js" ] }, { @@ -28,7 +28,7 @@ "@elastic/elasticsearch": ">=7.16.0" }, "files": [ - "elasticsearch.tap.js" + "elasticsearch.test.js" ] } ] diff --git a/test/versioned/esm-package/package.json b/test/versioned/esm-package/package.json index b95f430158..a46c5b400b 100644 --- a/test/versioned/esm-package/package.json +++ b/test/versioned/esm-package/package.json @@ -13,7 +13,7 @@ "parse-json": "6.0.2" }, "files": [ - "parse-json.tap.mjs" + "parse-json.test.mjs" ] } ], diff --git a/test/versioned/esm-package/parse-json.tap.mjs b/test/versioned/esm-package/parse-json.tap.mjs deleted file mode 100644 index 0935593821..0000000000 --- a/test/versioned/esm-package/parse-json.tap.mjs +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import helper from '../../lib/agent_helper.js' -import shimmer from '../../../lib/shimmer.js' -import parseJsonInstrumentation from './parse-json-instrumentation.mjs' - -shimmer.registerInstrumentation({ - moduleName: 'parse-json', - type: 'generic', - isEsm: true, - onRequire: parseJsonInstrumentation -}) - -tap.test('ESM Package Instrumentation', (t) => { - t.autoend() - - let agent - let parseJson - let JSONError - - t.before(async () => { - agent = helper.instrumentMockedAgent() - ;({ default: parseJson, JSONError } = await import('parse-json')) - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - t.test('should register instrumentation on default exports', (t) => { - const output = parseJson(JSON.stringify({ foo: 'bar' })) - t.ok(output.isInstrumented, 'should have the field we add in our test instrumentation') - t.end() - }) - - t.test('should register instrumentation on named exports', (t) => { - const err = new JSONError('test me') - t.ok(err.isInstrumented, 'JSONError should be instrumented') - t.end() - }) -}) diff --git a/test/versioned/esm-package/parse-json.test.mjs b/test/versioned/esm-package/parse-json.test.mjs new file mode 100644 index 0000000000..38a81528a1 --- /dev/null +++ b/test/versioned/esm-package/parse-json.test.mjs @@ -0,0 +1,36 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import helper from '../../lib/agent_helper.js' +import shimmer from '../../../lib/shimmer.js' +import parseJsonInstrumentation from './parse-json-instrumentation.mjs' + +shimmer.registerInstrumentation({ + moduleName: 'parse-json', + type: 'generic', + isEsm: true, + onRequire: parseJsonInstrumentation +}) + +test('ESM Package Instrumentation', async (t) => { + const agent = helper.instrumentMockedAgent() + const { default: parseJson, JSONError } = await import('parse-json') + + t.after(() => { + helper.unloadAgent(agent) + }) + + await t.test('should register instrumentation on default exports', () => { + const output = parseJson(JSON.stringify({ foo: 'bar' })) + assert.ok(output.isInstrumented, 'should have the field we add in our test instrumentation') + }) + + await t.test('should register instrumentation on named exports', () => { + const err = new JSONError('test me') + assert.ok(err.isInstrumented, 'JSONError should be instrumented') + }) +}) diff --git a/test/versioned/express-esm/helpers.mjs b/test/versioned/express-esm/helpers.mjs index f65a4fe4d2..ba933cfb07 100644 --- a/test/versioned/express-esm/helpers.mjs +++ b/test/versioned/express-esm/helpers.mjs @@ -12,12 +12,6 @@ const helpers = Object.create(null) * @returns { app, express } */ helpers.setup = async function setup() { - /** - * This rule is not fully fleshed out and the library is no longer maintained - * See: https://github.com/mysticatea/eslint-plugin-node/issues/250 - * Fix would be to migrate to use https://github.com/weiran-zsd/eslint-plugin-node - */ - const { default: express } = await import('express') const app = express() return { app, express } @@ -39,14 +33,13 @@ helpers.makeRequest = function makeRequest(server, endpoint) { * Listens to express app, makes request, and returns transaction when `transactionFinished` event fires * * @param {Object} params - * @param {Object} params.app express instance - * @param {Object} params.t tap test + * @param {Object} params.server the underlying core server instance of the + * express app * @param {Object} params.agent mocked agent * @param {string} params.endpoint URI */ helpers.makeRequestAndFinishTransaction = async function makeRequestAndFinishTransaction({ - app, - t, + server, agent, endpoint }) { @@ -59,13 +52,7 @@ helpers.makeRequestAndFinishTransaction = async function makeRequestAndFinishTra agent.on('transactionFinished', transactionHandler) - const server = app.listen(function () { - helpers.makeRequest(this, endpoint) - }) - t.teardown(() => { - server.close() - agent.removeListener('transactionFinished', transactionHandler) - }) + helpers.makeRequest(server, endpoint) return promise } diff --git a/test/versioned/express-esm/package.json b/test/versioned/express-esm/package.json index 924c057779..4abc12ec58 100644 --- a/test/versioned/express-esm/package.json +++ b/test/versioned/express-esm/package.json @@ -16,10 +16,9 @@ } }, "files": [ - "segments.tap.mjs", - "transaction-naming.tap.mjs" + "segments.test.mjs", + "transaction-naming.test.mjs" ] } - ], - "dependencies": {} + ] } diff --git a/test/versioned/express-esm/segments.tap.mjs b/test/versioned/express-esm/segments.tap.mjs deleted file mode 100644 index f497e87f7c..0000000000 --- a/test/versioned/express-esm/segments.tap.mjs +++ /dev/null @@ -1,735 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import semver from 'semver' -import helper from '../../lib/agent_helper.js' -import NAMES from '../../../lib/metrics/names.js' -import { findSegment } from '../../lib/metrics_helper.js' -import { test } from 'tap' -import expressHelpers from './helpers.mjs' -const { setup, makeRequestAndFinishTransaction } = expressHelpers -const assertSegmentsOptions = { - exact: true, - // in Node 8 the http module sometimes creates a setTimeout segment - // the query and expressInit middleware are registered under the hood up until express 5 - exclude: [NAMES.EXPRESS.MIDDLEWARE + 'query', NAMES.EXPRESS.MIDDLEWARE + 'expressInit'] -} -// import expressPkg from 'express/package.json' assert {type: 'json'} -// const pkgVersion = expressPkg.version -import { readFileSync } from 'node:fs' -const { version: pkgVersion } = JSON.parse(readFileSync('./node_modules/express/package.json')) -const isExpress5 = semver.gte(pkgVersion, '5.0.0') - -test('transaction segments tests', (t) => { - t.autoend() - - let agent - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - t.test('first two segments are built-in Express middleware', async function (t) { - const { app } = await setup() - - app.all('/test', function (req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) - }) - - t.test('middleware with child segment gets named correctly', async function (t) { - const { app } = await setup() - - app.all('/test', function (req, res) { - setTimeout(function () { - res.end() - }, 1) - }) - - const { transaction } = await runTest({ app, t }) - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) - }) - - t.test('segments for route handler', async function (t) { - const { app } = await setup() - - app.all('/test', function (req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) - }) - - t.test('route function names are in segment names', async function (t) { - const { app } = await setup() - - app.all('/test', function myHandler(req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test']) - }) - - t.test('middleware mounted on a path should produce correct names', async function (t) { - const { app } = await setup() - - app.use('/test/:id', function handler(req, res) { - res.send() - }) - - const { transaction } = await runTest({ app, t, endpoint: '/test/1' }) - const routeSegment = findSegment( - transaction.trace.root, - NAMES.EXPRESS.MIDDLEWARE + 'handler//test/:id' - ) - t.ok(routeSegment) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + 'handler//test/:id'], - '/test/:id' - ) - }) - - t.test('each handler in route has its own segment', async function (t) { - const { app } = await setup() - - app.all( - '/test', - function handler1(req, res, next) { - next() - }, - function handler2(req, res) { - res.send() - } - ) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Route Path: /test', - [NAMES.EXPRESS.MIDDLEWARE + 'handler1', NAMES.EXPRESS.MIDDLEWARE + 'handler2'] - ], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [ - NAMES.EXPRESS.MIDDLEWARE + 'handler1//test', - NAMES.EXPRESS.MIDDLEWARE + 'handler2//test' - ]) - }) - - t.test('segments for routers', async function (t) { - const { app, express } = await setup() - - const router = express.Router() // eslint-disable-line new-cap - router.all('/test', function (req, res) { - res.end() - }) - - app.use('/router1', router) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/router1/test' }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router1', - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + '//router1/test'], - '/router1/test' - ) - }) - - t.test('two root routers', async function (t) { - const { app, express } = await setup() - - const router1 = express.Router() // eslint-disable-line new-cap - router1.all('/', function (req, res) { - res.end() - }) - app.use('/', router1) - - const router2 = express.Router() // eslint-disable-line new-cap - router2.all('/test', function (req, res) { - res.end() - }) - app.use('/', router2) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /', - 'Expressjs/Router: /', - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] - ], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test'], '/test') - }) - - t.test('router mounted as a route handler', async function (t) { - const { app, express } = await setup() - - const router1 = express.Router() // eslint-disable-line new-cap - router1.all('/test', function testHandler(req, res) { - res.send('test') - }) - - let path = '*' - let segmentPath = '/*' - let metricsPath = segmentPath - - // express 5 router must be regular expressions - // need to handle the nuance of the segment vs metric name in express 5 - if (isExpress5) { - path = /(.*)/ - segmentPath = '/(.*)/' - metricsPath = '/(.*)' - } - - app.get(path, router1) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - [ - `Expressjs/Route Path: ${segmentPath}`, - [ - 'Expressjs/Router: /', - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'testHandler']] - ] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [`${NAMES.EXPRESS.MIDDLEWARE}testHandler/${metricsPath}/test`], - `${metricsPath}/test` - ) - }) - - t.test('segments for routers', async function (t) { - const { app, express } = await setup() - - const router = express.Router() // eslint-disable-line new-cap - router.all('/test', function (req, res) { - res.end() - }) - - app.use('/router1', router) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/router1/test' }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router1', - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + '//router1/test'], - '/router1/test' - ) - }) - - t.test('segments for sub-app', async function (t) { - const { app, express } = await setup() - - const subapp = express() - subapp.all('/test', function (req, res) { - res.end() - }) - - app.use('/subapp1', subapp) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/subapp1/test' }) - // express 5 no longer handles child routers as mounted applications - const firstSegment = isExpress5 - ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' - : 'Expressjs/Mounted App: /subapp1' - - t.assertSegments( - rootSegment, - [firstSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + '//subapp1/test'], - '/subapp1/test' - ) - }) - - t.test('segments for sub-app router', async function (t) { - const { app, express } = await setup() - - const subapp = express() - subapp.get( - '/test', - function (req, res, next) { - next() - }, - function (req, res, next) { - next() - } - ) - subapp.get('/test', function (req, res) { - res.end() - }) - - app.use('/subapp1', subapp) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/subapp1/test' }) - // express 5 no longer handles child routers as mounted applications - const firstSegment = isExpress5 - ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' - : 'Expressjs/Mounted App: /subapp1' - - t.assertSegments( - rootSegment, - [ - firstSegment, - [ - 'Expressjs/Route Path: /test', - [NAMES.EXPRESS.MIDDLEWARE + '', NAMES.EXPRESS.MIDDLEWARE + ''], - 'Expressjs/Route Path: /test', - [NAMES.EXPRESS.MIDDLEWARE + ''] - ] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + '//subapp1/test'], - '/subapp1/test' - ) - }) - - t.test('segments for wildcard', async function (t) { - const { app, express } = await setup() - - const subapp = express() - subapp.all('/:app', function (req, res) { - res.end() - }) - - app.use('/subapp1', subapp) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/subapp1/test' }) - // express 5 no longer handles child routers as mounted applications - const firstSegment = isExpress5 - ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' - : 'Expressjs/Mounted App: /subapp1' - - t.assertSegments( - rootSegment, - [firstSegment, ['Expressjs/Route Path: /:app', [NAMES.EXPRESS.MIDDLEWARE + '']]], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + '//subapp1/:app'], - '/subapp1/:app' - ) - }) - - t.test('router with subapp', async function (t) { - const { app, express } = await setup() - - const router = express.Router() // eslint-disable-line new-cap - const subapp = express() - subapp.all('/test', function (req, res) { - res.end() - }) - router.use('/subapp1', subapp) - app.use('/router1', router) - - const { rootSegment, transaction } = await runTest({ - app, - t, - endpoint: '/router1/subapp1/test' - }) - // express 5 no longer handles child routers as mounted applications - const subAppSegment = isExpress5 - ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' - : 'Expressjs/Mounted App: /subapp1' - - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router1', - [subAppSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + '//router1/subapp1/test'], - '/router1/subapp1/test' - ) - }) - - t.test('mounted middleware', async function (t) { - const { app } = await setup() - - app.use('/test', function myHandler(req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test'], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test']) - }) - - t.test('error middleware', async function (t) { - const { app } = await setup() - - app.get('/test', function () { - throw new Error('some error') - }) - - app.use(function myErrorHandler(err, req, res, next) { // eslint-disable-line - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Route Path: /test', - [NAMES.EXPRESS.MIDDLEWARE + ''], - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [ - NAMES.EXPRESS.MIDDLEWARE + '//test', - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//test' - ], - '/test' - ) - }) - - t.test('error handler in router', async function (t) { - const { app, express } = await setup() - - const router = express.Router() // eslint-disable-line new-cap - - router.get('/test', function () { - throw new Error('some error') - }) - - router.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line - res.end() - }) - - app.use('/router', router) - - const endpoint = '/router/test' - - const { rootSegment, transaction } = await runTest({ app, t, endpoint }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router', - [ - 'Expressjs/Route Path: /test', - [NAMES.EXPRESS.MIDDLEWARE + ''], - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' - ] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [ - NAMES.EXPRESS.MIDDLEWARE + '//router/test', - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router/test' - ], - endpoint - ) - }) - - t.test('error handler in second router', async function (t) { - const { app, express } = await setup() - - const router1 = express.Router() // eslint-disable-line new-cap - const router2 = express.Router() // eslint-disable-line new-cap - - router2.get('/test', function () { - throw new Error('some error') - }) - - router2.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line - res.end() - }) - - router1.use('/router2', router2) - app.use('/router1', router1) - - const endpoint = '/router1/router2/test' - - const { rootSegment, transaction } = await runTest({ app, t, endpoint }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router1', - [ - 'Expressjs/Router: /router2', - [ - 'Expressjs/Route Path: /test', - [NAMES.EXPRESS.MIDDLEWARE + ''], - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' - ] - ] - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [ - NAMES.EXPRESS.MIDDLEWARE + '//router1/router2/test', - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router1/router2/test' - ], - endpoint - ) - }) - - t.test('error handler outside of router', async function (t) { - const { app, express } = await setup() - - const router = express.Router() // eslint-disable-line new-cap - - router.get('/test', function () { - throw new Error('some error') - }) - - app.use('/router', router) - app.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line - res.end() - }) - - const endpoint = '/router/test' - - const { rootSegment, transaction } = await runTest({ app, t, endpoint }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router', - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [ - NAMES.EXPRESS.MIDDLEWARE + '//router/test', - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router/test' - ], - endpoint - ) - }) - - t.test('error handler outside of two routers', async function (t) { - const { app, express } = await setup() - - const router1 = express.Router() // eslint-disable-line new-cap - const router2 = express.Router() // eslint-disable-line new-cap - - router1.use('/router2', router2) - - router2.get('/test', function () { - throw new Error('some error') - }) - - app.use('/router1', router1) - app.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line - res.end() - }) - - const endpoint = '/router1/router2/test' - - const { rootSegment, transaction } = await runTest({ app, t, endpoint }) - t.assertSegments( - rootSegment, - [ - 'Expressjs/Router: /router1', - [ - 'Expressjs/Router: /router2', - ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] - ], - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' - ], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [ - NAMES.EXPRESS.MIDDLEWARE + '//router1/router2/test', - NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router1/router2/test' - ], - endpoint - ) - }) - - t.test('when using a route variable', async function (t) { - const { app } = await setup() - - app.get('/:foo/:bar', function myHandler(req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/a/b' }) - t.assertSegments( - rootSegment, - ['Expressjs/Route Path: /:foo/:bar', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], - assertSegmentsOptions - ) - - checkMetrics( - t, - transaction.metrics, - [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//:foo/:bar'], - '/:foo/:bar' - ) - }) - - t.test('when using a string pattern in path', async function (t) { - const { app } = await setup() - - const path = isExpress5 ? /ab?cd/ : '/ab?cd' - app.get(path, function myHandler(req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/abcd' }) - t.assertSegments( - rootSegment, - [`Expressjs/Route Path: ${path}`, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [`${NAMES.EXPRESS.MIDDLEWARE}myHandler/${path}`], path) - }) - - t.test('when using a regular expression in path', async function (t) { - const { app } = await setup() - - app.get(/a/, function myHandler(req, res) { - res.end() - }) - - const { rootSegment, transaction } = await runTest({ app, t, endpoint: '/a' }) - t.assertSegments( - rootSegment, - ['Expressjs/Route Path: /a/', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], - assertSegmentsOptions - ) - - checkMetrics(t, transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//a/'], '/a/') - }) - - async function runTest({ t, app, endpoint = '/test', errors = 0 }) { - const transaction = await makeRequestAndFinishTransaction({ t, app, agent, endpoint }) - const rootSegment = transaction.trace.root.children[0] - - t.equal(agent.errors.traceAggregator.errors.length, errors, `should have ${errors} errors`) - return { rootSegment, transaction } - } -}) - -function checkMetrics(t, metrics, expected, path) { - if (path === undefined) { - path = '/test' - } - const expectedAll = [ - [{ name: 'WebTransaction' }], - [{ name: 'WebTransactionTotalTime' }], - [{ name: 'HttpDispatcher' }], - [{ name: 'WebTransaction/Expressjs/GET/' + path }], - [{ name: 'WebTransactionTotalTime/Expressjs/GET/' + path }], - [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/all' }], - [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb' }], - [{ name: 'Apdex/Expressjs/GET/' + path }], - [{ name: 'Apdex' }] - ] - - for (let i = 0; i < expected.length; i++) { - const metric = expected[i] - expectedAll.push([{ name: metric }]) - expectedAll.push([{ name: metric, scope: 'WebTransaction/Expressjs/GET/' + path }]) - } - - t.assertMetrics(metrics, expectedAll, false, false) -} diff --git a/test/versioned/express-esm/segments.test.mjs b/test/versioned/express-esm/segments.test.mjs new file mode 100644 index 0000000000..b85e5feadd --- /dev/null +++ b/test/versioned/express-esm/segments.test.mjs @@ -0,0 +1,726 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import semver from 'semver' + +import assertions from '../../lib/custom-assertions/index.js' +const { assertMetrics, assertSegments } = assertions + +import helper from '../../lib/agent_helper.js' +import expressHelpers from './helpers.mjs' +import NAMES from '../../../lib/metrics/names.js' +import { findSegment } from '../../lib/metrics_helper.js' + +// import expressPkg from 'express/package.json' assert {type: 'json'} +// const pkgVersion = expressPkg.version +import { readFileSync } from 'node:fs' +const { version: pkgVersion } = JSON.parse(readFileSync('./node_modules/express/package.json')) +const isExpress5 = semver.gte(pkgVersion, '5.0.0') + +const { setup, makeRequestAndFinishTransaction } = expressHelpers +const assertSegmentsOptions = { + exact: true, + // in Node 8 the http module sometimes creates a setTimeout segment + // the query and expressInit middleware are registered under the hood up until express 5 + exclude: [NAMES.EXPRESS.MIDDLEWARE + 'query', NAMES.EXPRESS.MIDDLEWARE + 'expressInit'] +} + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const { app, express } = await setup() + ctx.nr.app = app + ctx.nr.express = express + + await new Promise((resolve) => { + const server = app.listen(() => { + ctx.nr.server = server + resolve() + }) + }) +}) + +test.afterEach((ctx) => { + ctx.nr.server.close() + helper.unloadAgent(ctx.nr.agent) +}) + +test('first two segments are built-in Express middleware', async (t) => { + const { agent, app, server } = t.nr + app.all('/test', (req, res) => { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], + assertSegmentsOptions + ) + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) +}) + +test('middleware with child segment gets named correctly', async (t) => { + const { agent, app, server } = t.nr + + app.all('/test', function (req, res) { + setTimeout(function () { + res.end() + }, 1) + }) + + const { transaction } = await runTest({ agent, server }) + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) +}) + +test('segments for route handler', async (t) => { + const { agent, app, server } = t.nr + + app.all('/test', function (req, res) { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], + assertSegmentsOptions + ) + + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) +}) + +test('route function names are in segment names', async (t) => { + const { agent, app, server } = t.nr + + app.all('/test', function myHandler(req, res) { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], + assertSegmentsOptions + ) + + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test']) +}) + +test('middleware mounted on a path should produce correct names', async (t) => { + const { agent, app, server } = t.nr + + app.use('/test/:id', function handler(req, res) { + res.send() + }) + + const { transaction } = await runTest({ agent, server, endpoint: '/test/1' }) + const routeSegment = findSegment( + transaction.trace.root, + NAMES.EXPRESS.MIDDLEWARE + 'handler//test/:id' + ) + assert.ok(routeSegment) + + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'handler//test/:id'], '/test/:id') +}) + +test('each handler in route has its own segment', async (t) => { + const { agent, app, server } = t.nr + + app.all( + '/test', + function handler1(req, res, next) { + next() + }, + function handler2(req, res) { + res.send() + } + ) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + [ + 'Expressjs/Route Path: /test', + [NAMES.EXPRESS.MIDDLEWARE + 'handler1', NAMES.EXPRESS.MIDDLEWARE + 'handler2'] + ], + assertSegmentsOptions + ) + + checkMetrics(transaction.metrics, [ + NAMES.EXPRESS.MIDDLEWARE + 'handler1//test', + NAMES.EXPRESS.MIDDLEWARE + 'handler2//test' + ]) +}) + +test('segments for routers', async (t) => { + const { agent, app, express, server } = t.nr + + const router = express.Router() // eslint-disable-line new-cap + router.all('/test', function (req, res) { + res.end() + }) + + app.use('/router1', router) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/router1/test' }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router1', + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + '//router1/test'], + '/router1/test' + ) +}) + +test('two root routers', async (t) => { + const { agent, app, express, server } = t.nr + + const router1 = express.Router() // eslint-disable-line new-cap + router1.all('/', function (req, res) { + res.end() + }) + app.use('/', router1) + + const router2 = express.Router() // eslint-disable-line new-cap + router2.all('/test', function (req, res) { + res.end() + }) + app.use('/', router2) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /', + 'Expressjs/Router: /', + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] + ], + assertSegmentsOptions + ) + + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test'], '/test') +}) + +test('router mounted as a route handler', async (t) => { + const { agent, app, express, server } = t.nr + + const router1 = express.Router() // eslint-disable-line new-cap + router1.all('/test', function testHandler(req, res) { + res.send('test') + }) + + let path = '*' + let segmentPath = '/*' + let metricsPath = segmentPath + + // express 5 router must be regular expressions + // need to handle the nuance of the segment vs metric name in express 5 + if (isExpress5) { + path = /(.*)/ + segmentPath = '/(.*)/' + metricsPath = '/(.*)' + } + + app.get(path, router1) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + [ + `Expressjs/Route Path: ${segmentPath}`, + [ + 'Expressjs/Router: /', + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'testHandler']] + ] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [`${NAMES.EXPRESS.MIDDLEWARE}testHandler/${metricsPath}/test`], + `${metricsPath}/test` + ) +}) + +test('segments for routers', async (t) => { + const { agent, app, express, server } = t.nr + + const router = express.Router() // eslint-disable-line new-cap + router.all('/test', function (req, res) { + res.end() + }) + + app.use('/router1', router) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/router1/test' }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router1', + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + '//router1/test'], + '/router1/test' + ) +}) + +test('segments for sub-app', async (t) => { + const { agent, app, express, server } = t.nr + + const subapp = express() + subapp.all('/test', function (req, res) { + res.end() + }) + + app.use('/subapp1', subapp) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/subapp1/test' }) + // express 5 no longer handles child routers as mounted applications + const firstSegment = isExpress5 + ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' + : 'Expressjs/Mounted App: /subapp1' + + assertSegments( + rootSegment, + [firstSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + '//subapp1/test'], + '/subapp1/test' + ) +}) + +test('segments for sub-app router', async (t) => { + const { agent, app, express, server } = t.nr + + const subapp = express() + subapp.get( + '/test', + function (req, res, next) { + next() + }, + function (req, res, next) { + next() + } + ) + subapp.get('/test', function (req, res) { + res.end() + }) + + app.use('/subapp1', subapp) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/subapp1/test' }) + // express 5 no longer handles child routers as mounted applications + const firstSegment = isExpress5 + ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' + : 'Expressjs/Mounted App: /subapp1' + + assertSegments( + rootSegment, + [ + firstSegment, + [ + 'Expressjs/Route Path: /test', + [NAMES.EXPRESS.MIDDLEWARE + '', NAMES.EXPRESS.MIDDLEWARE + ''], + 'Expressjs/Route Path: /test', + [NAMES.EXPRESS.MIDDLEWARE + ''] + ] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + '//subapp1/test'], + '/subapp1/test' + ) +}) + +test('segments for wildcard', async (t) => { + const { agent, app, express, server } = t.nr + + const subapp = express() + subapp.all('/:app', function (req, res) { + res.end() + }) + + app.use('/subapp1', subapp) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/subapp1/test' }) + // express 5 no longer handles child routers as mounted applications + const firstSegment = isExpress5 + ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' + : 'Expressjs/Mounted App: /subapp1' + + assertSegments( + rootSegment, + [firstSegment, ['Expressjs/Route Path: /:app', [NAMES.EXPRESS.MIDDLEWARE + '']]], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + '//subapp1/:app'], + '/subapp1/:app' + ) +}) + +test('router with subapp', async (t) => { + const { agent, app, express, server } = t.nr + + const router = express.Router() // eslint-disable-line new-cap + const subapp = express() + subapp.all('/test', function (req, res) { + res.end() + }) + router.use('/subapp1', subapp) + app.use('/router1', router) + + const { rootSegment, transaction } = await runTest({ + agent, + server, + endpoint: '/router1/subapp1/test' + }) + // express 5 no longer handles child routers as mounted applications + const subAppSegment = isExpress5 + ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' + : 'Expressjs/Mounted App: /subapp1' + + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router1', + [subAppSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + '//router1/subapp1/test'], + '/router1/subapp1/test' + ) +}) + +test('mounted middleware', async (t) => { + const { agent, app, server } = t.nr + + app.use('/test', function myHandler(req, res) { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments(rootSegment, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test'], assertSegmentsOptions) + + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test']) +}) + +test('error middleware', async (t) => { + const { agent, app, server } = t.nr + + app.get('/test', function () { + throw new Error('some error') + }) + + app.use(function myErrorHandler(err, req, res, next) { // eslint-disable-line + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server }) + assertSegments( + rootSegment, + [ + 'Expressjs/Route Path: /test', + [NAMES.EXPRESS.MIDDLEWARE + ''], + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [ + NAMES.EXPRESS.MIDDLEWARE + '//test', + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//test' + ], + '/test' + ) +}) + +test('error handler in router', async (t) => { + const { agent, app, express, server } = t.nr + + const router = express.Router() // eslint-disable-line new-cap + + router.get('/test', function () { + throw new Error('some error') + }) + + router.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line + res.end() + }) + + app.use('/router', router) + + const endpoint = '/router/test' + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router', + [ + 'Expressjs/Route Path: /test', + [NAMES.EXPRESS.MIDDLEWARE + ''], + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' + ] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [ + NAMES.EXPRESS.MIDDLEWARE + '//router/test', + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router/test' + ], + endpoint + ) +}) + +test('error handler in second router', async (t) => { + const { agent, app, express, server } = t.nr + + const router1 = express.Router() // eslint-disable-line new-cap + const router2 = express.Router() // eslint-disable-line new-cap + + router2.get('/test', function () { + throw new Error('some error') + }) + + router2.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line + res.end() + }) + + router1.use('/router2', router2) + app.use('/router1', router1) + + const endpoint = '/router1/router2/test' + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router1', + [ + 'Expressjs/Router: /router2', + [ + 'Expressjs/Route Path: /test', + [NAMES.EXPRESS.MIDDLEWARE + ''], + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' + ] + ] + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [ + NAMES.EXPRESS.MIDDLEWARE + '//router1/router2/test', + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router1/router2/test' + ], + endpoint + ) +}) + +test('error handler outside of router', async (t) => { + const { agent, app, express, server } = t.nr + + const router = express.Router() // eslint-disable-line new-cap + + router.get('/test', function () { + throw new Error('some error') + }) + + app.use('/router', router) + app.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line + res.end() + }) + + const endpoint = '/router/test' + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router', + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [ + NAMES.EXPRESS.MIDDLEWARE + '//router/test', + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router/test' + ], + endpoint + ) +}) + +test('error handler outside of two routers', async (t) => { + const { agent, app, express, server } = t.nr + + const router1 = express.Router() // eslint-disable-line new-cap + const router2 = express.Router() // eslint-disable-line new-cap + + router1.use('/router2', router2) + + router2.get('/test', function () { + throw new Error('some error') + }) + + app.use('/router1', router1) + app.use(function myErrorHandler(error, req, res, next) { // eslint-disable-line + res.end() + }) + + const endpoint = '/router1/router2/test' + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) + assertSegments( + rootSegment, + [ + 'Expressjs/Router: /router1', + [ + 'Expressjs/Router: /router2', + ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] + ], + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler' + ], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [ + NAMES.EXPRESS.MIDDLEWARE + '//router1/router2/test', + NAMES.EXPRESS.MIDDLEWARE + 'myErrorHandler//router1/router2/test' + ], + endpoint + ) +}) + +test('when using a route variable', async (t) => { + const { agent, app, server } = t.nr + + app.get('/:foo/:bar', function myHandler(req, res) { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/a/b' }) + assertSegments( + rootSegment, + ['Expressjs/Route Path: /:foo/:bar', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], + assertSegmentsOptions + ) + + checkMetrics( + transaction.metrics, + [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//:foo/:bar'], + '/:foo/:bar' + ) +}) + +test('when using a string pattern in path', async (t) => { + const { agent, app, server } = t.nr + + const path = isExpress5 ? /ab?cd/ : '/ab?cd' + app.get(path, function myHandler(req, res) { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/abcd' }) + assertSegments( + rootSegment, + [`Expressjs/Route Path: ${path}`, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], + assertSegmentsOptions + ) + + checkMetrics(transaction.metrics, [`${NAMES.EXPRESS.MIDDLEWARE}myHandler/${path}`], path) +}) + +test('when using a regular expression in path', async (t) => { + const { agent, app, server } = t.nr + + app.get(/a/, function myHandler(req, res) { + res.end() + }) + + const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/a' }) + assertSegments( + rootSegment, + ['Expressjs/Route Path: /a/', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], + assertSegmentsOptions + ) + + checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//a/'], '/a/') +}) + +async function runTest({ agent, server, endpoint = '/test', errors = 0 }) { + const transaction = await makeRequestAndFinishTransaction({ server, agent, endpoint }) + const rootSegment = transaction.trace.root.children[0] + + assert.equal(agent.errors.traceAggregator.errors.length, errors, `should have ${errors} errors`) + return { rootSegment, transaction } +} + +function checkMetrics(metrics, expected, path) { + if (path === undefined) { + path = '/test' + } + const expectedAll = [ + [{ name: 'WebTransaction' }], + [{ name: 'WebTransactionTotalTime' }], + [{ name: 'HttpDispatcher' }], + [{ name: 'WebTransaction/Expressjs/GET/' + path }], + [{ name: 'WebTransactionTotalTime/Expressjs/GET/' + path }], + [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/all' }], + [{ name: 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb' }], + [{ name: 'Apdex/Expressjs/GET/' + path }], + [{ name: 'Apdex' }] + ] + + for (let i = 0; i < expected.length; i++) { + const metric = expected[i] + expectedAll.push([{ name: metric }]) + expectedAll.push([{ name: metric, scope: 'WebTransaction/Expressjs/GET/' + path }]) + } + + assertMetrics(metrics, expectedAll, false, false) +} diff --git a/test/versioned/express-esm/transaction-naming.tap.mjs b/test/versioned/express-esm/transaction-naming.tap.mjs deleted file mode 100644 index aef6bce861..0000000000 --- a/test/versioned/express-esm/transaction-naming.tap.mjs +++ /dev/null @@ -1,624 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import helper from '../../lib/agent_helper.js' -import http from 'node:http' -import { test } from 'tap' -import semver from 'semver' -import expressHelpers from './helpers.mjs' -const { setup, makeRequest, makeRequestAndFinishTransaction } = expressHelpers -/** - * TODO: Update this later - * This is in stage 3 and eslint only supports stage 4 and do not want to - * install babel parsers just for this line - * See : https://github.com/eslint/eslint/discussions/15305 - * - */ -// import expressPkg from 'express/package.json' assert {type: 'json'} -// const pkgVersion = expressPkg.version -import { readFileSync } from 'node:fs' -const { version: pkgVersion } = JSON.parse(readFileSync('./node_modules/express/package.json')) -const isExpress5 = semver.gte(pkgVersion, '5.0.0') - -test('transaction naming tests', (t) => { - t.autoend() - - let agent - t.before(() => { - agent = helper.instrumentMockedAgent() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - }) - - t.test('transaction name with single route', async function (t) { - const { app } = await setup() - - app.get('/path1', function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name with no matched routes', async function (t) { - const { app } = await setup() - - app.get('/path1', function (req, res) { - res.end() - }) - - const endpoint = '/asdf' - const txPrefix = isExpress5 ? 'WebTransaction/Nodejs' : 'WebTransaction/Expressjs' - await runTest({ app, t, endpoint, txPrefix, expectedName: '(not found)' }) - }) - - t.test('transaction name with route that has multiple handlers', async function (t) { - const { app } = await setup() - - app.get( - '/path1', - function (req, res, next) { - next() - }, - function (req, res) { - res.end() - } - ) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name with router middleware', async function (t) { - const { app, express } = await setup() - - const router = new express.Router() - router.get('/path1', function (req, res) { - res.end() - }) - - app.use(router) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name with middleware function', async function (t) { - const { app } = await setup() - - app.use('/path1', function (req, res, next) { - next() - }) - - app.get('/path1', function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name with shared middleware function', async function (t) { - const { app } = await setup() - - app.use(['/path1', '/path2'], function (req, res, next) { - next() - }) - - app.get('/path1', function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name when ending in shared middleware', async function (t) { - const { app } = await setup() - - app.use(['/path1', '/path2'], function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/path1', expectedName: '/path1,/path2' }) - }) - - t.test('transaction name with subapp middleware', async function (t) { - const { app, express } = await setup() - - const subapp = express() - - subapp.get('/path1', function middleware(req, res) { - res.end() - }) - - app.use(subapp) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name with subrouter', async function (t) { - const { app, express } = await setup() - - const router = new express.Router() - - router.get('/path1', function (req, res) { - res.end() - }) - - app.use('/api', router) - - await runTest({ app, t, endpoint: '/api/path1' }) - }) - - t.test( - 'multiple route handlers with the same name do not duplicate transaction name', - async function (t) { - const { app } = await setup() - - app.get('/path1', function (req, res, next) { - next() - }) - - app.get('/path1', function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - } - ) - - t.test('responding from middleware', async function (t) { - const { app } = await setup() - - app.use('/test', function (req, res, next) { - res.send('ok') - next() - }) - - await runTest({ app, t, endpoint: '/test' }) - }) - - t.test('responding from middleware with parameter', async function (t) { - const { app } = await setup() - - app.use('/test', function (req, res, next) { - res.send('ok') - next() - }) - - await runTest({ app, t, endpoint: '/test/param', expectedName: '/test' }) - }) - - t.test('with error', async function (t) { - const { app } = await setup() - - app.get('/path1', function (req, res, next) { - next(new Error('some error')) - }) - - app.use(function (err, req, res) { - return res.status(500).end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('with error and path-specific error handler', async function (t) { - const { app } = await setup() - - app.get('/path1', function () { - throw new Error('some error') - }) - - app.use('/path1', function(err, req, res, next) { // eslint-disable-line - res.status(500).end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('when router error is handled outside of the router', async function (t) { - const { app, express } = await setup() - - const router = new express.Router() - - router.get('/path1', function (req, res, next) { - next(new Error('some error')) - }) - - app.use('/router1', router) - - // eslint-disable-next-line no-unused-vars - app.use(function (err, req, res, next) { - return res.status(500).end() - }) - - await runTest({ app, t, endpoint: '/router1/path1' }) - }) - - t.test('when using a route variable', async function (t) { - const { app } = await setup() - - app.get('/:foo/:bar', function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/foo/bar', expectedName: '/:foo/:bar' }) - }) - - t.test('when using a string pattern in path', async function (t) { - const { app } = await setup() - const path = isExpress5 ? /ab?cd/ : '/ab?cd' - - app.get(path, function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/abcd', expectedName: path }) - }) - - t.test('when using a regular expression in path', async function (t) { - const { app } = await setup() - - app.get(/a/, function (req, res) { - res.end() - }) - - await runTest({ app, t, endpoint: '/abcd', expectedName: '/a/' }) - }) - - t.test('when using router with a route variable', async function (t) { - const { app, express } = await setup() - - const router = express.Router() // eslint-disable-line new-cap - - router.get('/:var2/path1', function (req, res) { - res.end() - }) - - app.use('/:var1', router) - - await runTest({ app, t, endpoint: '/foo/bar/path1', expectedName: '/:var1/:var2/path1' }) - }) - - t.test('when mounting a subapp using a variable', async function (t) { - const { app, express } = await setup() - - const subapp = express() - subapp.get('/:var2/path1', function (req, res) { - res.end() - }) - - app.use('/:var1', subapp) - - await runTest({ app, t, endpoint: '/foo/bar/path1', expectedName: '/:var1/:var2/path1' }) - }) - - t.test('using two routers', async function (t) { - const { app, express } = await setup() - - const router1 = express.Router() // eslint-disable-line new-cap - const router2 = express.Router() // eslint-disable-line new-cap - - app.use('/:router1', router1) - router1.use('/:router2', router2) - - router2.get('/path1', function (req, res) { - res.end() - }) - - await runTest({ - app, - t, - endpoint: '/router1/router2/path1', - expectedName: '/:router1/:router2/path1' - }) - }) - - t.test('transactions running in parallel should be recorded correctly', async function (t) { - const { app, express } = await setup() - const router1 = express.Router() // eslint-disable-line new-cap - const router2 = express.Router() // eslint-disable-line new-cap - - app.use('/:router1', router1) - router1.use('/:router2', router2) - - router2.get('/path1', function (req, res) { - setTimeout(function () { - res.end() - }, 0) - }) - - const numTests = 4 - return new Promise((resolve) => { - app.listen(async function () { - const promises = [] - const handlers = [] - for (let i = 0; i < numTests; i++) { - const data = makeMultiRunner({ - t, - endpoint: '/router1/router2/path1', - expectedName: '/:router1/:router2/path1', - numTests, - server: this - }) - promises.push(data.promise) - handlers.push(data.transactionHandler) - } - - t.teardown(() => { - this.close() - handlers.forEach((handler) => { - agent.removeListener('transactionFinished', handler) - }) - }) - - await Promise.all(promises) - resolve() - }) - }) - }) - - t.test('names transaction when request is aborted', async function (t) { - const { app } = await setup() - - let request = null - - app.get('/test', function (req, res, next) { - t.comment('middleware') - t.ok(agent.getTransaction(), 'transaction exists') - - // generate error after client has aborted - request.abort() - setTimeout(function () { - t.comment('timed out') - t.ok(agent.getTransaction() == null, 'transaction has already ended') - next(new Error('some error')) - }, 100) - }) - - const promise = new Promise((resolve) => { - // eslint-disable-next-line no-unused-vars - app.use(function (error, req, res, next) { - t.comment('errorware') - t.ok(agent.getTransaction() == null, 'no active transaction when responding') - res.end() - resolve() - }) - }) - - const server = app.listen(function () { - t.comment('making request') - const port = this.address().port - request = http.request( - { - hostname: 'localhost', - port: port, - path: '/test' - }, - function () {} - ) - request.end() - - // add error handler, otherwise aborting will cause an exception - request.on('error', function (err) { - t.comment('request errored: ' + err) - }) - request.on('abort', function () { - t.comment('request aborted') - }) - }) - - const transactionHandler = function (tx) { - t.equal(tx.name, 'WebTransaction/Expressjs/GET//test') - } - - agent.on('transactionFinished', transactionHandler) - - t.teardown(() => { - server.close() - agent.removeListener('transactionFinished', transactionHandler) - }) - - return promise - }) - - t.test('Express transaction names are unaffected by errorware', async function (t) { - const { app } = await setup() - - let transactionHandler = null - const promise = new Promise((resolve) => { - transactionHandler = function (tx) { - const expected = 'WebTransaction/Expressjs/GET//test' - t.equal(tx.trace.root.children[0].name, expected) - resolve() - } - }) - - agent.on('transactionFinished', transactionHandler) - - app.use('/test', function () { - throw new Error('endpoint error') - }) - - // eslint-disable-next-line no-unused-vars - app.use('/test', function (err, req, res, next) { - res.send(err.message) - }) - - const server = app.listen(function () { - http.request({ port: this.address().port, path: '/test' }).end() - }) - - t.teardown(function () { - server.close() - agent.removeListener('transactionFinished', transactionHandler) - }) - - return promise - }) - - t.test('when next is called after transaction state loss', async function (t) { - // Uninstrumented work queue. This must be set up before the agent is loaded - // so that no transaction state is maintained. - const tasks = [] - const interval = setInterval(function () { - if (tasks.length) { - tasks.pop()() - } - }, 10) - - const { app } = await setup() - - let transactionsFinished = 0 - const transactionNames = [ - 'WebTransaction/Expressjs/GET//bar', - 'WebTransaction/Expressjs/GET//foo' - ] - - const transactionHandler = function (tx) { - t.equal( - tx.name, - transactionNames[transactionsFinished++], - 'should have expected name ' + transactionsFinished - ) - } - - agent.on('transactionFinished', transactionHandler) - - app.use('/foo', function (req, res, next) { - setTimeout(function () { - tasks.push(next) - }, 5) - }) - - app.get('/foo', function (req, res) { - setTimeout(function () { - res.send('foo done\n') - }, 500) - }) - - app.get('/bar', function (req, res) { - res.send('bar done\n') - }) - - let server = null - const promise = new Promise((resolve) => { - server = app.listen(function () { - const port = this.address().port - - // Send first request to `/foo` which is slow and uses the work queue. - http.get({ port: port, path: '/foo' }, function (res) { - res.resume() - res.on('end', function () { - t.equal(transactionsFinished, 2, 'should have two transactions done') - resolve() - }) - }) - - // Send the second request after a short wait `/bar` which is fast and - // does not use the work queue. - setTimeout(function () { - http.get({ port: port, path: '/bar' }, function (res) { - res.resume() - }) - }, 100) - }) - }) - - t.teardown(function () { - server.close() - clearInterval(interval) - agent.removeListener('transactionFinished', transactionHandler) - }) - - return promise - }) - - // express did not add array based middleware registration - // without path until 4.9.2 - // https://github.com/expressjs/express/blob/master/History.md#492--2014-09-17 - if (semver.satisfies(pkgVersion, '>=4.9.2')) { - t.test('transaction name with array of middleware with unspecified mount path', async (t) => { - const { app } = await setup() - - function mid1(req, res, next) { - t.pass('mid1 is executed') - next() - } - - function mid2(req, res, next) { - t.pass('mid2 is executed') - next() - } - - app.use([mid1, mid2]) - - app.get('/path1', (req, res) => { - res.end() - }) - - await runTest({ app, t, endpoint: '/path1' }) - }) - - t.test('transaction name when ending in array of unmounted middleware', async (t) => { - const { app } = await setup() - - function mid1(req, res, next) { - t.pass('mid1 is executed') - next() - } - - function mid2(req, res) { - t.pass('mid2 is executed') - res.end() - } - - app.use([mid1, mid2]) - - app.use(mid1) - - await runTest({ app, t, endpoint: '/path1', expectedName: '/' }) - }) - } - - function makeMultiRunner({ t, endpoint, expectedName, numTests, server }) { - let done = 0 - const seen = new Set() - - let transactionHandler = null - const promise = new Promise((resolve) => { - transactionHandler = function (transaction) { - t.notOk(seen.has(transaction), 'should never see the finishing transaction twice') - seen.add(transaction) - t.equal( - transaction.name, - 'WebTransaction/Expressjs/GET/' + expectedName, - 'transaction has expected name' - ) - transaction.end() - if (++done === numTests) { - done = 0 - resolve() - } - } - }) - - agent.on('transactionFinished', transactionHandler) - - makeRequest(server, endpoint) - return { promise, transactionHandler } - } - - async function runTest({ - app, - t, - endpoint, - expectedName = endpoint, - txPrefix = 'WebTransaction/Expressjs' - }) { - const transaction = await makeRequestAndFinishTransaction({ t, app, agent, endpoint }) - t.equal(transaction.name, `${txPrefix}/GET/${expectedName}`, 'transaction has expected name') - } -}) diff --git a/test/versioned/express-esm/transaction-naming.test.mjs b/test/versioned/express-esm/transaction-naming.test.mjs new file mode 100644 index 0000000000..8d94db031b --- /dev/null +++ b/test/versioned/express-esm/transaction-naming.test.mjs @@ -0,0 +1,580 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' +import http from 'node:http' +import semver from 'semver' +import tspl from '@matteo.collina/tspl' + +import helper from '../../lib/agent_helper.js' +import expressHelpers from './helpers.mjs' + +// import expressPkg from 'express/package.json' assert {type: 'json'} +// const pkgVersion = expressPkg.version +import { readFileSync } from 'node:fs' +const { version: pkgVersion } = JSON.parse(readFileSync('./node_modules/express/package.json')) +const isExpress5 = semver.gte(pkgVersion, '5.0.0') + +const { setup, makeRequest, makeRequestAndFinishTransaction } = expressHelpers + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const { app, express } = await setup() + ctx.nr.app = app + ctx.nr.express = express + + await new Promise((resolve) => { + const server = app.listen(() => { + ctx.nr.server = server + resolve() + }) + }) +}) + +test.afterEach((ctx) => { + ctx.nr.server.close() + helper.unloadAgent(ctx.nr.agent) +}) + +test('transaction name with single route', async (t) => { + const { agent, app, server } = t.nr + + app.get('/path1', function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('transaction name with no matched routes', async (t) => { + const { agent, app, server } = t.nr + + app.get('/path1', function (req, res) { + res.end() + }) + + const endpoint = '/asdf' + const txPrefix = isExpress5 ? 'WebTransaction/Nodejs' : 'WebTransaction/Expressjs' + await runTest({ agent, server, endpoint, txPrefix, expectedName: '(not found)' }) +}) + +test('transaction name with route that has multiple handlers', async (t) => { + const { agent, app, server } = t.nr + + app.get( + '/path1', + function (req, res, next) { + next() + }, + function (req, res) { + res.end() + } + ) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('transaction name with router middleware', async (t) => { + const { agent, app, express, server } = t.nr + + const router = new express.Router() + router.get('/path1', function (req, res) { + res.end() + }) + + app.use(router) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('transaction name with middleware function', async (t) => { + const { agent, app, server } = t.nr + + app.use('/path1', function (req, res, next) { + next() + }) + + app.get('/path1', function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('transaction name with shared middleware function', async (t) => { + const { agent, app, server } = t.nr + + app.use(['/path1', '/path2'], function (req, res, next) { + next() + }) + + app.get('/path1', function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('transaction name when ending in shared middleware', async (t) => { + const { agent, app, server } = t.nr + + app.use(['/path1', '/path2'], function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/path1', expectedName: '/path1,/path2' }) +}) + +test('transaction name with subapp middleware', async (t) => { + const { agent, app, express, server } = t.nr + + const subapp = express() + + subapp.get('/path1', function middleware(req, res) { + res.end() + }) + + app.use(subapp) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('transaction name with subrouter', async (t) => { + const { agent, app, express, server } = t.nr + + const router = new express.Router() + + router.get('/path1', function (req, res) { + res.end() + }) + + app.use('/api', router) + + await runTest({ agent, server, endpoint: '/api/path1' }) +}) + +test('multiple route handlers with the same name do not duplicate transaction name', async (t) => { + const { agent, app, server } = t.nr + + app.get('/path1', function (req, res, next) { + next() + }) + + app.get('/path1', function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('responding from middleware', async (t) => { + const { agent, app, server } = t.nr + + app.use('/test', function (req, res, next) { + res.send('ok') + next() + }) + + await runTest({ agent, server, endpoint: '/test' }) +}) + +test('responding from middleware with parameter', async (t) => { + const { agent, app, server } = t.nr + + app.use('/test', function (req, res, next) { + res.send('ok') + next() + }) + + await runTest({ agent, server, endpoint: '/test/param', expectedName: '/test' }) +}) + +test('with error', async (t) => { + const { agent, app, server } = t.nr + + app.get('/path1', function (req, res, next) { + next(Error('some error')) + }) + + app.use(function (err, req, res, next) { + res.status(500).end() + next() + }) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('with error and path-specific error handler', async (t) => { + const { agent, app, server } = t.nr + + app.get('/path1', function () { + throw new Error('some error') + }) + + app.use('/path1', function (err, req, res, next) { + res.status(500).end() + next() + }) + + await runTest({ agent, server, endpoint: '/path1' }) +}) + +test('when router error is handled outside of the router', async (t) => { + const { agent, app, express, server } = t.nr + + const router = new express.Router() + + router.get('/path1', function (req, res, next) { + next(new Error('some error')) + }) + + app.use('/router1', router) + + app.use(function (err, req, res, next) { + res.status(500).end() + next() + }) + + await runTest({ agent, server, endpoint: '/router1/path1' }) +}) + +test('when using a route variable', async (t) => { + const { agent, app, server } = t.nr + + app.get('/:foo/:bar', function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/foo/bar', expectedName: '/:foo/:bar' }) +}) + +test('when using a string pattern in path', async (t) => { + const { agent, app, server } = t.nr + const path = isExpress5 ? /ab?cd/ : '/ab?cd' + + app.get(path, function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/abcd', expectedName: path }) +}) + +test('when using a regular expression in path', async (t) => { + const { agent, app, server } = t.nr + + app.get(/a/, function (req, res) { + res.end() + }) + + await runTest({ agent, server, endpoint: '/abcd', expectedName: '/a/' }) +}) + +test('when using router with a route variable', async (t) => { + const { agent, app, express, server } = t.nr + + const router = express.Router() // eslint-disable-line new-cap + + router.get('/:var2/path1', function (req, res) { + res.end() + }) + + app.use('/:var1', router) + + await runTest({ agent, server, endpoint: '/foo/bar/path1', expectedName: '/:var1/:var2/path1' }) +}) + +test('when mounting a subapp using a variable', async (t) => { + const { agent, app, express, server } = t.nr + + const subapp = express() + subapp.get('/:var2/path1', function (req, res) { + res.end() + }) + + app.use('/:var1', subapp) + + await runTest({ agent, server, endpoint: '/foo/bar/path1', expectedName: '/:var1/:var2/path1' }) +}) + +test('using two routers', async (t) => { + const { agent, app, express, server } = t.nr + + const router1 = express.Router() // eslint-disable-line new-cap + const router2 = express.Router() // eslint-disable-line new-cap + + app.use('/:router1', router1) + router1.use('/:router2', router2) + + router2.get('/path1', function (req, res) { + res.end() + }) + + await runTest({ + agent, + server, + endpoint: '/router1/router2/path1', + expectedName: '/:router1/:router2/path1' + }) +}) + +test('transactions running in parallel should be recorded correctly', async (t) => { + const { agent, app, express, server } = t.nr + const router1 = express.Router() // eslint-disable-line new-cap + const router2 = express.Router() // eslint-disable-line new-cap + + app.use('/:router1', router1) + router1.use('/:router2', router2) + + router2.get('/path1', function (req, res) { + setTimeout(function () { + res.end() + }, 0) + }) + + const numTests = 4 + const promises = [] + for (let i = 0; i < numTests; i++) { + const data = makeMultiRunner({ + agent, + endpoint: '/router1/router2/path1', + expectedName: '/:router1/:router2/path1', + numTests, + server + }) + promises.push(data.promise) + } + + await Promise.all(promises) +}) + +test('names transaction when request is aborted', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, app, server } = t.nr + + let request = null + + app.get('/test', function (req, res, next) { + plan.ok(agent.getTransaction(), 'transaction exists') + + // generate error after client has aborted + request.abort() + setTimeout(function () { + plan.ok(agent.getTransaction() == null, 'transaction has already ended') + next(new Error('some error')) + }, 100) + }) + + const promise = new Promise((resolve) => { + // eslint-disable-next-line no-unused-vars + app.use(function (error, req, res, next) { + plan.ok(agent.getTransaction() == null, 'no active transaction when responding') + res.end() + resolve() + }) + }) + + const transactionHandler = function (tx) { + plan.equal(tx.name, 'WebTransaction/Expressjs/GET//test') + } + + agent.on('transactionFinished', transactionHandler) + + request = http.request({ ...server.address(), path: '/test' }) + request.on('error', () => { + // No-op error handler to suppress logging of the error to console. + }) + request.end() + + await Promise.all([promise, plan.completed]) +}) + +test('Express transaction names are unaffected by errorware', async (t) => { + const plan = tspl(t, { plan: 1 }) + const { agent, app, server } = t.nr + + let transactionHandler = null + const promise = new Promise((resolve) => { + transactionHandler = function (tx) { + const expected = 'WebTransaction/Expressjs/GET//test' + plan.equal(tx.trace.root.children[0].name, expected) + resolve() + } + }) + + agent.on('transactionFinished', transactionHandler) + + app.use('/test', function () { + throw Error('endpoint error') + }) + + app.use('/test', function (err, req, res, next) { + res.send(err.message) + next() + }) + + http.request({ ...server.address(), path: '/test' }).end() + + await Promise.all([promise, plan.completed]) +}) + +test('when next is called after transaction state loss', async (t) => { + const plan = tspl(t, { plan: 3 }) + const { agent, app, server } = t.nr + + // Uninstrumented work queue. + const tasks = [] + const interval = setInterval(function () { + if (tasks.length) { + tasks.pop()() + } + }, 10) + t.after(() => clearInterval(interval)) + + let transactionsFinished = 0 + const transactionNames = [ + 'WebTransaction/Expressjs/GET//bar', + 'WebTransaction/Expressjs/GET//foo' + ] + + agent.on('transactionFinished', (tx) => { + transactionsFinished += 1 + plan.equal(transactionNames.includes(tx.name), true, 'should have expected name ' + tx.name) + }) + + app.use('/foo', function (req, res, next) { + setTimeout(function () { + tasks.push(next) + }, 5) + }) + + app.get('/foo', function (req, res) { + setTimeout(function () { + res.send('foo done\n') + }, 500) + }) + + app.get('/bar', function (req, res) { + res.send('bar done\n') + }) + + // Send first request to `/foo` which is slow and uses the work queue. + http + .request({ ...server.address(), path: '/foo' }, (res) => { + res.resume() + res.on('end', () => { + plan.equal(transactionsFinished, 2, 'should have two transactions done') + }) + }) + .end() + + // Send the second request after a short wait to `/bar` which is fast + // and does not use the work queue. + setTimeout(() => { + http.request({ ...server.address(), path: '/bar' }).end() + }, 100) + + await plan.completed +}) + +// express did not add array based middleware registration +// without path until 4.9.2 +// https://github.com/expressjs/express/blob/master/History.md#492--2014-09-17 +const supportsArrayMiddleware = semver.satisfies(pkgVersion, '>=4.9.2') + +test( + 'transaction name with array of middleware with unspecified mount path', + { skip: supportsArrayMiddleware === false }, + async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, app, server } = t.nr + + function mid1(req, res, next) { + plan.ok('mid1 is executed') + next() + } + + function mid2(req, res, next) { + plan.ok('mid2 is executed') + next() + } + + app.use([mid1, mid2]) + + app.get('/path1', (req, res) => { + res.end() + }) + + await runTest({ agent, server, endpoint: '/path1' }) + await plan.completed + } +) + +test( + 'transaction name when ending in array of unmounted middleware', + { skip: supportsArrayMiddleware === false }, + async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, app, server } = t.nr + + function mid1(req, res, next) { + plan.ok('mid1 is executed') + next() + } + + function mid2(req, res) { + plan.ok('mid2 is executed') + res.end() + } + + app.use([mid1, mid2]) + + app.use(mid1) + + await runTest({ agent, server, endpoint: '/path1', expectedName: '/' }) + await plan.completed + } +) + +function makeMultiRunner({ agent, endpoint, expectedName, numTests, server }) { + let done = 0 + const seen = new Set() + + let transactionHandler = null + const promise = new Promise((resolve) => { + transactionHandler = function (transaction) { + assert.equal(seen.has(transaction), false, 'should never see the finishing transaction twice') + seen.add(transaction) + assert.equal( + transaction.name, + 'WebTransaction/Expressjs/GET/' + expectedName, + 'transaction has expected name' + ) + transaction.end() + if (++done === numTests) { + done = 0 + resolve() + } + } + }) + + agent.on('transactionFinished', transactionHandler) + + makeRequest(server, endpoint) + return { promise, transactionHandler } +} + +async function runTest({ + agent, + server, + endpoint, + expectedName = endpoint, + txPrefix = 'WebTransaction/Expressjs' +}) { + const transaction = await makeRequestAndFinishTransaction({ agent, server, endpoint }) + assert.equal(transaction.name, `${txPrefix}/GET/${expectedName}`, 'transaction has expected name') +} diff --git a/test/versioned/generic-pool/basic.tap.js b/test/versioned/generic-pool/basic.tap.js deleted file mode 100644 index 8c33a575e6..0000000000 --- a/test/versioned/generic-pool/basic.tap.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../lib/agent_helper') -const tap = require('tap') - -tap.test('generic-pool', function (t) { - t.autoend() - - let agent = null - let pool = null - const PoolClass = require('generic-pool').Pool - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - pool = require('generic-pool') - }) - - t.afterEach(function () { - helper.unloadAgent(agent) - pool = null - }) - - const tasks = [] - const decontextInterval = setInterval(function () { - if (tasks.length > 0) { - tasks.pop()() - } - }, 10) - - t.teardown(function () { - clearInterval(decontextInterval) - }) - - function addTask(cb, args) { - tasks.push(function () { - return cb.apply(null, args || []) - }) - } - - function id(tx) { - return tx && tx.id - } - - t.test('instantiation', function (t) { - t.plan(2) - - // As of generic-pool 3, it is not possible to instantiate Pool without `new`. - - t.doesNotThrow(function () { - const p = pool.createPool({ - create: function () { - return new Promise(function (res) { - addTask(res, {}) - }) - }, - destroy: function () { - return new Promise(function (res) { - addTask(res) - }) - } - }) - t.type(p, PoolClass, 'should create a Pool') - }, 'should be able to instantiate with createPool') - }) - - t.test('context maintenance', function (t) { - const p = pool.createPool( - { - create: function () { - return new Promise(function (res) { - addTask(res, {}) - }) - }, - destroy: function () { - return new Promise(function (res) { - addTask(res) - }) - } - }, - { - max: 2, - min: 0 - } - ) - - Array.from({ length: 6 }, async (_, i) => { - await run(i) - }) - - drain() - - async function run(n) { - return helper.runInTransaction(agent, async (tx) => { - const conn = await p.acquire() - t.equal(id(agent.getTransaction()), id(tx), n + ': should maintain tx state') - await new Promise((resolve) => { - addTask(() => { - p.release(conn) - resolve() - }) - }) - }) - } - - function drain() { - run('drain') - - helper.runInTransaction(agent, function (tx) { - p.drain() - .then(function () { - t.equal(id(agent.getTransaction()), id(tx), 'should have context through drain') - - return p.clear().then(function () { - t.equal(id(agent.getTransaction()), id(tx), 'should have context through destroy') - }) - }) - .then( - function () { - t.end() - }, - function (err) { - t.error(err) - t.end() - } - ) - }) - } - }) -}) diff --git a/test/versioned/generic-pool/basic.test.js b/test/versioned/generic-pool/basic.test.js new file mode 100644 index 0000000000..0054d2c75f --- /dev/null +++ b/test/versioned/generic-pool/basic.test.js @@ -0,0 +1,133 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') +const tspl = require('@matteo.collina/tspl') + +const { removeModules } = require('../../lib/cache-buster') +const helper = require('../../lib/agent_helper') + +let PoolClass +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + ctx.nr.pool = require('generic-pool') + PoolClass = ctx.nr.pool.Pool + + // Uinstrumented task manager: + ctx.nr.tasks = [] + ctx.nr.tasksInterval = setInterval(() => { + if (ctx.nr.tasks.length > 0) { + ctx.nr.tasks.pop()() + } + }, 10) + ctx.nr.addTask = (cb, args = []) => { + ctx.nr.tasks.push(() => { + return cb.apply(null, args) + }) + } +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + removeModules(['generic-pool']) + clearInterval(ctx.nr.tasksInterval) +}) + +function id(tx) { + return tx?.id +} + +test('instantiation', (t) => { + const plan = tspl(t, { plan: 2 }) + const { addTask, pool } = t.nr + + // As of generic-pool 3, it is not possible to instantiate Pool without `new`. + + plan.doesNotThrow(function () { + const p = pool.createPool({ + create: function () { + return new Promise(function (res) { + addTask(res, {}) + }) + }, + destroy: function () { + return new Promise(function (res) { + addTask(res) + }) + } + }) + plan.equal(p instanceof PoolClass, true, 'should create a Pool') + }, 'should be able to instantiate with createPool') +}) + +test('context maintenance', (t, end) => { + const { addTask, agent, pool } = t.nr + const p = pool.createPool( + { + create: function () { + return new Promise(function (res) { + addTask(res, {}) + }) + }, + destroy: function () { + return new Promise(function (res) { + addTask(res) + }) + } + }, + { + max: 2, + min: 0 + } + ) + + Array.from({ length: 6 }, async (_, i) => { + await run(i) + }) + + drain() + + async function run(n) { + return helper.runInTransaction(agent, async (tx) => { + const conn = await p.acquire() + assert.equal(id(agent.getTransaction()), id(tx), n + ': should maintain tx state') + await new Promise((resolve) => { + addTask(() => { + p.release(conn) + resolve() + }) + }) + }) + } + + function drain() { + run('drain') + + helper.runInTransaction(agent, function (tx) { + p.drain() + .then(function () { + assert.equal(id(agent.getTransaction()), id(tx), 'should have context through drain') + + return p.clear().then(function () { + assert.equal(id(agent.getTransaction()), id(tx), 'should have context through destroy') + }) + }) + .then( + function () { + end() + }, + function (err) { + assert.ifError(err) + end() + } + ) + }) + } +}) diff --git a/test/versioned/generic-pool/package.json b/test/versioned/generic-pool/package.json index 535852d8ab..b22c0e0ec1 100644 --- a/test/versioned/generic-pool/package.json +++ b/test/versioned/generic-pool/package.json @@ -12,9 +12,8 @@ "generic-pool": ">=3.0.0" }, "files": [ - "basic.tap.js" + "basic.test.js" ] } - ], - "dependencies": {} + ] } diff --git a/test/versioned/grpc-esm/client-unary.tap.mjs b/test/versioned/grpc-esm/client-unary.tap.mjs deleted file mode 100644 index 6130de0039..0000000000 --- a/test/versioned/grpc-esm/client-unary.tap.mjs +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import helper from '../../lib/agent_helper.js' -import { default as constants } from '../grpc/constants.cjs' -const { ERR_CODE, ERR_MSG } = constants -import { default as utils } from '../grpc/util.cjs' - -const { - assertError, - assertExternalSegment, - assertMetricsNotExisting, - makeUnaryRequest, - createServer, - getClient -} = utils - -tap.test('gRPC Client: Unary Requests', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.before(async () => { - agent = helper.instrumentMockedAgent() - grpc = await import('@grpc/grpc-js') - ;({ proto, server, port } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - agent.errors.traceAggregator.clear() - agent.metrics.clear() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - }) - - t.test('should track unary client requests as an external when in a transaction', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - function transactionFinished(transaction) { - if (transaction.name === 'clientTransaction') { - // Make sure we're in the client and not server transaction - assertExternalSegment({ t, tx: transaction, fnName: 'SayHello', port }) - t.end() - } - } - agent.on('transactionFinished', transactionFinished) - t.teardown(() => { - agent.removeListener('transactionFinished', transactionFinished) - }) - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - tx.end() - }) - }) - - t.test('should include distributed trace headers when enabled', (t) => { - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = { name: 'dt test' } - await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - const dtMeta = server.metadataMap.get(payload.name) - t.match( - dtMeta.get('traceparent')[0], - /^[\w\d\-]{55}$/, - 'should have traceparent in server metadata' - ) - t.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') - tx.end() - t.end() - }) - }) - - t.test('should not include distributed trace headers when not in transaction', async (t) => { - const payload = { name: 'dt not in transaction' } - await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - const dtMeta = server.metadataMap.get(payload.name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - }) - - t.test( - 'should not include distributed trace headers when distributed_tracing.enabled is set to false', - (t) => { - agent.config.distributed_tracing.enabled = false - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = { name: 'dt disabled' } - await makeUnaryRequest({ client, payload, fnName: 'sayHello' }) - const dtMeta = server.metadataMap.get(payload.name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - tx.end() - t.end() - }) - } - ) - - t.test('should not track external unary client requests outside of a transaction', async (t) => { - const payload = { name: 'New Relic' } - const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - assertMetricsNotExisting({ t, agent, port }) - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, (t) => { - const expectedStatusText = ERR_MSG - const expectedStatusCode = ERR_CODE - agent.config.grpc.record_errors = config.should - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - function transactionFinished(transaction) { - if (transaction.name === 'clientTransaction') { - assertError({ - port, - t, - transaction, - errors: agent.errors, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayError', - clientError: true - }) - t.end() - } - } - - agent.on('transactionFinished', transactionFinished) - t.teardown(() => { - agent.removeListener('transactionFinished', transactionFinished) - }) - - try { - const payload = { oh: 'noes' } - await makeUnaryRequest({ client, fnName: 'sayError', payload }) - } catch (err) { - t.ok(err, 'should get an error') - t.equal(err.code, expectedStatusCode, 'should get the right status code') - t.equal(err.details, expectedStatusText, 'should get the correct error message') - tx.end() - } - }) - }) - }) - - t.test('should bind callback to the proper transaction context', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - client.sayHello({ name: 'Callback' }, (err, response) => { - t.ok(response) - t.equal(response.message, 'Hello Callback') - t.ok(agent.getTransaction(), 'callback should have transaction context') - t.equal(agent.getTransaction(), tx, 'transaction should be the one we started with') - t.end() - }) - }) - }) -}) diff --git a/test/versioned/grpc-esm/client-unary.test.mjs b/test/versioned/grpc-esm/client-unary.test.mjs new file mode 100644 index 0000000000..1e6922e8cf --- /dev/null +++ b/test/versioned/grpc-esm/client-unary.test.mjs @@ -0,0 +1,184 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' + +import helper from '../../lib/agent_helper.js' + +import assertions from '../../lib/custom-assertions/index.js' +const { match } = assertions + +import constants from '../grpc/constants.cjs' +const { ERR_CODE, ERR_MSG } = constants + +import util from '../grpc/util.cjs' +const { + assertError, + assertExternalSegment, + assertMetricsNotExisting, + makeUnaryRequest, + createServer, + getClient +} = util + +// This suite has to juggle the side effect-y nature of this setup because +// we don't have any way of re-importing `@grpc/grpc-js` while bypassing the +// module cache. If we tried to set up this suite properly, we'd instrument the +// library for the first subtest, and then the module would lose its +// instrumentation when the agent is recreated between tests, thus breaking +// the subsequent tests. So, be cautious adding any event handlers; they must +// be unregistered before a test ends, or they will interfere with other tests. +const agent = helper.instrumentMockedAgent() +const grpc = await import('@grpc/grpc-js') +const { port, proto, server } = await createServer(grpc) +const client = getClient(grpc, proto, port) + +test.afterEach(() => { + agent.errors.traceAggregator.clear() + agent.metrics.clear() +}) + +test.after(() => { + helper.unloadAgent(agent) + server.forceShutdown() + client.close() +}) + +test('should track unary client requests as an external when in a transaction', (t, end) => { + function transactionFinished(transaction) { + if (transaction.name === 'clientTransaction') { + // Make sure we're in the client and not server transaction + assertExternalSegment({ tx: transaction, fnName: 'SayHello', port }) + end() + } + } + + agent.on('transactionFinished', transactionFinished) + t.after(() => { + agent.removeListener('transactionFinished', transactionFinished) + }) + + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + tx.end() + }) +}) + +test('should include distributed trace headers when enabled', (t, end) => { + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = { name: 'dt test' } + await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + const dtMeta = server.metadataMap.get(payload.name) + match( + dtMeta.get('traceparent')[0], + /^[\w\d\-]{55}$/, + 'should have traceparent in server metadata' + ) + assert.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not include distributed trace headers when not in transaction', async () => { + const payload = { name: 'dt not in transaction' } + await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + const dtMeta = server.metadataMap.get(payload.name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') +}) + +test('should not include distributed trace headers when distributed_tracing.enabled is set to false', (t, end) => { + agent.config.distributed_tracing.enabled = false + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = { name: 'dt disabled' } + await makeUnaryRequest({ client, payload, fnName: 'sayHello' }) + const dtMeta = server.metadataMap.get(payload.name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not track external unary client requests outside of a transaction', async () => { + const payload = { name: 'New Relic' } + const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + assertMetricsNotExisting({ agent, port }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, (t, end) => { + const expectedStatusText = ERR_MSG + const expectedStatusCode = ERR_CODE + agent.config.grpc.record_errors = config.should + + function transactionFinished(transaction) { + if (transaction.name === 'clientTransaction') { + assertError({ + port, + transaction, + errors: agent.errors, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayError', + clientError: true + }) + end() + } + } + + agent.on('transactionFinished', transactionFinished) + t.after(() => { + agent.removeListener('transactionFinished', transactionFinished) + }) + + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + + try { + const payload = { oh: 'noes' } + await makeUnaryRequest({ client, fnName: 'sayError', payload }) + } catch (err) { + assert.ok(err, 'should get an error') + assert.equal(err.code, expectedStatusCode, 'should get the right status code') + assert.equal(err.details, expectedStatusText, 'should get the correct error message') + tx.end() + } + }) + }) +} + +test('should bind callback to the proper transaction context', (t, end) => { + helper.runInTransaction(agent, 'web', async (tx) => { + client.sayHello({ name: 'Callback' }, (err, response) => { + assert.ok(response) + assert.equal(response.message, 'Hello Callback') + assert.ok(agent.getTransaction(), 'callback should have transaction context') + assert.equal(agent.getTransaction(), tx, 'transaction should be the one we started with') + end() + }) + }) +}) diff --git a/test/versioned/grpc-esm/package.json b/test/versioned/grpc-esm/package.json index c00003074c..b7cf953d64 100644 --- a/test/versioned/grpc-esm/package.json +++ b/test/versioned/grpc-esm/package.json @@ -13,8 +13,8 @@ "@grpc/grpc-js": ">=1.4.0" }, "files": [ - "client-unary.tap.mjs", - "server-unary.tap.mjs" + "client-unary.test.mjs", + "server-unary.test.mjs" ] } ] diff --git a/test/versioned/grpc-esm/server-unary.tap.mjs b/test/versioned/grpc-esm/server-unary.tap.mjs deleted file mode 100644 index 43bc5ad3b9..0000000000 --- a/test/versioned/grpc-esm/server-unary.tap.mjs +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -import tap from 'tap' -import helper from '../../lib/agent_helper.js' -import { default as constants } from '../grpc/constants.cjs' -const { ERR_CODE, ERR_SERVER_MSG } = constants -import { default as utils } from '../grpc/util.cjs' -import { DESTINATIONS } from '../../../lib/config/attribute-filter.js' -const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT - -const { - makeUnaryRequest, - createServer, - getClient, - getServerTransactionName, - assertError, - assertServerTransaction, - assertServerMetrics, - assertDistributedTracing -} = utils - -tap.test('gRPC Server: Unary Requests', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.before(async () => { - agent = helper.instrumentMockedAgent() - grpc = await import('@grpc/grpc-js') - ;({ proto, server, port } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - agent.errors.traceAggregator.clear() - agent.metrics.clear() - }) - - t.teardown(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - }) - - t.test('should track unary server requests', async (t) => { - let transaction - function transactionFinished(tx) { - transaction = tx - } - agent.on('transactionFinished', transactionFinished) - t.teardown(() => { - agent.removeListener('transactionFinished', transactionFinished) - }) - - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - t.ok(transaction, 'transaction exists') - assertServerTransaction({ t, transaction, fnName: 'SayHello' }) - assertServerMetrics({ t, agentMetrics: agent.metrics._metrics, fnName: 'SayHello' }) - t.end() - }) - - t.test('should add DT headers when `distributed_tracing` is enabled', async (t) => { - let serverTransaction - let clientTransaction - function transactionFinished(tx) { - if (tx.name === getServerTransactionName('SayHello')) { - serverTransaction = tx - } - } - agent.on('transactionFinished', transactionFinished) - t.teardown(() => { - agent.removeListener('transactionFinished', transactionFinished) - }) - - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - tx.end() - }) - - assertDistributedTracing({ t, clientTransaction, serverTransaction }) - t.end() - }) - - t.test( - 'should not include distributed trace headers when there is no client transaction', - async (t) => { - let serverTransaction - function transactionFinished(tx) { - serverTransaction = tx - } - agent.on('transactionFinished', transactionFinished) - t.teardown(() => { - agent.removeListener('transactionFinished', transactionFinished) - }) - const payload = { name: 'dt not in transaction' } - const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - t.ok(response, 'response exists') - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - } - ) - - t.test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { - let serverTransaction - let clientTransaction - - agent.on('transactionFinished', function transactionFinished(tx) { - if (tx.name === getServerTransactionName('SayHello')) { - serverTransaction = tx - } - }) - t.teardown(() => { - agent.removeListener('transactionFinished', function transactionFinished(tx) { - if (tx.name === getServerTransactionName('SayHello')) { - serverTransaction = tx - } - }) - }) - - agent.config.distributed_tracing.enabled = false - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - tx.end() - }) - - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, async (t) => { - agent.config.grpc.record_errors = config.should - const expectedStatusCode = ERR_CODE - const expectedStatusText = ERR_SERVER_MSG - let transaction - function transactionFinished(tx) { - if (tx.name === getServerTransactionName('SayError')) { - transaction = tx - } - } - agent.on('transactionFinished', transactionFinished) - t.teardown(() => { - agent.removeListener('transactionFinished', transactionFinished) - }) - - try { - await makeUnaryRequest({ - client, - fnName: 'sayError', - payload: { oh: 'noes' } - }) - } catch (err) { - // err tested in client tests - } - - assertError({ - t, - transaction, - errors: agent.errors, - agentMetrics: agent.metrics._metrics, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayError' - }) - t.end() - }) - }) -}) diff --git a/test/versioned/grpc-esm/server-unary.test.mjs b/test/versioned/grpc-esm/server-unary.test.mjs new file mode 100644 index 0000000000..5681aa5755 --- /dev/null +++ b/test/versioned/grpc-esm/server-unary.test.mjs @@ -0,0 +1,217 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import test from 'node:test' +import assert from 'node:assert' + +import helper from '../../lib/agent_helper.js' + +import assertions from '../../lib/custom-assertions/index.js' +const { notHas } = assertions + +import constants from '../grpc/constants.cjs' +const { ERR_CODE, ERR_SERVER_MSG } = constants + +import { DESTINATIONS } from '../../../lib/config/attribute-filter.js' +const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT + +import util from '../grpc/util.cjs' +const { + assertDistributedTracing, + assertError, + assertServerMetrics, + assertServerTransaction, + makeUnaryRequest, + createServer, + getClient, + getServerTransactionName +} = util + +// This suite has to juggle the side effect-y nature of this setup because +// we don't have any way of re-importing `@grpc/grpc-js` while bypassing the +// module cache. If we tried to set up this suite properly, we'd instrument the +// library for the first subtest, and then the module would lose its +// instrumentation when the agent is recreated between tests, thus breaking +// the subsequent tests. So, be cautious adding any event handlers; they must +// be unregistered before a test ends, or they will interfere with other tests. +const agent = helper.instrumentMockedAgent() +const grpc = await import('@grpc/grpc-js') +const { port, proto, server } = await createServer(grpc) +const client = getClient(grpc, proto, port) + +test.afterEach(() => { + agent.errors.traceAggregator.clear() + agent.metrics.clear() +}) + +test.after(() => { + helper.unloadAgent(agent) + server.forceShutdown() + client.close() +}) + +test('should track unary server requests', async (t) => { + let transaction + function transactionFinished(tx) { + transaction = tx + } + agent.on('transactionFinished', transactionFinished) + t.after(() => { + agent.removeListener('transactionFinished', transactionFinished) + }) + + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + assert.ok(transaction, 'transaction exists') + assertServerTransaction({ transaction, fnName: 'SayHello' }) + assertServerMetrics({ agentMetrics: agent.metrics._metrics, fnName: 'SayHello' }) +}) + +test('should add DT headers when `distributed_tracing` is enabled', async (t) => { + let serverTransaction + let clientTransaction + function transactionFinished(tx) { + if (tx.name === getServerTransactionName('SayHello')) { + serverTransaction = tx + } + } + agent.on('transactionFinished', transactionFinished) + t.after(() => { + agent.removeListener('transactionFinished', transactionFinished) + }) + + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + tx.end() + }) + + assertDistributedTracing({ clientTransaction, serverTransaction }) +}) + +test('should not include distributed trace headers when there is no client transaction', async (t) => { + let serverTransaction + function transactionFinished(tx) { + serverTransaction = tx + } + agent.on('transactionFinished', transactionFinished) + t.after(() => { + agent.removeListener('transactionFinished', transactionFinished) + }) + const payload = { name: 'dt not in transaction' } + const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + assert.ok(response, 'response exists') + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { + let serverTransaction + let clientTransaction + + agent.on('transactionFinished', function transactionFinished(tx) { + if (tx.name === getServerTransactionName('SayHello')) { + serverTransaction = tx + } + }) + t.after(() => { + agent.removeListener('transactionFinished', function transactionFinished(tx) { + if (tx.name === getServerTransactionName('SayHello')) { + serverTransaction = tx + } + }) + }) + + agent.config.distributed_tracing.enabled = false + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + tx.end() + }) + + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, async (t) => { + agent.config.grpc.record_errors = config.should + const expectedStatusCode = ERR_CODE + const expectedStatusText = ERR_SERVER_MSG + let transaction + function transactionFinished(tx) { + if (tx.name === getServerTransactionName('SayError')) { + transaction = tx + } + } + agent.on('transactionFinished', transactionFinished) + t.after(() => { + agent.removeListener('transactionFinished', transactionFinished) + }) + + try { + await makeUnaryRequest({ + client, + fnName: 'sayError', + payload: { oh: 'noes' } + }) + } catch (err) { + // err tested in client tests + } + + assertError({ + transaction, + errors: agent.errors, + agentMetrics: agent.metrics._metrics, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayError' + }) + }) +} diff --git a/test/versioned/grpc/client-bidi-streaming.tap.js b/test/versioned/grpc/client-bidi-streaming.tap.js deleted file mode 100644 index 07682c0d69..0000000000 --- a/test/versioned/grpc/client-bidi-streaming.tap.js +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const { ERR_CODE, ERR_MSG } = require('./constants.cjs') - -const { - assertError, - assertExternalSegment, - assertMetricsNotExisting, - makeBidiStreamingRequest, - createServer, - getClient -} = require('./util.cjs') - -tap.test('gRPC Client: Bidi Streaming', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test( - 'should track bidirectional streaming requests as an external when in a transaction', - (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - // Make sure we're in the client and not server transaction - assertExternalSegment({ t, tx: transaction, fnName: 'SayHelloBidiStream', port }) - t.end() - } - }) - - const names = [{ name: 'Huey' }, { name: 'Dewey' }, { name: 'Louie' }] - const responses = await makeBidiStreamingRequest({ - client, - fnName: 'sayHelloBidiStream', - payload: names - }) - names.forEach(({ name }, i) => { - t.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') - }) - - tx.end() - }) - } - ) - - t.test('should include distributed trace headers when enabled', (t) => { - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = [{ name: 'dt test' }] - await makeBidiStreamingRequest({ - client, - fnName: 'sayHelloBidiStream', - payload - }) - const dtMeta = server.metadataMap.get(payload[0].name) - t.match( - dtMeta.get('traceparent')[0], - /^[\w\d\-]{55}$/, - 'should have traceparent in server metadata' - ) - t.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') - tx.end() - t.end() - }) - }) - - t.test('should not include distributed trace headers when not in transaction', async (t) => { - const payload = [{ name: 'dt not in transaction' }] - await makeBidiStreamingRequest({ - client, - fnName: 'sayHelloBidiStream', - payload - }) - const dtMeta = server.metadataMap.get(payload[0].name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - }) - - t.test( - 'should not include distributed trace headers when distributed_tracing.enabled is set to false', - (t) => { - agent.config.distributed_tracing.enabled = false - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = [{ name: 'dt disabled' }] - await makeBidiStreamingRequest({ - client, - fnName: 'sayHelloBidiStream', - payload - }) - const dtMeta = server.metadataMap.get(payload[0].name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - tx.end() - t.end() - }) - } - ) - - t.test( - 'should not track external bidi streaming client requests outside of a transaction', - async (t) => { - const payload = [{ name: 'Moe' }, { name: 'Larry' }, { name: 'Curly' }] - const responses = await makeBidiStreamingRequest({ - client, - fnName: 'sayHelloBidiStream', - payload - }) - payload.forEach(({ name }, i) => { - t.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') - }) - assertMetricsNotExisting({ t, agent, port }) - t.end() - } - ) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, (t) => { - const expectedStatusText = ERR_MSG - const expectedStatusCode = ERR_CODE - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - assertError({ - port, - t, - transaction, - errors: agent.errors, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorBidiStream', - clientError: true - }) - t.end() - } - }) - - try { - const payload = [{ name: 'server-error' }] - await makeBidiStreamingRequest({ client, fnName: 'sayErrorBidiStream', payload }) - } catch (err) { - t.ok(err, 'should get an error') - t.equal(err.code, expectedStatusCode, 'should get the right status code') - t.equal(err.details, expectedStatusText, 'should get the correct error message') - tx.end() - } - }) - }) - }) -}) diff --git a/test/versioned/grpc/client-bidi-streaming.test.js b/test/versioned/grpc/client-bidi-streaming.test.js new file mode 100644 index 0000000000..5bee1a17db --- /dev/null +++ b/test/versioned/grpc/client-bidi-streaming.test.js @@ -0,0 +1,184 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') +const tspl = require('@matteo.collina/tspl') + +const { removeModules } = require('../../lib/cache-buster') +const { match } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const { ERR_CODE, ERR_MSG } = require('./constants.cjs') +const { + assertError, + assertExternalSegment, + assertMetricsNotExisting, + makeBidiStreamingRequest, + createServer, + getClient +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track bidirectional streaming requests as an external when in a transaction', (t, end) => { + const { agent, client, port } = t.nr + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + // Make sure we're in the client and not server transaction + assertExternalSegment({ t, tx: transaction, fnName: 'SayHelloBidiStream', port }) + end() + } + }) + + const names = [{ name: 'Huey' }, { name: 'Dewey' }, { name: 'Louie' }] + const responses = await makeBidiStreamingRequest({ + client, + fnName: 'sayHelloBidiStream', + payload: names + }) + names.forEach(({ name }, i) => { + assert.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') + }) + + tx.end() + }) +}) + +test('should include distributed trace headers when enabled', (t, end) => { + const { agent, client, server } = t.nr + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = [{ name: 'dt test' }] + await makeBidiStreamingRequest({ + client, + fnName: 'sayHelloBidiStream', + payload + }) + const dtMeta = server.metadataMap.get(payload[0].name) + match( + dtMeta.get('traceparent')[0], + /^[\w\d\-]{55}$/, + 'should have traceparent in server metadata' + ) + assert.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not include distributed trace headers when not in transaction', async (t) => { + const { client, server } = t.nr + const payload = [{ name: 'dt not in transaction' }] + await makeBidiStreamingRequest({ + client, + fnName: 'sayHelloBidiStream', + payload + }) + const dtMeta = server.metadataMap.get(payload[0].name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') +}) + +test('should not include distributed trace headers when distributed_tracing.enabled is set to false', (t, end) => { + const { agent, client, server } = t.nr + agent.config.distributed_tracing.enabled = false + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = [{ name: 'dt disabled' }] + await makeBidiStreamingRequest({ + client, + fnName: 'sayHelloBidiStream', + payload + }) + const dtMeta = server.metadataMap.get(payload[0].name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not track external bidi streaming client requests outside of a transaction', async (t) => { + const plan = tspl(t, { plan: 7 }) + const { agent, client, port } = t.nr + const payload = [{ name: 'Moe' }, { name: 'Larry' }, { name: 'Curly' }] + const responses = await makeBidiStreamingRequest({ + client, + fnName: 'sayHelloBidiStream', + payload + }) + payload.forEach(({ name }, i) => { + plan.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') + }) + assertMetricsNotExisting({ agent, port }, { assert: plan }) + + await plan.completed +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, (t, end) => { + const { agent, client, port } = t.nr + const expectedStatusText = ERR_MSG + const expectedStatusCode = ERR_CODE + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + assertError({ + port, + transaction, + errors: agent.errors, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorBidiStream', + clientError: true + }) + end() + } + }) + + try { + const payload = [{ name: 'server-error' }] + await makeBidiStreamingRequest({ client, fnName: 'sayErrorBidiStream', payload }) + } catch (err) { + assert.ok(err, 'should get an error') + assert.equal(err.code, expectedStatusCode, 'should get the right status code') + assert.equal(err.details, expectedStatusText, 'should get the correct error message') + tx.end() + } + }) + }) +} diff --git a/test/versioned/grpc/client-server-streaming.tap.js b/test/versioned/grpc/client-server-streaming.tap.js deleted file mode 100644 index b9659b84e9..0000000000 --- a/test/versioned/grpc/client-server-streaming.tap.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const { ERR_CODE, ERR_MSG } = require('./constants.cjs') - -const { - assertError, - assertExternalSegment, - assertMetricsNotExisting, - makeServerStreamingRequest, - createServer, - getClient -} = require('./util.cjs') - -tap.test('gRPC Client: Server Streaming', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track server streaming requests as an external when in a transaction', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - // Make sure we're in the client and not server transaction - assertExternalSegment({ t, tx: transaction, fnName: 'SayHelloServerStream', port }) - t.end() - } - }) - - const names = ['Bob', 'Jordi', 'Corey'] - const responses = await makeServerStreamingRequest({ - client, - fnName: 'sayHelloServerStream', - payload: { name: names } - }) - names.forEach((name, i) => { - t.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') - }) - tx.end() - }) - }) - - t.test('should include distributed trace headers when enabled', (t) => { - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = { name: ['dt test', 'dt test 2'] } - await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) - payload.name.forEach((name) => { - const dtMeta = server.metadataMap.get(name) - t.match( - dtMeta.get('traceparent')[0], - /^[\w\d\-]{55}$/, - 'should have traceparent in server metadata' - ) - t.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') - }) - tx.end() - t.end() - }) - }) - - t.test('should not include distributed trace headers when not in transaction', async (t) => { - const payload = { name: ['dt not in transaction'] } - await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) - const dtMeta = server.metadataMap.get(payload.name[0]) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - }) - - t.test( - 'should not include distributed trace headers when distributed_tracing.enabled is set to false', - (t) => { - agent.config.distributed_tracing.enabled = false - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = { name: ['dt not in transaction'] } - await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) - const dtMeta = server.metadataMap.get(payload.name[0]) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - tx.end() - t.end() - }) - } - ) - - t.test('should not track server streaming requests outside of a transaction', async (t) => { - const payload = { name: ['New Relic'] } - const responses = await makeServerStreamingRequest({ - client, - fnName: 'sayHelloServerStream', - payload - }) - t.ok(responses.length, 1) - t.equal(responses[0], 'Hello New Relic', 'response message is correct') - assertMetricsNotExisting({ t, agent, port }) - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, (t) => { - const expectedStatusText = ERR_MSG - const expectedStatusCode = ERR_CODE - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - assertError({ - port, - t, - transaction, - errors: agent.errors, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorServerStream', - clientError: true - }) - t.end() - } - }) - - try { - const payload = { name: ['noes'] } - await makeServerStreamingRequest({ client, fnName: 'sayErrorServerStream', payload }) - } catch (err) { - t.ok(err, 'should get an error') - t.equal(err.code, expectedStatusCode, 'should get the right status code') - t.equal(err.details, expectedStatusText, 'should get the correct error message') - tx.end() - } - }) - }) - }) -}) diff --git a/test/versioned/grpc/client-server-streaming.test.js b/test/versioned/grpc/client-server-streaming.test.js new file mode 100644 index 0000000000..32c97f1747 --- /dev/null +++ b/test/versioned/grpc/client-server-streaming.test.js @@ -0,0 +1,168 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { match } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const { ERR_CODE, ERR_MSG } = require('./constants.cjs') +const { + assertError, + assertExternalSegment, + assertMetricsNotExisting, + makeServerStreamingRequest, + createServer, + getClient +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track server streaming requests as an external when in a transaction', (t, end) => { + const { agent, client, port } = t.nr + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + // Make sure we're in the client and not server transaction + assertExternalSegment({ tx: transaction, fnName: 'SayHelloServerStream', port }) + end() + } + }) + + const names = ['Bob', 'Jordi', 'Corey'] + const responses = await makeServerStreamingRequest({ + client, + fnName: 'sayHelloServerStream', + payload: { name: names } + }) + names.forEach((name, i) => { + assert.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') + }) + tx.end() + }) +}) + +test('should include distributed trace headers when enabled', (t, end) => { + const { agent, client, server } = t.nr + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = { name: ['dt test', 'dt test 2'] } + await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) + payload.name.forEach((name) => { + const dtMeta = server.metadataMap.get(name) + match( + dtMeta.get('traceparent')[0], + /^[\w\d\-]{55}$/, + 'should have traceparent in server metadata' + ) + assert.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') + }) + tx.end() + end() + }) +}) + +test('should not include distributed trace headers when not in transaction', async (t) => { + const { client, server } = t.nr + const payload = { name: ['dt not in transaction'] } + await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) + const dtMeta = server.metadataMap.get(payload.name[0]) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') +}) + +test('should not include distributed trace headers when distributed_tracing.enabled is set to false', (t, end) => { + const { agent, client, server } = t.nr + agent.config.distributed_tracing.enabled = false + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = { name: ['dt not in transaction'] } + await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) + const dtMeta = server.metadataMap.get(payload.name[0]) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not track server streaming requests outside of a transaction', async (t) => { + const { agent, client, port } = t.nr + const payload = { name: ['New Relic'] } + const responses = await makeServerStreamingRequest({ + client, + fnName: 'sayHelloServerStream', + payload + }) + assert.ok(responses.length, 1) + assert.equal(responses[0], 'Hello New Relic', 'response message is correct') + assertMetricsNotExisting({ agent, port }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, (t, end) => { + const { agent, client, port } = t.nr + const expectedStatusText = ERR_MSG + const expectedStatusCode = ERR_CODE + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + assertError({ + port, + transaction, + errors: agent.errors, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorServerStream', + clientError: true + }) + end() + } + }) + + try { + const payload = { name: ['noes'] } + await makeServerStreamingRequest({ client, fnName: 'sayErrorServerStream', payload }) + } catch (err) { + assert.ok(err, 'should get an error') + assert.equal(err.code, expectedStatusCode, 'should get the right status code') + assert.equal(err.details, expectedStatusText, 'should get the correct error message') + tx.end() + } + }) + }) +} diff --git a/test/versioned/grpc/client-streaming.tap.js b/test/versioned/grpc/client-streaming.tap.js deleted file mode 100644 index fcc2c60905..0000000000 --- a/test/versioned/grpc/client-streaming.tap.js +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const { ERR_CODE, ERR_MSG, HALT_CODE, HALT_SERVER_ERR_MSG } = require('./constants.cjs') - -const { - assertError, - assertExternalSegment, - assertMetricsNotExisting, - makeClientStreamingRequest, - createServer, - getClient -} = require('./util.cjs') - -tap.test('gRPC Client: Client Streaming', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track client streaming requests as an external when in a transaction', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - // Make sure we're in the client and not server transaction - assertExternalSegment({ t, tx: transaction, fnName: 'SayHelloClientStream', port }) - t.end() - } - }) - - const names = [{ name: 'Bob' }, { name: 'Jordi' }, { name: 'Corey' }] - const response = await makeClientStreamingRequest({ - client, - fnName: 'sayHelloClientStream', - payload: names - }) - t.ok(response, 'response exists') - t.equal( - response.message, - `Hello ${names.map(({ name }) => name).join(', ')}`, - 'response message is correct' - ) - tx.end() - }) - }) - - t.test('should include distributed trace headers when enabled', (t) => { - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = [{ name: 'dt test' }, { name: 'dt test2' }] - await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) - payload.forEach(({ name }) => { - const dtMeta = server.metadataMap.get(name) - t.match( - dtMeta.get('traceparent')[0], - /^[\w\d\-]{55}$/, - 'should have traceparent in server metadata' - ) - t.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') - }) - tx.end() - t.end() - }) - }) - - t.test('should not include distributed trace headers when not in transaction', async (t) => { - const payload = [{ name: 'dt not in transaction' }] - await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) - const dtMeta = server.metadataMap.get(payload[0].name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - }) - - t.test( - 'should not include distributed trace headers when distributed_tracing.enabled is set to false', - (t) => { - agent.config.distributed_tracing.enabled = false - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = [{ name: 'dt disabled' }] - await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) - const dtMeta = server.metadataMap.get(payload[0].name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - tx.end() - t.end() - }) - } - ) - - t.test('should not track client streaming requests outside of a transaction', async (t) => { - const payload = [{ name: 'New Relic' }] - const response = await makeClientStreamingRequest({ - client, - fnName: 'sayHelloClientStream', - payload - }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - assertMetricsNotExisting({ t, agent, port }) - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, (t) => { - const expectedStatusText = ERR_MSG - const expectedStatusCode = ERR_CODE - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - assertError({ - port, - t, - transaction, - errors: agent.errors, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorClientStream', - clientError: true - }) - t.end() - } - }) - - try { - const payload = [{ oh: 'noes' }] - await makeClientStreamingRequest({ client, fnName: 'sayErrorClientStream', payload }) - } catch (err) { - t.ok(err, 'should get an error') - t.equal(err.code, expectedStatusCode, 'should get the right status code') - t.equal(err.details, expectedStatusText, 'should get the correct error message') - tx.end() - } - }) - }) - - t.test(`${should} record errors in a transaction when server sends error mid stream`, (t) => { - const expectedStatusText = HALT_SERVER_ERR_MSG - const expectedStatusCode = HALT_CODE - agent.config.grpc.record_errors = config.should - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - assertError({ - port, - t, - transaction, - errors: agent.errors, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorClientStream', - clientError: true - }) - t.end() - } - }) - - try { - const payload = [{ name: 'error' }] - await makeClientStreamingRequest({ - client, - fnName: 'sayErrorClientStream', - payload, - endStream: false - }) - } catch (err) { - t.ok(err, 'should get an error') - t.equal(err.code, expectedStatusCode, 'should get the right status code') - t.equal(err.details, expectedStatusText, 'should get the correct error message') - tx.end() - } - }) - }) - }) - - t.test('should bind callback to the proper transaction context', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - const call = client.sayHelloClientStream((err, response) => { - t.ok(response) - t.equal(response.message, 'Hello Callback') - t.ok(agent.getTransaction(), 'callback should have transaction context') - t.equal(agent.getTransaction(), tx, 'transaction should be the one we started with') - t.end() - }) - - call.write({ name: 'Callback' }) - call.end() - }) - }) -}) diff --git a/test/versioned/grpc/client-streaming.test.js b/test/versioned/grpc/client-streaming.test.js new file mode 100644 index 0000000000..e6a5622d01 --- /dev/null +++ b/test/versioned/grpc/client-streaming.test.js @@ -0,0 +1,227 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { match } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const { ERR_CODE, ERR_MSG, HALT_SERVER_ERR_MSG, HALT_CODE } = require('./constants.cjs') +const { + assertError, + assertExternalSegment, + assertMetricsNotExisting, + makeClientStreamingRequest, + createServer, + getClient +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track client streaming requests as an external when in a transaction', (t, end) => { + const { agent, client, port } = t.nr + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + // Make sure we're in the client and not server transaction + assertExternalSegment({ tx: transaction, fnName: 'SayHelloClientStream', port }) + end() + } + }) + + const names = [{ name: 'Bob' }, { name: 'Jordi' }, { name: 'Corey' }] + const response = await makeClientStreamingRequest({ + client, + fnName: 'sayHelloClientStream', + payload: names + }) + assert.ok(response, 'response exists') + assert.equal( + response.message, + `Hello ${names.map(({ name }) => name).join(', ')}`, + 'response message is correct' + ) + tx.end() + }) +}) + +test('should include distributed trace headers when enabled', (t, end) => { + const { agent, client, server } = t.nr + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = [{ name: 'dt test' }, { name: 'dt test2' }] + await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) + payload.forEach(({ name }) => { + const dtMeta = server.metadataMap.get(name) + match( + dtMeta.get('traceparent')[0], + /^[\w\d\-]{55}$/, + 'should have traceparent in server metadata' + ) + assert.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') + }) + tx.end() + end() + }) +}) + +test('should not include distributed trace headers when not in transaction', async (t) => { + const { client, server } = t.nr + const payload = [{ name: 'dt not in transaction' }] + await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) + const dtMeta = server.metadataMap.get(payload[0].name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') +}) + +test('should not include distributed trace headers when distributed_tracing.enabled is set to false', (t, end) => { + const { agent, client, server } = t.nr + agent.config.distributed_tracing.enabled = false + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = [{ name: 'dt disabled' }] + await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) + const dtMeta = server.metadataMap.get(payload[0].name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not track client streaming requests outside of a transaction', async (t) => { + const { agent, client, port } = t.nr + const payload = [{ name: 'New Relic' }] + const response = await makeClientStreamingRequest({ + client, + fnName: 'sayHelloClientStream', + payload + }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + assertMetricsNotExisting({ agent, port }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, (t, end) => { + const { agent, client, port } = t.nr + const expectedStatusText = ERR_MSG + const expectedStatusCode = ERR_CODE + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + assertError({ + port, + transaction, + errors: agent.errors, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorClientStream', + clientError: true + }) + end() + } + }) + + try { + const payload = [{ oh: 'noes' }] + await makeClientStreamingRequest({ client, fnName: 'sayErrorClientStream', payload }) + } catch (err) { + assert.ok(err, 'should get an error') + assert.equal(err.code, expectedStatusCode, 'should get the right status code') + assert.equal(err.details, expectedStatusText, 'should get the correct error message') + tx.end() + } + }) + }) + + test(`${should} record errors in a transaction when server sends error mid stream`, (t, end) => { + const { agent, client, port } = t.nr + const expectedStatusText = HALT_SERVER_ERR_MSG + const expectedStatusCode = HALT_CODE + agent.config.grpc.record_errors = config.should + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + assertError({ + port, + transaction, + errors: agent.errors, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorClientStream', + clientError: true + }) + end() + } + }) + + try { + const payload = [{ name: 'error' }] + await makeClientStreamingRequest({ + client, + fnName: 'sayErrorClientStream', + payload, + endStream: false + }) + } catch (err) { + assert.ok(err, 'should get an error') + assert.equal(err.code, expectedStatusCode, 'should get the right status code') + assert.equal(err.details, expectedStatusText, 'should get the correct error message') + tx.end() + } + }) + }) +} + +test('should bind callback to the proper transaction context', (t, end) => { + const { agent, client } = t.nr + helper.runInTransaction(agent, 'web', async (tx) => { + const call = client.sayHelloClientStream((err, response) => { + assert.ok(response) + assert.equal(response.message, 'Hello Callback') + assert.ok(agent.getTransaction(), 'callback should have transaction context') + assert.equal(agent.getTransaction(), tx, 'transaction should be the one we started with') + end() + }) + + call.write({ name: 'Callback' }) + call.end() + }) +}) diff --git a/test/versioned/grpc/client-unary.tap.js b/test/versioned/grpc/client-unary.tap.js deleted file mode 100644 index 314deadd03..0000000000 --- a/test/versioned/grpc/client-unary.tap.js +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const { ERR_CODE, ERR_MSG } = require('./constants.cjs') - -const { - assertError, - assertExternalSegment, - assertMetricsNotExisting, - makeUnaryRequest, - createServer, - getClient -} = require('./util.cjs') - -tap.test('gRPC Client: Unary Requests', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ proto, server, port } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track unary client requests as an external when in a transaction', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - // Make sure we're in the client and not server transaction - assertExternalSegment({ t, tx: transaction, fnName: 'SayHello', port }) - t.end() - } - }) - - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - tx.end() - }) - }) - - t.test('should include distributed trace headers when enabled', (t) => { - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = { name: 'dt test' } - await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - const dtMeta = server.metadataMap.get(payload.name) - t.match( - dtMeta.get('traceparent')[0], - /^[\w\d\-]{55}$/, - 'should have traceparent in server metadata' - ) - t.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') - tx.end() - t.end() - }) - }) - - t.test('should not include distributed trace headers when not in transaction', async (t) => { - const payload = { name: 'dt not in transaction' } - await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - const dtMeta = server.metadataMap.get(payload.name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - }) - - t.test( - 'should not include distributed trace headers when distributed_tracing.enabled is set to false', - (t) => { - agent.config.distributed_tracing.enabled = false - helper.runInTransaction(agent, 'dt-test', async (tx) => { - const payload = { name: 'dt disabled' } - await makeUnaryRequest({ client, payload, fnName: 'sayHello' }) - const dtMeta = server.metadataMap.get(payload.name) - t.notOk(dtMeta.has('traceparent'), 'should not have traceparent in server metadata') - t.notOk(dtMeta.has('newrelic'), 'should not have newrelic in server metadata') - tx.end() - t.end() - }) - } - ) - - t.test('should not track external unary client requests outside of a transaction', async (t) => { - const payload = { name: 'New Relic' } - const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - assertMetricsNotExisting({ t, agent, port }) - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, (t) => { - const expectedStatusText = ERR_MSG - const expectedStatusCode = ERR_CODE - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - helper.runInTransaction(agent, 'web', async (tx) => { - tx.name = 'clientTransaction' - agent.on('transactionFinished', (transaction) => { - if (transaction.name === 'clientTransaction') { - assertError({ - port, - t, - transaction, - errors: agent.errors, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayError', - clientError: true - }) - t.end() - } - }) - - try { - const payload = { oh: 'noes' } - await makeUnaryRequest({ client, fnName: 'sayError', payload }) - } catch (err) { - t.ok(err, 'should get an error') - t.equal(err.code, expectedStatusCode, 'should get the right status code') - t.equal(err.details, expectedStatusText, 'should get the correct error message') - tx.end() - } - }) - }) - }) - - t.test('should bind callback to the proper transaction context', (t) => { - helper.runInTransaction(agent, 'web', async (tx) => { - client.sayHello({ name: 'Callback' }, (err, response) => { - t.ok(response) - t.equal(response.message, 'Hello Callback') - t.ok(agent.getTransaction(), 'callback should have transaction context') - t.equal(agent.getTransaction(), tx, 'transaction should be the one we started with') - t.end() - }) - }) - }) -}) diff --git a/test/versioned/grpc/client-unary.test.js b/test/versioned/grpc/client-unary.test.js new file mode 100644 index 0000000000..756679b203 --- /dev/null +++ b/test/versioned/grpc/client-unary.test.js @@ -0,0 +1,173 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { match } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const { ERR_CODE, ERR_MSG } = require('./constants.cjs') +const { + assertError, + assertExternalSegment, + assertMetricsNotExisting, + makeUnaryRequest, + createServer, + getClient +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track unary client requests as an external when in a transaction', (t, end) => { + const { agent, client, port } = t.nr + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + // Make sure we're in the client and not server transaction + assertExternalSegment({ tx: transaction, fnName: 'SayHello', port }) + end() + } + }) + + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + tx.end() + }) +}) + +test('should include distributed trace headers when enabled', (t, end) => { + const { agent, client, server } = t.nr + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = { name: 'dt test' } + await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + const dtMeta = server.metadataMap.get(payload.name) + match( + dtMeta.get('traceparent')[0], + /^[\w\d\-]{55}$/, + 'should have traceparent in server metadata' + ) + assert.equal(dtMeta.get('newrelic')[0], '', 'should have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not include distributed trace headers when not in transaction', async (t) => { + const { client, server } = t.nr + const payload = { name: 'dt not in transaction' } + await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + const dtMeta = server.metadataMap.get(payload.name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') +}) + +test('should not include distributed trace headers when distributed_tracing.enabled is set to false', (t, end) => { + const { agent, client, server } = t.nr + agent.config.distributed_tracing.enabled = false + helper.runInTransaction(agent, 'dt-test', async (tx) => { + const payload = { name: 'dt disabled' } + await makeUnaryRequest({ client, payload, fnName: 'sayHello' }) + const dtMeta = server.metadataMap.get(payload.name) + assert.equal(dtMeta.has('traceparent'), false, 'should not have traceparent in server metadata') + assert.equal(dtMeta.has('newrelic'), false, 'should not have newrelic in server metadata') + tx.end() + end() + }) +}) + +test('should not track external unary client requests outside of a transaction', async (t) => { + const { agent, client, port } = t.nr + const payload = { name: 'New Relic' } + const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + assertMetricsNotExisting({ agent, port }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, (t, end) => { + const { agent, client, port } = t.nr + const expectedStatusText = ERR_MSG + const expectedStatusCode = ERR_CODE + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + helper.runInTransaction(agent, 'web', async (tx) => { + tx.name = 'clientTransaction' + agent.on('transactionFinished', (transaction) => { + if (transaction.name === 'clientTransaction') { + assertError({ + port, + transaction, + errors: agent.errors, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayError', + clientError: true + }) + end() + } + }) + + try { + const payload = { oh: 'noes' } + await makeUnaryRequest({ client, fnName: 'sayError', payload }) + } catch (err) { + assert.ok(err, 'should get an error') + assert.equal(err.code, expectedStatusCode, 'should get the right status code') + assert.equal(err.details, expectedStatusText, 'should get the correct error message') + tx.end() + } + }) + }) +} + +test('should bind callback to the proper transaction context', (t, end) => { + const { agent, client } = t.nr + helper.runInTransaction(agent, 'web', async (tx) => { + client.sayHello({ name: 'Callback' }, (err, response) => { + assert.ok(response) + assert.equal(response.message, 'Hello Callback') + assert.ok(agent.getTransaction(), 'callback should have transaction context') + assert.equal(agent.getTransaction(), tx, 'transaction should be the one we started with') + end() + }) + }) +}) diff --git a/test/versioned/grpc/package.json b/test/versioned/grpc/package.json index a795db2930..3b21d88482 100644 --- a/test/versioned/grpc/package.json +++ b/test/versioned/grpc/package.json @@ -12,14 +12,14 @@ "@grpc/grpc-js": ">=1.4.0" }, "files": [ - "client-unary.tap.js", - "client-streaming.tap.js", - "client-server-streaming.tap.js", - "client-bidi-streaming.tap.js", - "server-unary.tap.js", - "server-client-streaming.tap.js", - "server-streaming.tap.js", - "server-bidi-streaming.tap.js" + "client-unary.test.js", + "client-streaming.test.js", + "client-server-streaming.test.js", + "client-bidi-streaming.test.js", + "server-unary.test.js", + "server-client-streaming.test.js", + "server-streaming.test.js", + "server-bidi-streaming.test.js" ] } ] diff --git a/test/versioned/grpc/server-bidi-streaming.tap.js b/test/versioned/grpc/server-bidi-streaming.tap.js deleted file mode 100644 index e9fc21c117..0000000000 --- a/test/versioned/grpc/server-bidi-streaming.tap.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS -const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT -const { ERR_CODE, ERR_SERVER_MSG } = require('./constants.cjs') - -const { - makeBidiStreamingRequest, - createServer, - getClient, - getServerTransactionName, - assertError, - assertServerTransaction, - assertServerMetrics, - assertDistributedTracing -} = require('./util.cjs') - -tap.test('gRPC Server: Bidi Streaming', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track bidirectional requests', async (t) => { - let transaction - agent.on('transactionFinished', (tx) => { - transaction = tx - }) - - const names = [{ name: 'Huey' }, { name: 'Dewey' }, { name: 'Louie' }] - const responses = await makeBidiStreamingRequest({ - client, - fnName: 'sayHelloBidiStream', - payload: names - }) - names.forEach(({ name }, i) => { - t.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') - }) - - t.ok(transaction, 'transaction exists') - assertServerTransaction({ t, transaction, fnName: 'SayHelloBidiStream' }) - assertServerMetrics({ t, agentMetrics: agent.metrics._metrics, fnName: 'SayHelloBidiStream' }) - t.end() - }) - - t.test('should add DT headers when `distributed_tracing` is enabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHelloBidiStream')) { - serverTransaction = tx - } - }) - const payload = [{ name: 'dt test' }] - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - await makeBidiStreamingRequest({ client, fnName: 'sayHelloBidiStream', payload }) - tx.end() - }) - - assertDistributedTracing({ t, clientTransaction, serverTransaction }) - t.end() - }) - - t.test( - 'should not include distributed trace headers when there is no client transaction', - async (t) => { - let serverTransaction - agent.on('transactionFinished', (tx) => { - serverTransaction = tx - }) - const payload = [{ name: 'dt not in transaction' }] - await makeBidiStreamingRequest({ client, fnName: 'sayHelloBidiStream', payload }) - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - } - ) - - t.test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHelloBidiStream')) { - serverTransaction = tx - } - }) - - agent.config.distributed_tracing.enabled = false - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const payload = [{ name: 'dt disabled' }] - await makeBidiStreamingRequest({ client, fnName: 'sayHelloBidiStream', payload }) - tx.end() - }) - - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, async (t) => { - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - const expectedStatusCode = ERR_CODE - const expectedStatusText = ERR_SERVER_MSG - let transaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayErrorBidiStream')) { - transaction = tx - } - }) - - try { - const payload = [{ name: 'server-error' }] - await makeBidiStreamingRequest({ client, fnName: 'sayErrorBidiStream', payload }) - } catch (err) { - // err tested in client tests - } - - assertError({ - t, - transaction, - errors: agent.errors, - agentMetrics: agent.metrics._metrics, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorBidiStream' - }) - t.end() - }) - }) -}) diff --git a/test/versioned/grpc/server-bidi-streaming.test.js b/test/versioned/grpc/server-bidi-streaming.test.js new file mode 100644 index 0000000000..45d4e5b94d --- /dev/null +++ b/test/versioned/grpc/server-bidi-streaming.test.js @@ -0,0 +1,183 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { notHas } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS +const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT + +const { ERR_CODE, ERR_SERVER_MSG } = require('./constants.cjs') +const { + assertError, + assertDistributedTracing, + assertServerMetrics, + assertServerTransaction, + makeBidiStreamingRequest, + createServer, + getClient, + getServerTransactionName +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track bidirectional requests', async (t) => { + const { agent, client } = t.nr + let transaction + agent.on('transactionFinished', (tx) => { + transaction = tx + }) + + const names = [{ name: 'Huey' }, { name: 'Dewey' }, { name: 'Louie' }] + const responses = await makeBidiStreamingRequest({ + client, + fnName: 'sayHelloBidiStream', + payload: names + }) + names.forEach(({ name }, i) => { + assert.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') + }) + + assert.ok(transaction, 'transaction exists') + assertServerTransaction({ transaction, fnName: 'SayHelloBidiStream' }) + assertServerMetrics({ agentMetrics: agent.metrics._metrics, fnName: 'SayHelloBidiStream' }) +}) + +test('should add DT headers when `distributed_tracing` is enabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHelloBidiStream')) { + serverTransaction = tx + } + }) + const payload = [{ name: 'dt test' }] + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + await makeBidiStreamingRequest({ client, fnName: 'sayHelloBidiStream', payload }) + tx.end() + }) + + assertDistributedTracing({ clientTransaction, serverTransaction }) +}) + +test('should not include distributed trace headers when there is no client transaction', async (t) => { + const { agent, client } = t.nr + let serverTransaction + agent.on('transactionFinished', (tx) => { + serverTransaction = tx + }) + const payload = [{ name: 'dt not in transaction' }] + await makeBidiStreamingRequest({ client, fnName: 'sayHelloBidiStream', payload }) + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHelloBidiStream')) { + serverTransaction = tx + } + }) + + agent.config.distributed_tracing.enabled = false + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const payload = [{ name: 'dt disabled' }] + await makeBidiStreamingRequest({ client, fnName: 'sayHelloBidiStream', payload }) + tx.end() + }) + + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, async (t) => { + const { agent, client } = t.nr + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + const expectedStatusCode = ERR_CODE + const expectedStatusText = ERR_SERVER_MSG + let transaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayErrorBidiStream')) { + transaction = tx + } + }) + + try { + const payload = [{ name: 'server-error' }] + await makeBidiStreamingRequest({ client, fnName: 'sayErrorBidiStream', payload }) + } catch (err) { + // err tested in client tests + } + + assertError({ + transaction, + errors: agent.errors, + agentMetrics: agent.metrics._metrics, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorBidiStream' + }) + }) +} diff --git a/test/versioned/grpc/server-client-streaming.tap.js b/test/versioned/grpc/server-client-streaming.tap.js deleted file mode 100644 index 768335c8ff..0000000000 --- a/test/versioned/grpc/server-client-streaming.tap.js +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS -const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT -const { ERR_CODE, ERR_SERVER_MSG, HALT_CODE, HALT_GRPC_SERVER_MSG } = require('./constants.cjs') - -const { - makeClientStreamingRequest, - createServer, - getClient, - getServerTransactionName, - assertError, - assertServerTransaction, - assertServerMetrics, - assertDistributedTracing -} = require('./util.cjs') - -tap.test('gRPC Server: Client Streaming', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track client streaming requests', async (t) => { - let transaction - agent.on('transactionFinished', (tx) => { - transaction = tx - }) - - const names = [{ name: 'Bob' }, { name: 'Jordi' }, { name: 'Corey' }] - const response = await makeClientStreamingRequest({ - client, - fnName: 'sayHelloClientStream', - payload: names - }) - t.ok(response, 'response exists') - t.equal( - response.message, - `Hello ${names.map(({ name }) => name).join(', ')}`, - 'response message is correct' - ) - assertServerTransaction({ t, transaction, fnName: 'SayHelloClientStream' }) - assertServerMetrics({ t, agentMetrics: agent.metrics._metrics, fnName: 'SayHelloClientStream' }) - }) - - t.test('should add DT headers when `distributed_tracing` is enabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHelloClientStream')) { - serverTransaction = tx - } - }) - const payload = [{ name: 'dt test' }, { name: 'dt test2' }] - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) - tx.end() - }) - - payload.forEach(({ name }) => { - // TODO: gotta instrument and test event listeners on client streaming - t.test(`adding '${name}' should create a server trace segment`) - }) - assertDistributedTracing({ t, clientTransaction, serverTransaction }) - t.end() - }) - - t.test( - 'should not include distributed trace headers when there is no client transaction', - async (t) => { - let serverTransaction - agent.on('transactionFinished', (tx) => { - serverTransaction = tx - }) - const payload = [{ name: 'dt test' }, { name: 'dt test2' }] - await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - } - ) - - t.test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHelloClientStream')) { - serverTransaction = tx - } - }) - - agent.config.distributed_tracing.enabled = false - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const payload = [{ name: 'dt test' }, { name: 'dt test2' }] - await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) - tx.end() - }) - - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, async (t) => { - const expectedStatusCode = ERR_CODE - const expectedStatusText = ERR_SERVER_MSG - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - let transaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayErrorClientStream')) { - transaction = tx - } - }) - - try { - const payload = [{ oh: 'noes' }] - await makeClientStreamingRequest({ client, fnName: 'sayErrorClientStream', payload }) - } catch (err) { - // err tested in client tests - } - - assertError({ - t, - transaction, - errors: agent.errors, - agentMetrics: agent.metrics._metrics, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorClientStream' - }) - t.end() - }) - }) - - t.test('should not record errors if `grpc.record_errors` is disabled', async (t) => { - agent.config.grpc.record_errors = false - - let transaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayErrorClientStream')) { - transaction = tx - } - }) - - try { - const payload = [{ oh: 'noes' }] - await makeClientStreamingRequest({ client, fnName: 'sayErrorClientStream', payload }) - } catch (err) { - // err tested in client tests - } - t.ok(transaction, 'transaction exists') - t.equal(agent.errors.traceAggregator.errors.length, 0, 'should not record any errors') - assertServerTransaction({ - t, - transaction, - fnName: 'SayErrorClientStream', - expectedStatusCode: ERR_CODE - }) - assertServerMetrics({ - t, - agentMetrics: agent.metrics._metrics, - fnName: 'SayErrorClientStream', - expectedStatusCode: ERR_CODE - }) - t.end() - }) - - t.test( - 'should record errors if `grpc.record_errors` is enabled and server sends error mid stream', - async (t) => { - let transaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayErrorClientStream')) { - transaction = tx - } - }) - - try { - const payload = [{ name: 'error' }] - await makeClientStreamingRequest({ - client, - fnName: 'sayErrorClientStream', - payload, - endStream: false - }) - } catch (err) { - // err tested in client tests - } - t.ok(transaction, 'transaction exists') - t.equal(agent.errors.traceAggregator.errors.length, 1, 'should record a single error') - const error = agent.errors.traceAggregator.errors[0][2] - t.equal(error, HALT_GRPC_SERVER_MSG, 'should have the error message') - assertServerTransaction({ - t, - transaction, - fnName: 'SayErrorClientStream', - expectedStatusCode: HALT_CODE - }) - assertServerMetrics({ - t, - agentMetrics: agent.metrics._metrics, - fnName: 'SayErrorClientStream', - expectedStatusCode: HALT_CODE - }) - t.end() - } - ) -}) diff --git a/test/versioned/grpc/server-client-streaming.test.js b/test/versioned/grpc/server-client-streaming.test.js new file mode 100644 index 0000000000..6d45709a3f --- /dev/null +++ b/test/versioned/grpc/server-client-streaming.test.js @@ -0,0 +1,256 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { notHas } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS +const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT + +const { ERR_CODE, ERR_SERVER_MSG, HALT_CODE, HALT_GRPC_SERVER_MSG } = require('./constants.cjs') +const { + assertError, + assertDistributedTracing, + assertServerMetrics, + assertServerTransaction, + makeClientStreamingRequest, + createServer, + getClient, + getServerTransactionName +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track client streaming requests', async (t) => { + const { agent, client } = t.nr + let transaction + agent.on('transactionFinished', (tx) => { + transaction = tx + }) + + const names = [{ name: 'Bob' }, { name: 'Jordi' }, { name: 'Corey' }] + const response = await makeClientStreamingRequest({ + client, + fnName: 'sayHelloClientStream', + payload: names + }) + assert.ok(response, 'response exists') + assert.equal( + response.message, + `Hello ${names.map(({ name }) => name).join(', ')}`, + 'response message is correct' + ) + assertServerTransaction({ transaction, fnName: 'SayHelloClientStream' }) + assertServerMetrics({ agentMetrics: agent.metrics._metrics, fnName: 'SayHelloClientStream' }) +}) + +test('should add DT headers when `distributed_tracing` is enabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHelloClientStream')) { + serverTransaction = tx + } + }) + const payload = [{ name: 'dt test' }, { name: 'dt test2' }] + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) + tx.end() + }) + + payload.forEach(({ name }) => { + // TODO: gotta instrument and test event listeners on client streaming + // t.test(`adding '${name}' should create a server trace segment`) + t.diagnostic(`adding '${name}' should create a server trace segment`) + }) + assertDistributedTracing({ clientTransaction, serverTransaction }) +}) + +test('should not include distributed trace headers when there is no client transaction', async (t) => { + const { agent, client } = t.nr + let serverTransaction + agent.on('transactionFinished', (tx) => { + serverTransaction = tx + }) + const payload = [{ name: 'dt test' }, { name: 'dt test2' }] + await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHelloClientStream')) { + serverTransaction = tx + } + }) + + agent.config.distributed_tracing.enabled = false + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const payload = [{ name: 'dt test' }, { name: 'dt test2' }] + await makeClientStreamingRequest({ client, fnName: 'sayHelloClientStream', payload }) + tx.end() + }) + + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, async (t) => { + const { agent, client } = t.nr + const expectedStatusCode = ERR_CODE + const expectedStatusText = ERR_SERVER_MSG + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + let transaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayErrorClientStream')) { + transaction = tx + } + }) + + try { + const payload = [{ oh: 'noes' }] + await makeClientStreamingRequest({ client, fnName: 'sayErrorClientStream', payload }) + } catch (err) { + // err tested in client tests + } + + assertError({ + transaction, + errors: agent.errors, + agentMetrics: agent.metrics._metrics, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorClientStream' + }) + }) +} + +test('should not record errors if `grpc.record_errors` is disabled', async (t) => { + const { agent, client } = t.nr + agent.config.grpc.record_errors = false + + let transaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayErrorClientStream')) { + transaction = tx + } + }) + + try { + const payload = [{ oh: 'noes' }] + await makeClientStreamingRequest({ client, fnName: 'sayErrorClientStream', payload }) + } catch (err) { + // err tested in client tests + } + assert.ok(transaction, 'transaction exists') + assert.equal(agent.errors.traceAggregator.errors.length, 0, 'should not record any errors') + assertServerTransaction({ + transaction, + fnName: 'SayErrorClientStream', + expectedStatusCode: ERR_CODE + }) + assertServerMetrics({ + agentMetrics: agent.metrics._metrics, + fnName: 'SayErrorClientStream', + expectedStatusCode: ERR_CODE + }) +}) + +test('should record errors if `grpc.record_errors` is enabled and server sends error mid stream', async (t) => { + const { agent, client } = t.nr + let transaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayErrorClientStream')) { + transaction = tx + } + }) + + try { + const payload = [{ name: 'error' }] + await makeClientStreamingRequest({ + client, + fnName: 'sayErrorClientStream', + payload, + endStream: false + }) + } catch (err) { + // err tested in client tests + } + assert.ok(transaction, 'transaction exists') + assert.equal(agent.errors.traceAggregator.errors.length, 1, 'should record a single error') + const error = agent.errors.traceAggregator.errors[0][2] + assert.equal(error, HALT_GRPC_SERVER_MSG, 'should have the error message') + assertServerTransaction({ + transaction, + fnName: 'SayErrorClientStream', + expectedStatusCode: HALT_CODE + }) + assertServerMetrics({ + agentMetrics: agent.metrics._metrics, + fnName: 'SayErrorClientStream', + expectedStatusCode: HALT_CODE + }) +}) diff --git a/test/versioned/grpc/server-streaming.tap.js b/test/versioned/grpc/server-streaming.tap.js deleted file mode 100644 index 606a71c20a..0000000000 --- a/test/versioned/grpc/server-streaming.tap.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS -const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT -const { ERR_CODE, ERR_SERVER_MSG } = require('./constants.cjs') - -const { - makeServerStreamingRequest, - createServer, - getClient, - getServerTransactionName, - assertError, - assertServerTransaction, - assertServerMetrics, - assertDistributedTracing -} = require('./util.cjs') - -tap.test('gRPC Server: Server Streaming', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track server-streaming requests', async (t) => { - let transaction - agent.on('transactionFinished', (tx) => { - transaction = tx - }) - - const names = ['Bob', 'Jordi', 'Corey'] - const responses = await makeServerStreamingRequest({ - client, - fnName: 'sayHelloServerStream', - payload: { name: names } - }) - names.forEach((name, i) => { - t.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') - }) - t.ok(transaction, 'transaction exists') - assertServerTransaction({ t, transaction, fnName: 'SayHelloServerStream' }) - assertServerMetrics({ t, agentMetrics: agent.metrics._metrics, fnName: 'SayHelloServerStream' }) - t.end() - }) - - t.test('should add DT headers when `distributed_tracing` is enabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHelloServerStream')) { - serverTransaction = tx - } - }) - const payload = { name: ['dt test', 'dt test 2'] } - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) - tx.end() - }) - - assertDistributedTracing({ t, clientTransaction, serverTransaction }) - t.end() - }) - - t.test( - 'should not include distributed trace headers when there is no client transaction', - async (t) => { - let serverTransaction - agent.on('transactionFinished', (tx) => { - serverTransaction = tx - }) - const payload = { name: ['dt test', 'dt test 2'] } - await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - } - ) - - t.test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHelloServerStream')) { - serverTransaction = tx - } - }) - - agent.config.distributed_tracing.enabled = false - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const payload = { name: ['dt test', 'dt test 2'] } - await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) - tx.end() - }) - - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, async (t) => { - const expectedStatusCode = ERR_CODE - const expectedStatusText = ERR_SERVER_MSG - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - let transaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayErrorServerStream')) { - transaction = tx - } - }) - - try { - const payload = { name: ['noes'] } - await makeServerStreamingRequest({ client, fnName: 'sayErrorServerStream', payload }) - } catch (err) { - // err tested in client tests - } - - assertError({ - t, - transaction, - errors: agent.errors, - agentMetrics: agent.metrics._metrics, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayErrorServerStream' - }) - t.end() - }) - }) -}) diff --git a/test/versioned/grpc/server-streaming.test.js b/test/versioned/grpc/server-streaming.test.js new file mode 100644 index 0000000000..394f8c5be5 --- /dev/null +++ b/test/versioned/grpc/server-streaming.test.js @@ -0,0 +1,182 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { notHas } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS +const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT + +const { ERR_CODE, ERR_SERVER_MSG } = require('./constants.cjs') +const { + assertError, + assertDistributedTracing, + assertServerMetrics, + assertServerTransaction, + makeServerStreamingRequest, + createServer, + getClient, + getServerTransactionName +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track server-streaming requests', async (t) => { + const { agent, client } = t.nr + let transaction + agent.on('transactionFinished', (tx) => { + transaction = tx + }) + + const names = ['Bob', 'Jordi', 'Corey'] + const responses = await makeServerStreamingRequest({ + client, + fnName: 'sayHelloServerStream', + payload: { name: names } + }) + names.forEach((name, i) => { + assert.equal(responses[i], `Hello ${name}`, 'response stream message should be correct') + }) + assert.ok(transaction, 'transaction exists') + assertServerTransaction({ transaction, fnName: 'SayHelloServerStream' }) + assertServerMetrics({ agentMetrics: agent.metrics._metrics, fnName: 'SayHelloServerStream' }) +}) + +test('should add DT headers when `distributed_tracing` is enabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHelloServerStream')) { + serverTransaction = tx + } + }) + const payload = { name: ['dt test', 'dt test 2'] } + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) + tx.end() + }) + + assertDistributedTracing({ clientTransaction, serverTransaction }) +}) + +test('should not include distributed trace headers when there is no client transaction', async (t) => { + const { agent, client } = t.nr + let serverTransaction + agent.on('transactionFinished', (tx) => { + serverTransaction = tx + }) + const payload = { name: ['dt test', 'dt test 2'] } + await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHelloServerStream')) { + serverTransaction = tx + } + }) + + agent.config.distributed_tracing.enabled = false + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const payload = { name: ['dt test', 'dt test 2'] } + await makeServerStreamingRequest({ client, fnName: 'sayHelloServerStream', payload }) + tx.end() + }) + + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, async (t) => { + const { agent, client } = t.nr + const expectedStatusCode = ERR_CODE + const expectedStatusText = ERR_SERVER_MSG + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + let transaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayErrorServerStream')) { + transaction = tx + } + }) + + try { + const payload = { name: ['noes'] } + await makeServerStreamingRequest({ client, fnName: 'sayErrorServerStream', payload }) + } catch (err) { + // err tested in client tests + } + + assertError({ + transaction, + errors: agent.errors, + agentMetrics: agent.metrics._metrics, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayErrorServerStream' + }) + }) +} diff --git a/test/versioned/grpc/server-unary.tap.js b/test/versioned/grpc/server-unary.tap.js deleted file mode 100644 index e1d8beafae..0000000000 --- a/test/versioned/grpc/server-unary.tap.js +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2022 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const { removeModules } = require('../../lib/cache-buster') -const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS -const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT -const { ERR_CODE, ERR_SERVER_MSG } = require('./constants.cjs') - -const { - makeUnaryRequest, - createServer, - getClient, - getServerTransactionName, - assertError, - assertServerTransaction, - assertServerMetrics, - assertDistributedTracing -} = require('./util.cjs') - -tap.test('gRPC Server: Unary Requests', (t) => { - t.autoend() - - let agent - let client - let server - let proto - let grpc - let port - - t.beforeEach(async () => { - agent = helper.instrumentMockedAgent() - grpc = require('@grpc/grpc-js') - ;({ port, proto, server } = await createServer(grpc)) - client = getClient(grpc, proto, port) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - server.forceShutdown() - client.close() - grpc = null - proto = null - removeModules(['@grpc/grpc-js']) - }) - - t.test('should track unary server requests', async (t) => { - let transaction - agent.on('transactionFinished', (tx) => { - transaction = tx - }) - - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - t.equal(response.message, 'Hello New Relic', 'response message is correct') - t.ok(transaction, 'transaction exists') - assertServerTransaction({ t, transaction, fnName: 'SayHello' }) - assertServerMetrics({ t, agentMetrics: agent.metrics._metrics, fnName: 'SayHello' }) - t.end() - }) - - t.test('should add DT headers when `distributed_tracing` is enabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHello')) { - serverTransaction = tx - } - }) - - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - tx.end() - }) - - assertDistributedTracing({ t, clientTransaction, serverTransaction }) - t.end() - }) - - t.test( - 'should not include distributed trace headers when there is no client transaction', - async (t) => { - let serverTransaction - agent.on('transactionFinished', (tx) => { - serverTransaction = tx - }) - const payload = { name: 'dt not in transaction' } - const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) - t.ok(response, 'response exists') - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - } - ) - - t.test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { - let serverTransaction - let clientTransaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayHello')) { - serverTransaction = tx - } - }) - - agent.config.distributed_tracing.enabled = false - await helper.runInTransaction(agent, 'web', async (tx) => { - clientTransaction = tx - clientTransaction.name = 'clientTransaction' - const response = await makeUnaryRequest({ - client, - fnName: 'sayHello', - payload: { name: 'New Relic' } - }) - t.ok(response, 'response exists') - tx.end() - }) - - const attributes = serverTransaction.trace.attributes.get(DESTINATION) - t.notHas(attributes, 'request.header.newrelic', 'should not have newrelic in headers') - t.notHas(attributes, 'request.header.traceparent', 'should not have traceparent in headers') - t.end() - }) - - const grpcConfigs = [ - { record_errors: true, ignore_status_codes: [], should: true }, - { record_errors: false, ignore_status_codes: [], should: false }, - { record_errors: true, ignore_status_codes: [9], should: false } - ] - grpcConfigs.forEach((config) => { - const should = config.should ? 'should' : 'should not' - const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` - t.test(testName, async (t) => { - agent.config.grpc.record_errors = config.record_errors - agent.config.grpc.ignore_status_codes = config.ignore_status_codes - const expectedStatusCode = ERR_CODE - const expectedStatusText = ERR_SERVER_MSG - let transaction - agent.on('transactionFinished', (tx) => { - if (tx.name === getServerTransactionName('SayError')) { - transaction = tx - } - }) - - try { - await makeUnaryRequest({ - client, - fnName: 'sayError', - payload: { oh: 'noes' } - }) - } catch (err) { - // err tested in client tests - } - - assertError({ - t, - transaction, - errors: agent.errors, - agentMetrics: agent.metrics._metrics, - expectErrors: config.should, - expectedStatusCode, - expectedStatusText, - fnName: 'SayError' - }) - t.end() - }) - }) -}) diff --git a/test/versioned/grpc/server-unary.test.js b/test/versioned/grpc/server-unary.test.js new file mode 100644 index 0000000000..8e6a7a8798 --- /dev/null +++ b/test/versioned/grpc/server-unary.test.js @@ -0,0 +1,193 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const { notHas } = require('../../lib/custom-assertions') +const helper = require('../../lib/agent_helper') + +const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS +const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT + +const { ERR_CODE, ERR_SERVER_MSG } = require('./constants.cjs') +const { + assertError, + assertDistributedTracing, + assertServerMetrics, + assertServerTransaction, + makeUnaryRequest, + createServer, + getClient, + getServerTransactionName +} = require('./util.cjs') + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.grpc = require('@grpc/grpc-js') + + const { port, proto, server } = await createServer(ctx.nr.grpc) + ctx.nr.port = port + ctx.nr.proto = proto + ctx.nr.server = server + ctx.nr.client = getClient(ctx.nr.grpc, proto, port) +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + ctx.nr.server.forceShutdown() + ctx.nr.client.close() + removeModules(['@grpc/grpc-js']) +}) + +test('should track unary server requests', async (t) => { + const { agent, client } = t.nr + let transaction + agent.on('transactionFinished', (tx) => { + transaction = tx + }) + + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + assert.equal(response.message, 'Hello New Relic', 'response message is correct') + assert.ok(transaction, 'transaction exists') + assertServerTransaction({ transaction, fnName: 'SayHello' }) + assertServerMetrics({ agentMetrics: agent.metrics._metrics, fnName: 'SayHello' }) +}) + +test('should add DT headers when `distributed_tracing` is enabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHello')) { + serverTransaction = tx + } + }) + + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + tx.end() + }) + + assertDistributedTracing({ clientTransaction, serverTransaction }) +}) + +test('should not include distributed trace headers when there is no client transaction', async (t) => { + const { agent, client } = t.nr + let serverTransaction + agent.on('transactionFinished', (tx) => { + serverTransaction = tx + }) + const payload = { name: 'dt not in transaction' } + const response = await makeUnaryRequest({ client, fnName: 'sayHello', payload }) + assert.ok(response, 'response exists') + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +test('should not add DT headers when `distributed_tracing` is disabled', async (t) => { + const { agent, client } = t.nr + let serverTransaction + let clientTransaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayHello')) { + serverTransaction = tx + } + }) + + agent.config.distributed_tracing.enabled = false + await helper.runInTransaction(agent, 'web', async (tx) => { + clientTransaction = tx + clientTransaction.name = 'clientTransaction' + const response = await makeUnaryRequest({ + client, + fnName: 'sayHello', + payload: { name: 'New Relic' } + }) + assert.ok(response, 'response exists') + tx.end() + }) + + const attributes = serverTransaction.trace.attributes.get(DESTINATION) + notHas({ + found: attributes, + doNotWant: 'request.header.newrelic', + msg: 'should not have newrelic in headers' + }) + notHas({ + found: attributes, + doNotWant: 'request.header.traceparent', + msg: 'should not have traceparent in headers' + }) +}) + +const grpcConfigs = [ + { record_errors: true, ignore_status_codes: [], should: true }, + { record_errors: false, ignore_status_codes: [], should: false }, + { record_errors: true, ignore_status_codes: [9], should: false } +] +for (const config of grpcConfigs) { + const should = config.should ? 'should' : 'should not' + const testName = `${should} record errors in a transaction when ignoring ${config.ignore_status_codes}` + + test(testName, async (t) => { + const { agent, client } = t.nr + agent.config.grpc.record_errors = config.record_errors + agent.config.grpc.ignore_status_codes = config.ignore_status_codes + const expectedStatusCode = ERR_CODE + const expectedStatusText = ERR_SERVER_MSG + let transaction + agent.on('transactionFinished', (tx) => { + if (tx.name === getServerTransactionName('SayError')) { + transaction = tx + } + }) + + try { + await makeUnaryRequest({ + client, + fnName: 'sayError', + payload: { oh: 'noes' } + }) + } catch (err) { + // err tested in client tests + } + + assertError({ + transaction, + errors: agent.errors, + agentMetrics: agent.metrics._metrics, + expectErrors: config.should, + expectedStatusCode, + expectedStatusText, + fnName: 'SayError' + }) + }) +} diff --git a/test/versioned/grpc/util.cjs b/test/versioned/grpc/util.cjs index 1a743da252..2ae0864095 100644 --- a/test/versioned/grpc/util.cjs +++ b/test/versioned/grpc/util.cjs @@ -4,6 +4,7 @@ */ 'use strict' + const util = module.exports const metricsHelpers = require('../../lib/metrics_helper') const protoLoader = require('@grpc/proto-loader') @@ -11,6 +12,8 @@ const serverImpl = require('./grpc-server.cjs') const DESTINATIONS = require('../../../lib/config/attribute-filter').DESTINATIONS const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT +const { assertMetrics, assertSegments, match } = require('../../lib/custom-assertions') + const SERVER_ADDR = '0.0.0.0' const CLIENT_ADDR = 'localhost' const SERVER_TX_PREFIX = 'WebTransaction/WebFrameworkUri/gRPC/' @@ -32,14 +35,16 @@ function buildExpectedMetrics(port) { * Iterates over all metrics created during a transaction and asserts no gRPC metrics were created * * @param {Object} params - * @param {Object} params.t tap test * @param {Object} params.agent test agent */ -util.assertMetricsNotExisting = function assertMetricsNotExisting({ t, agent, port }) { +util.assertMetricsNotExisting = function assertMetricsNotExisting( + { agent, port }, + { assert = require('node:assert') } = {} +) { const metrics = buildMetrics(port) metrics.forEach((metricName) => { const metric = agent.metrics.getMetric(metricName) - t.notOk(metric, `${metricName} should not be recorded`) + assert.equal(metric, undefined, `${metricName} should not be recorded`) }) } @@ -126,44 +131,39 @@ util.getServerTransactionName = function getRPCName(fnName) { * procedure, grpc.statusCode, grpc.statusText * * @param {Object} params - * @param {Object} params.t tap test * @param {Object} params.tx transaction under test * @param {string} params.fnName gRPC method name * @param {number} [params.expectedStatusCode=0] expected status code for test * @param {string} [params.expectedStatusText=OK] expected status text for test */ -util.assertExternalSegment = function assertExternalSegment({ - t, - tx, - fnName, - expectedStatusCode = 0, - expectedStatusText = 'OK', - port -}) { +util.assertExternalSegment = function assertExternalSegment( + { tx, fnName, expectedStatusCode = 0, expectedStatusText = 'OK', port }, + { assert = require('node:assert') } = {} +) { const methodName = util.getRPCName(fnName) const segmentName = `${EXTERNAL.PREFIX}${CLIENT_ADDR}:${port}${methodName}` - t.assertSegments(tx.trace.root, [segmentName], { exact: false }) + assertSegments(tx.trace.root, [segmentName], { exact: false }, { assert }) const segment = metricsHelpers.findSegment(tx.trace.root, segmentName) const attributes = segment.getAttributes() - t.equal( + assert.equal( attributes.url, `grpc://${CLIENT_ADDR}:${port}${methodName}`, 'http.url attribute should be correct' ) - t.equal(attributes.procedure, methodName, 'method name should be correct') - t.equal( + assert.equal(attributes.procedure, methodName, 'method name should be correct') + assert.equal( attributes['grpc.statusCode'], expectedStatusCode, `status code should be ${expectedStatusCode}` ) - t.equal( + assert.equal( attributes['grpc.statusText'], expectedStatusText, `status text should be ${expectedStatusText}` ) - t.equal(attributes.component, 'gRPC', 'should have the component set to "gRPC"') + assert.equal(attributes.component, 'gRPC', 'should have the component set to "gRPC"') const expectedMetrics = buildExpectedMetrics(port) - t.assertMetrics(tx.metrics, [expectedMetrics], false, false) + assertMetrics(tx.metrics, [expectedMetrics], false, false, { assert }) } /** @@ -171,39 +171,39 @@ util.assertExternalSegment = function assertExternalSegment({ * request.method, request.uri * * @param {Object} params - * @param {Object} params.t tap test * @param {Object} params.tx transaction under test * @param {string} params.fnName gRPC method name * @param {number} [params.expectedStatusCode=0] expected status code for test */ -util.assertServerTransaction = function assertServerTransaction({ - t, - transaction, - fnName, - expectedStatusCode = 0 -}) { +util.assertServerTransaction = function assertServerTransaction( + { transaction, fnName, expectedStatusCode = 0 }, + { assert = require('node:assert') } = {} +) { const attributes = transaction.trace.attributes.get(DESTINATION) const expectedMethod = `/helloworld.Greeter/${fnName}` const expectedUri = `/helloworld.Greeter/${fnName}` - t.equal( + assert.equal( transaction.name, util.getServerTransactionName(fnName), 'should have the right transaction name' ) - t.equal( + assert.equal( attributes['response.status'], expectedStatusCode, `status code should be ${expectedStatusCode}` ) - t.equal( + assert.equal( attributes['request.method'], expectedMethod, `should have server method ${expectedMethod}` ) - t.equal(attributes['request.uri'], expectedUri, `should have server uri ${expectedUri}`) + assert.equal(attributes['request.uri'], expectedUri, `should have server uri ${expectedUri}`) } -util.assertServerMetrics = function assertServerMetrics({ t, agentMetrics, fnName }) { +util.assertServerMetrics = function assertServerMetrics( + { agentMetrics, fnName }, + { assert = require('node:assert') } = {} +) { const expectedServerMetrics = [ [{ name: 'WebTransaction' }], [{ name: 'WebTransactionTotalTime' }], @@ -213,26 +213,21 @@ util.assertServerMetrics = function assertServerMetrics({ t, agentMetrics, fnNam [{ name: `Apdex/WebFrameworkUri/gRPC//helloworld.Greeter/${fnName}` }], [{ name: 'Apdex' }] ] - t.assertMetrics(agentMetrics, expectedServerMetrics, false, false) + assertMetrics(agentMetrics, expectedServerMetrics, false, false, { assert }) } -util.assertDistributedTracing = function assertDistributedTracing({ - t, - clientTransaction, - serverTransaction -}) { +util.assertDistributedTracing = function assertDistributedTracing( + { clientTransaction, serverTransaction }, + { assert = require('node:assert') } = {} +) { const serverAttributes = serverTransaction.trace.attributes.get(DESTINATION) - t.ok( + assert.ok( clientTransaction.id !== serverTransaction.id, 'should get different transactions for client and server' ) - t.match( - serverAttributes['request.headers.traceparent'], - /^[\w\d\-]{55}$/, - 'should have traceparent in server attribute headers' - ) - t.equal(serverAttributes['request.headers.newrelic'], '', 'should have the newrelic header') - t.equal( + match(serverAttributes['request.headers.traceparent'], /^[\w\d\-]{55}$/, { assert }) + assert.equal(serverAttributes['request.headers.newrelic'], '', 'should have the newrelic header') + assert.equal( clientTransaction.traceId, serverTransaction.traceId, 'should have matching traceIds on client and server transactions' @@ -353,7 +348,6 @@ util.makeBidiStreamingRequest = function makeBidiStreamingRequest({ client, fnNa * If the client use case it will assert the external call segment * * @param {Object} params - * @param {Object} params.t tap test * @param {Object} params.transaction transaction under test * @param {Array} params.errors agent errors array * @param {boolean} [params.expectErrors=true] flag to indicate if errors will exist @@ -363,50 +357,58 @@ util.makeBidiStreamingRequest = function makeBidiStreamingRequest({ client, fnNa * @param {number} params.expectedStatusCode expected status code for test * @param {string} params.expectedStatusText expected status text for test */ -util.assertError = function assertError({ - t, - transaction, - errors, - expectErrors = true, - clientError = false, - agentMetrics, - fnName, - expectedStatusText, - expectedStatusCode, - port -}) { +util.assertError = function assertError( + { + transaction, + errors, + expectErrors = true, + clientError = false, + agentMetrics, + fnName, + expectedStatusText, + expectedStatusCode, + port + }, + { assert = require('node:assert') } = {} +) { // when testing client the transaction will contain both server and client information. so we need to extract the client error which is always the 2nd const errorLength = expectErrors ? (clientError ? 2 : 1) : 0 - t.equal(errors.traceAggregator.errors.length, errorLength, `should be ${errorLength} errors`) + assert.equal(errors.traceAggregator.errors.length, errorLength, `should be ${errorLength} errors`) if (expectErrors) { const errorPosition = clientError ? 1 : 0 const error = errors.traceAggregator.errors[errorPosition][2] - t.equal(error, expectedStatusText, 'should have the error message') + assert.equal(error, expectedStatusText, 'should have the error message') } if (clientError) { - util.assertExternalSegment({ - t, - tx: transaction, - fnName, - expectedStatusText, - port, - expectedStatusCode - }) + util.assertExternalSegment( + { + tx: transaction, + fnName, + expectedStatusText, + port, + expectedStatusCode + }, + { assert } + ) } else { - util.assertServerTransaction({ - t, - transaction, - fnName, - expectedStatusCode - }) - util.assertServerMetrics({ - t, - agentMetrics, - fnName, - expectedStatusCode - }) + util.assertServerTransaction( + { + transaction, + fnName, + expectedStatusCode + }, + { assert } + ) + util.assertServerMetrics( + { + agentMetrics, + fnName, + expectedStatusCode + }, + { assert } + ) } } diff --git a/test/versioned/mysql/basic-pool.js b/test/versioned/mysql/basic-pool.js new file mode 100644 index 0000000000..17a7b3bff5 --- /dev/null +++ b/test/versioned/mysql/basic-pool.js @@ -0,0 +1,702 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const fs = require('fs/promises') +const test = require('node:test') +const assert = require('node:assert') +const helper = require('../../lib/agent_helper') +const params = require('../../lib/params') +const urltils = require('../../../lib/util/urltils') +const { exec } = require('child_process') +const setup = require('./setup') +const semver = require('semver') +const util = require('util') +const execAsync = util.promisify(exec) + +module.exports = function ({ factory, constants, pkgVersion }) { + const { USER, DATABASE, TABLE } = constants + + const config = getConfig({}) + function getConfig(extras) { + const conf = { + connectionLimit: 10, + host: params.mysql_host, + port: params.mysql_port, + user: USER, + database: DATABASE + } + + // eslint-disable-next-line guard-for-in + for (const key in extras) { + conf[key] = extras[key] + } + + return conf + } + + test('See if mysql is running', async function () { + assert.doesNotThrow(async () => { + await setup(USER, DATABASE, TABLE, factory()) + }) + }) + + test('bad config', function (t, end) { + const agent = helper.instrumentMockedAgent() + t.after(function () { + helper.unloadAgent(agent) + }) + + const mysql = factory() + const badConfig = { + connectionLimit: 10, + host: 'nohost', + user: USER, + database: DATABASE + } + + const poolCluster = mysql.createPoolCluster() + + poolCluster.add(badConfig) // anonymous group + poolCluster.getConnection(function (err) { + // umm... so this test is pretty hacky, but i want to make sure we don't + // wrap the callback multiple times. + + const stack = new Error().stack + const frames = stack.split('\n').slice(3, 8) + + assert.notEqual(frames[0], frames[1], 'do not multi-wrap') + assert.notEqual(frames[0], frames[2], 'do not multi-wrap') + assert.notEqual(frames[0], frames[3], 'do not multi-wrap') + assert.notEqual(frames[0], frames[4], 'do not multi-wrap') + + assert.ok(err, 'should be an error') + poolCluster.end() + end() + }) + }) + + // TODO: test variable argument calling + // TODO: test error conditions + // TODO: test .query without callback + // TODO: test notice errors + // TODO: test sql capture + test('mysql built-in connection pools', async function (t) { + t.beforeEach(async function (ctx) { + await setup(USER, DATABASE, TABLE, factory()) + const agent = helper.instrumentMockedAgent() + const mysql = factory() + const pool = mysql.createPool(config) + ctx.nr = { + agent, + mysql, + pool + } + }) + + t.afterEach(function (ctx) { + const { pool, agent } = ctx.nr + return new Promise((resolve) => { + helper.unloadAgent(agent) + pool.end(resolve) + }) + }) + + // make sure a connection exists in the pool before any tests are run + // we want to make sure connections are allocated outside any transaction + // this is to avoid tests that 'happen' to work because of how CLS works + await t.test('primer', function (t, end) { + const { agent, pool } = t.nr + pool.query('SELECT 1 + 1 AS solution', function (err) { + assert.ok(!err, 'are you sure mysql is running?') + assert.ok(!agent.getTransaction(), 'transaction should not exist') + end() + }) + }) + + await t.test('ensure host and port are set on segment', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.query('SELECT 1 + 1 AS solution', function (err) { + const seg = txn.trace.root.children[0].children.filter(function (trace) { + return /Datastore\/statement\/MySQL/.test(trace.name) + })[0] + + const attributes = seg.getAttributes() + assert.ok(!err, 'should not error') + assert.ok(seg, 'should have a segment (' + (seg && seg.name) + ')') + assert.equal( + attributes.host, + urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, + 'set host' + ) + assert.equal(attributes.database_name, DATABASE, 'set database name') + assert.equal(attributes.port_path_or_id, String(config.port), 'set port') + assert.equal(attributes.product, 'MySQL', 'set product attribute') + txn.end() + end() + }) + }) + }) + + await t.test('respects `datastore_tracer.instance_reporting`', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + agent.config.datastore_tracer.instance_reporting.enabled = false + pool.query('SELECT 1 + 1 AS solution', function (err) { + const seg = getDatastoreSegment(agent.tracer.getSegment()) + assert.ok(!err, 'should not error making query') + assert.ok(seg, 'should have a segment') + + const attributes = seg.getAttributes() + assert.ok(!attributes.host, 'should have no host parameter') + assert.ok(!attributes.port_path_or_id, 'should have no port parameter') + assert.equal(attributes.database_name, DATABASE, 'should set database name') + assert.equal(attributes.product, 'MySQL', 'should set product attribute') + agent.config.datastore_tracer.instance_reporting.enabled = true + txn.end() + end() + }) + }) + }) + + await t.test('respects `datastore_tracer.database_name_reporting`', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + agent.config.datastore_tracer.database_name_reporting.enabled = false + pool.query('SELECT 1 + 1 AS solution', function (err) { + const seg = getDatastoreSegment(agent.tracer.getSegment()) + const attributes = seg.getAttributes() + assert.ok(!err, 'no errors') + assert.ok(seg, 'there is a segment') + assert.equal( + attributes.host, + urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, + 'set host' + ) + assert.equal(attributes.port_path_or_id, String(config.port), 'set port') + assert.ok(!attributes.database_name, 'should have no database name parameter') + assert.equal(attributes.product, 'MySQL', 'should set product attribute') + agent.config.datastore_tracer.database_name_reporting.enabled = true + txn.end() + end() + }) + }) + }) + + await t.test('ensure host is the default (localhost) when not supplied', function (t, end) { + const { agent, mysql } = t.nr + const defaultConfig = getConfig({ + host: null + }) + const defaultPool = mysql.createPool(defaultConfig) + helper.runInTransaction(agent, function transactionInScope(txn) { + defaultPool.query('SELECT 1 + 1 AS solution', function (err) { + assert.ok(!err, 'should not fail to execute query') + + // In the case where you don't have a server running on + // localhost the data will still be correctly associated + // with the query. + const seg = getDatastoreSegment(agent.tracer.getSegment()) + const attributes = seg.getAttributes() + assert.ok(seg, 'there is a segment') + assert.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') + assert.equal(attributes.database_name, DATABASE, 'set database name') + assert.equal(attributes.port_path_or_id, String(defaultConfig.port), 'set port') + assert.equal(attributes.product, 'MySQL', 'should set product attribute') + txn.end() + defaultPool.end(end) + }) + }) + }) + + await t.test('ensure port is the default (3306) when not supplied', function (t, end) { + const { agent, mysql } = t.nr + const defaultConfig = getConfig({ + host: null + }) + const defaultPool = mysql.createPool(defaultConfig) + helper.runInTransaction(agent, function transactionInScope(txn) { + defaultPool.query('SELECT 1 + 1 AS solution', function (err) { + const seg = getDatastoreSegment(agent.tracer.getSegment()) + const attributes = seg.getAttributes() + + assert.ok(!err, 'should not error making query') + assert.ok(seg, 'should have a segment') + assert.equal( + attributes.host, + urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, + 'should set host' + ) + assert.equal(attributes.database_name, DATABASE, 'should set database name') + assert.equal(attributes.port_path_or_id, '3306', 'should set port') + assert.equal(attributes.product, 'MySQL', 'should set product attribute') + txn.end() + defaultPool.end(end()) + }) + }) + }) + + await t.test('query with error', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.query('BLARG', function (err) { + assert.ok(err) + assert.ok(agent.getTransaction(), 'transaction should exit') + txn.end() + end() + }) + }) + }) + + await t.test('lack of callback does not explode', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.query('SET SESSION auto_increment_increment=1') + setTimeout(() => { + txn.end() + end() + }, 500) + }) + }) + + await t.test('pool.query', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.query('SELECT 1 + 1 AS solution123123123123', function (err) { + const transaction = agent.getTransaction() + const segment = agent.tracer.getSegment().parent + + assert.ok(!err, 'no error occurred') + assert.ok(transaction, 'transaction should exist') + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'MySQL Pool#query', 'is named') + txn.end() + end() + }) + }) + }) + + await t.test('pool.query with values', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.query('SELECT ? + ? AS solution', [1, 1], function (err) { + const transaction = agent.getTransaction() + assert.ok(!err) + assert.ok(transaction, 'should not lose transaction') + if (transaction) { + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'MySQL Pool#query', 'is named') + } + + txn.end() + end() + }) + }) + }) + + await t.test('pool.getConnection -> connection.query', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.getConnection(function shouldBeWrapped(err, connection) { + assert.ok(!err, 'should not have error') + assert.ok(agent.getTransaction(), 'transaction should exit') + t.after(function () { + connection.release() + }) + + connection.query('SELECT 1 + 1 AS solution', function (err) { + const transaction = agent.getTransaction() + const segment = agent.tracer.getSegment().parent + + assert.ok(!err, 'no error occurred') + assert.ok(transaction, 'transaction should exist') + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + txn.end() + end() + }) + }) + }) + }) + + await t.test('pool.getConnection -> connection.query with values', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + pool.getConnection(function shouldBeWrapped(err, connection) { + assert.ok(!err, 'should not have error') + assert.ok(agent.getTransaction(), 'transaction should exit') + t.after(function () { + connection.release() + }) + + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + const transaction = agent.getTransaction() + assert.ok(!err) + assert.ok(transaction, 'should not lose transaction') + if (transaction) { + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + } + + txn.end() + end() + }) + }) + }) + }) + + const socketPath = await getDomainSocketPath() + await t.test( + 'ensure host and port are set on segment when using a domain socket', + { skip: !socketPath }, + function (t, end) { + const { agent, mysql } = t.nr + const socketConfig = getConfig({ + socketPath + }) + const socketPool = mysql.createPool(socketConfig) + helper.runInTransaction(agent, function transactionInScope(txn) { + socketPool.query('SELECT 1 + 1 AS solution', function (err) { + assert.ok(!err, 'should not error making query') + + const seg = getDatastoreSegment(agent.tracer.getSegment()) + const attributes = seg.getAttributes() + + // In the case where you don't have a server running on localhost + // the data will still be correctly associated with the query. + assert.ok(seg, 'there is a segment') + assert.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') + assert.equal(attributes.port_path_or_id, socketPath, 'set path') + assert.equal(attributes.database_name, DATABASE, 'set database name') + assert.equal(attributes.product, 'MySQL', 'should set product attribute') + txn.end() + socketPool.end(end) + }) + }) + } + ) + }) + + test('poolCluster', async function (t) { + t.beforeEach(async function (ctx) { + await setup(USER, DATABASE, TABLE, factory()) + const agent = helper.instrumentMockedAgent() + const mysql = factory() + const poolCluster = mysql.createPoolCluster() + + poolCluster.add(config) // anonymous group + poolCluster.add('MASTER', config) + poolCluster.add('REPLICA', config) + ctx.nr = { + agent, + mysql, + poolCluster + } + }) + + t.afterEach(function (ctx) { + const { agent, poolCluster } = ctx.nr + poolCluster.end() + helper.unloadAgent(agent) + }) + + await t.test('primer', function (t, end) { + const { agent, poolCluster } = t.nr + poolCluster.getConnection(function (err, connection) { + assert.ok(!err, 'should not be an error') + assert.ok(!agent.getTransaction(), 'transaction should not exist') + + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + assert.ok(!err) + assert.ok(!agent.getTransaction(), 'transaction should not exist') + + connection.release() + end() + }) + }) + }) + + await t.test('get any connection', function (t, end) { + const { agent, poolCluster } = t.nr + helper.runInTransaction(agent, function (txn) { + poolCluster.getConnection(function (err, connection) { + assert.ok(!err, 'should not have error') + assert.ok(agent.getTransaction(), 'transaction should exist') + assert.equal(agent.getTransaction(), txn, 'transaction must be original') + + txn.end() + connection.release() + end() + }) + }) + }) + + await t.test('get any connection', function (t, end) { + const { agent, poolCluster } = t.nr + poolCluster.getConnection(function (err, connection) { + assert.ok(!err, 'should not have error') + + helper.runInTransaction(agent, function (txn) { + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + assert.ok(!err, 'no error occurred') + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction.id, txn.id, 'transaction must be same') + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + txn.end() + connection.release() + end() + }) + }) + }) + }) + + await t.test('get MASTER connection', function (t, end) { + const { agent, poolCluster } = t.nr + helper.runInTransaction(agent, function (txn) { + poolCluster.getConnection('MASTER', function (err, connection) { + assert.ok(!err) + assert.ok(agent.getTransaction()) + assert.equal(agent.getTransaction(), txn) + + txn.end() + connection.release() + end() + }) + }) + }) + + await t.test('get MASTER connection', function (t, end) { + const { agent, poolCluster } = t.nr + poolCluster.getConnection('MASTER', function (err, connection) { + helper.runInTransaction(agent, function (txn) { + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + assert.ok(!err, 'no error occurred') + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction.id, txn.id, 'transaction must be same') + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + txn.end() + connection.release() + end() + }) + }) + }) + }) + + await t.test('get glob', function (t, end) { + const { agent, poolCluster } = t.nr + helper.runInTransaction(agent, function (txn) { + poolCluster.getConnection('REPLICA*', 'ORDER', function (err, connection) { + assert.ok(!err) + assert.ok(agent.getTransaction()) + assert.equal(agent.getTransaction(), txn) + + txn.end() + connection.release() + end() + }) + }) + }) + + await t.test('get glob', function (t, end) { + const { agent, poolCluster } = t.nr + poolCluster.getConnection('REPLICA*', 'ORDER', function (err, connection) { + helper.runInTransaction(agent, function (txn) { + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + assert.ok(!err, 'no error occurred') + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction.id, txn.id, 'transaction must be same') + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + txn.end() + connection.release() + end() + }) + }) + }) + }) + + await t.test('get star', function (t, end) { + const { agent, poolCluster } = t.nr + helper.runInTransaction(agent, function () { + poolCluster.of('*').getConnection(function (err, connection) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'transaction should exist') + + agent.getTransaction().end() + connection.release() + end() + }) + }) + }) + + await t.test('get star', function (t, end) { + const { agent, poolCluster } = t.nr + poolCluster.of('*').getConnection(function (err, connection) { + helper.runInTransaction(agent, function (txn) { + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + assert.ok(!err, 'no error occurred') + const transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction.id, txn.id, 'transaction must be same') + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + txn.end() + connection.release() + end() + }) + }) + }) + }) + + await t.test('get wildcard', function (t, end) { + const { agent, poolCluster } = t.nr + helper.runInTransaction(agent, function () { + const pool = poolCluster.of('REPLICA*', 'RANDOM') + pool.getConnection(function (err, connection) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'should have transaction') + + agent.getTransaction().end() + connection.release() + end() + }) + }) + }) + + await t.test('get wildcard', function (t, end) { + const { agent, poolCluster } = t.nr + const pool = poolCluster.of('REPLICA*', 'RANDOM') + pool.getConnection(function (err, connection) { + helper.runInTransaction(agent, function (txn) { + connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { + assert.ok(!err, 'no error occurred') + const currentTransaction = agent.getTransaction() + assert.ok(currentTransaction, 'transaction should exist') + assert.equal(currentTransaction.id, txn.id, 'transaction must be same') + const segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + txn.end() + connection.release() + end() + }) + }) + }) + }) + + // not added until 2.12.0 + // https://github.com/mysqljs/mysql/blob/master/Changes.md#v2120-2016-11-02 + if (semver.satisfies(pkgVersion, '>=2.12.0')) { + await t.test('poolCluster query', function (t, end) { + const { agent, poolCluster } = t.nr + const masterPool = poolCluster.of('MASTER', 'RANDOM') + const replicaPool = poolCluster.of('REPLICA', 'RANDOM') + helper.runInTransaction(agent, function (txn) { + replicaPool.query('SELECT ? + ? AS solution', [1, 1], function (err) { + let transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction, txn, 'transaction must be same') + + let segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + assert.ok(!err, 'no error occurred') + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction, txn, 'transaction must be same') + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + masterPool.query('SELECT ? + ? AS solution', [1, 1], function (err) { + transaction = agent.getTransaction() + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction, txn, 'transaction must be same') + + segment = agent.tracer.getSegment().parent + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + assert.ok(!err, 'no error occurred') + assert.ok(transaction, 'transaction should exist') + assert.equal(transaction, txn, 'transaction must be same') + assert.ok(segment, 'segment should exist') + assert.ok(segment.timer.start > 0, 'starts at a positive time') + assert.ok(segment.timer.start <= Date.now(), 'starts in past') + assert.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') + + txn.end() + end() + }) + }) + }) + }) + } + }) +} + +async function getDomainSocketPath() { + try { + const { stdout, stderr } = await execAsync('mysql_config --socket') + if (stderr.toString()) { + return false + } + + const sock = stdout.toString().trim() + await fs.access(sock) + return sock + } catch (err) { + return false + } +} + +function getDatastoreSegment(segment) { + return segment.parent.children.filter(function (s) { + return /^Datastore/.test(s && s.name) + })[0] +} diff --git a/test/versioned/mysql/basic-pool.tap.js b/test/versioned/mysql/basic-pool.tap.js deleted file mode 100644 index a103ae30fe..0000000000 --- a/test/versioned/mysql/basic-pool.tap.js +++ /dev/null @@ -1,686 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const fs = require('fs') -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const params = require('../../lib/params') -const urltils = require('../../../lib/util/urltils') -const exec = require('child_process').exec -const setup = require('./setup') -const { version: pkgVersion } = require('mysql/package') -const semver = require('semver') - -const { USER, DATABASE } = setup - -const config = getConfig({}) -function getConfig(extras) { - const conf = { - connectionLimit: 10, - host: params.mysql_host, - port: params.mysql_port, - user: USER, - database: DATABASE - } - - // eslint-disable-next-line guard-for-in - for (const key in extras) { - conf[key] = extras[key] - } - - return conf -} - -tap.test('See if mysql is running', function (t) { - t.resolves(setup(require('mysql'))) - t.end() -}) - -tap.test('bad config', function (t) { - t.autoend() - - const agent = helper.instrumentMockedAgent() - const mysql = require('mysql') - const badConfig = { - connectionLimit: 10, - host: 'nohost', - user: USER, - database: DATABASE - } - - t.test(function (t) { - const poolCluster = mysql.createPoolCluster() - - poolCluster.add(badConfig) // anonymous group - poolCluster.getConnection(function (err) { - // umm... so this test is pretty hacky, but i want to make sure we don't - // wrap the callback multiple times. - - const stack = new Error().stack - const frames = stack.split('\n').slice(3, 8) - - t.not(frames[0], frames[1], 'do not multi-wrap') - t.not(frames[0], frames[2], 'do not multi-wrap') - t.not(frames[0], frames[3], 'do not multi-wrap') - t.not(frames[0], frames[4], 'do not multi-wrap') - - t.ok(err, 'should be an error') - poolCluster.end() - t.end() - }) - }) - - t.teardown(function () { - helper.unloadAgent(agent) - }) -}) - -// TODO: test variable argument calling -// TODO: test error conditions -// TODO: test .query without callback -// TODO: test notice errors -// TODO: test sql capture -tap.test('mysql built-in connection pools', function (t) { - let agent = null - let mysql = null - let pool = null - - t.beforeEach(async function () { - await setup(require('mysql')) - agent = helper.instrumentMockedAgent() - mysql = require('mysql') - pool = mysql.createPool(config) - }) - - t.afterEach(function () { - return new Promise((resolve) => { - helper.unloadAgent(agent) - pool.end(resolve) - - agent = null - mysql = null - pool = null - }) - }) - - // make sure a connection exists in the pool before any tests are run - // we want to make sure connections are allocated outside any transaction - // this is to avoid tests that 'happen' to work because of how CLS works - t.test('primer', function (t) { - pool.query('SELECT 1 + 1 AS solution', function (err) { - t.notOk(err, 'are you sure mysql is running?') - t.notOk(agent.getTransaction(), 'transaction should not exist') - t.end() - }) - }) - - t.test('ensure host and port are set on segment', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SELECT 1 + 1 AS solution', function (err) { - let seg = txn.trace.root.children[0].children[1] - // 2.16 introduced an extra segment - if (seg && seg.name === 'timers.setTimeout') { - seg = txn.trace.root.children[0].children[2] - } - const attributes = seg.getAttributes() - t.error(err, 'should not error') - t.ok(seg, 'should have a segment (' + (seg && seg.name) + ')') - t.equal( - attributes.host, - urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, - 'set host' - ) - t.equal(attributes.database_name, DATABASE, 'set database name') - t.equal(attributes.port_path_or_id, String(config.port), 'set port') - t.equal(attributes.product, 'MySQL', 'set product attribute') - txn.end() - t.end() - }) - }) - }) - - t.test('respects `datastore_tracer.instance_reporting`', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - agent.config.datastore_tracer.instance_reporting.enabled = false - pool.query('SELECT 1 + 1 AS solution', function (err) { - const seg = getDatastoreSegment(agent.tracer.getSegment()) - t.error(err, 'should not error making query') - t.ok(seg, 'should have a segment') - - const attributes = seg.getAttributes() - t.notOk(attributes.host, 'should have no host parameter') - t.notOk(attributes.port_path_or_id, 'should have no port parameter') - t.equal(attributes.database_name, DATABASE, 'should set database name') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - agent.config.datastore_tracer.instance_reporting.enabled = true - txn.end() - t.end() - }) - }) - }) - - t.test('respects `datastore_tracer.database_name_reporting`', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - agent.config.datastore_tracer.database_name_reporting.enabled = false - pool.query('SELECT 1 + 1 AS solution', function (err) { - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - t.notOk(err, 'no errors') - t.ok(seg, 'there is a segment') - t.equal( - attributes.host, - urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, - 'set host' - ) - t.equal(attributes.port_path_or_id, String(config.port), 'set port') - t.notOk(attributes.database_name, 'should have no database name parameter') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - agent.config.datastore_tracer.database_name_reporting.enabled = true - txn.end() - t.end() - }) - }) - }) - - t.test('ensure host is the default (localhost) when not supplied', function (t) { - const defaultConfig = getConfig({ - host: null - }) - const defaultPool = mysql.createPool(defaultConfig) - helper.runInTransaction(agent, function transactionInScope(txn) { - defaultPool.query('SELECT 1 + 1 AS solution', function (err) { - t.error(err, 'should not fail to execute query') - - // In the case where you don't have a server running on - // localhost the data will still be correctly associated - // with the query. - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - t.ok(seg, 'there is a segment') - t.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') - t.equal(attributes.database_name, DATABASE, 'set database name') - t.equal(attributes.port_path_or_id, String(defaultConfig.port), 'set port') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - txn.end() - defaultPool.end(t.end) - }) - }) - }) - - t.test('ensure port is the default (3306) when not supplied', function (t) { - const defaultConfig = getConfig({ - host: null - }) - const defaultPool = mysql.createPool(defaultConfig) - helper.runInTransaction(agent, function transactionInScope(txn) { - defaultPool.query('SELECT 1 + 1 AS solution', function (err) { - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - - t.error(err, 'should not error making query') - t.ok(seg, 'should have a segment') - t.equal( - attributes.host, - urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, - 'should set host' - ) - t.equal(attributes.database_name, DATABASE, 'should set database name') - t.equal(attributes.port_path_or_id, '3306', 'should set port') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - txn.end() - defaultPool.end(t.end) - }) - }) - }) - - t.test('query with error', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('BLARG', function (err) { - t.ok(err) - t.ok(agent.getTransaction(), 'transaction should exit') - txn.end() - t.end() - }) - }) - }) - - t.test('lack of callback does not explode', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SET SESSION auto_increment_increment=1') - txn.end() - t.end() - }) - }) - - t.test('pool.query', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SELECT 1 + 1 AS solution123123123123', function (err) { - const transaction = agent.getTransaction() - const segment = agent.tracer.getSegment().parent - - t.error(err, 'no error occurred') - t.ok(transaction, 'transaction should exist') - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'MySQL Pool#query', 'is named') - txn.end() - t.end() - }) - }) - }) - - t.test('pool.query with values', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SELECT ? + ? AS solution', [1, 1], function (err) { - const transaction = agent.getTransaction() - t.error(err) - t.ok(transaction, 'should not lose transaction') - if (transaction) { - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'MySQL Pool#query', 'is named') - } - - txn.end() - t.end() - }) - }) - }) - - t.test('pool.getConnection -> connection.query', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.getConnection(function shouldBeWrapped(err, connection) { - t.error(err, 'should not have error') - t.ok(agent.getTransaction(), 'transaction should exit') - t.teardown(function () { - connection.release() - }) - - connection.query('SELECT 1 + 1 AS solution', function (err) { - const transaction = agent.getTransaction() - const segment = agent.tracer.getSegment().parent - - t.error(err, 'no error occurred') - t.ok(transaction, 'transaction should exist') - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - txn.end() - t.end() - }) - }) - }) - }) - - t.test('pool.getConnection -> connection.query with values', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.getConnection(function shouldBeWrapped(err, connection) { - t.error(err, 'should not have error') - t.ok(agent.getTransaction(), 'transaction should exit') - t.teardown(function () { - connection.release() - }) - - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - const transaction = agent.getTransaction() - t.error(err) - t.ok(transaction, 'should not lose transaction') - if (transaction) { - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - } - - txn.end() - t.end() - }) - }) - }) - }) - - // The domain socket tests should only be run if there is a domain socket - // to connect to, which only happens if there is a MySQL instance running on - // the same box as these tests. - getDomainSocketPath(function (socketPath) { - const shouldTestDomain = socketPath - t.test( - 'ensure host and port are set on segment when using a domain socket', - { skip: !shouldTestDomain }, - function (t) { - const socketConfig = getConfig({ - socketPath: socketPath - }) - const socketPool = mysql.createPool(socketConfig) - helper.runInTransaction(agent, function transactionInScope(txn) { - socketPool.query('SELECT 1 + 1 AS solution', function (err) { - t.error(err, 'should not error making query') - - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - - // In the case where you don't have a server running on localhost - // the data will still be correctly associated with the query. - t.ok(seg, 'there is a segment') - t.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') - t.equal(attributes.port_path_or_id, socketPath, 'set path') - t.equal(attributes.database_name, DATABASE, 'set database name') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - txn.end() - socketPool.end(t.end) - }) - }) - } - ) - - t.end() - }) -}) - -tap.test('poolCluster', function (t) { - t.autoend() - - let agent = null - let mysql = null - let poolCluster = null - - t.beforeEach(async function () { - await setup(require('mysql')) - agent = helper.instrumentMockedAgent() - mysql = require('mysql') - poolCluster = mysql.createPoolCluster() - - poolCluster.add(config) // anonymous group - poolCluster.add('MASTER', config) - poolCluster.add('REPLICA', config) - }) - - t.afterEach(function () { - poolCluster.end() - helper.unloadAgent(agent) - - agent = null - mysql = null - poolCluster = null - }) - - t.test('primer', function (t) { - poolCluster.getConnection(function (err, connection) { - t.error(err, 'should not be an error') - t.notOk(agent.getTransaction(), 'transaction should not exist') - - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err) - t.notOk(agent.getTransaction(), 'transaction should not exist') - - connection.release() - t.end() - }) - }) - }) - - t.test('get any connection', function (t) { - helper.runInTransaction(agent, function (txn) { - poolCluster.getConnection(function (err, connection) { - t.error(err, 'should not have error') - t.ok(agent.getTransaction(), 'transaction should exist') - t.equal(agent.getTransaction(), txn, 'transaction must be original') - - txn.end() - connection.release() - t.end() - }) - }) - }) - - t.test('get any connection', function (t) { - poolCluster.getConnection(function (err, connection) { - t.error(err, 'should not have error') - - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get MASTER connection', function (t) { - helper.runInTransaction(agent, function (txn) { - poolCluster.getConnection('MASTER', function (err, connection) { - t.notOk(err) - t.ok(agent.getTransaction()) - t.equal(agent.getTransaction(), txn) - - txn.end() - connection.release() - t.end() - }) - }) - }) - - t.test('get MASTER connection', function (t) { - poolCluster.getConnection('MASTER', function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get glob', function (t) { - helper.runInTransaction(agent, function (txn) { - poolCluster.getConnection('REPLICA*', 'ORDER', function (err, connection) { - t.notOk(err) - t.ok(agent.getTransaction()) - t.equal(agent.getTransaction(), txn) - - txn.end() - connection.release() - t.end() - }) - }) - }) - - t.test('get glob', function (t) { - poolCluster.getConnection('REPLICA*', 'ORDER', function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get star', function (t) { - helper.runInTransaction(agent, function () { - poolCluster.of('*').getConnection(function (err, connection) { - t.notOk(err) - t.ok(agent.getTransaction(), 'transaction should exist') - - agent.getTransaction().end() - connection.release() - t.end() - }) - }) - }) - - t.test('get star', function (t) { - poolCluster.of('*').getConnection(function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get wildcard', function (t) { - helper.runInTransaction(agent, function () { - const pool = poolCluster.of('REPLICA*', 'RANDOM') - pool.getConnection(function (err, connection) { - t.error(err) - t.ok(agent.getTransaction(), 'should have transaction') - - agent.getTransaction().end() - connection.release() - t.end() - }) - }) - }) - - t.test('get wildcard', function (t) { - const pool = poolCluster.of('REPLICA*', 'RANDOM') - pool.getConnection(function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const currentTransaction = agent.getTransaction() - t.ok(currentTransaction, 'transaction should exist') - t.equal(currentTransaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - // not added until 2.12.0 - // https://github.com/mysqljs/mysql/blob/master/Changes.md#v2120-2016-11-02 - if (semver.satisfies(pkgVersion, '>=2.12.0')) { - t.test('poolCluster query', function (t) { - const masterPool = poolCluster.of('MASTER', 'RANDOM') - const replicaPool = poolCluster.of('REPLICA', 'RANDOM') - helper.runInTransaction(agent, function (txn) { - replicaPool.query('SELECT ? + ? AS solution', [1, 1], function (err) { - let transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction, txn, 'transaction must be same') - - let segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - t.error(err, 'no error occurred') - t.ok(transaction, 'transaction should exist') - t.equal(transaction, txn, 'transaction must be same') - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - masterPool.query('SELECT ? + ? AS solution', [1, 1], function (err) { - transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction, txn, 'transaction must be same') - - segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - t.error(err, 'no error occurred') - t.ok(transaction, 'transaction should exist') - t.equal(transaction, txn, 'transaction must be same') - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - t.end() - }) - }) - }) - }) - } -}) - -function getDomainSocketPath(callback) { - exec('mysql_config --socket', function (err, stdout, stderr) { - if (err || stderr.toString()) { - return callback(null) - } - - const sock = stdout.toString().trim() - fs.access(sock, function (err) { - callback(err ? null : sock) - }) - }) -} - -function getDatastoreSegment(segment) { - return segment.parent.children.filter(function (s) { - return /^Datastore/.test(s && s.name) - })[0] -} diff --git a/test/versioned/mysql/basic-pool.test.js b/test/versioned/mysql/basic-pool.test.js new file mode 100644 index 0000000000..d1da56ef6d --- /dev/null +++ b/test/versioned/mysql/basic-pool.test.js @@ -0,0 +1,12 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const basicPoolTests = require('./basic-pool') +const constants = require('./constants') +const { version: pkgVersion } = require('mysql/package') + +basicPoolTests({ factory: () => require('mysql'), constants, pkgVersion }) diff --git a/test/versioned/mysql/basic.js b/test/versioned/mysql/basic.js new file mode 100644 index 0000000000..5b0c11763c --- /dev/null +++ b/test/versioned/mysql/basic.js @@ -0,0 +1,403 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +process.env.NEW_RELIC_HOME = __dirname + +const test = require('node:test') +const assert = require('node:assert') +const logger = require('../../../lib/logger') +const helper = require('../../lib/agent_helper') +const urltils = require('../../../lib/util/urltils') +const params = require('../../lib/params') +const setup = require('./setup') +const { getClient } = require('./utils') + +module.exports = function ({ lib, factory, poolFactory, constants }) { + const { USER, DATABASE, TABLE } = constants + test('Basic run through mysql functionality', { timeout: 30 * 1000 }, async function (t) { + t.beforeEach(async function (ctx) { + const poolLogger = logger.child({ component: 'pool' }) + const agent = helper.instrumentMockedAgent() + const mysql = factory() + const genericPool = poolFactory() + const pool = setup.pool(USER, DATABASE, mysql, genericPool, poolLogger) + await setup(USER, DATABASE, TABLE, mysql) + ctx.nr = { + agent, + mysql, + pool + } + }) + + t.afterEach(function (ctx) { + const { agent, pool } = ctx.nr + return new Promise((resolve) => { + pool.drain(function () { + pool.destroyAllNow() + helper.unloadAgent(agent) + resolve() + }) + }) + }) + + await t.test('basic transaction', function testTransaction(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + client.query('SELECT 1', function (err) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + pool.release(client) + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const query of agent.queries.samples.values()) { + assert.ok(query.total > 0, 'the samples should have positive duration') + } + + const metrics = agent.metrics._metrics.unscoped + const hostPortMetric = Object.entries(metrics).find((entry) => + /Datastore\/instance\/MySQL\/[0-9a-zA-Z.-]+\/3306/.test(entry[0]) + ) + assert.ok(hostPortMetric, 'has host:port metric') + assert.equal(hostPortMetric[1].callCount, 1, 'host:port metric has been incremented') + + end() + }) + }) + }) + }) + + await t.test('query with values', function testCallbackOnly(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + client.query('SELECT 1', [], function (err) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + pool.release(client) + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const query of agent.queries.samples.values()) { + assert.ok(query.total > 0, 'the samples should have positive duration') + } + end() + }) + }) + }) + }) + + await t.test('query with options streaming should work', function testCallbackOnly(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + const query = client.query('SELECT 1', []) + let results = false + + query.on('result', function () { + results = true + }) + + query.on('error', function (err) { + assert.ok(!err) + }) + + query.on('end', function () { + assert.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + pool.release(client) + assert.ok(results, 'results should be received') + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const sample of agent.queries.samples.values()) { + assert.ok(sample.total > 0, 'the samples should have positive duration') + } + end() + }) + }) + }) + }) + + await t.test('ensure database name changes with a use statement', function (t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + getClient(pool, function (err, client) { + assert.ok(!err) + client.query('create database if not exists test_db;', function (err) { + assert.ok(!err, 'should not fail to create database') + + client.query('use test_db;', function (err) { + assert.ok(!err, 'should not fail to set database') + + client.query('SELECT 1 + 1 AS solution', function (err) { + const seg = agent.tracer.getSegment().parent + const attributes = seg.getAttributes() + + assert.ok(!err, 'no errors') + assert.ok(seg, 'there is a segment') + assert.equal( + attributes.host, + urltils.isLocalhost(params.mysql_host) + ? agent.config.getHostnameSafe() + : params.mysql_host, + 'set host' + ) + assert.equal(attributes.database_name, 'test_db', 'set database name') + assert.equal(attributes.port_path_or_id, '3306', 'set port') + assert.equal(attributes.product, 'MySQL', 'should set product attribute') + pool.release(client) + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const sample of agent.queries.samples.values()) { + assert.ok(sample.total > 0, 'the samples should have positive duration') + } + end() + }) + }) + }) + }) + }) + }) + + await t.test( + 'query via execute() should be instrumented', + { skip: lib === 'mysql' }, + function testTransaction(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + client.execute('SELECT 1', function (err) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + pool.release(client) + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const sample of agent.queries.samples.values()) { + assert.ok(sample.total > 0, 'the samples should have positive duration') + } + end() + }) + }) + }) + } + ) + + await t.test('streaming query should be timed correctly', function testCB(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + const query = client.query('SELECT SLEEP(1)', []) + const start = Date.now() + let duration = null + let results = false + let ended = false + + query.on('result', function () { + results = true + }) + + query.on('error', function (err) { + assert.ok(!err, 'streaming should not fail') + }) + + query.on('end', function () { + duration = Date.now() - start + ended = true + }) + + setTimeout(function actualEnd() { + const transaction = agent.getTransaction().end() + pool.release(client) + assert.ok(results && ended, 'result and end events should occur') + const traceRoot = transaction.trace.root + const traceRootDuration = traceRoot.timer.getDurationInMillis() + const segment = findSegment(traceRoot, 'Datastore/statement/MySQL/unknown/select') + const queryNodeDuration = segment.timer.getDurationInMillis() + + assert.ok( + Math.abs(duration - queryNodeDuration) < 50, + 'query duration should be roughly be the time between query and end' + ) + + assert.ok( + traceRootDuration - queryNodeDuration > 900, + 'query duration should be small compared to transaction duration' + ) + + end() + }, 2000) + }) + }) + }) + + await t.test('streaming query children should nest correctly', function testCB(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + const query = client.query('SELECT 1', []) + + query.on('result', function resultCallback() { + setTimeout(function resultTimeout() {}, 10) + }) + + query.on('error', function errorCallback(err) { + assert.ok(!err, 'streaming should not fail') + }) + + query.on('end', function endCallback() { + setTimeout(function actualEnd() { + const transaction = agent.getTransaction().end() + pool.release(client) + const traceRoot = transaction.trace.root + const querySegment = traceRoot.children[0] + assert.equal( + querySegment.children.length, + 2, + 'the query segment should have two children' + ) + + const childSegment = querySegment.children[1] + assert.equal( + childSegment.name, + 'Callback: endCallback', + 'children should be callbacks' + ) + const grandChildSegment = childSegment.children[0] + assert.equal( + grandChildSegment.name, + 'timers.setTimeout', + 'grand children should be timers' + ) + end() + }, 100) + }) + }) + }) + }) + + await t.test('query with options object rather than sql', function testCallbackOnly(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + client.query({ sql: 'SELECT 1' }, function (err) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + pool.release(client) + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const sample of agent.queries.samples.values()) { + assert.ok(sample.total > 0, 'the samples should have positive duration') + } + end() + }) + }) + }) + }) + + await t.test('query with options object and values', function testCallbackOnly(t, end) { + const { agent, pool } = t.nr + assert.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, function transactionInScope() { + assert.ok(agent.getTransaction(), 'we should be in a transaction') + + getClient(pool, function (err, client) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') + client.query({ sql: 'SELECT 1' }, [], function (err) { + assert.ok(!err) + assert.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + pool.release(client) + agent.getTransaction().end() + assert.ok(agent.queries.samples.size > 0, 'there should be a query sample') + for (const sample of agent.queries.samples.values()) { + assert.ok(sample.total > 0, 'the samples should have positive duration') + } + end() + }) + }) + }) + }) + + await t.test('ensure database name changes with a use statement', function (t, end) { + const { agent, pool } = t.nr + helper.runInTransaction(agent, function transactionInScope(txn) { + getClient(pool, function (err, client) { + assert.ok(!err) + client.query('create database if not exists test_db;', function (err) { + assert.ok(!err) + client.query('use test_db;', function (err) { + assert.ok(!err) + client.query('SELECT 1 + 1 AS solution', function (err) { + const seg = agent.tracer.getSegment().parent + const attributes = seg.getAttributes() + assert.ok(!err) + assert.ok(seg, 'should have a segment') + assert.equal( + attributes.host, + urltils.isLocalhost(params.mysql_host) + ? agent.config.getHostnameSafe() + : params.mysql_host, + 'should set host parameter' + ) + assert.equal(attributes.database_name, 'test_db', 'should use new database name') + assert.equal(attributes.port_path_or_id, '3306', 'should set port parameter') + client.query('drop test_db;', function () { + pool.release(client) + txn.end() + end() + }) + }) + }) + }) + }) + }) + }) + }) +} + +function findSegment(root, segmentName) { + for (let i = 0; i < root.children.length; i++) { + const segment = root.children[i] + if (segment.name === segmentName) { + return segment + } + } +} diff --git a/test/versioned/mysql/basic.tap.js b/test/versioned/mysql/basic.tap.js deleted file mode 100644 index b57dc9efdc..0000000000 --- a/test/versioned/mysql/basic.tap.js +++ /dev/null @@ -1,420 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -process.env.NEW_RELIC_HOME = __dirname - -const tap = require('tap') -const logger = require('../../../lib/logger') -const helper = require('../../lib/agent_helper') -const urltils = require('../../../lib/util/urltils') -const params = require('../../lib/params') -const setup = require('./setup') - -tap.test('Basic run through mysql functionality', { timeout: 30 * 1000 }, function (t) { - t.autoend() - - let agent = null - let mysql = null - const poolLogger = logger.child({ component: 'pool' }) - let pool = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - mysql = require('mysql') - pool = setup.pool(mysql, poolLogger) - - return setup(mysql) - }) - - t.afterEach(function () { - return new Promise((resolve) => { - pool.drain(function () { - pool.destroyAllNow() - helper.unloadAgent(agent) - resolve() - }) - }) - }) - - const withRetry = { - getClient: function (callback, counter) { - if (!counter) { - counter = 1 - } - counter++ - - pool.acquire(function (err, client) { - if (err) { - poolLogger.error('Failed to get connection from the pool: %s', err) - - if (counter < 10) { - pool.destroy(client) - this.getClient(callback, counter) - } else { - return callback(new Error("Couldn't connect to DB after 10 attempts.")) - } - } else { - callback(null, client) - } - }) - }, - - release: function (client) { - pool.release(client) - } - } - - t.test('basic transaction', function testTransaction(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query('SELECT 1', function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const query of agent.queries.samples.values()) { - t.ok(query.total > 0, 'the samples should have positive duration') - } - - const metrics = agent.metrics._metrics.unscoped - const hostPortMetric = Object.entries(metrics).find((entry) => - /Datastore\/instance\/MySQL\/[0-9a-zA-Z.-]+\/3306/.test(entry[0]) - ) - t.ok(hostPortMetric, 'has host:port metric') - t.equal(hostPortMetric[1].callCount, 1, 'host:port metric has been incremented') - - t.end() - }) - }) - }) - }) - - t.test('query with values', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query('SELECT 1', [], function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const query of agent.queries.samples.values()) { - t.ok(query.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('query with options streaming should work', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - const query = client.query('SELECT 1', []) - let results = false - - query.on('result', function () { - results = true - }) - - query.on('error', function (err) { - if (err) { - return t.fail(err) - } - }) - - query.on('end', function () { - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - t.ok(results, 'results should be received') - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('ensure database name changes with a use statement', function (t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - client.query('create database if not exists test_db;', function (err) { - t.error(err, 'should not fail to create database') - - client.query('use test_db;', function (err) { - t.error(err, 'should not fail to set database') - - client.query('SELECT 1 + 1 AS solution', function (err) { - const seg = agent.tracer.getSegment().parent - const attributes = seg.getAttributes() - - t.notOk(err, 'no errors') - t.ok(seg, 'there is a segment') - t.equal( - attributes.host, - urltils.isLocalhost(params.mysql_host) - ? agent.config.getHostnameSafe() - : params.mysql_host, - 'set host' - ) - t.equal(attributes.database_name, 'test_db', 'set database name') - t.equal(attributes.port_path_or_id, '3306', 'set port') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - }) - }) - - t.test('streaming query should be timed correctly', function testCB(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - const query = client.query('SELECT SLEEP(1)', []) - const start = Date.now() - let duration = null - let results = false - let ended = false - - query.on('result', function () { - results = true - }) - - query.on('error', function (err) { - if (err) { - return t.fail(err, 'streaming should not fail') - } - }) - - query.on('end', function () { - duration = Date.now() - start - ended = true - }) - - setTimeout(function actualEnd() { - const transaction = agent.getTransaction().end() - withRetry.release(client) - t.ok(results && ended, 'result and end events should occur') - const traceRoot = transaction.trace.root - const traceRootDuration = traceRoot.timer.getDurationInMillis() - const segment = findSegment(traceRoot, 'Datastore/statement/MySQL/unknown/select') - const queryNodeDuration = segment.timer.getDurationInMillis() - - t.ok( - Math.abs(duration - queryNodeDuration) < 50, - 'query duration should be roughly be the time between query and end' - ) - - t.ok( - traceRootDuration - queryNodeDuration > 900, - 'query duration should be small compared to transaction duration' - ) - - t.end() - }, 2000) - }) - }) - }) - - t.test('streaming query children should nest correctly', function testCB(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - const query = client.query('SELECT 1', []) - - query.on('result', function resultCallback() { - setTimeout(function resultTimeout() {}, 10) - }) - - query.on('error', function errorCallback(err) { - if (err) { - return t.fail(err, 'streaming should not fail') - } - }) - - query.on('end', function endCallback() { - setTimeout(function actualEnd() { - const transaction = agent.getTransaction().end() - withRetry.release(client) - const traceRoot = transaction.trace.root - const querySegment = traceRoot.children[0] - t.equal(querySegment.children.length, 2, 'the query segment should have two children') - - const childSegment = querySegment.children[1] - t.equal(childSegment.name, 'Callback: endCallback', 'children should be callbacks') - const grandChildSegment = childSegment.children[0] - t.equal(grandChildSegment.name, 'timers.setTimeout', 'grand children should be timers') - t.end() - }, 100) - }) - }) - }) - }) - - t.test('query with options object rather than sql', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query({ sql: 'SELECT 1' }, function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('query with options object and values', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query({ sql: 'SELECT 1' }, [], function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('ensure database name changes with a use statement', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - withRetry.getClient(function (err, client) { - client.query('create database if not exists test_db;', function (err) { - t.error(err) - client.query('use test_db;', function (err) { - t.error(err) - client.query('SELECT 1 + 1 AS solution', function (err) { - const seg = agent.tracer.getSegment().parent - const attributes = seg.getAttributes() - t.error(err) - if (t.ok(seg, 'should have a segment')) { - t.equal( - attributes.host, - urltils.isLocalhost(params.mysql_host) - ? agent.config.getHostnameSafe() - : params.mysql_host, - 'should set host parameter' - ) - t.equal(attributes.database_name, 'test_db', 'should use new database name') - t.equal(attributes.port_path_or_id, '3306', 'should set port parameter') - } - client.query('drop test_db;', function () { - withRetry.release(client) - txn.end() - t.end() - }) - }) - }) - }) - }) - }) - }) -}) - -function findSegment(root, segmentName) { - for (let i = 0; i < root.children.length; i++) { - const segment = root.children[i] - if (segment.name === segmentName) { - return segment - } - } -} diff --git a/test/versioned/mysql/basic.test.js b/test/versioned/mysql/basic.test.js new file mode 100644 index 0000000000..6c690d33d7 --- /dev/null +++ b/test/versioned/mysql/basic.test.js @@ -0,0 +1,14 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const basicTests = require('./basic') +const constants = require('./constants') +basicTests({ + lib: 'mysql', + factory: () => require('mysql'), + poolFactory: () => require('generic-pool'), + constants +}) diff --git a/test/versioned/mysql/constants.js b/test/versioned/mysql/constants.js new file mode 100644 index 0000000000..6c6f0eaf77 --- /dev/null +++ b/test/versioned/mysql/constants.js @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const USER = 'mysql_test_user' +const DATABASE = 'mysql_agent_integration' +const TABLE = 'test' + +module.exports = { + DATABASE, + TABLE, + USER +} diff --git a/test/versioned/mysql/package.json b/test/versioned/mysql/package.json index ba5de65852..261a3c8ec2 100644 --- a/test/versioned/mysql/package.json +++ b/test/versioned/mysql/package.json @@ -13,15 +13,11 @@ "generic-pool": "2.4" }, "files": [ - "basic-pool.tap.js", - "basic.tap.js", - "pooling.tap.js", - "transactions.tap.js" + "basic-pool.test.js", + "basic.test.js", + "pooling.test.js", + "transactions.test.js" ] } - ], - "dependencies": { - "generic-pool": "^2.4.6", - "mysql": "^2.16.0" - } + ] } diff --git a/test/versioned/mysql/pooling.js b/test/versioned/mysql/pooling.js new file mode 100644 index 0000000000..06e3ff8cbb --- /dev/null +++ b/test/versioned/mysql/pooling.js @@ -0,0 +1,74 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const { tspl } = require('@matteo.collina/tspl') +const logger = require('../../../lib/logger') +const helper = require('../../lib/agent_helper') +const setup = require('./setup') +const { lookup } = require('./utils') + +module.exports = function ({ factory, poolFactory, constants }) { + const { USER, DATABASE, TABLE } = constants + test('MySQL instrumentation with a connection pool', { timeout: 30000 }, async function (t) { + const plan = tspl(t, { plan: 13 }) + const poolLogger = logger.child({ component: 'pool' }) + const agent = helper.instrumentMockedAgent() + const mysql = factory() + const genericPool = poolFactory() + const pool = setup.pool(USER, DATABASE, mysql, genericPool, poolLogger) + + t.after(function () { + pool.drain(function () { + pool.destroyAllNow() + helper.unloadAgent(agent) + }) + }) + + await setup(USER, DATABASE, TABLE, mysql) + plan.ok(!agent.getTransaction(), 'no transaction should be in play yet') + await helper.runInTransaction(agent, async function transactionInScope() { + const params = { + id: 1 + } + lookup({ pool, params, database: DATABASE, table: TABLE }, function tester(error, row) { + plan.ok(!error) + // need to inspect on next tick, otherwise calling transaction.end() here + // in the callback (which is its own segment) would mark it as truncated + // (since it has not finished executing) + setImmediate(inspect, row) + }) + }) + + await plan.completed + + function inspect(row) { + const transaction = agent.getTransaction() + plan.ok(transaction, 'transaction should be visible') + plan.equal(row.id, 1, 'node-mysql should still work (found id)') + plan.equal(row.test_value, 'hamburgefontstiv', 'mysql driver should still work (found value)') + transaction.end() + const trace = transaction.trace + plan.ok(trace, 'trace should exist') + plan.ok(trace.root, 'root element should exist.') + plan.equal(trace.root.children.length, 1, 'There should be only one child.') + + const selectSegment = trace.root.children[0] + plan.ok(selectSegment, 'trace segment for first SELECT should exist') + + plan.equal( + selectSegment.name, + `Datastore/statement/MySQL/${DATABASE}.${TABLE}/select`, + 'should register as SELECT' + ) + + plan.equal(selectSegment.children.length, 1, 'should only have a callback segment') + plan.equal(selectSegment.children[0].name, 'Callback: ') + plan.equal(selectSegment.children[0].children.length, 0) + } + }) +} diff --git a/test/versioned/mysql/pooling.tap.js b/test/versioned/mysql/pooling.tap.js deleted file mode 100644 index da59aaabd6..0000000000 --- a/test/versioned/mysql/pooling.tap.js +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const logger = require('../../../lib/logger') -const helper = require('../../lib/agent_helper') -const setup = require('./setup') - -const { DATABASE, TABLE } = setup - -tap.test('MySQL instrumentation with a connection pool', { timeout: 30000 }, function (t) { - const poolLogger = logger.child({ component: 'pool' }) - const agent = helper.instrumentMockedAgent() - const mysql = require('mysql') - const pool = setup.pool(mysql, poolLogger) - - t.teardown(function () { - pool.drain(function () { - pool.destroyAllNow() - helper.unloadAgent(agent) - }) - }) - - const withRetry = { - getClient: function (callback, counter) { - if (!counter) { - counter = 1 - } - counter++ - - pool.acquire(function (err, client) { - if (err) { - poolLogger.error('Failed to get connection from the pool: %s', err) - - if (counter < 10) { - pool.destroy(client) - this.getClient(callback, counter) - } else { - return callback(new Error("Couldn't connect to DB after 10 attempts.")) - } - } else { - callback(null, client) - } - }) - }, - - release: function (client) { - pool.release(client) - } - } - - const dal = { - lookup: function (params, callback) { - if (!params.id) { - return callback(new Error('Must include ID to look up.')) - } - - withRetry.getClient((err, client) => { - if (err) { - return callback(err) - } - - const query = 'SELECT *' + ' FROM ' + DATABASE + '.' + TABLE + ' WHERE id = ?' - client.query(query, [params.id], function (err, results) { - withRetry.release(client) // always release back to the pool - - if (err) { - return callback(err) - } - - callback(null, results.length ? results[0] : results) - }) - }) - } - } - - setup(mysql).then(() => { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - const context = { - id: 1 - } - dal.lookup(context, function tester(error, row) { - if (error) { - t.fail(error) - return t.end() - } - - // need to inspect on next tick, otherwise calling transaction.end() here - // in the callback (which is its own segment) would mark it as truncated - // (since it has not finished executing) - setImmediate(inspect, row) - }) - }) - - function inspect(row) { - const transaction = agent.getTransaction() - if (!transaction) { - t.fail('transaction should be visible') - return t.end() - } - - t.equal(row.id, 1, 'node-mysql should still work (found id)') - t.equal(row.test_value, 'hamburgefontstiv', 'mysql driver should still work (found value)') - - transaction.end() - - const trace = transaction.trace - t.ok(trace, 'trace should exist') - t.ok(trace.root, 'root element should exist.') - t.equal(trace.root.children.length, 1, 'There should be only one child.') - - const selectSegment = trace.root.children[0] - t.ok(selectSegment, 'trace segment for first SELECT should exist') - - t.equal( - selectSegment.name, - `Datastore/statement/MySQL/${DATABASE}.${TABLE}/select`, - 'should register as SELECT' - ) - - t.equal(selectSegment.children.length, 1, 'should only have a callback segment') - t.equal(selectSegment.children[0].name, 'Callback: ') - - selectSegment.children[0].children - .map(function (segment) { - return segment.name - }) - .forEach(function (segmentName) { - if ( - segmentName !== 'timers.setTimeout' && - segmentName !== 'Truncated/timers.setTimeout' - ) { - t.fail('callback segment should have only timeout children') - } - }) - t.end() - } - }) -}) diff --git a/test/versioned/mysql/pooling.test.js b/test/versioned/mysql/pooling.test.js new file mode 100644 index 0000000000..e524940e2f --- /dev/null +++ b/test/versioned/mysql/pooling.test.js @@ -0,0 +1,14 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const poolingTests = require('./pooling') +const constants = require('./constants') + +poolingTests({ + factory: () => require('mysql'), + poolFactory: () => require('generic-pool'), + constants +}) diff --git a/test/versioned/mysql/setup.js b/test/versioned/mysql/setup.js index 44dd30e7ae..02f7377bd3 100644 --- a/test/versioned/mysql/setup.js +++ b/test/versioned/mysql/setup.js @@ -7,20 +7,10 @@ const params = require('../../lib/params') const setup = require('./helpers') - -const USER = 'mysql_test_user' -const DATABASE = 'mysql_agent_integration' -const TABLE = 'test' - -module.exports = exports = setup.bind(null, USER, DATABASE, TABLE) +module.exports = exports = setup exports.pool = setupPool -exports.USER = USER -exports.DATABASE = DATABASE -exports.TABLE = TABLE - -function setupPool(mysql, logger) { - const generic = require('generic-pool') +function setupPool(user, database, mysql, generic, logger) { return new generic.Pool({ name: 'mysql', min: 2, @@ -33,8 +23,8 @@ function setupPool(mysql, logger) { create: function (callback) { const client = mysql.createConnection({ - user: USER, - database: DATABASE, + user, + database, host: params.mysql_host, port: params.mysql_port }) diff --git a/test/versioned/mysql/transactions.js b/test/versioned/mysql/transactions.js new file mode 100644 index 0000000000..78c4ce4447 --- /dev/null +++ b/test/versioned/mysql/transactions.js @@ -0,0 +1,52 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const helper = require('../../lib/agent_helper') +const params = require('../../lib/params') +const setup = require('./setup') +const { tspl } = require('@matteo.collina/tspl') + +module.exports = function ({ factory, constants }) { + const { USER, DATABASE, TABLE } = constants + test('MySQL transactions', { timeout: 30000 }, async function (t) { + const plan = tspl(t, { plan: 6 }) + // set up the instrumentation before loading MySQL + const agent = helper.instrumentMockedAgent() + const mysql = factory() + + await setup(USER, DATABASE, TABLE, mysql) + const client = mysql.createConnection({ + user: USER, + database: DATABASE, + host: params.mysql_host, + port: params.mysql_port + }) + + t.after(function () { + helper.unloadAgent(agent) + client.end() + }) + + plan.ok(!agent.getTransaction(), 'no transaction should be in play yet') + helper.runInTransaction(agent, async function transactionInScope() { + plan.ok(agent.getTransaction(), 'we should be in a transaction') + client.beginTransaction(function (err) { + plan.ok(!err) + // trying the object mode of client.query + client.query({ sql: 'SELECT 1', timeout: 2000 }, function (err) { + plan.ok(!err) + client.commit(function (err) { + plan.ok(!err) + plan.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') + }) + }) + }) + }) + await plan.completed + }) +} diff --git a/test/versioned/mysql/transactions.tap.js b/test/versioned/mysql/transactions.tap.js deleted file mode 100644 index 77d6c5ddb1..0000000000 --- a/test/versioned/mysql/transactions.tap.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const params = require('../../lib/params') -const setup = require('./setup') - -const { USER, DATABASE } = setup - -tap.test('MySQL transactions', { timeout: 30000 }, function (t) { - t.plan(6) - - // set up the instrumentation before loading MySQL - const agent = helper.instrumentMockedAgent() - const mysql = require('mysql') - - setup(mysql).then(() => { - const client = mysql.createConnection({ - user: USER, - database: DATABASE, - host: params.mysql_host, - port: params.mysql_port - }) - - t.teardown(function () { - helper.unloadAgent(agent) - client.end() - }) - - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - client.beginTransaction(function (err) { - if (!t.error(err, 'should not error')) { - return t.end() - } - - // trying the object mode of client.query - client.query({ sql: 'SELECT 1', timeout: 2000 }, function (err) { - if (!t.error(err, 'should not error')) { - return t.end() - } - - client.commit(function (err) { - if (!t.error(err, 'should not error')) { - return t.end() - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - }) - }) - }) - }) - }) -}) diff --git a/test/versioned/mysql/transactions.test.js b/test/versioned/mysql/transactions.test.js new file mode 100644 index 0000000000..7144ab4021 --- /dev/null +++ b/test/versioned/mysql/transactions.test.js @@ -0,0 +1,10 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const transactionTests = require('./transactions') +const constants = require('./constants') + +transactionTests({ factory: () => require('mysql'), constants }) diff --git a/test/versioned/mysql/utils.js b/test/versioned/mysql/utils.js new file mode 100644 index 0000000000..dd2e2d1948 --- /dev/null +++ b/test/versioned/mysql/utils.js @@ -0,0 +1,50 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +function getClient(pool, callback, counter = 1) { + counter++ + + pool.acquire(function (err, client) { + if (err) { + if (counter < 10) { + pool.destroy(client) + getClient(pool, callback, counter) + } else { + return callback(new Error("Couldn't connect to DB after 10 attempts.")) + } + } else { + callback(null, client) + } + }) +} + +function lookup({ pool, params, database, table }, callback) { + if (!params.id) { + return callback(new Error('Must include ID to look up.')) + } + + getClient(pool, (err, client) => { + if (err) { + return callback(err) + } + + const query = 'SELECT *' + ' FROM ' + database + '.' + table + ' WHERE id = ?' + client.query(query, [params.id], function (err, results) { + pool.release(client) // always release back to the pool + + if (err) { + return callback(err) + } + + callback(null, results.length ? results[0] : results) + }) + }) +} + +module.exports = { + getClient, + lookup +} diff --git a/test/versioned/mysql2/basic-pool.tap.js b/test/versioned/mysql2/basic-pool.tap.js deleted file mode 100644 index a13c7c824d..0000000000 --- a/test/versioned/mysql2/basic-pool.tap.js +++ /dev/null @@ -1,626 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const fs = require('fs') -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const params = require('../../lib/params') -const urltils = require('../../../lib/util/urltils') -const exec = require('child_process').exec -const setup = require('./setup') - -const { USER, DATABASE } = setup - -const config = getConfig({}) -function getConfig(extras) { - const conf = { - connectionLimit: 10, - host: params.mysql_host, - port: params.mysql_port, - user: USER, - database: DATABASE - } - - // eslint-disable-next-line guard-for-in - for (const key in extras) { - conf[key] = extras[key] - } - - return conf -} - -tap.test('See if mysql is running', function (t) { - t.resolves(setup(require('mysql2'))) - t.end() -}) - -tap.test('bad config', function (t) { - t.autoend() - - const agent = helper.instrumentMockedAgent() - const mysql = require('mysql2') - const badConfig = { - connectionLimit: 10, - host: 'nohost', - user: USER, - database: DATABASE - } - - t.test(function (t) { - const poolCluster = mysql.createPoolCluster() - - poolCluster.add(badConfig) // anonymous group - poolCluster.getConnection(function (err) { - // umm... so this test is pretty hacky, but i want to make sure we don't - // wrap the callback multiple times. - - const stack = new Error().stack - const frames = stack.split('\n').slice(3, 8) - - t.not(frames[0], frames[1], 'do not multi-wrap') - t.not(frames[0], frames[2], 'do not multi-wrap') - t.not(frames[0], frames[3], 'do not multi-wrap') - t.not(frames[0], frames[4], 'do not multi-wrap') - - t.ok(err, 'should be an error') - poolCluster.end() - t.end() - }) - }) - - t.teardown(function () { - helper.unloadAgent(agent) - }) -}) - -// TODO: test variable argument calling -// TODO: test error conditions -// TODO: test .query without callback -// TODO: test notice errors -// TODO: test sql capture -tap.test('mysql2 built-in connection pools', function (t) { - t.autoend() - - let agent = null - let mysql = null - let pool = null - - t.beforeEach(async function () { - await setup(require('mysql2')) - agent = helper.instrumentMockedAgent() - mysql = require('mysql2') - pool = mysql.createPool(config) - }) - - t.afterEach(function () { - return new Promise((resolve) => { - helper.unloadAgent(agent) - pool.end(resolve) - - agent = null - mysql = null - pool = null - }) - }) - - // make sure a connection exists in the pool before any tests are run - // we want to make sure connections are allocated outside any transaction - // this is to avoid tests that 'happen' to work because of how CLS works - t.test('primer', function (t) { - pool.query('SELECT 1 + 1 AS solution', function (err) { - t.notOk(err, 'are you sure mysql is running?') - t.notOk(agent.getTransaction(), 'transaction should not exist') - t.end() - }) - }) - - t.test('ensure host and port are set on segment', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SELECT 1 + 1 AS solution', function (err) { - // depending on the minor version of mysql2, - // relevant segment is either first or second index - const seg = txn.trace.root.children[0].children.filter(function (trace) { - return /Datastore\/statement\/MySQL/.test(trace.name) - })[0] - const attributes = seg.getAttributes() - t.error(err, 'should not error') - t.ok(seg, 'should have a segment (' + (seg && seg.name) + ')') - t.equal( - attributes.host, - urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, - 'set host' - ) - t.equal(attributes.database_name, DATABASE, 'set database name') - t.equal(attributes.port_path_or_id, String(config.port), 'set port') - txn.end() - t.end() - }) - }) - }) - - t.test('respects `datastore_tracer.instance_reporting`', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - agent.config.datastore_tracer.instance_reporting.enabled = false - pool.query('SELECT 1 + 1 AS solution', function (err) { - const seg = getDatastoreSegment(agent.tracer.getSegment()) - t.error(err, 'should not error making query') - t.ok(seg, 'should have a segment') - - const attributes = seg.getAttributes() - t.notOk(attributes.host, 'should have no host parameter') - t.notOk(attributes.port_path_or_id, 'should have no port parameter') - t.equal(attributes.database_name, DATABASE, 'should set database name') - agent.config.datastore_tracer.instance_reporting.enabled = true - txn.end() - t.end() - }) - }) - }) - - t.test('respects `datastore_tracer.database_name_reporting`', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - agent.config.datastore_tracer.database_name_reporting.enabled = false - pool.query('SELECT 1 + 1 AS solution', function (err) { - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - t.notOk(err, 'no errors') - t.ok(seg, 'there is a segment') - t.equal( - attributes.host, - urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, - 'set host' - ) - t.equal(attributes.port_path_or_id, String(config.port), 'set port') - t.notOk(attributes.database_name, 'should have no database name parameter') - agent.config.datastore_tracer.database_name_reporting.enabled = true - txn.end() - t.end() - }) - }) - }) - - t.test('ensure host is the default (localhost) when not supplied', function (t) { - const defaultConfig = getConfig({ - host: null - }) - const defaultPool = mysql.createPool(defaultConfig) - helper.runInTransaction(agent, function transactionInScope(txn) { - defaultPool.query('SELECT 1 + 1 AS solution', function (err) { - t.error(err, 'should not fail to execute query') - - // In the case where you don't have a server running on - // localhost the data will still be correctly associated - // with the query. - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - t.ok(seg, 'there is a segment') - t.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') - t.equal(attributes.database_name, DATABASE, 'set database name') - t.equal(attributes.port_path_or_id, String(defaultConfig.port), 'set port') - txn.end() - defaultPool.end(t.end) - }) - }) - }) - - t.test('ensure port is the default (3306) when not supplied', function (t) { - const defaultConfig = getConfig({ - host: null - }) - const defaultPool = mysql.createPool(defaultConfig) - helper.runInTransaction(agent, function transactionInScope(txn) { - defaultPool.query('SELECT 1 + 1 AS solution', function (err) { - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - - t.error(err, 'should not error making query') - t.ok(seg, 'should have a segment') - t.equal( - attributes.host, - urltils.isLocalhost(config.host) ? agent.config.getHostnameSafe() : config.host, - 'should set host' - ) - t.equal(attributes.database_name, DATABASE, 'should set database name') - t.equal(attributes.port_path_or_id, '3306', 'should set port') - txn.end() - defaultPool.end(t.end) - }) - }) - }) - - t.test('query with error', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('BLARG', function (err) { - t.ok(err) - t.ok(agent.getTransaction(), 'transaction should exit') - txn.end() - t.end() - }) - }) - }) - - t.test('lack of callback does not explode', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SET SESSION auto_increment_increment=1') - setTimeout(function () { - // without the timeout, the pool is closed before the query is able to execute - txn.end() - t.end() - }, 500) - }) - }) - - t.test('pool.query', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SELECT 1 + 1 AS solution123123123123', function (err) { - const transaction = agent.getTransaction() - const segment = agent.tracer.getSegment().parent - - t.error(err, 'no error occurred') - t.ok(transaction, 'transaction should exist') - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'MySQL Pool#query', 'is named') - txn.end() - t.end() - }) - }) - }) - - t.test('pool.query with values', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.query('SELECT ? + ? AS solution', [1, 1], function (err) { - const transaction = agent.getTransaction() - t.error(err) - t.ok(transaction, 'should not lose transaction') - if (transaction) { - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'MySQL Pool#query', 'is named') - } - - txn.end() - t.end() - }) - }) - }) - - t.test('pool.getConnection -> connection.query', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.getConnection(function shouldBeWrapped(err, connection) { - t.error(err, 'should not have error') - t.ok(agent.getTransaction(), 'transaction should exit') - t.teardown(function () { - connection.release() - }) - - connection.query('SELECT 1 + 1 AS solution', function (err) { - const transaction = agent.getTransaction() - const segment = agent.tracer.getSegment().parent - - t.error(err, 'no error occurred') - t.ok(transaction, 'transaction should exist') - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - txn.end() - t.end() - }) - }) - }) - }) - - t.test('pool.getConnection -> connection.query with values', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - pool.getConnection(function shouldBeWrapped(err, connection) { - t.error(err, 'should not have error') - t.ok(agent.getTransaction(), 'transaction should exit') - t.teardown(function () { - connection.release() - }) - - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - const transaction = agent.getTransaction() - t.error(err) - t.ok(transaction, 'should not lose transaction') - if (transaction) { - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - } - - txn.end() - t.end() - }) - }) - }) - }) - - // The domain socket tests should only be run if there is a domain socket - // to connect to, which only happens if there is a MySQL instance running on - // the same box as these tests. - getDomainSocketPath(function (socketPath) { - const shouldTestDomain = socketPath - t.test( - 'ensure host and port are set on segment when using a domain socket', - { skip: !shouldTestDomain }, - function (t) { - const socketConfig = getConfig({ - socketPath: socketPath - }) - const socketPool = mysql.createPool(socketConfig) - helper.runInTransaction(agent, function transactionInScope(txn) { - socketPool.query('SELECT 1 + 1 AS solution', function (err) { - t.error(err, 'should not error making query') - - const seg = getDatastoreSegment(agent.tracer.getSegment()) - const attributes = seg.getAttributes() - - // In the case where you don't have a server running on localhost - // the data will still be correctly associated with the query. - t.ok(seg, 'there is a segment') - t.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') - t.equal(attributes.port_path_or_id, socketPath, 'set path') - t.equal(attributes.database_name, DATABASE, 'set database name') - txn.end() - socketPool.end(t.end) - }) - }) - } - ) - }) -}) - -tap.test('poolCluster', function (t) { - t.autoend() - - let agent = null - let mysql = null - let poolCluster = null - - t.beforeEach(async function () { - await setup(require('mysql2')) - agent = helper.instrumentMockedAgent() - mysql = require('mysql2') - poolCluster = mysql.createPoolCluster() - - poolCluster.add(config) // anonymous group - poolCluster.add('MASTER', config) - poolCluster.add('REPLICA', config) - }) - - t.afterEach(function () { - poolCluster.end() - helper.unloadAgent(agent) - - agent = null - mysql = null - poolCluster = null - }) - - t.test('primer', function (t) { - poolCluster.getConnection(function (err, connection) { - t.error(err, 'should not be an error') - t.notOk(agent.getTransaction(), 'transaction should not exist') - - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err) - t.notOk(agent.getTransaction(), 'transaction should not exist') - - connection.release() - t.end() - }) - }) - }) - - t.test('get any connection', function (t) { - helper.runInTransaction(agent, function (txn) { - poolCluster.getConnection(function (err, connection) { - t.error(err, 'should not have error') - t.ok(agent.getTransaction(), 'transaction should exist') - t.equal(agent.getTransaction(), txn, 'transaction must be original') - - txn.end() - connection.release() - t.end() - }) - }) - }) - - t.test('get any connection', function (t) { - poolCluster.getConnection(function (err, connection) { - t.error(err, 'should not have error') - - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get MASTER connection', function (t) { - helper.runInTransaction(agent, function (txn) { - poolCluster.getConnection('MASTER', function (err, connection) { - t.notOk(err) - t.ok(agent.getTransaction()) - t.equal(agent.getTransaction(), txn) - - txn.end() - connection.release() - t.end() - }) - }) - }) - - t.test('get MASTER connection', function (t) { - poolCluster.getConnection('MASTER', function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get glob', function (t) { - helper.runInTransaction(agent, function (txn) { - poolCluster.getConnection('REPLICA*', 'ORDER', function (err, connection) { - t.notOk(err) - t.ok(agent.getTransaction()) - t.equal(agent.getTransaction(), txn) - - txn.end() - connection.release() - t.end() - }) - }) - }) - - t.test('get glob', function (t) { - poolCluster.getConnection('REPLICA*', 'ORDER', function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get star', function (t) { - helper.runInTransaction(agent, function () { - poolCluster.of('*').getConnection(function (err, connection) { - t.notOk(err) - t.ok(agent.getTransaction(), 'transaction should exist') - - agent.getTransaction().end() - connection.release() - t.end() - }) - }) - }) - - t.test('get star', function (t) { - poolCluster.of('*').getConnection(function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const transaction = agent.getTransaction() - t.ok(transaction, 'transaction should exist') - t.equal(transaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) - - t.test('get wildcard', function (t) { - helper.runInTransaction(agent, function () { - const pool = poolCluster.of('REPLICA*', 'RANDOM') - pool.getConnection(function (err, connection) { - t.error(err) - t.ok(agent.getTransaction(), 'should have transaction') - - agent.getTransaction().end() - connection.release() - t.end() - }) - }) - }) - - t.test('get wildcard', function (t) { - const pool = poolCluster.of('REPLICA*', 'RANDOM') - pool.getConnection(function (err, connection) { - helper.runInTransaction(agent, function (txn) { - connection.query('SELECT ? + ? AS solution', [1, 1], function (err) { - t.error(err, 'no error occurred') - const currentTransaction = agent.getTransaction() - t.ok(currentTransaction, 'transaction should exist') - t.equal(currentTransaction.id, txn.id, 'transaction must be same') - const segment = agent.tracer.getSegment().parent - t.ok(segment, 'segment should exist') - t.ok(segment.timer.start > 0, 'starts at a positive time') - t.ok(segment.timer.start <= Date.now(), 'starts in past') - t.equal(segment.name, 'Datastore/statement/MySQL/unknown/select', 'is named') - - txn.end() - connection.release() - t.end() - }) - }) - }) - }) -}) - -function getDomainSocketPath(callback) { - exec('mysql_config --socket', function (err, stdout, stderr) { - if (err || stderr.toString()) { - return callback(null) - } - - const sock = stdout.toString().trim() - fs.access(sock, function (err) { - callback(err ? null : sock) - }) - }) -} - -function getDatastoreSegment(segment) { - return segment.parent.children.filter(function (s) { - return /^Datastore/.test(s && s.name) - })[0] -} diff --git a/test/versioned/mysql2/basic-pool.test.js b/test/versioned/mysql2/basic-pool.test.js new file mode 100644 index 0000000000..b453bc840b --- /dev/null +++ b/test/versioned/mysql2/basic-pool.test.js @@ -0,0 +1,21 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const basicPoolTests = require('../mysql/basic-pool') +const constants = require('./constants') +const fs = require('fs') +// exports are defined in newer versions so must read file directly +let pkgVersion +try { + ;({ version: pkgVersion } = require('mysql2/package')) +} catch { + ;({ version: pkgVersion } = JSON.parse( + fs.readFileSync(`${__dirname}/node_modules/mysql2/package.json`) + )) +} + +basicPoolTests({ factory: () => require('mysql2'), constants, pkgVersion }) diff --git a/test/versioned/mysql2/basic.tap.js b/test/versioned/mysql2/basic.tap.js deleted file mode 100644 index 26da21b8d4..0000000000 --- a/test/versioned/mysql2/basic.tap.js +++ /dev/null @@ -1,451 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -process.env.NEW_RELIC_HOME = __dirname - -const tap = require('tap') -const logger = require('../../../lib/logger') -const helper = require('../../lib/agent_helper') -const urltils = require('../../../lib/util/urltils') -const params = require('../../lib/params') -const setup = require('./setup') - -tap.test('Basic run through mysql functionality', { timeout: 30 * 1000 }, function (t) { - t.autoend() - - let agent = null - let mysql = null - const poolLogger = logger.child({ component: 'pool' }) - let pool = null - - t.beforeEach(function () { - agent = helper.instrumentMockedAgent() - mysql = require('mysql2') - pool = setup.pool(mysql, poolLogger) - - return setup(mysql) - }) - - t.afterEach(function () { - return new Promise((resolve) => { - pool.drain(function () { - pool.destroyAllNow() - helper.unloadAgent(agent) - resolve() - }) - }) - }) - - const withRetry = { - getClient: function (callback, counter) { - if (!counter) { - counter = 1 - } - counter++ - - pool.acquire(function (err, client) { - if (err) { - poolLogger.error('Failed to get connection from the pool: %s', err) - - if (counter < 10) { - pool.destroy(client) - this.getClient(callback, counter) - } else { - return callback(new Error("Couldn't connect to DB after 10 attempts.")) - } - } else { - callback(null, client) - } - }) - }, - - release: function (client) { - pool.release(client) - } - } - - t.test('basic transaction', function testTransaction(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query('SELECT 1', function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - - const metrics = agent.metrics._metrics.unscoped - const hostPortMetric = Object.entries(metrics).find((entry) => - /Datastore\/instance\/MySQL\/[0-9a-zA-Z.-]+\/3306/.test(entry[0]) - ) - t.ok(hostPortMetric, 'has host:port metric') - t.equal(hostPortMetric[1].callCount, 1, 'host:port metric has been incremented') - - t.end() - }) - }) - }) - }) - - t.test('query with values', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query('SELECT 1', [], function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('query with options streaming should work', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - const query = client.query('SELECT 1', []) - let results = false - - query.on('result', function () { - results = true - }) - - query.on('error', function (err) { - if (err) { - return t.fail(err) - } - }) - - query.on('end', function () { - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - t.ok(results, 'results should be received') - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('ensure database name changes with a use statement', function (t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - client.query('create database if not exists test_db;', function (err) { - t.error(err, 'should not fail to create database') - - client.query('use test_db;', function (err) { - t.error(err, 'should not fail to set database') - - client.query('SELECT 1 + 1 AS solution', function (err) { - const seg = agent.tracer.getSegment().parent - const attributes = seg.getAttributes() - - t.notOk(err, 'no errors') - t.ok(seg, 'there is a segment') - t.equal( - attributes.host, - urltils.isLocalhost(params.mysql_host) - ? agent.config.getHostnameSafe() - : params.mysql_host, - 'set host' - ) - t.equal(attributes.database_name, 'test_db', 'set database name') - t.equal(attributes.port_path_or_id, '3306', 'set port') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - }) - }) - - t.test('query via execute() should be instrumented', function testTransaction(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.execute('SELECT 1', function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('streaming query should be timed correctly', function testCB(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - const query = client.query('SELECT SLEEP(1)', []) - const start = Date.now() - let duration = null - let results = false - let ended = false - - query.on('result', function () { - results = true - }) - - query.on('error', function (err) { - if (err) { - return t.fail(err, 'streaming should not fail') - } - }) - - query.on('end', function () { - duration = Date.now() - start - ended = true - }) - - setTimeout(function actualEnd() { - const transaction = agent.getTransaction().end() - withRetry.release(client) - t.ok(results && ended, 'result and end events should occur') - const traceRoot = transaction.trace.root - const traceRootDuration = traceRoot.timer.getDurationInMillis() - const segment = findSegment(traceRoot, 'Datastore/statement/MySQL/unknown/select') - const queryNodeDuration = segment.timer.getDurationInMillis() - - t.ok( - Math.abs(duration - queryNodeDuration) < 50, - 'query duration should be roughly be the time between query and end' - ) - - t.ok( - traceRootDuration - queryNodeDuration > 900, - 'query duration should be small compared to transaction duration' - ) - - t.end() - }, 2000) - }) - }) - }) - - t.test('streaming query children should nest correctly', function testCB(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - const query = client.query('SELECT 1', []) - - query.on('result', function resultCallback() { - setTimeout(function resultTimeout() {}, 10) - }) - - query.on('error', function errorCallback(err) { - if (err) { - return t.fail(err, 'streaming should not fail') - } - }) - - query.on('end', function endCallback() { - setTimeout(function actualEnd() { - const transaction = agent.getTransaction().end() - withRetry.release(client) - const traceRoot = transaction.trace.root - const querySegment = traceRoot.children[0] - t.equal(querySegment.children.length, 2, 'the query segment should have two children') - - const childSegment = querySegment.children[1] - t.equal(childSegment.name, 'Callback: endCallback', 'children should be callbacks') - const grandChildSegment = childSegment.children[0] - t.equal(grandChildSegment.name, 'timers.setTimeout', 'grand children should be timers') - t.end() - }, 100) - }) - }) - }) - }) - - t.test('query with options object rather than sql', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query({ sql: 'SELECT 1' }, function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('query with options object and values', function testCallbackOnly(t) { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - - withRetry.getClient(function (err, client) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'generic-pool should not lose the transaction') - client.query({ sql: 'SELECT 1' }, [], function (err) { - if (err) { - return t.fail(err) - } - - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - withRetry.release(client) - agent.getTransaction().end() - t.ok(agent.queries.samples.size > 0, 'there should be a query sample') - for (const sample of agent.queries.samples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } - t.end() - }) - }) - }) - }) - - t.test('ensure database name changes with a use statement', function (t) { - helper.runInTransaction(agent, function transactionInScope(txn) { - withRetry.getClient(function (err, client) { - client.query('create database if not exists test_db;', function (err) { - t.error(err) - client.query('use test_db;', function (err) { - t.error(err) - client.query('SELECT 1 + 1 AS solution', function (err) { - const seg = agent.tracer.getSegment().parent - const attributes = seg.getAttributes() - t.error(err) - if (t.ok(seg, 'should have a segment')) { - t.equal( - attributes.host, - urltils.isLocalhost(params.mysql_host) - ? agent.config.getHostnameSafe() - : params.mysql_host, - 'should set host parameter' - ) - t.equal(attributes.database_name, 'test_db', 'should use new database name') - t.equal(attributes.port_path_or_id, '3306', 'should set port parameter') - t.equal(attributes.product, 'MySQL', 'should set product attribute') - } - client.query('drop test_db;', function () { - withRetry.release(client) - txn.end() - t.end() - }) - }) - }) - }) - }) - }) - }) -}) - -function findSegment(root, segmentName) { - for (let i = 0; i < root.children.length; i++) { - const segment = root.children[i] - if (segment.name === segmentName) { - return segment - } - } -} diff --git a/test/versioned/mysql2/basic.test.js b/test/versioned/mysql2/basic.test.js new file mode 100644 index 0000000000..18062732c0 --- /dev/null +++ b/test/versioned/mysql2/basic.test.js @@ -0,0 +1,15 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const basicTests = require('../mysql/basic') +const constants = require('./constants') +basicTests({ + lib: 'mysql2', + factory: () => require('mysql2'), + poolFactory: () => require('generic-pool'), + constants +}) diff --git a/test/versioned/mysql2/constants.js b/test/versioned/mysql2/constants.js new file mode 100644 index 0000000000..d9bf3e3ae4 --- /dev/null +++ b/test/versioned/mysql2/constants.js @@ -0,0 +1,15 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const USER = 'mysql2_test_user' +const DATABASE = 'mysql2_agent_integration' +const TABLE = 'test' + +module.exports = { + DATABASE, + TABLE, + USER +} diff --git a/test/versioned/mysql2/package.json b/test/versioned/mysql2/package.json index 9522e4e704..e06133ce20 100644 --- a/test/versioned/mysql2/package.json +++ b/test/versioned/mysql2/package.json @@ -13,12 +13,15 @@ "generic-pool": ">=2.4 <2.5 || latest" }, "files": [ - "basic-pool.tap.js", - "basic.tap.js", - "pooling.tap.js", - "promises.tap.js", - "transaction.tap.js" + "basic-pool.test.js", + "basic.test.js", + "pooling.test.js", + "promises.test.js", + "transaction.test.js" ] } - ] + ], + "engines": { + "node": ">=18" + } } diff --git a/test/versioned/mysql2/pooling.tap.js b/test/versioned/mysql2/pooling.tap.js deleted file mode 100644 index 5966ecb9f8..0000000000 --- a/test/versioned/mysql2/pooling.tap.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const logger = require('../../../lib/logger') -const helper = require('../../lib/agent_helper') -const setup = require('./setup') - -const { DATABASE, TABLE } = setup - -tap.test('MySQL2 instrumentation with a connection pool', { timeout: 60000 }, function (t) { - // set up the instrumentation before loading MySQL - const poolLogger = logger.child({ component: 'pool' }) - const agent = helper.instrumentMockedAgent() - const mysql = require('mysql2') - const pool = setup.pool(mysql, poolLogger) - - t.teardown(function () { - pool.drain(function () { - pool.destroyAllNow() - helper.unloadAgent(agent) - }) - }) - - const withRetry = { - getClient: function (callback, counter) { - if (!counter) { - counter = 1 - } - counter++ - - pool.acquire(function (err, client) { - if (err) { - poolLogger.error('Failed to get connection from the pool: %s', err) - - if (counter < 10) { - pool.destroy(client) - this.getClient(callback, counter) - } else { - return callback(new Error("Couldn't connect to DB after 10 attempts.")) - } - } else { - callback(null, client) - } - }) - }, - - release: function (client) { - pool.release(client) - } - } - - const dal = { - lookup: function (params, callback) { - if (!params.id) { - return callback(new Error('Must include ID to look up.')) - } - - withRetry.getClient((err, client) => { - if (err) { - return callback(err) - } - - const query = 'SELECT *' + ' FROM ' + DATABASE + '.' + TABLE + ' WHERE id = ?' - client.query(query, [params.id], function (err, results) { - withRetry.release(client) // always release back to the pool - - if (err) { - return callback(err) - } - - callback(null, results.length ? results[0] : results) - }) - }) - } - } - - setup(mysql).then(() => { - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - dal.lookup({ id: 1 }, function (error, row) { - if (error) { - t.fail(error) - } - - // need to inspect on next tick, otherwise calling transaction.end() here - // in the callback (which is its own segment) would mark it as truncated - // (since it has not finished executing) - setImmediate(inspect, row) - }) - }) - - function inspect(row) { - const transaction = agent.getTransaction() - if (!transaction) { - t.fail('transaction should be visible') - return t.end() - } - - t.equal(row.id, 1, 'mysql2 should still work (found id)') - t.equal(row.test_value, 'hamburgefontstiv', 'mysql driver should still work (found value)') - - transaction.end() - - const trace = transaction.trace - t.ok(trace, 'trace should exist') - t.ok(trace.root, 'root element should exist.') - t.equal(trace.root.children.length, 1, 'There should be only one child.') - - const selectSegment = trace.root.children[0] - t.ok(selectSegment, 'trace segment for first SELECT should exist') - t.equal( - selectSegment.name, - `Datastore/statement/MySQL/${DATABASE}.${TABLE}/select`, - 'should register as SELECT' - ) - - t.equal(selectSegment.children.length, 1, 'should only have a callback segment') - t.equal(selectSegment.children[0].name, 'Callback: ') - - selectSegment.children[0].children - .map(function (segment) { - return segment.name - }) - .forEach(function (segmentName) { - if ( - segmentName !== 'timers.setTimeout' && - segmentName !== 'Truncated/timers.setTimeout' - ) { - t.fail('callback segment should have only timeout children') - } - }) - t.end() - } - }) -}) diff --git a/test/versioned/mysql2/pooling.test.js b/test/versioned/mysql2/pooling.test.js new file mode 100644 index 0000000000..d5276e33d6 --- /dev/null +++ b/test/versioned/mysql2/pooling.test.js @@ -0,0 +1,14 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const poolingTests = require('../mysql/pooling') +const constants = require('./constants') + +poolingTests({ + factory: () => require('mysql2'), + poolFactory: () => require('generic-pool'), + constants +}) diff --git a/test/versioned/mysql2/promises.tap.js b/test/versioned/mysql2/promises.tap.js deleted file mode 100644 index adf9d340d7..0000000000 --- a/test/versioned/mysql2/promises.tap.js +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const setup = require('./setup') -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const params = require('../../lib/params') -const urltils = require('../../../lib/util/urltils') - -const { USER, DATABASE } = setup - -tap.test('mysql2 promises', { timeout: 30000 }, (t) => { - t.autoend() - - let mysql = null - let client = null - let agent = null - - t.beforeEach(async () => { - await setup(require('mysql2')) - agent = helper.instrumentMockedAgent() - - mysql = require('mysql2/promise') - - client = await mysql.createConnection({ - user: USER, - database: DATABASE, - host: params.mysql_host, - port: params.mysql_port - }) - }) - - t.afterEach(async () => { - helper.unloadAgent(agent) - if (client) { - await client.end() - client = null - } - }) - - t.test('basic transaction', (t) => { - return helper - .runInTransaction(agent, (tx) => { - return client.query('SELECT 1').then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - tx.end() - }) - }) - .then(() => checkQueries(t, agent)) - }) - - t.test('query with values', (t) => { - return helper - .runInTransaction(agent, (tx) => { - return client.query('SELECT 1', []).then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - tx.end() - }) - }) - .then(() => checkQueries(t, agent)) - }) - - t.test('database name should change with use statement', (t) => { - return helper - .runInTransaction(agent, (tx) => { - return client - .query('create database if not exists test_db') - .then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - return client.query('use test_db') - }) - .then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - return client.query('SELECT 1 + 1 AS solution') - }) - .then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - - const segment = agent.getTransaction().trace.root.children[2] - const attributes = segment.getAttributes() - t.equal( - attributes.host, - urltils.isLocalhost(params.mysql_host) - ? agent.config.getHostnameSafe() - : params.mysql_host, - 'should set host name' - ) - t.equal(attributes.database_name, 'test_db', 'should follow use statement') - t.equal(attributes.port_path_or_id, '3306', 'should set port') - - tx.end() - }) - }) - .then(() => checkQueries(t, agent)) - }) - - t.test('query with options object rather than sql', (t) => { - return helper - .runInTransaction(agent, (tx) => { - return client.query({ sql: 'SELECT 1' }).then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - tx.end() - }) - }) - .then(() => checkQueries(t, agent)) - }) - - t.test('query with options object and values', (t) => { - return helper - .runInTransaction(agent, (tx) => { - return client.query({ sql: 'SELECT 1' }, []).then(() => { - const activeTx = agent.getTransaction() - t.equal(tx.name, activeTx.name) - tx.end() - }) - }) - .then(() => checkQueries(t, agent)) - }) -}) - -function checkQueries(t, agent) { - const querySamples = agent.queries.samples - t.ok(querySamples.size > 0, 'there should be a query sample') - for (const sample of querySamples.values()) { - t.ok(sample.total > 0, 'the samples should have positive duration') - } -} diff --git a/test/versioned/mysql2/promises.test.js b/test/versioned/mysql2/promises.test.js new file mode 100644 index 0000000000..b55e743e2f --- /dev/null +++ b/test/versioned/mysql2/promises.test.js @@ -0,0 +1,119 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const setup = require('../mysql/setup') +const test = require('node:test') +const assert = require('node:assert') +const helper = require('../../lib/agent_helper') +const params = require('../../lib/params') +const urltils = require('../../../lib/util/urltils') +const { DATABASE, USER, TABLE } = require('./constants') + +test('mysql2 promises', { timeout: 30000 }, async (t) => { + t.beforeEach(async (ctx) => { + await setup(USER, DATABASE, TABLE, require('mysql2')) + const agent = helper.instrumentMockedAgent() + + const mysql = require('mysql2/promise') + + const client = await mysql.createConnection({ + user: USER, + database: DATABASE, + host: params.mysql_host, + port: params.mysql_port + }) + ctx.nr = { + agent, + client + } + }) + + t.afterEach(async (ctx) => { + const { agent, client } = ctx.nr + helper.unloadAgent(agent) + await client.end() + }) + + await t.test('basic transaction', async (t) => { + const { agent, client } = t.nr + await helper.runInTransaction(agent, async (tx) => { + await client.query('SELECT 1') + const activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + tx.end() + }) + checkQueries(agent) + }) + + await t.test('query with values', async (t) => { + const { agent, client } = t.nr + await helper.runInTransaction(agent, async (tx) => { + await client.query('SELECT 1', []) + const activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + tx.end() + }) + checkQueries(agent) + }) + + await t.test('database name should change with use statement', async (t) => { + const { agent, client } = t.nr + await helper.runInTransaction(agent, async (tx) => { + await client.query('create database if not exists test_db') + let activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + await client.query('use test_db') + activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + await client.query('SELECT 1 + 1 AS solution') + activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + + const segment = agent.getTransaction().trace.root.children[2] + const attributes = segment.getAttributes() + assert.equal( + attributes.host, + urltils.isLocalhost(params.mysql_host) ? agent.config.getHostnameSafe() : params.mysql_host, + 'should set host name' + ) + assert.equal(attributes.database_name, 'test_db', 'should follow use statement') + assert.equal(attributes.port_path_or_id, '3306', 'should set port') + tx.end() + }) + checkQueries(agent) + }) + + await t.test('query with options object rather than sql', async (t) => { + const { agent, client } = t.nr + await helper.runInTransaction(agent, async (tx) => { + await client.query({ sql: 'SELECT 1' }) + const activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + tx.end() + }) + checkQueries(agent) + }) + + await t.test('query with options object and values', async (t) => { + const { agent, client } = t.nr + await helper.runInTransaction(agent, async (tx) => { + await client.query({ sql: 'SELECT 1' }, []) + const activeTx = agent.getTransaction() + assert.equal(tx.name, activeTx.name) + tx.end() + }) + checkQueries(agent) + }) +}) + +function checkQueries(agent) { + const querySamples = agent.queries.samples + assert.ok(querySamples.size > 0, 'there should be a query sample') + for (const sample of querySamples.values()) { + assert.ok(sample.total > 0, 'the samples should have positive duration') + } +} diff --git a/test/versioned/mysql2/setup.js b/test/versioned/mysql2/setup.js deleted file mode 100644 index 07f41c9102..0000000000 --- a/test/versioned/mysql2/setup.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const params = require('../../lib/params') -const setup = require('../mysql/helpers') -const USER = 'mysql2_test_user' -const DATABASE = 'mysql2_agent_integration' -const TABLE = 'test' - -module.exports = exports = setup.bind(null, USER, DATABASE, TABLE) -exports.pool = setupPool -exports.USER = USER -exports.DATABASE = DATABASE -exports.TABLE = TABLE - -function setupPool(mysql, logger) { - const generic = require('generic-pool') - - const pool = new generic.Pool({ - name: 'mysql2', - min: 2, - max: 6, - idleTimeoutMillis: 250, - - log: function (message) { - logger.info(message) - }, - - create: function (callback) { - const client = mysql.createConnection({ - user: USER, - database: DATABASE, - host: params.mysql_host, - port: params.mysql_port - }) - - client.on('error', function (err) { - logger.error('MySQL connection errored out, destroying connection') - logger.error(err) - pool.destroy(client) - }) - - client.connect((err) => { - if (err) { - logger.error('MySQL client failed to connect. Does `agent_integration` exist?') - } - - callback(err, client) - }) - }, - - destroy: function (client) { - logger.info('Destroying MySQL connection') - client.end() - } - }) - - return pool -} diff --git a/test/versioned/mysql2/transaction.tap.js b/test/versioned/mysql2/transaction.tap.js deleted file mode 100644 index d91ab597ff..0000000000 --- a/test/versioned/mysql2/transaction.tap.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const params = require('../../lib/params') -const setup = require('./setup') - -const { USER, DATABASE } = setup - -tap.test('MySQL transactions', { timeout: 30000 }, function (t) { - t.plan(3) - - // set up the instrumentation before loading MySQL - const agent = helper.instrumentMockedAgent() - const mysql = require('mysql2') - - setup(mysql).then(() => { - const client = mysql.createConnection({ - user: USER, - database: DATABASE, - host: params.mysql_host, - port: params.mysql_port - }) - - t.teardown(function () { - helper.unloadAgent(agent) - client.end() - }) - - t.notOk(agent.getTransaction(), 'no transaction should be in play yet') - helper.runInTransaction(agent, function transactionInScope() { - t.ok(agent.getTransaction(), 'we should be in a transaction') - client.beginTransaction(function (err) { - if (err) { - return t.fail(err) - } - // trying the object mode of client.query - client.query({ sql: 'SELECT 1' }, function (err) { - if (err) { - return t.fail(err) - } - client.commit(function (err) { - if (err) { - return t.fail(err) - } - t.ok(agent.getTransaction(), 'MySQL query should not lose the transaction') - }) - }) - }) - }) - }) -}) diff --git a/test/versioned/mysql2/transaction.test.js b/test/versioned/mysql2/transaction.test.js new file mode 100644 index 0000000000..680d2de660 --- /dev/null +++ b/test/versioned/mysql2/transaction.test.js @@ -0,0 +1,10 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const transactionTests = require('../mysql/transactions') +const constants = require('./constants') + +transactionTests({ factory: () => require('mysql2'), constants })