diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b5e048dfcd..be225d80a0 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -17,6 +17,8 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic * [@grpc/grpc-js](#grpcgrpc-js) * [@grpc/proto-loader](#grpcproto-loader) * [@newrelic/security-agent](#newrelicsecurity-agent) +* [@opentelemetry/api](#opentelemetryapi) +* [@opentelemetry/semantic-conventions](#opentelemetrysemantic-conventions) * [@tyriar/fibonacci-heap](#tyriarfibonacci-heap) * [concat-stream](#concat-stream) * [https-proxy-agent](#https-proxy-agent) @@ -39,6 +41,7 @@ code, the source code can be found at [https://github.com/newrelic/node-newrelic * [@newrelic/newrelic-oss-cli](#newrelicnewrelic-oss-cli) * [@newrelic/test-utilities](#newrelictest-utilities) * [@octokit/rest](#octokitrest) +* [@opentelemetry/sdk-trace-base](#opentelemetrysdk-trace-base) * [@slack/bolt](#slackbolt) * [@smithy/eventstream-codec](#smithyeventstream-codec) * [@smithy/util-utf8](#smithyutil-utf8) @@ -549,6 +552,424 @@ This license terminates when the Software stops being provided by New Relic or w 3. The Software may contain third-party software, including open source software (“OSS”). The third party software is governed by its own license and subject to its own terms, disclosed in the provided licensing information associated with the third party components. If a source code disclosure is required under the terms of the license covering any OSS component, the source code can be requested of New Relic by emailing opensource@newrelic.com. +``` + +### @opentelemetry/api + +This product includes source derived from [@opentelemetry/api](https://github.com/open-telemetry/opentelemetry-js) ([v1.9.0](https://github.com/open-telemetry/opentelemetry-js/tree/v1.9.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v1.9.0/LICENSE): + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +``` + +### @opentelemetry/semantic-conventions + +This product includes source derived from [@opentelemetry/semantic-conventions](https://github.com/open-telemetry/opentelemetry-js) ([v1.27.0](https://github.com/open-telemetry/opentelemetry-js/tree/v1.27.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v1.27.0/LICENSE): + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + ``` ### @tyriar/fibonacci-heap @@ -1016,7 +1437,7 @@ SOFTWARE. ### semver -This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.6.3](https://github.com/npm/node-semver/tree/v7.6.3)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.6.3/LICENSE): +This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.6.2](https://github.com/npm/node-semver/tree/v7.6.2)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.6.2/LICENSE): ``` The ISC License @@ -2202,6 +2623,215 @@ THE SOFTWARE. ``` +### @opentelemetry/sdk-trace-base + +This product includes source derived from [@opentelemetry/sdk-trace-base](https://github.com/open-telemetry/opentelemetry-js) ([v1.27.0](https://github.com/open-telemetry/opentelemetry-js/tree/v1.27.0)), distributed under the [Apache-2.0 License](https://github.com/open-telemetry/opentelemetry-js/blob/v1.27.0/LICENSE): + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +``` + ### @slack/bolt This product includes source derived from [@slack/bolt](https://github.com/slackapi/bolt) ([v3.22.0](https://github.com/slackapi/bolt/tree/v3.22.0)), distributed under the [MIT License](https://github.com/slackapi/bolt/blob/v3.22.0/LICENSE): diff --git a/api.js b/api.js index b0c98607ee..619dcc4991 100644 --- a/api.js +++ b/api.js @@ -961,19 +961,21 @@ API.prototype.startWebTransaction = function startWebTransaction(url, handle) { const shim = this.shim const tracer = this.agent.tracer - const parent = tracer.getTransaction() + const parentTx = tracer.getTransaction() assignCLMSymbol(shim, handle) return tracer.transactionNestProxy('web', function startWebSegment() { - const tx = tracer.getTransaction() + const context = tracer.getContext() + const tx = context?.transaction + const parent = context?.segment if (!tx) { return handle.apply(this, arguments) } - if (tx === parent) { + if (tx === parentTx) { logger.debug('not creating nested transaction %s using transaction %s', url, tx.id) - return tracer.addSegment(url, null, null, true, handle) + return tracer.addSegment(url, null, parent, true, handle) } logger.debug( @@ -985,10 +987,16 @@ API.prototype.startWebTransaction = function startWebTransaction(url, handle) { tx.nameState.setName(NAMES.CUSTOM, null, NAMES.ACTION_DELIMITER, url) tx.url = url tx.applyUserNamingRules(tx.url) - tx.baseSegment = tracer.createSegment(url, recordWeb) + tx.baseSegment = tracer.createSegment({ + name: url, + recorder: recordWeb, + transaction: tx, + parent + }) + const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment }) tx.baseSegment.start() - const boundHandle = tracer.bindFunction(handle, tx.baseSegment) + const boundHandle = tracer.bindFunction(handle, newContext) maybeAddCLMAttributes(handle, tx.baseSegment) let returnResult = boundHandle.call(this) if (returnResult && shim.isPromise(returnResult)) { @@ -1061,19 +1069,21 @@ function startBackgroundTransaction(name, group, handle) { const tracer = this.agent.tracer const shim = this.shim const txName = group + '/' + name - const parent = tracer.getTransaction() + const parentTx = tracer.getTransaction() assignCLMSymbol(shim, handle) return tracer.transactionNestProxy('bg', function startBackgroundSegment() { - const tx = tracer.getTransaction() + const context = tracer.getContext() + const tx = context?.transaction + const parent = context?.segment if (!tx) { return handle.apply(this, arguments) } - if (tx === parent) { + if (tx === parentTx) { logger.debug('not creating nested transaction %s using transaction %s', txName, tx.id) - return tracer.addSegment(txName, null, null, true, handle) + return tracer.addSegment(txName, null, parent, true, handle) } logger.debug( @@ -1085,11 +1095,17 @@ function startBackgroundTransaction(name, group, handle) { ) tx._partialName = txName - tx.baseSegment = tracer.createSegment(name, recordBackground) + tx.baseSegment = tracer.createSegment({ + name, + recorder: recordBackground, + transaction: tx, + parent + }) + const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment }) tx.baseSegment.partialName = group tx.baseSegment.start() - const boundHandle = tracer.bindFunction(handle, tx.baseSegment) + const boundHandle = tracer.bindFunction(handle, newContext) maybeAddCLMAttributes(handle, tx.baseSegment) let returnResult = boundHandle.call(this) if (returnResult && shim.isPromise(returnResult)) { @@ -1525,12 +1541,13 @@ API.prototype.getTraceMetadata = function getTraceMetadata() { const metadata = {} const segment = this.agent.tracer.getSegment() - if (!segment) { + const transaction = this.agent.tracer.getTransaction() + if (!(segment || transaction)) { logger.debug('No transaction found when calling API#getTraceMetadata') } else if (!this.agent.config.distributed_tracing.enabled) { logger.debug('Distributed tracing disabled when calling API#getTraceMetadata') } else { - metadata.traceId = segment.transaction.traceId + metadata.traceId = transaction.traceId const spanId = segment.getSpanId() if (spanId) { diff --git a/lib/agent.js b/lib/agent.js index f634a28064..8e80354496 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -29,7 +29,6 @@ const TxSegmentNormalizer = require('./metrics/normalizer/tx_segment') const uninstrumented = require('./uninstrumented') const util = require('util') const createSpanEventAggregator = require('./spans/create-span-event-aggregator') -const createContextManager = require('./context-manager/create-context-manager') const { maybeAddDatabaseAttributes, maybeAddExternalAttributes, @@ -258,9 +257,8 @@ function Agent(config) { this.errors = new ErrorCollector(config, errorTraceAggregator, errorEventAggregator, this.metrics) - this._contextManager = createContextManager(this.config) // Transaction tracing. - this.tracer = new Tracer(this, this._contextManager) + this.tracer = new Tracer(this) this.traces = new TransactionTraceAggregator( { periodMs: DEFAULT_HARVEST_INTERVAL_MS, @@ -294,13 +292,7 @@ function Agent(config) { // Set up all the configuration events the agent needs to listen for. this._listenForConfigChanges() - // Entity tracking metrics. - this.totalActiveSegments = 0 - this.segmentsCreatedInHarvest = 0 - this.segmentsClearedInHarvest = 0 - // Used by shutdown code as well as entity tracking stats - this.activeTransactions = 0 - + this.initCounters() this.llm = {} // Finally, add listeners for the agent's own events. @@ -599,11 +591,33 @@ Agent.prototype._generateEntityStatsAndClear = function _generateHarvestMetrics( ) } - // Reset the counters. + this.resetCounters() +} + +Agent.prototype.initCounters = function initCounters() { + // Entity tracking metrics. + this.totalActiveSegments = 0 this.segmentsCreatedInHarvest = 0 this.segmentsClearedInHarvest = 0 + // Used by shutdown code as well as entity tracking stats + this.activeTransactions = 0 } +Agent.prototype.incrementCounters = function incrementCounters() { + ++this.totalActiveSegments + ++this.segmentsCreatedInHarvest +} + +Agent.prototype.decrementCounters = function decrementCounters(transaction) { + --this.activeTransactions + this.totalActiveSegments -= transaction.numSegments + this.segmentsClearedInHarvest += transaction.numSegments +} + +Agent.prototype.resetCounters = function resetCounters() { + this.segmentsCreatedInHarvest = 0 + this.segmentsClearedInHarvest = 0 +} /** * Public interface for passing configuration data from the collector * on to the configuration, in an effort to keep them at least somewhat @@ -755,9 +769,7 @@ Agent.prototype._transactionFinished = function _transactionFinished(transaction logger.debug('Ignoring %s (%s).', transaction.name, transaction.id) } - --this.activeTransactions - this.totalActiveSegments -= transaction.numSegments - this.segmentsClearedInHarvest += transaction.numSegments + this.decrementCounters(transaction) } Agent.prototype.setLambdaArn = function setLambdaArn(arn) { @@ -839,12 +851,13 @@ Agent.prototype._listenForConfigChanges = function _listenForConfigChanges() { */ Agent.prototype.getLinkingMetadata = function getLinkingMetadata(excludeServiceLinks = false) { const segment = this.tracer.getSegment() + const transaction = this.tracer.getTransaction() const config = this.config const linkingMetadata = {} - if (config.distributed_tracing.enabled && segment) { - linkingMetadata['trace.id'] = segment.transaction.traceId + if (config.distributed_tracing.enabled && segment && transaction) { + linkingMetadata['trace.id'] = transaction.traceId const spanId = segment.getSpanId() if (spanId) { linkingMetadata['span.id'] = spanId diff --git a/lib/context-manager/async-local-context-manager.js b/lib/context-manager/async-local-context-manager.js index 9ad596b550..e3cafa2e10 100644 --- a/lib/context-manager/async-local-context-manager.js +++ b/lib/context-manager/async-local-context-manager.js @@ -6,6 +6,7 @@ 'use strict' const { AsyncLocalStorage } = require('async_hooks') +const Context = require('./context') /** * Class for managing state in the agent. @@ -17,12 +18,7 @@ const { AsyncLocalStorage } = require('async_hooks') * @class */ class AsyncLocalContextManager { - /** - * @param {object} config New Relic config instance - */ - constructor(config) { - this._config = config - + constructor() { this._asyncLocalStorage = new AsyncLocalStorage() } @@ -32,7 +28,7 @@ class AsyncLocalContextManager { * @returns {object} The current active context. */ getContext() { - return this._asyncLocalStorage.getStore() || null + return this._asyncLocalStorage.getStore() || new Context() } /** diff --git a/lib/context-manager/context.js b/lib/context-manager/context.js new file mode 100644 index 0000000000..eef668a758 --- /dev/null +++ b/lib/context-manager/context.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = class Context { + constructor(transaction, segment) { + this._transaction = transaction + this._segment = segment + } + + get segment() { + return this._segment + } + + get transaction() { + return this._transaction + } + + enterSegment({ segment, transaction = this.transaction }) { + return new this.constructor(transaction, segment) + } + + enterTransaction(transaction) { + return new this.constructor(transaction, transaction.trace.root) + } +} diff --git a/lib/context-manager/create-context-manager.js b/lib/context-manager/create-context-manager.js deleted file mode 100644 index 336d79c1af..0000000000 --- a/lib/context-manager/create-context-manager.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const logger = require('../logger') - -/** - * Factory function to create context manager implementations used by the - * ContextManager class. - * - * @param {object} config New Relic config instance. - * @returns {*} The appropriate underlying context manager implementation based on - * the current configuration. - */ -function createContextManager(config) { - return createAsyncLocalContextManager(config) -} - -function createAsyncLocalContextManager(config) { - logger.info('Using AsyncLocalContextManager') - - const AsyncLocalContextManager = require('./async-local-context-manager') - return new AsyncLocalContextManager(config) -} - -module.exports = createContextManager diff --git a/lib/db/query-parsers/elasticsearch.js b/lib/db/query-parsers/elasticsearch.js new file mode 100644 index 0000000000..f68d4d9ae2 --- /dev/null +++ b/lib/db/query-parsers/elasticsearch.js @@ -0,0 +1,102 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const logger = require('../../logger').child({ component: 'elasticsearch_query_parser' }) +const { isNotEmpty } = require('../../util/objects') + +/** + * Parses the parameters sent to elasticsearch for collection, + * method, and query + * + * @param {object} params Query object received by the datashim. + * Required properties: path {string}, method {string}. + * Optional properties: querystring {string}, body {object}, and + * bulkBody {object} + * @returns {object} consisting of collection {string}, operation {string}, + * and query {string} + */ +function queryParser(params) { + params = JSON.parse(params) + const { collection, operation } = parsePath(params.path, params.method) + + // the substance of the query may be in querystring or in body. + let queryParam = {} + if (isNotEmpty(params.querystring)) { + queryParam = params.querystring + } + // let body or bulkBody override querystring, as some requests have both + if (isNotEmpty(params.body)) { + queryParam = params.body + } else if (Array.isArray(params.bulkBody) && params.bulkBody.length) { + queryParam = params.bulkBody + } + // The helper interface provides a simpler API: + + const query = JSON.stringify(queryParam) + + return { + collection, + operation, + query + } +} + +/** + * Convenience function for parsing the params.path sent to the queryParser + * for normalized collection and operation + * + * @param {string} pathString params.path supplied to the query parser + * @param {string} method http method called by @elastic/elasticsearch + * @returns {object} consisting of collection {string} and operation {string} + */ +function parsePath(pathString, method) { + let collection + let operation + const defaultCollection = 'any' + const actions = { + GET: 'get', + PUT: 'create', + POST: 'create', + DELETE: 'delete', + HEAD: 'exists' + } + const suffix = actions[method] + + try { + const path = pathString.split('/') + if (method === 'PUT' && path.length === 2) { + collection = path?.[1] || defaultCollection + operation = 'index.create' + return { collection, operation } + } + path.forEach((segment, idx) => { + const prev = idx - 1 + let opname + if (segment === '_search') { + collection = path?.[prev] || defaultCollection + operation = 'search' + } else if (segment[0] === '_') { + opname = segment.substring(1) + collection = path?.[prev] || defaultCollection + operation = `${opname}.${suffix}` + } + }) + if (!operation && !collection) { + // likely creating an index--no underscore segments + collection = path?.[1] || defaultCollection + operation = `index.${suffix}` + } + } catch (e) { + logger.warn('Failed to parse path for operation and collection. Using defaults') + logger.warn(e) + collection = defaultCollection + operation = 'unknown' + } + + return { collection, operation } +} + +module.exports = { queryParser, parsePath } diff --git a/lib/db/query-parsers/mongodb.js b/lib/db/query-parsers/mongodb.js new file mode 100644 index 0000000000..c07cbed872 --- /dev/null +++ b/lib/db/query-parsers/mongodb.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * parser used to grab the collection and operation + * from a running query + * + * @param {object} operation mongodb operation + * @returns {object} { operation, collection } parsed operation and collection + */ +function queryParser(operation) { + let collection = this.collectionName || 'unknown' + + // cursor methods have collection on namespace.collection + if (this?.namespace?.collection) { + collection = this.namespace.collection + // (un)ordered bulk operations have collection on different key + } else if (this?.s?.collection?.collectionName) { + collection = this.s.collection.collectionName + } + + return { operation, collection } +} + +module.exports = queryParser diff --git a/lib/db/query-sample.js b/lib/db/query-sample.js index 46051d38e4..985f258286 100644 --- a/lib/db/query-sample.js +++ b/lib/db/query-sample.js @@ -34,7 +34,7 @@ QuerySample.prototype.merge = function merge(sample) { } QuerySample.prototype.prepareJSON = function prepareJSON(done) { - const transaction = this.trace.segment.transaction + const transaction = this.trace.transaction const sample = this const trace = sample.trace @@ -56,7 +56,7 @@ QuerySample.prototype.prepareJSON = function prepareJSON(done) { } QuerySample.prototype.prepareJSONSync = function prepareJSONSync() { - const transaction = this.trace.segment.transaction + const transaction = this.trace.transaction const sample = this const trace = sample.trace @@ -99,7 +99,7 @@ QuerySample.prototype.getParams = function getParams() { } if (this.tracer.config.distributed_tracing.enabled) { - this.trace.segment.transaction.addDistributedTraceIntrinsics(params) + this.trace.transaction.addDistributedTraceIntrinsics(params) } return params diff --git a/lib/db/query-trace-aggregator.js b/lib/db/query-trace-aggregator.js index bb3e2700a3..2f4b6047da 100644 --- a/lib/db/query-trace-aggregator.js +++ b/lib/db/query-trace-aggregator.js @@ -48,7 +48,7 @@ class QueryTraceAggregator extends Aggregator { } } - add(segment, type, query, trace) { + add({ segment, transaction, type, query, trace }) { const ttConfig = this.config.transaction_tracer // If DT is enabled and the segment is part of a sampled transaction @@ -57,12 +57,12 @@ class QueryTraceAggregator extends Aggregator { let slowQuery switch (ttConfig.record_sql) { case 'raw': - slowQuery = new SlowQuery(segment, type, query, trace) + slowQuery = new SlowQuery({ segment, transaction, type, query, trace }) logger.trace('recording raw sql') segment.addAttribute('sql', slowQuery.query, true) break case 'obfuscated': - slowQuery = new SlowQuery(segment, type, query, trace) + slowQuery = new SlowQuery({ transaction, segment, type, query, trace }) logger.trace('recording obfuscated sql') segment.addAttribute('sql_obfuscated', slowQuery.obfuscated, true) break @@ -78,7 +78,7 @@ class QueryTraceAggregator extends Aggregator { return } - slowQuery = slowQuery || new SlowQuery(segment, type, query, trace) + slowQuery = slowQuery || new SlowQuery({ segment, transaction, type, query, trace }) segment.addAttribute('backtrace', slowQuery.trace) diff --git a/lib/db/slow-query.js b/lib/db/slow-query.js index 2b460d1168..634b5bf718 100644 --- a/lib/db/slow-query.js +++ b/lib/db/slow-query.js @@ -10,7 +10,7 @@ const crypto = require('crypto') const path = require('path') const NR_ROOT = path.resolve(__dirname, '..') -function SlowQuery(segment, type, query, trace) { +function SlowQuery({ segment, transaction, type, query, trace }) { this.obfuscated = obfuscate(query, type) this.normalized = this.obfuscated.replace(/\?\s*,\s*|\s*/g, '') this.id = normalizedHash(this.normalized) @@ -18,6 +18,7 @@ function SlowQuery(segment, type, query, trace) { this.query = query this.metric = segment.name this.trace = formatTrace(trace) + this.transaction = transaction this.duration = segment.getDurationInMillis() } diff --git a/lib/instrumentation/@elastic/elasticsearch.js b/lib/instrumentation/@elastic/elasticsearch.js index dc2976acbe..15fe7f1699 100644 --- a/lib/instrumentation/@elastic/elasticsearch.js +++ b/lib/instrumentation/@elastic/elasticsearch.js @@ -7,8 +7,7 @@ const { QuerySpec } = require('../../shim/specs') const semver = require('semver') -const logger = require('../../logger').child({ component: 'ElasticSearch' }) -const { isNotEmpty } = require('../../util/objects') +const { queryParser } = require('../../db/query-parsers/elasticsearch') /** * Instruments the `@elastic/elasticsearch` module. This function is @@ -46,98 +45,6 @@ module.exports = function initialize(_agent, elastic, _moduleName, shim) { }) } -/** - * Parses the parameters sent to elasticsearch for collection, - * method, and query - * - * @param {object} params Query object received by the datashim. - * Required properties: path {string}, method {string}. - * Optional properties: querystring {string}, body {object}, and - * bulkBody {object} - * @returns {object} consisting of collection {string}, operation {string}, - * and query {string} - */ -function queryParser(params) { - params = JSON.parse(params) - const { collection, operation } = parsePath(params.path, params.method) - - // the substance of the query may be in querystring or in body. - let queryParam = {} - if (isNotEmpty(params.querystring)) { - queryParam = params.querystring - } - // let body or bulkBody override querystring, as some requests have both - if (isNotEmpty(params.body)) { - queryParam = params.body - } else if (Array.isArray(params.bulkBody) && params.bulkBody.length) { - queryParam = params.bulkBody - } - // The helper interface provides a simpler API: - - const query = JSON.stringify(queryParam) - - return { - collection, - operation, - query - } -} - -/** - * Convenience function for parsing the params.path sent to the queryParser - * for normalized collection and operation - * - * @param {string} pathString params.path supplied to the query parser - * @param {string} method http method called by @elastic/elasticsearch - * @returns {object} consisting of collection {string} and operation {string} - */ -function parsePath(pathString, method) { - let collection - let operation - const defaultCollection = 'any' - const actions = { - GET: 'get', - PUT: 'create', - POST: 'create', - DELETE: 'delete', - HEAD: 'exists' - } - const suffix = actions[method] - - try { - const path = pathString.split('/') - if (method === 'PUT' && path.length === 2) { - collection = path?.[1] || defaultCollection - operation = 'index.create' - return { collection, operation } - } - path.forEach((segment, idx) => { - const prev = idx - 1 - let opname - if (segment === '_search') { - collection = path?.[prev] || defaultCollection - operation = 'search' - } else if (segment[0] === '_') { - opname = segment.substring(1) - collection = path?.[prev] || defaultCollection - operation = `${opname}.${suffix}` - } - }) - if (!operation && !collection) { - // likely creating an index--no underscore segments - collection = path?.[1] || defaultCollection - operation = `index.${suffix}` - } - } catch (e) { - logger.warn('Failed to parse path for operation and collection. Using defaults') - logger.warn(e) - collection = defaultCollection - operation = 'unknown' - } - - return { collection, operation } -} - /** * Convenience function for deriving connection information from * elasticsearch @@ -152,6 +59,4 @@ function getConnection(shim) { return shim.captureInstanceAttributes(host[0], port) } -module.exports.queryParser = queryParser -module.exports.parsePath = parsePath module.exports.getConnection = getConnection diff --git a/lib/instrumentation/@nestjs/core.js b/lib/instrumentation/@nestjs/core.js index 93cbce1c7e..0628da11cc 100644 --- a/lib/instrumentation/@nestjs/core.js +++ b/lib/instrumentation/@nestjs/core.js @@ -20,8 +20,7 @@ module.exports = function initialize(agent, core, moduleName, shim) { shim.wrap(core.BaseExceptionFilter.prototype, 'handleUnknownError', (shim, original) => { return function wrappedHandleUnknownError(exception) { - const segment = shim.getActiveSegment() - const transaction = segment?.transaction + const transaction = shim.tracer.getTransaction() if (transaction) { shim.agent.errors.add(transaction, exception) logger.trace(exception, 'Captured error handled by Nest.js exception filter.') diff --git a/lib/instrumentation/@opensearch-project/opensearch.js b/lib/instrumentation/@opensearch-project/opensearch.js index 12fb670dbc..d26ca62deb 100644 --- a/lib/instrumentation/@opensearch-project/opensearch.js +++ b/lib/instrumentation/@opensearch-project/opensearch.js @@ -7,8 +7,7 @@ const { QuerySpec } = require('../../shim/specs') const semver = require('semver') -const logger = require('../../logger').child({ component: 'OpenSearch' }) -const { isNotEmpty } = require('../../util/objects') +const { queryParser } = require('../../db/query-parsers/elasticsearch') /** * Instruments the `@opensearch-project/opensearch` module. This function is @@ -50,98 +49,6 @@ module.exports = function initialize(_agent, opensearch, _moduleName, shim) { ) } -/** - * Parses the parameters sent to opensearch for collection, - * method, and query - * - * @param {object} params Query object received by the datashim. - * Required properties: path {string}, method {string}. - * Optional properties: querystring {string}, body {object}, and - * bulkBody {object} - * @returns {object} consisting of collection {string}, operation {string}, - * and query {string} - */ -function queryParser(params) { - params = JSON.parse(params) - const { collection, operation } = parsePath(params.path, params.method) - - // the substance of the query may be in querystring or in body. - let queryParam = {} - if (isNotEmpty(params.querystring)) { - queryParam = params.querystring - } - // let body or bulkBody override querystring, as some requests have both - if (isNotEmpty(params.body)) { - queryParam = params.body - } else if (Array.isArray(params.bulkBody) && params.bulkBody.length) { - queryParam = params.bulkBody - } - // The helper interface provides a simpler API: - - const query = JSON.stringify(queryParam) - - return { - collection, - operation, - query - } -} - -/** - * Convenience function for parsing the params.path sent to the queryParser - * for normalized collection and operation - * - * @param {string} pathString params.path supplied to the query parser - * @param {string} method http method called by @opensearch-project/opensearch - * @returns {object} consisting of collection {string} and operation {string} - */ -function parsePath(pathString, method) { - let collection - let operation - const defaultCollection = 'any' - const actions = { - GET: 'get', - PUT: 'create', - POST: 'create', - DELETE: 'delete', - HEAD: 'exists' - } - const suffix = actions[method] - - try { - const path = pathString.split('/') - if (method === 'PUT' && path.length === 2) { - collection = path?.[1] || defaultCollection - operation = 'index.create' - return { collection, operation } - } - path.forEach((segment, idx) => { - const prev = idx - 1 - let opname - if (segment === '_search') { - collection = path?.[prev] || defaultCollection - operation = 'search' - } else if (segment[0] === '_') { - opname = segment.substring(1) - collection = path?.[prev] || defaultCollection - operation = `${opname}.${suffix}` - } - }) - if (!operation && !collection) { - // likely creating an index--no underscore segments - collection = path?.[1] || defaultCollection - operation = `index.${suffix}` - } - } catch (e) { - logger.warn('Failed to parse path for operation and collection. Using defaults') - logger.warn(e) - collection = defaultCollection - operation = 'unknown' - } - - return { collection, operation } -} - /** * Convenience function for deriving connection information from * opensearch @@ -156,6 +63,4 @@ function getConnection(shim) { return shim.captureInstanceAttributes(host[0], port) } -module.exports.queryParser = queryParser -module.exports.parsePath = parsePath module.exports.getConnection = getConnection diff --git a/lib/instrumentation/aws-sdk/v3/bedrock.js b/lib/instrumentation/aws-sdk/v3/bedrock.js index 7fe1b4d277..54d5ef4616 100644 --- a/lib/instrumentation/aws-sdk/v3/bedrock.js +++ b/lib/instrumentation/aws-sdk/v3/bedrock.js @@ -70,10 +70,11 @@ function recordEvent({ agent, type, msg }) { * @param {object} params input params * @param {Agent} params.agent NR agent instance * @param {TraceSegment} params.segment active segment + * @param {Transaction} params.transaction active transaction */ -function addLlmMeta({ agent, segment }) { +function addLlmMeta({ agent, segment, transaction }) { agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount() - segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) + transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) // end segment to get a consistent segment duration // for both the LLM events and the segment segment.end() @@ -90,13 +91,15 @@ function addLlmMeta({ agent, segment }) { * @param {object} params.segment active segment * @param {BedrockCommand} params.bedrockCommand parsed input * @param {Error|null} params.err error from request if exists - * @param params.bedrockResponse - * @param params.shim + * @param {BedrockResponse} params.bedrockResponse parsed response + * @param {Shim} params.shim shim instace + * @param {Transaction} params.transaction active transaction */ function recordChatCompletionMessages({ agent, shim, segment, + transaction, bedrockCommand, bedrockResponse, err @@ -110,6 +113,7 @@ function recordChatCompletionMessages({ agent, bedrockResponse, bedrockCommand, + transaction, segment, isError: err !== null }) @@ -119,6 +123,7 @@ function recordChatCompletionMessages({ segment, bedrockCommand, bedrockResponse, + transaction, index: 0, completionId: summary.id }) @@ -128,6 +133,7 @@ function recordChatCompletionMessages({ const chatCompletionMessage = new LlmChatCompletionMessage({ agent, segment, + transaction, bedrockCommand, bedrockResponse, isResponse: true, @@ -142,7 +148,7 @@ function recordChatCompletionMessages({ if (err) { const llmError = new LlmError({ bedrockResponse, err, summary }) - agent.errors.add(segment.transaction, err, llmError) + agent.errors.add(transaction, err, llmError) } } @@ -156,9 +162,18 @@ function recordChatCompletionMessages({ * @param {object} params.segment active segment * @param {BedrockCommand} params.bedrockCommand parsed input * @param {Error|null} params.err error from request if exists - * @param params.bedrockResponse + * @param {BedrockResponse} params.bedrockResponse parsed response + * @param {Transaction} params.transaction active transaction */ -function recordEmbeddingMessage({ agent, shim, segment, bedrockCommand, bedrockResponse, err }) { +function recordEmbeddingMessage({ + agent, + shim, + segment, + transaction, + bedrockCommand, + bedrockResponse, + err +}) { if (shouldSkipInstrumentation(agent.config) === true) { shim.logger.debug('skipping sending of ai data') return @@ -167,6 +182,7 @@ function recordEmbeddingMessage({ agent, shim, segment, bedrockCommand, bedrockR const embedding = new LlmEmbedding({ agent, segment, + transaction, bedrockCommand, bedrockResponse, isError: err !== null @@ -175,7 +191,7 @@ function recordEmbeddingMessage({ agent, shim, segment, bedrockCommand, bedrockR recordEvent({ agent, type: 'LlmEmbedding', msg: embedding }) if (err) { const llmError = new LlmError({ bedrockResponse, err, embedding }) - agent.errors.add(segment.transaction, err, llmError) + agent.errors.add(transaction, err, llmError) } } @@ -204,9 +220,8 @@ function createBedrockResponse({ bedrockCommand, response, err }) { * Registers the specification for instrumentation bedrock calls * * @param {object} params { config, commandName } aws config and command name - * @param {Shim} _shim instance of shim - * @param params.commandName - * @param shim + * @param {string} params.commandName name of command + * @param {Shim} shim instance of shim * @param {function} _original original middleware function * @param {string} _name function name * @param {array} args argument passed to middleware @@ -222,12 +237,13 @@ function getBedrockSpec({ commandName }, shim, _original, _name, args) { return new RecorderSpec({ promise: true, name: `Llm/${modelType}/Bedrock/${commandName}`, - after: ({ shim, error: err, result: response, segment }) => { + after: ({ shim, error: err, result: response, segment, transaction }) => { const passThroughParams = { shim, err, response, segment, + transaction, bedrockCommand, modelType } @@ -250,22 +266,23 @@ function getBedrockSpec({ commandName }, shim, _original, _name, args) { 'ai_monitoring.streaming.enabled is set to `false`, stream will not be instrumented.' ) agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount() - addLlmMeta({ agent, segment }) + addLlmMeta({ agent, segment, transaction }) } } }) } -function handleResponse({ shim, err, response, segment, bedrockCommand, modelType }) { +function handleResponse({ shim, err, response, segment, transaction, bedrockCommand, modelType }) { const { agent } = shim const bedrockResponse = createBedrockResponse({ bedrockCommand, response, err }) - addLlmMeta({ agent, segment }) + addLlmMeta({ agent, segment, transaction }) if (modelType === 'completion') { recordChatCompletionMessages({ agent, shim, segment, + transaction, bedrockCommand, bedrockResponse, err @@ -275,6 +292,7 @@ function handleResponse({ shim, err, response, segment, bedrockCommand, modelTyp agent, shim, segment, + transaction, bedrockCommand, bedrockResponse, err diff --git a/lib/instrumentation/core/globals.js b/lib/instrumentation/core/globals.js index 2cd4a2dfba..d97fe67a18 100644 --- a/lib/instrumentation/core/globals.js +++ b/lib/instrumentation/core/globals.js @@ -43,9 +43,7 @@ function initialize(agent, nodule, name, shim) { !process.domain && process.listenerCount('unhandledRejection') === 0 ) { - // If there are no unhandledRejection handlers report the error. - const segment = promise[symbols.context] && promise[symbols.context].getSegment() - const tx = segment && segment.transaction + const tx = promise[symbols.context] && promise[symbols.context].getTransaction() shim.logger.trace('Captured unhandled rejection for transaction %s', tx && tx.id) agent.errors.add(tx, error) } diff --git a/lib/instrumentation/core/http-outbound.js b/lib/instrumentation/core/http-outbound.js index 2cf58c81fa..a135ef429a 100644 --- a/lib/instrumentation/core/http-outbound.js +++ b/lib/instrumentation/core/http-outbound.js @@ -15,7 +15,6 @@ const copy = require('../../util/copy') const symbols = require('../../symbols') const synthetics = require('../../synthetics') const { URL } = require('node:url') - const NAMES = require('../../metrics/names') const DEFAULT_HOST = 'localhost' const DEFAULT_HTTP_PORT = 80 @@ -168,10 +167,14 @@ module.exports = function instrumentOutbound(agent, opts, makeRequest) { * @param {string} params.hostname domain of outbound request * @param {string} params.port port of outbound request * @param {TraceSegment} segment outbound http segment + * @param {Transaction} transaction active tx * @returns {http.IncomingMessage} request actual http outbound request */ -function instrumentRequest({ agent, opts, makeRequest, host, port, hostname }, segment) { - const transaction = segment.transaction +function instrumentRequest( + { agent, opts, makeRequest, host, port, hostname }, + segment, + transaction +) { const outboundHeaders = Object.create(null) opts.headers = opts.headers || {} @@ -180,7 +183,15 @@ function instrumentRequest({ agent, opts, makeRequest, host, port, hostname }, s maybeAddDtCatHeaders(agent, transaction, outboundHeaders, opts?.headers) opts.headers = assignOutgoingHeaders(opts.headers, outboundHeaders) - const request = applySegment({ opts, makeRequest, hostname, host, port, segment }) + const request = applySegment({ + opts, + makeRequest, + hostname, + host, + port, + segment, + config: agent.config + }) instrumentRequestEmit(agent, host, segment, request) @@ -248,13 +259,14 @@ function assignOutgoingHeaders(currentHeaders, outboundHeaders) { * @param {string} params.port port of outbound request * @param {string} params.hostname host + port of outbound request * @param {TraceSegment} params.segment outbound http segment + * @param {object} params.config agent config * @returns {http.IncomingMessage} request actual http outbound request */ -function applySegment({ opts, makeRequest, host, port, hostname, segment }) { +function applySegment({ opts, makeRequest, host, port, hostname, segment, config }) { segment.start() const request = makeRequest(opts) const parsed = urltils.scrubAndParseParameters(request.path) - parsed.path = urltils.obfuscatePath(segment.transaction.agent.config, parsed.path) + parsed.path = urltils.obfuscatePath(config, parsed.path) const proto = parsed.protocol || opts.protocol || 'http:' segment.name += parsed.path segment.captureExternalAttributes({ @@ -283,13 +295,16 @@ function applySegment({ opts, makeRequest, host, port, hostname, segment }) { */ function instrumentRequestEmit(agent, host, segment, request) { shimmer.wrapMethod(request, 'request.emit', 'emit', function wrapEmit(emit) { - const boundEmit = agent.tracer.bindFunction(emit, segment) + const context = agent.tracer.getContext() + const newContext = context.enterSegment({ segment }) + const boundEmit = agent.tracer.bindFunction(emit, newContext) return function wrappedRequestEmit(evnt, arg) { + const transaction = agent.tracer.getTransaction() if (evnt === 'error') { segment.end() - handleError(segment, request, arg) + handleError({ transaction, req: request, error: arg }) } else if (evnt === 'response') { - handleResponse(segment, host, arg) + handleResponse({ agent, segment, transaction, host, res: arg }) } return boundEmit.apply(this, arguments) @@ -303,47 +318,50 @@ function instrumentRequestEmit(agent, host, segment, request) { * Notices the given error if there is no listener for the `error` event on the * request object. * - * @param {object} segment TraceSegment instance - * @param {object} req http.ClientRequest - * @param {Error} error If provided, unhandled error that occurred during request + * @param {object} params to function + * @param {Transaction} params.transaction active transaction + * @param {object} params.req http.ClientRequest + * @param {Error} params.error If provided, unhandled error that occurred during request * @returns {boolean} True if the error will be collected by New Relic. */ -function handleError(segment, req, error) { +function handleError({ transaction, req, error }) { if (req.listenerCount('error') > 0) { logger.trace(error, 'Not capturing outbound error because user has already handled it.') return false } logger.trace(error, 'Captured outbound error on behalf of the user.') - const tx = segment.transaction - tx.agent.errors.add(tx, error) + transaction.agent.errors.add(transaction, error) return true } /** * Ties the response object to the request segment. * - * @param {object} segment TraceSegment instance - * @param {string} hostname host of the HTTP request - * @param host - * @param {object} res http.ServerResponse + * @param {object} params to function + * @param {Agent} params.agent agent instance + * @param {TraceSegment} params.segment active segment + * @param {Transaction} params.transaction active transaction + * @param {string} params.host hostname of the HTTP request + * @param {object} params.res http.ServerResponse */ -function handleResponse(segment, host, res) { +function handleResponse({ agent, segment, transaction, host, res }) { // Add response attributes for spans segment.addSpanAttribute('http.statusCode', res.statusCode) segment.addSpanAttribute('http.statusText', res.statusMessage) // If CAT is enabled, grab those headers! - const agent = segment.transaction.agent if (agent.config.cross_application_tracer.enabled && !agent.config.distributed_tracing.enabled) { const { appData } = cat.extractCatHeaders(res.headers) const decodedAppData = cat.parseAppData(agent.config, appData) - cat.assignCatToSegment(decodedAppData, segment, host) + cat.assignCatToSegment({ appData: decodedAppData, segment, host, transaction }) } // Again a custom emit wrapper because we want to watch for the `end` event. shimmer.wrapMethod(res, 'response', 'emit', function wrapEmit(emit) { - const boundEmit = agent.tracer.bindFunction(emit, segment) + const context = agent.tracer.getContext() + const newContext = context.enterSegment({ segment }) + const boundEmit = agent.tracer.bindFunction(emit, newContext) return function wrappedResponseEmit(evnt) { if (evnt === 'end') { segment.end() diff --git a/lib/instrumentation/core/http.js b/lib/instrumentation/core/http.js index 4d256c93b7..9f31636f01 100644 --- a/lib/instrumentation/core/http.js +++ b/lib/instrumentation/core/http.js @@ -18,7 +18,6 @@ const synthetics = require('../../synthetics') const NAMES = require('../../metrics/names') const DESTS = require('../../config/attribute-filter').DESTINATIONS - const symbols = require('../../symbols') // For incoming requests this instrumentation functions by wrapping @@ -34,6 +33,7 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) { const tracer = agent.tracer const transport = isHTTPS ? 'HTTPS' : 'HTTP' return tracer.transactionProxy(function wrappedHandler(evnt, request, response) { + const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { return emit.apply(this, arguments) @@ -51,7 +51,12 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) { // Create the transaction segment using the request URL for now. Once a // better name can be determined this segment will be renamed to that. - const segment = tracer.createSegment(request.url, recordWeb) + const segment = tracer.createSegment({ + name: request.url, + recorder: recordWeb, + parent: context.segment, + transaction + }) segment.start() if (request.method != null) { @@ -108,7 +113,8 @@ function wrapEmitWithTransaction(agent, emit, isHTTPS) { response.once('finish', instrumentedFinish.bind(response, segment, transaction)) response.once('close', instrumentedFinish.bind(response, segment, transaction)) - return tracer.bindFunction(emit, segment).apply(this, arguments) + const newContext = context.enterSegment({ segment }) + return tracer.bindFunction(emit, newContext).apply(this, arguments) }) } @@ -490,18 +496,24 @@ module.exports = function initialize(agent, http, moduleName) { 'createConnection', function wrapCreateConnection(original) { return function wrappedCreateConnection() { + const context = agent.tracer.getContext() if (!agent.getTransaction()) { return original.apply(this, arguments) } - const segment = agent.tracer.createSegment('http.Agent#createConnection') + const segment = agent.tracer.createSegment({ + name: 'http.Agent#createConnection', + parent: context.segment, + transaction: context.transaction + }) const args = agent.tracer.slice(arguments) + const newContext = context.enterSegment({ segment }) if (typeof args[1] === 'function') { - args[1] = agent.tracer.bindFunction(args[1], segment, true) + args[1] = agent.tracer.bindFunction(args[1], newContext, true) } - return agent.tracer.bindFunction(original, segment, true).apply(this, args) + return agent.tracer.bindFunction(original, newContext, true).apply(this, args) } } ) diff --git a/lib/instrumentation/core/net.js b/lib/instrumentation/core/net.js index c7df7090ce..736eebcad4 100644 --- a/lib/instrumentation/core/net.js +++ b/lib/instrumentation/core/net.js @@ -14,7 +14,7 @@ module.exports = function initialize(agent, net, moduleName, shim) { return fn.apply(this, arguments) } - const child = shim.createSegment('net.' + name, null, segment) + const child = shim.createSegment({ name: 'net.' + name, parent: segment }) const sock = shim.applySegment(fn, child, true, this, arguments) wrapSocket(sock, child) return sock @@ -43,6 +43,7 @@ module.exports = function initialize(agent, net, moduleName, shim) { function wrapListen2(shim, fn) { return function wrappedListen2() { + const context = shim.tracer.getContext() const segment = shim.getActiveSegment() const emit = this.emit @@ -59,13 +60,20 @@ module.exports = function initialize(agent, net, moduleName, shim) { return emit.apply(this, arguments) } - const child = shim.createSegment('net.Server.onconnection', segment) + const child = shim.createSegment({ name: 'net.Server.onconnection', parent: segment }) + const newContext = context.enterSegment({ segment: child }) if (socket._handle.onread) { - shim.bindSegment(socket._handle, 'onread', child) + shim.bindContext({ nodule: socket._handle, property: 'onread', context: newContext }) } - return shim.applySegment(emit, child, true, this, arguments) + return shim.applyContext({ + func: emit, + context: newContext, + full: true, + boundThis: this, + args: arguments + }) } } } @@ -75,11 +83,12 @@ module.exports = function initialize(agent, net, moduleName, shim) { if (!agent.getTransaction()) { return fn.apply(this, arguments) } + const context = agent.tracer.getContext() const socket = this const args = normalizeConnectArgs(arguments) - const segment = shim.createSegment('net.Socket.connect') + const segment = shim.createSegment({ name: 'net.Socket.connect', parent: context.segment }) if (args[1]) { args[1] = shim.bindSegment(args[1], segment) diff --git a/lib/instrumentation/grpc-js/grpc.js b/lib/instrumentation/grpc-js/grpc.js index 438facefec..b223e5c69e 100644 --- a/lib/instrumentation/grpc-js/grpc.js +++ b/lib/instrumentation/grpc-js/grpc.js @@ -48,6 +48,7 @@ function wrapStart(shim, original) { return original.apply(this, arguments) } + const transaction = shim.tracer.getTransaction() const channel = this.channel const authorityName = (channel.target && channel.target.path) || channel.getDefaultAuthority // in 1.8.0 this changed from methodName to method @@ -56,7 +57,8 @@ function wrapStart(shim, original) { const segment = shim.createSegment({ name: `External/${authorityName}${method}`, opaque: true, - recorder: recordExternal(authorityName, 'gRPC') + recorder: recordExternal(authorityName, 'gRPC'), + parent: activeSegment }) return shim.applySegment(callStart, segment, true, this, arguments) @@ -64,8 +66,6 @@ function wrapStart(shim, original) { function callStart() { const args = shim.argsToArray.apply(shim, arguments) - const transaction = segment.transaction - const originalMetadata = args[0] const nrMetadata = originalMetadata.clone() @@ -93,7 +93,7 @@ function wrapStart(shim, original) { const config = agent.config if (shouldTrackError(code, config)) { - shim.agent.errors.add(segment.transaction, details) + shim.agent.errors.add(transaction, details) } segment.addAttribute('component', 'gRPC') @@ -171,12 +171,12 @@ function wrapRegister(shim, original) { } function createTransaction() { - const parent = shim.getActiveSegment() - const transaction = parent.transaction + const context = shim.tracer.getContext() + const transaction = context.transaction // Create the transaction segment using the request URL for now. Once a // better name can be determined this segment will be renamed to that. - const segment = shim.createSegment(name, recordHttp) + const segment = shim.createSegment(name, recordHttp, context.segment) segment.start() transaction.type = 'web' diff --git a/lib/instrumentation/kafkajs/consumer.js b/lib/instrumentation/kafkajs/consumer.js index 7e1eb1b708..a04ab32a70 100644 --- a/lib/instrumentation/kafkajs/consumer.js +++ b/lib/instrumentation/kafkajs/consumer.js @@ -100,14 +100,14 @@ function handler({ consumer }) { * * @param {MessageShim} shim instance of shim * @param {Array} args arguments passed to the `eachMessage` function of the `consumer.run` + * @param {Transaction} tx active transaction * @returns {MessageSpec} spec for message handling */ - return function messageHandler(shim, args) { + return function messageHandler(shim, args, tx) { recordMethodMetric({ agent: shim.agent, name: 'eachMessage' }) const [data] = args const { topic } = data - const segment = shim.getActiveSegment() recordLinkingMetrics({ agent: shim.agent, @@ -116,8 +116,7 @@ function handler({ consumer }) { producer: false }) - if (segment?.transaction) { - const tx = segment.transaction + if (tx) { const byteLength = data?.message.value?.byteLength const metricPrefix = `Message/Kafka/Topic/Named/${topic}/Received` // This will always be 1 diff --git a/lib/instrumentation/koa/instrumentation.js b/lib/instrumentation/koa/instrumentation.js index 7514529017..602f02895c 100644 --- a/lib/instrumentation/koa/instrumentation.js +++ b/lib/instrumentation/koa/instrumentation.js @@ -117,14 +117,12 @@ function wrapMatchedRoute(shim, context) { set: (val) => { // match should never be undefined given _matchedRoute was set if (val) { - const currentSegment = shim.getActiveSegment() + const transaction = shim.tracer.getTransaction() // Segment/Transaction may be null, see: // - https://github.com/newrelic/node-newrelic-koa/issues/32 // - https://github.com/newrelic/node-newrelic-koa/issues/33 - if (currentSegment) { - const transaction = currentSegment.transaction - + if (transaction) { if (context[symbols.koaMatchedRoute]) { transaction.nameState.popPath() } diff --git a/lib/instrumentation/langchain/runnable.js b/lib/instrumentation/langchain/runnable.js index b6d76f4ba6..52882ad866 100644 --- a/lib/instrumentation/langchain/runnable.js +++ b/lib/instrumentation/langchain/runnable.js @@ -62,8 +62,9 @@ function instrumentInvokeChain({ langchain, shim }) { return new RecorderSpec({ name: `${LANGCHAIN.CHAIN}/${fnName}`, promise: true, - after({ error: err, result: output, segment }) { + after({ error: err, result: output, segment, transaction }) { recordChatCompletionEvents({ + transaction, segment, messages: [output], events: [request, output], @@ -97,14 +98,15 @@ function instrumentStream({ langchain, shim }) { return new RecorderSpec({ name: `${LANGCHAIN.CHAIN}/${fnName}`, promise: true, - after({ error: err, result: output, segment }) { + after({ error: err, result: output, segment, transaction }) { // Input error occurred which means a stream was not created. // Skip instrumenting streaming and create Llm Events from // the data we have if (output?.next) { - wrapNextHandler({ shim, output, segment, request, metadata, tags }) + wrapNextHandler({ shim, output, segment, request, metadata, tags, transaction }) } else { recordChatCompletionEvents({ + transaction, segment, messages: [], events: [request], @@ -131,8 +133,9 @@ function instrumentStream({ langchain, shim }) { * @param {string} params.request the prompt message * @param {object} params.metadata metadata for the call * @param {Array} params.tags tags for the call + * @param {Transaction} params.transaction active transaction */ -function wrapNextHandler({ shim, output, segment, request, metadata, tags }) { +function wrapNextHandler({ shim, output, segment, transaction, request, metadata, tags }) { shim.wrap(output, 'next', function wrapIterator(shim, orig) { let content = '' return async function wrappedIterator() { @@ -141,6 +144,7 @@ function wrapNextHandler({ shim, output, segment, request, metadata, tags }) { // only create Llm events when stream iteration is done if (result?.done) { recordChatCompletionEvents({ + transaction, segment, messages: [content], events: [request, content], @@ -154,6 +158,7 @@ function wrapNextHandler({ shim, output, segment, request, metadata, tags }) { return result } catch (error) { recordChatCompletionEvents({ + transaction, segment, messages: [content], events: [request, content], @@ -183,8 +188,18 @@ function wrapNextHandler({ shim, output, segment, request, metadata, tags }) { * @param {Array} params.tags tags for the call * @param {Error} params.err error object from call * @param {Shim} params.shim shim instance + * @param {Transaction} params.transaction active transaction */ -function recordChatCompletionEvents({ segment, messages, events, metadata, tags, err, shim }) { +function recordChatCompletionEvents({ + segment, + transaction, + messages, + events, + metadata, + tags, + err, + shim +}) { const { pkgVersion, agent } = shim segment.end() @@ -202,6 +217,7 @@ function recordChatCompletionEvents({ segment, messages, events, metadata, tags, metadata, tags, segment, + transaction, error: err != null, runId: segment[langchainRunId] }) @@ -221,12 +237,13 @@ function recordChatCompletionEvents({ segment, messages, events, metadata, tags, completionSummary, agent, segment, - shim + shim, + transaction }) if (err) { agent.errors.add( - segment.transaction, + transaction, err, new LlmErrorMessage({ response: {}, @@ -236,7 +253,7 @@ function recordChatCompletionEvents({ segment, messages, events, metadata, tags, ) } - segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) + transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) } /** @@ -248,8 +265,9 @@ function recordChatCompletionEvents({ segment, messages, events, metadata, tags, * @param {Agent} params.agent instance of agent * @param {TraceSegment} params.segment active segment * @param {Shim} params.shim shim instance + * @param {Transaction} params.transaction active transaction */ -function recordCompletions({ events, completionSummary, agent, segment, shim }) { +function recordCompletions({ events, completionSummary, agent, segment, shim, transaction }) { for (let i = 0; i < events.length; i += 1) { let msg = events[i] if (msg?.content) { @@ -270,6 +288,7 @@ function recordCompletions({ events, completionSummary, agent, segment, shim }) content: msgString, completionId: completionSummary.id, segment, + transaction, runId: segment[langchainRunId], // We call the final output in a LangChain "chain" the "response": isResponse: i === events.length - 1 diff --git a/lib/instrumentation/langchain/tools.js b/lib/instrumentation/langchain/tools.js index 17b0178998..2554e70103 100644 --- a/lib/instrumentation/langchain/tools.js +++ b/lib/instrumentation/langchain/tools.js @@ -31,7 +31,7 @@ module.exports = function initialize(shim, tools) { return new RecorderSpec({ name: `${LANGCHAIN.TOOL}/${name}`, promise: true, - after({ error: err, result: output, segment }) { + after({ error: err, result: output, segment, transaction }) { const metadata = mergeMetadata(instanceMeta, paramsMeta) const tags = mergeTags(instanceTags, paramsTags) segment.end() @@ -50,6 +50,7 @@ module.exports = function initialize(shim, tools) { name, runId: segment[langchainRunId], metadata, + transaction, tags, input: request?.input, output, @@ -60,7 +61,7 @@ module.exports = function initialize(shim, tools) { if (err) { agent.errors.add( - segment.transaction, + transaction, err, new LlmErrorMessage({ response: {}, @@ -70,7 +71,7 @@ module.exports = function initialize(shim, tools) { ) } - segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) + transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) } }) }) diff --git a/lib/instrumentation/langchain/vectorstore.js b/lib/instrumentation/langchain/vectorstore.js index 61d27174b0..3d7ae673d5 100644 --- a/lib/instrumentation/langchain/vectorstore.js +++ b/lib/instrumentation/langchain/vectorstore.js @@ -27,11 +27,23 @@ const LlmErrorMessage = require('../../llm-events/error-message') * @param {TraceSegment} params.segment active segment from vector search * @param {string} params.pkgVersion langchain version * @param {Error} params.err if it exists + * @param {Transaction} params.transaction active transaction */ -function recordVectorSearch({ request, k, output, agent, shim, segment, pkgVersion, err }) { +function recordVectorSearch({ + request, + k, + output, + agent, + shim, + segment, + transaction, + pkgVersion, + err +}) { const vectorSearch = new LangChainVectorSearch({ agent, segment, + transaction, query: request, k, documents: output, @@ -47,7 +59,8 @@ function recordVectorSearch({ request, k, output, agent, shim, segment, pkgVersi metadata: document.metadata, pageContent: document.pageContent, sequence, - search_id: vectorSearch.id + search_id: vectorSearch.id, + transaction }) recordEvent({ @@ -61,7 +74,7 @@ function recordVectorSearch({ request, k, output, agent, shim, segment, pkgVersi if (err) { agent.errors.add( - segment.transaction, + transaction, err, new LlmErrorMessage({ response: output, @@ -91,7 +104,7 @@ module.exports = function initialize(shim, vectorstores) { return new RecorderSpec({ name: `${LANGCHAIN.VECTORSTORE}/${fnName}`, promise: true, - after({ error: err, result: output, segment }) { + after({ error: err, result: output, segment, transaction }) { if (!output) { // If we get an error, it is possible that `output = null`. // In that case, we define it to be an empty array. @@ -107,9 +120,19 @@ module.exports = function initialize(shim, vectorstores) { return } - recordVectorSearch({ request, k, output, agent, shim, segment, pkgVersion, err }) + recordVectorSearch({ + request, + k, + output, + agent, + shim, + segment, + transaction, + pkgVersion, + err + }) - segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) + transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) } }) } diff --git a/lib/instrumentation/mongodb/v4-mongo.js b/lib/instrumentation/mongodb/v4-mongo.js index a36c44fcde..0148674531 100644 --- a/lib/instrumentation/mongodb/v4-mongo.js +++ b/lib/instrumentation/mongodb/v4-mongo.js @@ -13,27 +13,7 @@ const { instrumentDb, parseAddress } = require('./common') - -/** - * parser used to grab the collection and operation - * from a running query - * - * @param {object} operation mongodb operation - * @returns {object} { operation, collection } parsed operation and collection - */ -function queryParser(operation) { - let collection = this.collectionName || 'unknown' - - // cursor methods have collection on namespace.collection - if (this?.namespace?.collection) { - collection = this.namespace.collection - // (un)ordered bulk operations have collection on different key - } else if (this?.s?.collection?.collectionName) { - collection = this.s.collection.collectionName - } - - return { operation, collection } -} +const queryParser = require('../../db/query-parsers/mongodb') /** * `commandStarted` handler used to diff --git a/lib/instrumentation/nextjs/next-server.js b/lib/instrumentation/nextjs/next-server.js index c2528be5d6..37272e6a35 100644 --- a/lib/instrumentation/nextjs/next-server.js +++ b/lib/instrumentation/nextjs/next-server.js @@ -122,10 +122,8 @@ module.exports = function initialize(shim, nextServer) { } function assignParameters(shim, parameters) { - const activeSegment = shim.getActiveSegment() - if (activeSegment) { - const transaction = activeSegment.transaction - + const transaction = shim.tracer.getTransaction() + if (transaction) { const prefixedParameters = shim.prefixRouteParameters(parameters) // We have to add params because this framework doesn't diff --git a/lib/instrumentation/nextjs/utils.js b/lib/instrumentation/nextjs/utils.js index 95c0645a32..3e0aef73f0 100644 --- a/lib/instrumentation/nextjs/utils.js +++ b/lib/instrumentation/nextjs/utils.js @@ -56,16 +56,3 @@ utils.isMiddlewareInstrumentationSupported = function isMiddlewareInstrumentatio semver.gte(version, MIN_MW_SUPPORTED_VERSION) && semver.lte(version, MAX_MW_SUPPORTED_VERSION) ) } - -/** - * Depending on the Next.js version the segment tree varies as it adds setTimeout segments. - * This util will find the segment that has `getServerSideProps` in the name - * - * @param {object} rootSegment trace root - * @returns {object} getServerSideProps segment - */ -utils.getServerSidePropsSegment = function getServerSidePropsSegment(rootSegment) { - return rootSegment.children[0].children.find((segment) => - segment.name.includes('getServerSideProps') - ) -} diff --git a/lib/instrumentation/openai.js b/lib/instrumentation/openai.js index d159669224..22f8cd6240 100644 --- a/lib/instrumentation/openai.js +++ b/lib/instrumentation/openai.js @@ -89,11 +89,11 @@ function recordEvent({ agent, type, msg }) { * * @param {object} params input params * @param {Agent} params.agent NR agent instance - * @param {TraceSegment} params.segment active segment + * @param {Transaction} params.transaction active transaction */ -function addLlmMeta({ agent, segment }) { +function addLlmMeta({ agent, transaction }) { agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount() - segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) + transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) } /** @@ -110,8 +110,17 @@ function addLlmMeta({ agent, segment }) { * @param {object} params.request chat completion params * @param {object} params.response chat completion response * @param {boolean} [params.err] err if it exists + * @param {Transaction} params.transaction active transaction */ -function recordChatCompletionMessages({ agent, shim, segment, request, response, err }) { +function recordChatCompletionMessages({ + agent, + shim, + segment, + request, + response, + err, + transaction +}) { if (shouldSkipInstrumentation(agent.config, shim) === true) { shim.logger.debug('skipping sending of ai data') return @@ -130,6 +139,7 @@ function recordChatCompletionMessages({ agent, shim, segment, request, response, const completionSummary = new LlmChatCompletionSummary({ agent, segment, + transaction, request, response, withError: err != null @@ -141,6 +151,7 @@ function recordChatCompletionMessages({ agent, shim, segment, request, response, const completionMsg = new LlmChatCompletionMessage({ agent, segment, + transaction, request, response, index, @@ -155,7 +166,7 @@ function recordChatCompletionMessages({ agent, shim, segment, request, response, if (err) { const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response }) - agent.errors.add(segment.transaction, err, llmError) + agent.errors.add(transaction, err, llmError) } delete response.headers @@ -170,7 +181,7 @@ function recordChatCompletionMessages({ agent, shim, segment, request, response, * messages * */ -function instrumentStream({ agent, shim, request, response, segment }) { +function instrumentStream({ agent, shim, request, response, segment, transaction }) { if (!agent.config.ai_monitoring.streaming.enabled) { shim.logger.warn( '`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.' @@ -209,6 +220,7 @@ function instrumentStream({ agent, shim, request, response, segment }) { agent: shim.agent, shim, segment, + transaction, request, response: chunk, err @@ -275,21 +287,22 @@ module.exports = function initialize(agent, openai, moduleName, shim) { return new RecorderSpec({ name: OPENAI.COMPLETION, promise: true, - after({ error: err, result: response, segment }) { + after({ error: err, result: response, segment, transaction }) { if (request.stream) { - instrumentStream({ agent, shim, request, response, segment }) + instrumentStream({ agent, shim, request, response, segment, transaction }) } else { recordChatCompletionMessages({ agent, shim, segment, + transaction, request, response, err }) } - addLlmMeta({ agent, segment }) + addLlmMeta({ agent, transaction }) } }) } @@ -307,8 +320,8 @@ module.exports = function initialize(agent, openai, moduleName, shim) { return new RecorderSpec({ name: OPENAI.EMBEDDING, promise: true, - after({ error: err, result: response, segment }) { - addLlmMeta({ agent, segment }) + after({ error: err, result: response, segment, transaction }) { + addLlmMeta({ agent, transaction }) if (!response) { // If we get an error, it is possible that `response = null`. @@ -332,6 +345,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) { const embedding = new LlmEmbedding({ agent, segment, + transaction, request, response, withError: err != null @@ -341,7 +355,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) { if (err) { const llmError = new LlmErrorMessage({ cause: err, embedding, response }) - shim.agent.errors.add(segment.transaction, err, llmError) + shim.agent.errors.add(transaction, err, llmError) } // cleanup keys on response before returning to user code diff --git a/lib/instrumentation/restify.js b/lib/instrumentation/restify.js index 85960bf157..463cc733de 100644 --- a/lib/instrumentation/restify.js +++ b/lib/instrumentation/restify.js @@ -24,13 +24,13 @@ module.exports = function initialize(_agent, restify, _moduleName, shim) { shim.wrap(http.ServerResponse.prototype, methods, function wrapMethod(shim, fn) { return function wrappedMethod() { - const segment = shim.getActiveSegment() + const transaction = shim.tracer.getTransaction() - if (segment) { + if (transaction) { // Freezing the name state prevents transaction names from potentially being // manipulated by asynchronous response methods executed as part of res.send() // but before `next()` is called. - segment.transaction.nameState.freeze() + transaction.nameState.freeze() } return fn.apply(this, arguments) diff --git a/lib/instrumentation/superagent.js b/lib/instrumentation/superagent.js index 185925ee55..94aaa64cc9 100644 --- a/lib/instrumentation/superagent.js +++ b/lib/instrumentation/superagent.js @@ -48,8 +48,8 @@ function wrapHttpReq(shim, fn, name, req) { function wrapCallback(shim, callback) { return function wrappedCallback() { - const segment = shim.getSegment(this) - if (segment && segment.transaction.isActive()) { + const segment = shim.getActiveSegment(this) + if (segment) { shim.bindCallbackSegment(null, this, '_callback', segment) } return callback.apply(this, arguments) @@ -58,7 +58,7 @@ function wrapCallback(shim, callback) { function wrapThen(shim, then) { return function wrappedThen(resolve, reject) { - const segment = shim.getSegment(this) || shim.getActiveSegment() + const segment = shim.getActiveSegment(this) if (!segment) { return then.apply(this, arguments) } diff --git a/lib/instrumentation/undici.js b/lib/instrumentation/undici.js index fca5d17bb7..3291f324ab 100644 --- a/lib/instrumentation/undici.js +++ b/lib/instrumentation/undici.js @@ -50,8 +50,8 @@ module.exports.unsubscribe = function unsubscribe() { } /** - * Retrieves the current segment in transaction(parent in our context) from executionAsyncResource - * or from `shim.getSegment()` then adds to the executionAsyncResource for future + * Retrieves the current segment(parent in our context) and transaction from executionAsyncResource + * or from context manager then adds to the executionAsyncResource for future * undici requests within same async context. * * It was found that when running concurrent undici requests @@ -63,12 +63,12 @@ module.exports.unsubscribe = function unsubscribe() { * and the request to the transaction is using a keep alive there is a chance the * executionAsyncResource may be incorrect because of shared connections. To revert to a more * naive tracking of parent set `config.feature_flag.undici_async_tracking: false` and - * it will just call `shim.getSegment()` + * it will just get the segment and transaction from the context manager. * * @param {Shim} shim instance of shim * @returns {TraceSegment} parent segment */ -function getParentSegment(shim) { +function getParentContext(shim) { const { config } = shim.agent if (config.feature_flag.undici_async_tracking) { const resource = executionAsyncResource() @@ -76,10 +76,13 @@ function getParentSegment(shim) { if (!resource[symbols.parentSegment]) { const parent = shim.getSegment() resource[symbols.parentSegment] = parent + const tx = shim.tracer.getTransaction() + resource[symbols.transaction] = tx } - return resource[symbols.parentSegment] + return { segment: resource[symbols.parentSegment], transaction: resource[symbols.transaction] } } - return shim.getSegment() + + return shim.tracer.getContext() } /** @@ -113,25 +116,25 @@ function addDTHeaders({ transaction, config, request }) { * @param {object} params object to fn * @param {Shim} params.shim instance of shim * @param {object} params.request undici request object - * @param {object} params.parentSegment current active, about to be parent of external segment + * @param {TraceSegment} params.segment current active, about to be parent of external segment */ -function createExternalSegment({ shim, request, parentSegment }) { +function createExternalSegment({ shim, request, segment }) { const url = new URL(request.origin + request.path) const obfuscatedPath = urltils.obfuscatePath(shim.agent.config, url.pathname) const name = NAMES.EXTERNAL.PREFIX + url.host + obfuscatedPath // Metrics for `External/` will have a suffix of undici // We will have to see if this matters for people only using fetch // It's undici under the hood so ¯\_(ツ)_/¯ - const segment = shim.createSegment(name, recordExternal(url.host, 'undici'), parentSegment) + const externalSegment = shim.createSegment(name, recordExternal(url.host, 'undici'), segment) // the captureExternalAttributes expects queryParams to be an object, do conversion // to object see: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams const queryParams = Object.fromEntries(url.searchParams.entries()) - if (segment) { - segment.start() - shim.setActiveSegment(segment) - segment.captureExternalAttributes({ + if (externalSegment) { + externalSegment.start() + shim.setActiveSegment(externalSegment) + externalSegment.captureExternalAttributes({ protocol: url.protocol, hostname: url.hostname, host: url.host, @@ -141,7 +144,7 @@ function createExternalSegment({ shim, request, parentSegment }) { queryParams }) - request[symbols.segment] = segment + request[symbols.segment] = externalSegment } } @@ -157,21 +160,22 @@ function createExternalSegment({ shim, request, parentSegment }) { */ function requestCreateHook(shim, { request }) { const { config } = shim.agent - const parentSegment = getParentSegment(shim) - request[symbols.parentSegment] = parentSegment - if (!parentSegment || (parentSegment && parentSegment.opaque)) { + const { transaction, segment } = getParentContext(shim) + request[symbols.parentSegment] = segment + request[symbols.transaction] = transaction + if (!(segment || transaction) || segment?.opaque) { logger.trace( 'Not capturing data for outbound request (%s) because parent segment opaque (%s)', request.path, - parentSegment && parentSegment.name + segment?.name ) return } try { - addDTHeaders({ transaction: parentSegment.transaction, config, request }) - createExternalSegment({ shim, request, parentSegment }) + addDTHeaders({ transaction, config, request }) + createExternalSegment({ shim, request, segment }) } catch (err) { logger.warn(err, 'Unable to create external segment') } @@ -190,6 +194,7 @@ function requestCreateHook(shim, { request }) { function requestHeadersHook(shim, { request, response }) { const { config } = shim.agent const activeSegment = request[symbols.segment] + const transaction = request[symbols.transaction] if (!activeSegment) { return } @@ -203,7 +208,12 @@ function requestHeadersHook(shim, { request, response }) { const decodedAppData = cat.parseAppData(config, appData) const attrs = activeSegment.getAttributes() const url = new URL(attrs.url) - cat.assignCatToSegment(decodedAppData, activeSegment, url.host) + cat.assignCatToSegment({ + appData: decodedAppData, + segment: activeSegment, + host: url.host, + transaction + }) } catch (err) { logger.warn(err, 'Cannot add CAT data to segment') } @@ -223,11 +233,12 @@ function requestHeadersHook(shim, { request, response }) { function endAndRestoreSegment(shim, { request, error }) { const activeSegment = request[symbols.segment] const parentSegment = request[symbols.parentSegment] + const tx = request[symbols.transaction] if (activeSegment) { activeSegment.end() if (error) { - handleError(shim, activeSegment, error) + handleError(shim, tx, error) } if (parentSegment) { @@ -240,11 +251,10 @@ function endAndRestoreSegment(shim, { request, error }) { * Adds the error to the active transaction * * @param {Shim} shim instance of shim - * @param {TraceSegment} activeSegment active segment + * @param {Transaction} tx active transaction * @param {Error} error error from undici request */ -function handleError(shim, activeSegment, error) { +function handleError(shim, tx, error) { logger.trace(error, 'Captured outbound error on behalf of the user.') - const { transaction } = activeSegment - shim.agent.errors.add(transaction, error) + shim.agent.errors.add(tx, error) } diff --git a/lib/instrumentation/when/contextualizer.js b/lib/instrumentation/when/contextualizer.js index f1050fc0a5..ec089cda20 100644 --- a/lib/instrumentation/when/contextualizer.js +++ b/lib/instrumentation/when/contextualizer.js @@ -6,8 +6,9 @@ 'use strict' const symbols = require('../../symbols') -function Context(segment) { +function Context(segment, transaction) { this.segments = [segment] + this.transaction = transaction } Context.prototype = Object.create(null) @@ -86,10 +87,11 @@ function bindChild(ctxlzr, next) { * * @param {Function} prev previous function in chain * @param {Function} next next function in chain - * @param {object} segment proper segment to bind + * @param {TraceSegment} segment proper segment to bind + * @param {Transaction} transaction proper transaction to bind * @returns {void} */ -Contextualizer.link = function link(prev, next, segment) { +Contextualizer.link = function link(prev, next, segment, transaction) { let ctxlzr = prev && prev[symbols.context] if (ctxlzr && !ctxlzr.isActive()) { ctxlzr = prev[symbols.context] = null @@ -100,7 +102,7 @@ Contextualizer.link = function link(prev, next, segment) { } else if (segment) { // This next promise is the root of a chain. Either there was no previous // promise or the promise was created out of context. - next[symbols.context] = new Contextualizer(0, new Context(segment)) + next[symbols.context] = new Contextualizer(0, new Context(segment, transaction)) } } @@ -114,7 +116,7 @@ Contextualizer.prototype = Object.create(null) Contextualizer.prototype.isActive = function isActive() { const segments = this.context.segments const segment = segments[this.idx] || segments[this.parentIdx] || segments[0] - return segment && segment.transaction.isActive() + return segment && this.context.transaction.isActive() } /** @@ -122,7 +124,7 @@ Contextualizer.prototype.isActive = function isActive() { * If there is none it will get the segment at the parent index or the first one * then assign to current index. * - * @returns {object} segment by idx or parentIdx or the first one + * @returns {TraceSegment} segment by idx or parentIdx or the first one */ Contextualizer.prototype.getSegment = function getSegment() { const segments = this.context.segments @@ -133,11 +135,20 @@ Contextualizer.prototype.getSegment = function getSegment() { return segment } +/** + * Gets the transaction from the context. + * + * @returns {Transaction} transaction from context + */ +Contextualizer.prototype.getTransaction = function getTransaction() { + return this.context.transaction +} + /** * Sets the set to the appropriate index * - * @param {object} segment segment to assign to index - * @returns {object} same segment passed in + * @param {TraceSegment} segment segment to assign to index + * @returns {TraceSegment} same segment passed in */ Contextualizer.prototype.setSegment = function setSegment(segment) { this.context.segments[this.idx] = segment diff --git a/lib/instrumentation/when/index.js b/lib/instrumentation/when/index.js index daa4216b87..b047403c5f 100644 --- a/lib/instrumentation/when/index.js +++ b/lib/instrumentation/when/index.js @@ -82,11 +82,13 @@ module.exports = function initialize(shim, when) { return new Promise(executor) } - const parent = agent.tracer.getSegment() + const context = agent.tracer.getContext() + const parent = context.segment + const transaction = context.transaction let promise = null if ( !parent || - !parent.transaction.isActive() || + !transaction.isActive() || typeof executor !== 'function' || arguments.length !== 1 ) { @@ -98,21 +100,22 @@ module.exports = function initialize(shim, when) { promise = new (Promise.bind.apply(Promise, cnstrctArgs))() } else { const segmentName = 'Promise ' + (executor.name || ANONYMOUS) - const context = { + const ctx = { promise: null, self: null, args: null } - promise = new Promise(wrapExecutorContext(context)) - context.promise = promise - const segment = _createSegment(segmentName) - Contextualizer.link(null, promise, segment) + promise = new Promise(wrapExecutorContext(ctx)) + ctx.promise = promise + const segment = _createSegment({ name: segmentName, transaction, parent }) + const newContext = context.enterSegment({ segment }) + Contextualizer.link(null, promise, segment, transaction) try { // Must run after promise is defined so that `__NR_wrapper` can be set. - agent.tracer.bindFunction(executor, segment, true).apply(context.self, context.args) + agent.tracer.bindFunction(executor, newContext, true).apply(ctx.self, ctx.args) } catch (e) { - context.args[1](e) + ctx.args[1](e) } } @@ -235,6 +238,7 @@ module.exports = function initialize(shim, when) { const segmentNamePrefix = 'Promise#' + name + ' ' const thenSegment = agent.tracer.getSegment() + const transaction = agent.tracer.getTransaction() const promise = this const ctx = { next: undefined, useAllParams, isWrapped: false, segmentNamePrefix } @@ -244,7 +248,7 @@ module.exports = function initialize(shim, when) { // If we got a promise (which we should have), link the parent's context. if (!ctx.isWrapped && ctx.next instanceof Promise && ctx.next !== promise) { - Contextualizer.link(promise, ctx.next, thenSegment) + Contextualizer.link(promise, ctx.next, thenSegment, transaction) } return ctx.next } @@ -277,15 +281,19 @@ module.exports = function initialize(shim, when) { let promSegment = ctx.next[symbols.context].getSegment() const segmentName = ctx.segmentNamePrefix + (fn.name || ANONYMOUS) - const segment = _createSegment(segmentName, promSegment) + const transaction = ctx.next[symbols.context].getTransaction() + const segment = _createSegment({ name: segmentName, parent: promSegment, transaction }) if (segment && segment !== promSegment) { ctx.next[symbols.context].setSegment(segment) promSegment = segment } + let context = agent.tracer.getContext() + context = context.enterSegment({ transaction, segment: promSegment }) + let ret = null try { - ret = agent.tracer.bindFunction(fn, promSegment, true).apply(this, arguments) + ret = agent.tracer.bindFunction(fn, context, true).apply(this, arguments) } finally { if (ret && typeof ret.then === 'function') { // eslint-disable-next-line sonarjs/no-dead-store @@ -311,10 +319,12 @@ module.exports = function initialize(shim, when) { const CAST_SEGMENT_NAME = 'Promise.' + name // eslint-disable-next-line camelcase return function __NR_wrappedCast() { - const segment = _createSegment(CAST_SEGMENT_NAME) + const transaction = agent.tracer.getTransaction() + const parent = agent.tracer.getSegment() + const segment = _createSegment({ name: CAST_SEGMENT_NAME, transaction, parent }) const prom = cast.apply(this, arguments) if (segment) { - Contextualizer.link(null, prom, segment) + Contextualizer.link(null, prom, segment, transaction) } return prom } @@ -325,13 +335,15 @@ module.exports = function initialize(shim, when) { * if `config.feature_flag.promise_segments` is true * Otherwise it just returns the current if existing or gets the current * - * @param {string} name name of segment to create - * @param {object} parent current parent segment - * @returns {object} segment + * @param {object} params to function + * @param {string} params.name name of segment to create + * @param {TraceSegment} params.parent current parent segment + * @param {Transaction} params.transaction active transaction + * @returns {TraceSegment} segment */ - function _createSegment(name, parent) { + function _createSegment({ name, parent, transaction }) { return agent.config.feature_flag.promise_segments === true - ? agent.tracer.createSegment(name, null, parent) + ? agent.tracer.createSegment({ name, parent, transaction }) : parent || agent.tracer.getSegment() } } diff --git a/lib/llm-events/aws-bedrock/event.js b/lib/llm-events/aws-bedrock/event.js index 407b2cab59..f0107c8a0d 100644 --- a/lib/llm-events/aws-bedrock/event.js +++ b/lib/llm-events/aws-bedrock/event.js @@ -22,9 +22,8 @@ const defaultParams = { agent: {}, bedrockCommand: {}, bedrockResponse: {}, - segment: { - transaction: {} - } + transaction: {}, + segment: {} } /** @@ -47,7 +46,7 @@ class LlmEvent { params = Object.assign({}, defaultParams, params) this.constructionParams = params - const { agent, bedrockCommand, bedrockResponse, segment } = params + const { agent, bedrockCommand, bedrockResponse, segment, transaction } = params this.bedrockCommand = bedrockCommand this.bedrockResponse = bedrockResponse @@ -56,7 +55,7 @@ class LlmEvent { this.ingest_source = 'Node' this.appName = agent.config.applications()[0] this.span_id = segment.id - this.trace_id = segment.transaction.traceId + this.trace_id = transaction.traceId this.request_id = this.bedrockResponse.requestId this.metadata = agent diff --git a/lib/llm-events/langchain/event.js b/lib/llm-events/langchain/event.js index 1beea0eced..622d5fd563 100644 --- a/lib/llm-events/langchain/event.js +++ b/lib/llm-events/langchain/event.js @@ -28,9 +28,8 @@ const { isSimpleObject } = require('../../util/objects') */ const defaultParams = { agent: {}, - segment: { - transaction: {} - }, + transaction: {}, + segment: {}, tags: [], runId: '', metadata: {}, @@ -54,12 +53,12 @@ class LangChainEvent extends BaseEvent { constructor(params = defaultParams) { params = Object.assign({}, defaultParams, params) super(params) - const { agent, segment } = params + const { agent, segment, transaction } = params this.appName = agent.config.applications()[0] this.span_id = segment?.id this.request_id = params.runId - this.trace_id = segment?.transaction?.traceId + this.trace_id = transaction?.traceId this.langchainMeta = params.metadata this.metadata = agent this.tags = Array.isArray(params.tags) ? params.tags.join(',') : params.tags diff --git a/lib/llm-events/openai/chat-completion-message.js b/lib/llm-events/openai/chat-completion-message.js index 61f101a464..14465229a3 100644 --- a/lib/llm-events/openai/chat-completion-message.js +++ b/lib/llm-events/openai/chat-completion-message.js @@ -7,8 +7,17 @@ const LlmEvent = require('./event') module.exports = class LlmChatCompletionMessage extends LlmEvent { - constructor({ agent, segment, request = {}, response = {}, index = 0, message, completionId }) { - super({ agent, segment, request, response }) + constructor({ + agent, + segment, + request = {}, + response = {}, + index = 0, + message, + completionId, + transaction + }) { + super({ agent, segment, request, response, transaction }) this.id = `${response.id}-${index}` this.role = message?.role this.sequence = index diff --git a/lib/llm-events/openai/chat-completion-summary.js b/lib/llm-events/openai/chat-completion-summary.js index 0d84982bd9..0b5435c150 100644 --- a/lib/llm-events/openai/chat-completion-summary.js +++ b/lib/llm-events/openai/chat-completion-summary.js @@ -7,8 +7,8 @@ const LlmEvent = require('./event') module.exports = class LlmChatCompletionSummary extends LlmEvent { - constructor({ agent, segment, request = {}, response = {}, withError = false }) { - super({ agent, segment, request, response, responseAttrs: true }) + constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) { + super({ agent, segment, request, response, responseAttrs: true, transaction }) this.error = withError this['request.max_tokens'] = request.max_tokens this['request.temperature'] = request.temperature diff --git a/lib/llm-events/openai/embedding.js b/lib/llm-events/openai/embedding.js index 1f2ee78bfb..004088cbd7 100644 --- a/lib/llm-events/openai/embedding.js +++ b/lib/llm-events/openai/embedding.js @@ -7,8 +7,8 @@ const LlmEvent = require('./event') module.exports = class LlmEmbedding extends LlmEvent { - constructor({ agent, segment, request = {}, response = {}, withError = false }) { - super({ agent, segment, request, response, responseAttrs: true }) + constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) { + super({ agent, segment, request, response, responseAttrs: true, transaction }) this.error = withError if (agent.config.ai_monitoring.record_content.enabled === true) { diff --git a/lib/llm-events/openai/event.js b/lib/llm-events/openai/event.js index e31e491e29..6a84681d82 100644 --- a/lib/llm-events/openai/event.js +++ b/lib/llm-events/openai/event.js @@ -9,13 +9,13 @@ const BaseEvent = require('../event') const { makeId } = require('../../util/hashes') module.exports = class LlmEvent extends BaseEvent { - constructor({ agent, segment, request, response, responseAttrs = false }) { + constructor({ agent, segment, request, response, responseAttrs = false, transaction }) { super() this.id = makeId(36) this.appName = agent.config.applications()[0] this.request_id = response?.headers?.['x-request-id'] - this.trace_id = segment?.transaction?.traceId + this.trace_id = transaction?.traceId this.span_id = segment?.id this['response.model'] = response.model this.vendor = 'openai' diff --git a/lib/metrics/recorders/custom.js b/lib/metrics/recorders/custom.js index b2d1c588c2..f4623202e1 100644 --- a/lib/metrics/recorders/custom.js +++ b/lib/metrics/recorders/custom.js @@ -7,10 +7,9 @@ const NAMES = require('../names') -function record(segment, scope) { +function record(segment, scope, transaction) { const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction + const exclusive = segment.getExclusiveDurationInMillis(transaction.trace) const name = NAMES.CUSTOM + NAMES.ACTION_DELIMITER + segment.name if (scope) { diff --git a/lib/metrics/recorders/database-operation.js b/lib/metrics/recorders/database-operation.js index 11e4c6ea46..c602aec2b2 100644 --- a/lib/metrics/recorders/database-operation.js +++ b/lib/metrics/recorders/database-operation.js @@ -20,14 +20,9 @@ const metrics = require('../names') * @see DatastoreShim#recordOperation * @see MetricFunction */ -function recordOperationMetrics(segment, scope) { - if (!segment) { - return - } - +function recordOperationMetrics(segment, scope, transaction) { const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction + const exclusive = segment.getExclusiveDurationInMillis(transaction.trace) const type = transaction.isWeb() ? 'allWeb' : 'allOther' const operation = segment.name diff --git a/lib/metrics/recorders/database.js b/lib/metrics/recorders/database.js index 7d4e172ec6..c53a1e73d0 100644 --- a/lib/metrics/recorders/database.js +++ b/lib/metrics/recorders/database.js @@ -14,10 +14,9 @@ const { DESTINATIONS } = require('../../config/attribute-filter') * @param {string} [scope] - The scope of the segment. */ -function recordQueryMetrics(segment, scope) { +function recordQueryMetrics(segment, scope, transaction) { const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction + const exclusive = segment.getExclusiveDurationInMillis(transaction.trace) const type = transaction.isWeb() ? DB.WEB : DB.OTHER const thisTypeSlash = this.type + '/' const operation = DB.OPERATION + '/' + thisTypeSlash + this.operation @@ -61,7 +60,13 @@ function recordQueryMetrics(segment, scope) { } if (this.raw) { - segment.transaction.agent.queries.add(segment, this.type.toLowerCase(), this.raw, this.trace) + transaction.agent.queries.add({ + segment, + transaction, + type: this.type.toLowerCase(), + query: this.raw, + trace: this.trace + }) } } diff --git a/lib/metrics/recorders/generic.js b/lib/metrics/recorders/generic.js index 3deed95d32..945b61c3c6 100644 --- a/lib/metrics/recorders/generic.js +++ b/lib/metrics/recorders/generic.js @@ -5,10 +5,9 @@ 'use strict' -function record(segment, scope) { +function record(segment, scope, transaction) { const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction + const exclusive = segment.getExclusiveDurationInMillis(transaction.trace) if (scope) { transaction.measure(segment.name, scope, duration, exclusive) diff --git a/lib/metrics/recorders/http.js b/lib/metrics/recorders/http.js index 83a0880bb4..4ef57aff07 100644 --- a/lib/metrics/recorders/http.js +++ b/lib/metrics/recorders/http.js @@ -10,13 +10,12 @@ const recordDistributedTrace = require('./distributed-trace') const TO_MILLIS = 1e3 -function recordWeb(segment, scope) { +function recordWeb(segment, scope, tx) { // in web metrics, scope is required if (!scope) { return } - const tx = segment.transaction // if there was a nested webTransaction use its recorder instead if (tx.type === 'web' && tx.baseSegment && segment !== tx.baseSegment) { return @@ -24,9 +23,9 @@ function recordWeb(segment, scope) { const duration = segment.getDurationInMillis() const totalTime = tx.trace.getTotalTimeDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() + const exclusive = segment.getExclusiveDurationInMillis(tx.trace) const partial = segment.partialName - const config = segment.transaction.agent.config + const config = tx.agent.config // named / key transaction support requires per-name apdexT const keyApdexInMillis = config.web_transactions_apdex[scope] * TO_MILLIS || 0 diff --git a/lib/metrics/recorders/http_external.js b/lib/metrics/recorders/http_external.js index 2e3bd7e2e4..0a656b407f 100644 --- a/lib/metrics/recorders/http_external.js +++ b/lib/metrics/recorders/http_external.js @@ -8,10 +8,9 @@ const EXTERNAL = require('../../metrics/names').EXTERNAL function recordExternal(host, library) { - return function externalRecorder(segment, scope) { + return function externalRecorder(segment, scope, transaction) { const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction + const exclusive = segment.getExclusiveDurationInMillis(transaction.trace) const metricName = EXTERNAL.PREFIX + host + '/' + library const rollupType = transaction.isWeb() ? EXTERNAL.WEB : EXTERNAL.OTHER const rollupHost = EXTERNAL.PREFIX + host + '/all' diff --git a/lib/metrics/recorders/message-transaction.js b/lib/metrics/recorders/message-transaction.js index ed7172d953..740223d867 100644 --- a/lib/metrics/recorders/message-transaction.js +++ b/lib/metrics/recorders/message-transaction.js @@ -7,15 +7,14 @@ const NAMES = require('../../metrics/names.js') -function recordMessageTransaction(segment, scope) { - const tx = segment.transaction +function recordMessageTransaction(segment, scope, tx) { if (tx.type !== 'message' || tx.baseSegment !== segment) { return } const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const totalTime = segment.transaction.trace.getTotalTimeDurationInMillis() + const exclusive = segment.getExclusiveDurationInMillis(tx.trace) + const totalTime = tx.trace.getTotalTimeDurationInMillis() if (scope) { tx.measure(scope, null, duration, exclusive) diff --git a/lib/metrics/recorders/middleware.js b/lib/metrics/recorders/middleware.js index 61693cfb88..57adfd0096 100644 --- a/lib/metrics/recorders/middleware.js +++ b/lib/metrics/recorders/middleware.js @@ -14,10 +14,9 @@ * @returns {Function} recorder for middleware */ function makeMiddlewareRecorder(_shim, metricName) { - return function middlewareMetricRecorder(segment, scope) { + return function middlewareMetricRecorder(segment, scope, transaction) { const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const transaction = segment.transaction + const exclusive = segment.getExclusiveDurationInMillis(transaction.trace) if (scope) { transaction.measure(metricName, scope, duration, exclusive) diff --git a/lib/metrics/recorders/other.js b/lib/metrics/recorders/other.js index 11d472d394..47f0c642c1 100644 --- a/lib/metrics/recorders/other.js +++ b/lib/metrics/recorders/other.js @@ -8,16 +8,15 @@ const NAMES = require('../../metrics/names') const recordDistributedTrace = require('./distributed-trace') -function recordBackground(segment, scope) { +function recordBackground(segment, scope, tx) { // if there was a nested otherTransaction use its recorder instead - const tx = segment.transaction if (tx.type === 'bg' && tx.baseSegment && segment !== tx.baseSegment) { return } const duration = segment.getDurationInMillis() - const exclusive = segment.getExclusiveDurationInMillis() - const totalTime = segment.transaction.trace.getTotalTimeDurationInMillis() + const exclusive = segment.getExclusiveDurationInMillis(tx.trace) + const totalTime = tx.trace.getTotalTimeDurationInMillis() const name = segment.partialName if (scope) { diff --git a/lib/otel/rules.js b/lib/otel/rules.js new file mode 100644 index 0000000000..e69b4dd97c --- /dev/null +++ b/lib/otel/rules.js @@ -0,0 +1,241 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const { SpanKind } = require('@opentelemetry/api') +const srcJson = require('./rules.json') + +/** + * Mapping rules are stored, and provided to the agent, as JSON. We call these + * representations `SerializedOtelMappingRule`. + * + * TODO: rewrite all of this once the spec inventor settles on a final shape. + * + * @typedef {object} SerializedOtelMappingRule + * @property {string} name The name of the rule. This will be used to detect + * the kind of span (server or client). + * @property {object} matcher Defines the required properties of the OTEL span + * that are needed in order for the rule to match. + * @property {string[]} matcher.required_metric_names The metrics present + * on the span that must be present. When not set, the rule does not match + * metrics. + * @property {string} [matcher.metric_assumed_unit] Specifies the unit to + * use when the metric does not include a specifier. When not set, the rule does + * not match metrics. TODO: what are the possible units? + * @property {string} [matcher.metric_unit_validator] An identifier for a + * validator to be used to validate the metric's unit. Not applicable if the + * rule is not matching a metric. + * @property {string[]} matcher.required_span_kinds List of OTEL span kinds that + * this rule will match. Possible values are "server", "client", and "internal". + * @property {string[]} matcher.required_attribute_keys List of OTEL span + * attributes that must be present for the rule to match. Each value is a dot + * separated path. + * @property {object} target Describes the New Relic entity(ies) that the rule + * will map to. + * @property {string[]} target.target_metrics The + * @property {object[{attribute, template}]} target.attribute_mappings + */ + +/** + * Represents an OTEL to New Relic span mapping rule. + */ +class Rule { + static OTEL_SPAN_KIND_SERVER = 'server' + static OTEL_SPAN_KIND_CLIENT = 'client' + static OTEL_SPAN_KIND_PRODUCER = 'producer' + static OTEL_SPAN_KIND_INTERNAL = 'internal' + + #name + #spanKinds + #requiredAttributes + #type + #mappings + + /** + * @param {SerializedOtelMappingRule} input A serialized rule to parse. + */ + constructor(input) { + // See https://opentelemetry.io/docs/specs/otel/trace/api/#spankind for + // information about how OTEL classifies server vs client. + + if (Object.hasOwn(input.matcher, 'required_span_kinds') === false) { + throw Error('we only support "span" matching rules') + } + + this.#name = input.name + this.#type = input.type + this.#spanKinds = input.matcher.required_span_kinds?.map((v) => v.toLowerCase()) ?? [] + this.#requiredAttributes = input.matcher.required_attribute_keys ?? [] + this.#mappings = input.target.attribute_mappings ?? [] + } + + get name() { + return this.#name + } + + get type() { + if (this.#type) { + return this.#type + } + return this.isServerRule ? 'server' : 'client' + } + + get isClientRule() { + return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_CLIENT) + } + + get isConsumer() { + return this.#spanKinds.includes('consumer') + } + + get isInternalRule() { + return this.#spanKinds.includes('internal') + } + + get isProducerRule() { + return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_PRODUCER) + } + + get isServerRule() { + return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_SERVER) || this.isConsumer + } + + /** + * Determines if the given span satifies this rule's requirements. + * + * @param {object} span An OTEL span instance. + * + * @returns {boolean} `true` if the span satisfies the rule. + */ + matches(span) { + let result = false + + let attrCount = 0 + for (const attr of this.#requiredAttributes) { + if (Object.hasOwn(span.attributes, attr) === true) { + attrCount += 1 + } + } + if (attrCount === this.#requiredAttributes.length) { + result = true + } + + return result + } +} + +class RulesEngine { + #serverRules = new Map() + #fallbackServerRules = new Map() + #clientRules = new Map() + #fallbackClientRules = new Map() + #fallbackInternalRules = new Map() + #fallbackProducerRules = new Map() + + constructor() { + for (const inputRule of srcJson) { + const rule = new Rule(inputRule) + + if (/fallback/i.test(rule.name) === true) { + if (rule.isServerRule === true) { + this.#fallbackServerRules.set(rule.name, rule) + } else if (rule.isClientRule === true) { + this.#fallbackClientRules.set(rule.name, rule) + } else if (rule.isProducerRule === true) { + this.#fallbackProducerRules.set(rule.name, rule) + } else if (rule.isInternalRule === true) { + this.#fallbackInternalRules.set(rule.name, rule) + } + continue + } + + if (rule.isServerRule === true) { + this.#serverRules.set(rule.name, rule) + } else if (rule.isClientRule === true) { + this.#clientRules.set(rule.name, rule) + } + } + } + + /** + * Determines if the span matches any known rules. If the span matches, then + * the matching rule will be returned. + * + * @param {object} otelSpan The span to test. + * + * @returns {Rule|undefined} + */ + // eslint-disable-next-line sonarjs/cognitive-complexity + test(otelSpan) { + let result + + // eslint-disable-next-line sonarjs/no-labels, no-labels + detector: switch (otelSpan.kind) { + case SpanKind.SERVER: + case SpanKind.CONSUMER: { + for (const rule of this.#serverRules.values()) { + if (rule.matches(otelSpan) === true) { + result = rule + // eslint-disable-next-line no-labels + break detector + } + } + for (const rule of this.#fallbackServerRules.values()) { + if (rule.matches(otelSpan) === true) { + result = rule + break + } + } + break + } + + case SpanKind.CLIENT: { + for (const rule of this.#clientRules.values()) { + if (rule.matches(otelSpan) === true) { + result = rule + // eslint-disable-next-line no-labels + break detector + } + } + for (const rule of this.#fallbackClientRules.values()) { + if (rule.matches(otelSpan) === true) { + result = rule + break + } + } + break + } + + // there currently are no producer rules, just fallback + // if we add new rules they will have to be wired up + case SpanKind.PRODUCER: { + for (const rule of this.#fallbackProducerRules.values()) { + if (rule.matches(otelSpan) === true) { + result = rule + break + } + } + break + } + + // there currently are no internal rules, just fallback + // if we add new rules they will have to be wired up + case SpanKind.INTERNAL: { + for (const rule of this.#fallbackInternalRules.values()) { + if (rule.matches(otelSpan) === true) { + result = rule + break + } + } + break + } + } + + return result + } +} + +module.exports = { RulesEngine, Rule } diff --git a/lib/otel/rules.json b/lib/otel/rules.json new file mode 100644 index 0000000000..43afcb912d --- /dev/null +++ b/lib/otel/rules.json @@ -0,0 +1,500 @@ +[ + { + "name": "OtelHttpServer1_23", + "type": "server", + "matcher": { + "required_metric_names": [ + "http.server.request.duration" + ], + "metric_assumed_unit": "s", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "server" + ], + "required_attribute_keys": [ + "http.request.method" + ] + }, + "target": { + "target_metrics": [ + "transaction", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "transactionName", + "template": "WebTransaction/server/${http.request.method} ${http.route}" + }, + { + "attribute": "transactionType", + "template": "Web" + }, + { + "attribute": "error.type", + "template": "${error.type}" + }, + { + "attribute": "metricTimesliceName", + "template": "WebTransaction/server/${http.request.method} ${http.route}" + } + ] + } + }, + { + "name": "OtelHttpServer1_20", + "type": "server", + "matcher": { + "required_metric_names": [ + "http.server.duration" + ], + "metric_assumed_unit": "ms", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "server" + ], + "required_attribute_keys": [ + "http.method" + ] + }, + "target": { + "target_metrics": [ + "transaction", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "transactionName", + "template": "WebTransaction/server/${http.method} ${http.route}" + }, + { + "attribute": "transactionType", + "template": "Web" + }, + { + "attribute": "error.type", + "template": "__http_error_status_code_or_null__" + }, + { + "attribute": "metricTimesliceName", + "template": "WebTransaction/server/${http.method} ${http.route}" + } + ] + } + }, + { + "name": "OtelRpcServer1_20", + "type": "server", + "matcher": { + "required_metric_names": [ + "rpc.server.duration" + ], + "metric_assumed_unit": "ms", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "server" + ], + "required_attribute_keys": [ + "rpc.system" + ] + }, + "target": { + "target_metrics": [ + "transaction", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "transactionName", + "template": "WebTransaction/server/${rpc.system}/${rpc.service:-unknown}.${rpc.method:-unknown}" + }, + { + "attribute": "transactionType", + "template": "Web" + }, + { + "attribute": "error.type", + "template": "__rpc_error_status_code_or_null__" + }, + { + "attribute": "metricTimesliceName", + "template": "WebTransaction/server/${rpc.system}/${rpc.service:-unknown}.${rpc.method:-unknown}" + } + ] + } + }, + { + "name": "FallbackServer", + "type": "server", + "matcher": { + "required_metric_names": [ + "rpc.server.duration", + "http.server.duration", + "http.server.request.duration" + ], + "metric_assumed_unit": "s", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "server" + ], + "required_attribute_keys": [] + }, + "target": { + "target_metrics": [ + "transaction", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "transactionName", + "template": "WebTransaction/server/unknown" + }, + { + "attribute": "transactionType", + "template": "Web" + }, + { + "attribute": "error.type", + "template": "__null__" + }, + { + "attribute": "metricTimesliceName", + "template": "WebTransaction/server/unknown" + } + ] + } + }, + { + "name": "OtelMessagingConsumer1_24", + "type": "consumer", + "matcher": { + "required_span_kinds": [ + "consumer" + ], + "required_attribute_keys": [ + "messaging.operation" + ] + }, + "target": { + "target_metrics": [ + "transaction", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "transactionName", + "template": "OtherTransaction/consumer/${messaging.operation:-unknown}/${messaging.destination.template:-${messaging.destination.name:-unknown}}" + }, + { + "attribute": "transactionType", + "template": "Other" + }, + { + "attribute": "error.type", + "template": "__null__" + }, + { + "attribute": "metricTimesliceName", + "template": "OtherTransaction/consumer/${messaging.operation:-unknown}/${messaging.destination.template:-${messaging.destination.name:-unknown}}" + } + ] + } + }, + { + "name": "FallbackConsumer", + "type": "consumer", + "matcher": { + "required_span_kinds": [ + "consumer" + ], + "required_attribute_keys": [] + }, + "target": { + "target_metrics": [ + "transaction", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "transactionName", + "template": "OtherTransaction/consumer/unknown" + }, + { + "attribute": "transactionType", + "template": "Other" + }, + { + "attribute": "error.type", + "template": "__null__" + }, + { + "attribute": "metricTimesliceName", + "template": "OtherTransaction/consumer/unknown" + } + ] + } + }, + { + "name": "OtelDbClientRedis1_24", + "type": "db", + "matcher": { + "required_span_kinds": [ + "client" + ], + "required_attribute_keys": [ + "db.system" + ], + "attribute_conditions": { + "db.system": "redis" + } + }, + "target": { + "target_metrics": [ + "database", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "Datastore/statement/${db.system}/${db.sql.table}/${name:-unknown}" + }, + { + "attribute": "db.system", + "template": "${db.system}" + }, + { + "attribute": "db.operation", + "template": "${name:-unknown}" + }, + { + "attribute": "db.sql.table", + "template": "${db.sql.table}" + } + ] + } + }, + { + "name": "OtelDbClient1_24", + "type": "db", + "matcher": { + "required_span_kinds": [ + "client" + ], + "required_attribute_keys": [ + "db.system" + ] + }, + "target": { + "target_metrics": [ + "database", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "Datastore/statement/${db.system}/${db.sql.table}/${db.operation:-unknown}" + }, + { + "attribute": "db.system", + "template": "${db.system}" + }, + { + "attribute": "db.operation", + "template": "${db.operation:-unknown}" + }, + { + "attribute": "db.sql.table", + "template": "${db.sql.table}" + } + ] + } + }, + { + "name": "OtelHttpClient1_23", + "type": "external", + "matcher": { + "required_metric_names": [ + "http.client.request.duration" + ], + "metric_assumed_unit": "s", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "client" + ], + "required_attribute_keys": [ + "http.request.method" + ] + }, + "target": { + "target_metrics": [ + "external", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "External/${server.address:-unknown}/all" + }, + { + "attribute": "server.address", + "template": "${server.address:-unknown}" + }, + { + "attribute": "external.host", + "template": "${server.address:-unknown}" + } + ] + } + }, + { + "name": "OtelHttpClient1_20", + "type": "external", + "matcher": { + "required_metric_names": [ + "http.client.duration" + ], + "metric_assumed_unit": "ms", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "client" + ], + "required_attribute_keys": [ + "http.method" + ] + }, + "target": { + "target_metrics": [ + "external", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "External/${net.peer.name:-unknown}/all" + }, + { + "attribute": "server.address", + "template": "${net.peer.name:-unknown}" + }, + { + "attribute": "external.host", + "template": "${net.peer.name:-unknown}" + } + ] + } + }, + { + "name": "OtelRpcClient1_20", + "matcher": { + "required_metric_names": [ + "rpc.client.duration" + ], + "metric_assumed_unit": "ms", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "client" + ], + "required_attribute_keys": [ + "rpc.system" + ] + }, + "target": { + "target_metrics": [ + "external", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "External/${net.peer.name:-unknown}/all" + }, + { + "attribute": "server.address", + "template": "${net.peer.name:-unknown}" + }, + { + "attribute": "external.host", + "template": "${net.peer.name:-unknown}" + } + ] + } + }, + { + "name": "FallbackClient", + "type": "external", + "matcher": { + "required_metric_names": [ + "rpc.client.duration", + "http.client.duration", + "http.client.request.duration" + ], + "metric_assumed_unit": "s", + "metric_unit_validator": "time_unit", + "required_span_kinds": [ + "client" + ], + "required_attribute_keys": [] + }, + "target": { + "target_metrics": [ + "external", + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "External/unknown/all" + }, + { + "attribute": "server.address", + "template": "unknown" + }, + { + "attribute": "external.host", + "template": "unknown" + } + ] + } + }, + { + "name": "FallbackProducer", + "type": "producer", + "matcher": { + "required_span_kinds": [ + "producer" + ], + "required_attribute_keys": [] + }, + "target": { + "target_metrics": [ + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "Producer/${name:-unknown}" + } + ] + } + }, + { + "name": "Fallback", + "type": "internal", + "matcher": { + "required_span_kinds": [ + "internal" + ], + "required_attribute_keys": [] + }, + "target": { + "target_metrics": [ + "blame" + ], + "attribute_mappings": [ + { + "attribute": "metricTimesliceName", + "template": "Custom/${name:-unknown}" + } + ] + } + } +] diff --git a/lib/otel/segment-synthesis.js b/lib/otel/segment-synthesis.js new file mode 100644 index 0000000000..251278e5fe --- /dev/null +++ b/lib/otel/segment-synthesis.js @@ -0,0 +1,68 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { RulesEngine } = require('./rules') +const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' }) +const { + createConsumerSegment, + createDbSegment, + createHttpExternalSegment, + createInternalSegment, + createProducerSegment, + createServerSegment +} = require('./segments') + +class SegmentSynthesizer { + constructor(agent, { logger = defaultLogger } = {}) { + this.agent = agent + this.logger = logger + this.engine = new RulesEngine() + } + + synthesize(otelSpan) { + const rule = this.engine.test(otelSpan) + if (!rule?.type) { + this.logger.debug( + 'Cannot match a rule to span name: %s, kind %s', + otelSpan?.name, + otelSpan?.kind + ) + return + } + + switch (rule.type) { + case 'consumer': { + return createConsumerSegment(this.agent, otelSpan) + } + + case 'db': { + return createDbSegment(this.agent, otelSpan) + } + + case 'external': { + return createHttpExternalSegment(this.agent, otelSpan) + } + + case 'internal': { + return createInternalSegment(this.agent, otelSpan) + } + + case 'producer': { + return createProducerSegment(this.agent, otelSpan) + } + + case 'server': { + return createServerSegment(this.agent, otelSpan) + } + + default: { + this.logger.debug('Found type: %s, no synthesis rule currently built', rule.type) + } + } + } +} + +module.exports = SegmentSynthesizer diff --git a/lib/otel/segments/consumer.js b/lib/otel/segments/consumer.js new file mode 100644 index 0000000000..ee02fc57ae --- /dev/null +++ b/lib/otel/segments/consumer.js @@ -0,0 +1,50 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = createConsumerSegment + +// Notes: +// + https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/messaging/messaging-spans.md +// + We probably want to inspect `messaging.system` so that we can generate +// attributes according to our own internal specs. + +const Transaction = require('../../transaction/') +const { DESTINATIONS, TYPES } = Transaction + +const { + SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_MESSAGING_DESTINATION, + SEMATTRS_MESSAGING_DESTINATION_KIND +} = require('@opentelemetry/semantic-conventions') + +function createConsumerSegment(agent, otelSpan) { + const transaction = new Transaction(agent) + transaction.type = TYPES.BG + + const system = otelSpan.attributes[SEMATTRS_MESSAGING_SYSTEM] ?? 'unknown' + const destination = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION] ?? 'unknown' + const destKind = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION_KIND] ?? 'unknown' + const segmentName = `OtherTransaction/Message/${system}/${destKind}/Named/${destination}` + + const txAttrs = transaction.trace.attributes + txAttrs.addAttribute(DESTINATIONS.TRANS_SCOPE, 'message.queueName', destination) + // txAttrs.addAttribute( + // DESTINATIONS.TRANS_SCOPE, + // 'host', + // + // ) + transaction.name = segmentName + + const segment = agent.tracer.createSegment({ + name: segmentName, + parent: transaction.trace.root, + transaction + }) + transaction.baseSegment = segment + + return { segment, transaction } +} diff --git a/lib/otel/segments/database.js b/lib/otel/segments/database.js new file mode 100644 index 0000000000..42bf0d8861 --- /dev/null +++ b/lib/otel/segments/database.js @@ -0,0 +1,68 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { + SEMATTRS_DB_MONGODB_COLLECTION, + SEMATTRS_DB_SYSTEM, + SEMATTRS_DB_SQL_TABLE, + SEMATTRS_DB_OPERATION, + SEMATTRS_DB_STATEMENT, + DbSystemValues +} = require('@opentelemetry/semantic-conventions') +const parseSql = require('../../db/query-parsers/sql') + +// TODO: This probably has some holes +// I did analysis and tried to apply the best logic +// to extract table/operation +module.exports = function createDbSegment(agent, otelSpan) { + const context = agent.tracer.getContext() + const name = setName(otelSpan) + const segment = agent.tracer.createSegment({ + name, + parent: context.segment, + transaction: context.transaction + }) + return { segment, transaction: context.transaction } +} + +function parseStatement(otelSpan, system) { + let table = otelSpan.attributes[SEMATTRS_DB_SQL_TABLE] + let operation = otelSpan.attributes[SEMATTRS_DB_OPERATION] + const statement = otelSpan.attributes[SEMATTRS_DB_STATEMENT] + if (statement && !(table || operation)) { + const parsed = parseSql({ sql: statement }) + if (parsed.operation && !operation) { + operation = parsed.operation + } + + if (parsed.collection && !table) { + table = parsed.collection + } + } + if (system === DbSystemValues.MONGODB) { + table = otelSpan.attributes[SEMATTRS_DB_MONGODB_COLLECTION] + } + + if (system === DbSystemValues.REDIS && statement) { + ;[operation] = statement.split(' ') + } + + table = table || 'Unknown' + operation = operation || 'Unknown' + + return { operation, table } +} + +function setName(otelSpan) { + const system = otelSpan.attributes[SEMATTRS_DB_SYSTEM] + const { operation, table } = parseStatement(otelSpan, system) + let name = `Datastore/statement/${system}/${table}/${operation}` + // All segment name shapes are same except redis/memcached + if (system === DbSystemValues.REDIS || system === DbSystemValues.MEMCACHED) { + name = `Datastore/operation/${system}/${operation}` + } + return name +} diff --git a/lib/otel/segments/http-external.js b/lib/otel/segments/http-external.js new file mode 100644 index 0000000000..226c97c6ff --- /dev/null +++ b/lib/otel/segments/http-external.js @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const NAMES = require('../../metrics/names') +const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions') + +module.exports = function createHttpExternalSegment(agent, otelSpan) { + const context = agent.tracer.getContext() + const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown' + const name = NAMES.EXTERNAL.PREFIX + host + const segment = agent.tracer.createSegment({ + name, + parent: context.segment, + transaction: context.transaction + }) + return { segment, transaction: context.transaction } +} diff --git a/lib/otel/segments/index.js b/lib/otel/segments/index.js new file mode 100644 index 0000000000..1af9eebb9b --- /dev/null +++ b/lib/otel/segments/index.js @@ -0,0 +1,22 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const createConsumerSegment = require('./consumer') +const createDbSegment = require('./database') +const createHttpExternalSegment = require('./http-external') +const createProducerSegment = require('./producer') +const createServerSegment = require('./server') +const createInternalSegment = require('./internal') + +module.exports = { + createConsumerSegment, + createDbSegment, + createHttpExternalSegment, + createInternalSegment, + createProducerSegment, + createServerSegment +} diff --git a/lib/otel/segments/internal.js b/lib/otel/segments/internal.js new file mode 100644 index 0000000000..827ac8288d --- /dev/null +++ b/lib/otel/segments/internal.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = function createInternalSegment(agent, otelSpan) { + const context = agent.tracer.getContext() + const name = `Custom/${otelSpan.name}` + const segment = agent.tracer.createSegment({ + name, + parent: context.segment, + transaction: context.transaction + }) + return { segment, transaction: context.transaction } +} diff --git a/lib/otel/segments/producer.js b/lib/otel/segments/producer.js new file mode 100644 index 0000000000..846a8e7e44 --- /dev/null +++ b/lib/otel/segments/producer.js @@ -0,0 +1,29 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { + SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_MESSAGING_DESTINATION, + SEMATTRS_MESSAGING_DESTINATION_KIND +} = require('@opentelemetry/semantic-conventions') + +module.exports = function createProducerSegment(agent, otelSpan) { + const context = agent.tracer.getContext() + const name = setName(otelSpan) + const segment = agent.tracer.createSegment({ + name, + parent: context.segment, + transaction: context.transaction + }) + return { segment, transaction: context.transaction } +} + +function setName(otelSpan) { + const system = otelSpan.attributes[SEMATTRS_MESSAGING_SYSTEM] || 'Unknown' + const destKind = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION_KIND] || 'Unknown' + const destination = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION] || 'Unknown' + return `MessageBroker/${system}/${destKind}/Produce/Named/${destination}` +} diff --git a/lib/otel/segments/server.js b/lib/otel/segments/server.js new file mode 100644 index 0000000000..52c9cfb2df --- /dev/null +++ b/lib/otel/segments/server.js @@ -0,0 +1,85 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const { + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_ROUTE, + SEMATTRS_HTTP_URL, + SEMATTRS_RPC_SYSTEM, + SEMATTRS_RPC_SERVICE, + SEMATTRS_RPC_METHOD +} = require('@opentelemetry/semantic-conventions') +const { DESTINATIONS } = require('../../config/attribute-filter') +const DESTINATION = DESTINATIONS.TRANS_COMMON +const Transaction = require('../../transaction') +const urltils = require('../../util/urltils') +const url = require('url') + +module.exports = function createServerSegment(agent, otelSpan) { + const transaction = new Transaction(agent) + transaction.type = 'web' + const rpcSystem = otelSpan.attributes[SEMATTRS_RPC_SYSTEM] + const httpMethod = otelSpan.attributes[SEMATTRS_HTTP_METHOD] + let segment + if (rpcSystem) { + segment = rpcSegment({ agent, otelSpan, transaction, rpcSystem }) + } else if (httpMethod) { + segment = httpSegment({ agent, otelSpan, transaction, httpMethod }) + } else { + segment = genericHttpSegment({ agent, transaction }) + } + transaction.baseSegment = segment + return { segment, transaction } +} + +function rpcSegment({ agent, otelSpan, transaction, rpcSystem }) { + const rpcService = otelSpan.attributes[SEMATTRS_RPC_SERVICE] || 'Unknown' + const rpcMethod = otelSpan.attributes[SEMATTRS_RPC_METHOD] || 'Unknown' + const name = `WebTransaction/WebFrameworkUri/${rpcSystem}/${rpcService}.${rpcMethod}` + transaction.name = name + transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', rpcMethod) + transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', name) + transaction.url = name + const segment = agent.tracer.createSegment({ + name, + parent: transaction.trace.root, + transaction + }) + segment.addAttribute('component', rpcSystem) + return segment +} + +// most instrumentation will hit this case +// I find that if the request is in a web framework, the web framework instrumentation +// sets `http.route` and when the span closes it pulls that attribute in +// we'll most likely need to wire up some naming reconciliation +// to handle this use case. +function httpSegment({ agent, otelSpan, transaction, httpMethod }) { + const httpRoute = otelSpan.attributes[SEMATTRS_HTTP_ROUTE] || 'Unknown' + const httpUrl = otelSpan.attributes[SEMATTRS_HTTP_URL] || '/Unknown' + const requestUrl = url.parse(httpUrl, true) + const name = `WebTransaction/Nodejs/${httpMethod}/${httpRoute}` + transaction.name = name + transaction.url = urltils.obfuscatePath(agent.config, requestUrl.pathname) + transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', transaction.url) + transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', httpMethod) + return agent.tracer.createSegment({ + name, + parent: transaction.trace.root, + transaction + }) +} + +function genericHttpSegment({ agent, transaction }) { + const name = 'WebTransaction/NormalizedUri/*' + transaction.name = name + return agent.tracer.createSegment({ + name, + parent: transaction.trace.root, + transaction + }) +} diff --git a/lib/serverless/aws-lambda.js b/lib/serverless/aws-lambda.js index d8f69a7995..8761109855 100644 --- a/lib/serverless/aws-lambda.js +++ b/lib/serverless/aws-lambda.js @@ -138,12 +138,13 @@ class AwsLambda { if (!transaction) { return handler.apply(this, arguments) } + const activeSegment = shim.tracer.getSegment() transaction.setPartialName(transactionName) const isApiGatewayLambdaProxy = apiGateway.isLambdaProxyEvent(event) const segmentRecorder = isApiGatewayLambdaProxy ? recordWeb : recordBackground - const segment = shim.createSegment(functionName, segmentRecorder) + const segment = shim.createSegment(functionName, segmentRecorder, activeSegment) transaction.baseSegment = segment // resultProcessor is used to execute additional logic based on the // payload supplied to the callback. diff --git a/lib/shim/datastore-shim.js b/lib/shim/datastore-shim.js index 3691e943af..3e2c7836a5 100644 --- a/lib/shim/datastore-shim.js +++ b/lib/shim/datastore-shim.js @@ -439,7 +439,7 @@ function bindRowCallbackSegment(args, cbIdx, parentSegment) { if (++callCounter === 1) { const realParent = parentSegment || shim.getSegment() realParent && realParent.touch() - segment = shim.createSegment(segmentName, realParent) + segment = shim.createSegment({ name: segmentName, parent: realParent }) if (segment) { segment.async = false @@ -471,7 +471,7 @@ function bindRowCallbackSegment(args, cbIdx, parentSegment) { function captureInstanceAttributes(host, port, database) { // See if we are currently in a segment created by us. const segment = this.getSegment() - if (!segment || segment.shim !== this) { + if (!segment || segment.shimId !== this.id) { this.logger.trace( 'Not adding db instance metric attributes to segment %j', segment && segment.name @@ -554,7 +554,6 @@ function _recordQuery(suffix, nodule, properties, querySpec) { return this.record(nodule, properties, function queryRecorder(shim, fn, fnName, args) { shim.logger.trace('Determining query information for %j', fnName) - const segDesc = _getSpec.call(this, { spec: querySpec, shim, fn, fnName, args }) // Adjust the segment name with the metric prefix and add a recorder. diff --git a/lib/shim/message-shim/subscribe-consume.js b/lib/shim/message-shim/subscribe-consume.js index 0ce7d70e75..de35aef70f 100644 --- a/lib/shim/message-shim/subscribe-consume.js +++ b/lib/shim/message-shim/subscribe-consume.js @@ -151,7 +151,7 @@ function createConsumerWrapper({ shim, spec, consumer }) { return consumer.apply(this, args) } - const msgDesc = spec.messageHandler.call(this, shim, args) + const msgDesc = spec.messageHandler.call(this, shim, args, tx) // If message could not be handled, immediately kill this transaction. if (!msgDesc) { @@ -167,7 +167,8 @@ function createConsumerWrapper({ shim, spec, consumer }) { tx.setPartialName(txName) tx.baseSegment = shim.createSegment({ name: tx.getFullName(), - recorder: messageTransactionRecorder + recorder: messageTransactionRecorder, + parent: tx.trace.root }) // Add would-be baseSegment attributes to transaction trace @@ -212,7 +213,7 @@ function createConsumerWrapper({ shim, spec, consumer }) { } } if (msgDesc.headers) { - shim.handleMqTracingHeaders(msgDesc.headers, tx.baseSegment, shim._transportType) + shim.handleMqTracingHeaders(msgDesc.headers, tx.baseSegment, shim._transportType, tx) } shim.logger.trace('Started message transaction %s named %s', tx.id, txName) diff --git a/lib/shim/promise-shim.js b/lib/shim/promise-shim.js index 02b925bf42..70db69ebb1 100644 --- a/lib/shim/promise-shim.js +++ b/lib/shim/promise-shim.js @@ -118,6 +118,7 @@ class PromiseShim extends Shim { }, // eslint-disable-next-line sonarjs/no-globals-shadowing post: function postPromise(shim, Promise, name, args) { + const transaction = shim.tracer.getTransaction() // This extra property is added by `_wrapExecutorContext` in the pre step. const executor = args[0] const context = executor && executor[symbols.executorContext] @@ -126,7 +127,7 @@ class PromiseShim extends Shim { } context.promise = this - Contextualizer.link(null, this, shim.getSegment()) + Contextualizer.link(null, this, shim.getSegment(), transaction) try { // Must run after promise is defined so that `__NR_wrapper` can be set. context.executor.apply(context.self, context.args) @@ -165,12 +166,13 @@ class PromiseShim extends Shim { return function wrappedExecutorCaller(executor) { const parent = shim.getActiveSegment() + const transaction = shim.tracer.getTransaction() if (!this || !parent) { return caller.apply(this, arguments) } if (!this[symbols.context]) { - Contextualizer.link(null, this, parent) + Contextualizer.link(null, this, parent, transaction) } const args = shim.argsToArray.apply(shim, arguments) @@ -222,9 +224,10 @@ class PromiseShim extends Shim { return function __NR_wrappedCast() { const segment = shim.getSegment() + const transaction = shim.tracer.getTransaction() const prom = cast.apply(this, arguments) if (segment) { - Contextualizer.link(null, prom, segment) + Contextualizer.link(null, prom, segment, transaction) } return prom } @@ -321,13 +324,21 @@ class PromiseShim extends Shim { * @returns {Promise} promise bound to active segment */ function __NR_wrappedPromisified() { + const context = shim.tracer.getContext() const segment = shim.getActiveSegment() + const transaction = shim.tracer.getTransaction() if (!segment) { return promisified.apply(this, arguments) } - const prom = shim.applySegment(promisified, segment, true, this, arguments) - Contextualizer.link(null, prom, segment) + const prom = shim.applyContext({ + func: promisified, + context, + full: true, + boundThis: this, + args: arguments + }) + Contextualizer.link(null, prom, segment, transaction) return prom } } @@ -415,8 +426,17 @@ function wrapHandler({ handler, index, argsLength, useAllParams, ctx, shim }) { ctx.handler[symbols.context].setSegment(segment) promSegment = segment } - - let ret = shim.applySegment(handler, promSegment, true, this, arguments) + const transaction = ctx.handler[symbols.context].getTransaction() + + let context = shim.tracer.getContext() + context = context.enterSegment({ transaction, segment: promSegment }) + let ret = shim.applyContext({ + func: handler, + context, + full: true, + boundThis: this, + args: arguments + }) if (ret && typeof ret.then === 'function') { ret = ctx.handler[symbols.context].continueContext(ret) } @@ -444,6 +464,8 @@ function _wrapThen(shim, fn, _name, useAllParams) { return fn.apply(this, arguments) } + const transaction = shim.tracer.getTransaction() + const thenSegment = shim.getSegment() const promise = this @@ -465,7 +487,7 @@ function _wrapThen(shim, fn, _name, useAllParams) { // If we got a promise (which we should have), link the parent's context. if (!ctx.isWrapped && ctx.handler instanceof shim._class && ctx.handler !== promise) { - Contextualizer.link(promise, ctx.handler, thenSegment) + Contextualizer.link(promise, ctx.handler, thenSegment, transaction) } return ctx.handler } @@ -475,8 +497,9 @@ function _wrapThen(shim, fn, _name, useAllParams) { * @private */ class Context { - constructor(segment) { + constructor(segment, transaction) { this.segments = [segment] + this.transaction = transaction } branch() { @@ -534,7 +557,7 @@ class Contextualizer { ctxlzr.child = false } - static link(prev, next, segment) { + static link(prev, next, segment, transaction) { let ctxlzr = prev && prev[symbols.context] if (ctxlzr && !ctxlzr.isActive()) { ctxlzr = prev[symbols.context] = null @@ -561,14 +584,18 @@ class Contextualizer { } else if (segment) { // This next promise is the root of a chain. Either there was no previous // promise or the promise was created out of context. - next[symbols.context] = new Contextualizer(0, new Context(segment)) + next[symbols.context] = new Contextualizer(0, new Context(segment, transaction)) } } isActive() { const segments = this.context.segments const segment = segments[this.idx] || segments[this.parentIdx] || segments[0] - return segment && segment.transaction.isActive() + return segment && this.context.transaction.isActive() + } + + getTransaction() { + return this.context.transaction } getSegment() { diff --git a/lib/shim/shim.js b/lib/shim/shim.js index bb93e2d922..8dee883f0e 100644 --- a/lib/shim/shim.js +++ b/lib/shim/shim.js @@ -15,6 +15,7 @@ const symbols = require('../symbols') const { addCLMAttributes: maybeAddCLMAttributes } = require('../util/code-level-metrics') const { makeId } = require('../util/hashes') const { isBuiltin } = require('module') +const TraceSegment = require('../transaction/trace/segment') // Some modules do terrible things, like change the prototype of functions. To // avoid crashing things we'll use a cached copy of apply everywhere. @@ -39,7 +40,6 @@ function Shim(agent, moduleName, resolvedName, shimName, pkgVersion) { this._logger = logger.child({ module: moduleName }) this._agent = agent - this._contextManager = agent._contextManager this._toExport = null this._debug = false this.defineProperty(this, 'moduleName', moduleName) @@ -97,6 +97,7 @@ defineProperties(Shim.prototype, { Shim.prototype.wrap = wrap Shim.prototype.bindSegment = bindSegment +Shim.prototype.bindContext = bindContext Shim.prototype.bindPromise = bindPromise Shim.prototype.execute = execute @@ -116,6 +117,7 @@ Shim.prototype.setActiveSegment = setActiveSegment Shim.prototype.storeSegment = storeSegment Shim.prototype.bindCallbackSegment = bindCallbackSegment Shim.prototype.applySegment = applySegment +Shim.prototype.applyContext = applyContext Shim.prototype.createSegment = createSegment Shim.prototype.getName = getName Shim.prototype.isObject = isObject @@ -669,23 +671,22 @@ function recordWrapper({ shim, fn, name, recordNamer }) { return function wrapper() { // Create the segment that will be recorded. const args = argsToArray.apply(shim, arguments) - const segDesc = recordNamer.call(this, shim, fn, name, args) - if (!segDesc) { + const spec = recordNamer.call(this, shim, fn, name, args) + if (!spec) { shim.logger.trace('No segment descriptor for "%s", not recording.', name) return fnApply.call(fn, this, args) } - // See if we're in an active transaction. - let parent - if (segDesc.parent) { - // We only want to continue recording in a transaction if the - // transaction is active. - parent = segDesc.parent.transaction.isActive() ? segDesc.parent : null - } else { - parent = shim.getActiveSegment() - } + // middleweare recorders pass in parent segment + // we need to destructure this as it is not needed past this function + // and will overwhelm trace level loggers with logging the entire spec + const { parent: specParent, ...segDesc } = spec + + const context = shim.tracer.getContext() + const transaction = context.transaction + const parent = transaction?.isActive() && specParent ? specParent : context.segment - if (!parent) { + if (!transaction?.isActive()) { shim.logger.debug('Not recording function %s, not in a transaction.', name) return fnApply.call(fn, this, arguments) } @@ -699,13 +700,25 @@ function recordWrapper({ shim, fn, name, recordNamer }) { // - OR the parent segment is either not internal or not from this shim. const shouldCreateSegment = !( parent.opaque || - (segDesc.internal && parent.internal && shim === parent.shim) + (segDesc.internal && parent.internal && shim.id === parent.shimId) ) - const segment = shouldCreateSegment ? _rawCreateSegment(shim, segDesc) : parent + const segment = shouldCreateSegment + ? _rawCreateSegment({ shim, spec: segDesc, parent, transaction }) + : parent + + const newContext = context.enterSegment({ segment }) maybeAddCLMAttributes(fn, segment) - return _doRecord.call(this, { segment, args, segDesc, shouldCreateSegment, shim, fn, name }) + return _doRecord.call(this, { + context: newContext, + args, + segDesc, + shouldCreateSegment, + shim, + fn, + name + }) } } @@ -737,17 +750,18 @@ function _hasValidCallbackArg(shim, args, specCallback) { * * @private * @param {object} params to function - * @param {TraceSegment} params.segment The trace segment to be recorded * @param {Array} params.args The arguments to the wrapped callback * @param {Spec} params.segDesc Segment descriptor spec * @param {boolean} params.shouldCreateSegment Whether the recorder should create a segment * @param {Shim} params.shim instance of shim * @param {Function} params.fn function being wrapped + * @param {Context} params.context agent context to run in * @param {string} params.name name of function being wrapped * @returns {shim|promise} Returns a shim or promise with recorder segment and * bound callbacks, if applicable */ -function _doRecord({ segment, args, segDesc, shouldCreateSegment, shim, fn, name }) { +function _doRecord({ context, args, segDesc, shouldCreateSegment, shim, fn, name }) { + const { segment } = context // Now bind any callbacks specified in the segment descriptor. _bindAllCallbacks.call(this, shim, fn, name, args, { spec: segDesc, @@ -759,7 +773,15 @@ function _doRecord({ segment, args, segDesc, shouldCreateSegment, shim, fn, name // The reason there is no check for `segment` is because it should // be guaranteed by the parent and active transaction check // at the beginning of this function. - let ret = _applyRecorderSegment({ segment, ctx: this, args, segDesc, shim, fn, name }) + let ret = _applyRecorderSegment({ + context, + boundThis: this, + args, + segDesc, + shim, + fn, + name + }) if (ret) { if (segDesc.stream) { shim.logger.trace('Binding return value as stream.') @@ -780,33 +802,41 @@ function _doRecord({ segment, args, segDesc, shouldCreateSegment, shim, fn, name * * @private * @param {object} params to function - * @param {TraceSegment} params.segment The trace segment being applied to the wrapped function - * @param {context} params.ctx Context supplied to + * @param {Context} params.context agent context to run in * @param {Array} params.args The arguments to the wrapped callback * @param {Spec} params.segDesc Segment descriptor spec * @param {Shim} params.shim instance of shim * @param {Function} params.fn function being wrapped + * @param {*} params.boundThis the function context to run in * @param {string} params.name name of function being wrapped * @returns {*} return value of wrapped function */ -function _applyRecorderSegment({ segment, ctx, args, segDesc, shim, fn, name }) { +function _applyRecorderSegment({ context, boundThis, args, segDesc, shim, fn, name }) { + const { segment, transaction } = context let error = null let promised = false let ret try { - ret = shim.applySegment(fn, segment, true, ctx, args, segDesc.inContext) + ret = shim.applyContext({ + func: fn, + context, + full: true, + boundThis, + args, + inContextCB: segDesc.inContext + }) if (segDesc.after && segDesc.promise && shim.isPromise(ret)) { promised = true return ret.then( function onThen(val) { segment.touch() // passing in error as some instrumentation checks if it's not equal to `null` - segDesc.after({ shim, fn, name, error, result: val, segment }) + segDesc.after({ shim, fn, name, error, result: val, segment, transaction }) return val }, function onCatch(err) { segment.touch() - segDesc.after({ shim, fn, name, error: err, segment }) + segDesc.after({ shim, fn, name, error: err, segment, transaction }) throw err // NOTE: This is not an error from our instrumentation. } ) @@ -817,7 +847,7 @@ function _applyRecorderSegment({ segment, ctx, args, segDesc, shim, fn, name }) throw err // Just rethrowing this error, not our error! } finally { if (segDesc.after && (error || !promised)) { - segDesc.after({ shim, fn, name, error, result: ret, segment }) + segDesc.after({ shim, fn, name, error, result: ret, segment, transaction }) } } } @@ -950,10 +980,39 @@ function bindSegment(nodule, property, segment, full) { property = null } - // This protects against the `bindSegment(func, null, true)` case, where the - // segment is `null`, and thus `true` (the full param) is detected as the - // segment. - if (segment != null && !this.isObject(segment)) { + const context = this.tracer.getContext() + segment = segment || context?.segment + const newContext = context.enterSegment({ segment }) + return this.bindContext({ nodule, property, context: newContext, full }) +} + +/** + * + * Binds the execution of a function to a context instance. + * Similar to bindSegment but this requires passing in of an instance of Context. + * @memberof Shim.prototype + * @param {object} params to function + * @param {object | Function} params.nodule + * The source for the property or a single function to bind to a segment. + * @param {string} [params.property] + * The property to bind. If omitted, the `nodule` parameter is assumed + * to be the function to bind the segment to. + * @param {Context} [params.context] + * The context to bind the execution of the function to. + * @param {boolean} [params.full] + * Indicates if the full lifetime of the segment is bound to this function. + * @returns {object | Function} The first parameter after wrapping. + */ +function bindContext({ nodule, property, context, full = false }) { + const { segment } = context + // Don't bind to null arguments. + if (!nodule) { + return nodule + } + + // This protects against the case where the + // segment is `null`. + if (!(segment instanceof TraceSegment)) { this.logger.debug({ segment }, 'Segment is not a segment, not binding.') return nodule } @@ -963,13 +1022,7 @@ function bindSegment(nodule, property, segment, full) { return func } - // Wrap up the function with this segment. - segment = segment || shim.getSegment() - if (!segment) { - return func - } - - const binder = _makeBindWrapper(shim, func, segment, full || false) + const binder = _makeBindWrapper(shim, func, context, full) shim.storeSegment(binder, segment) return binder }) @@ -1038,30 +1091,41 @@ function bindCallbackSegment(spec, args, cbIdx, parentSegment) { function wrapCallback({ shim, args, cbIdx, parentSegment, spec }) { const cb = args[cbIdx] const realParent = parentSegment || shim.getSegment() + const context = shim.tracer.getContext() + const transaction = context?.transaction + args[cbIdx] = shim.wrap(cb, null, function callbackWrapper(shim, fn, name) { return function wrappedCallback() { if (realParent) { realParent.opaque = false } - const segment = _rawCreateSegment( + const segment = _rawCreateSegment({ shim, - new specs.SegmentSpec({ - name: 'Callback: ' + name, - parent: realParent + parent: realParent, + transaction, + spec: new specs.SegmentSpec({ + name: 'Callback: ' + name }) - ) + }) if (segment) { segment.async = false } if (spec?.after) { - spec.after({ shim, fn, name, args: arguments, segment: realParent }) + spec.after({ shim, fn, name, args: arguments, segment: realParent, transaction }) } // CB may end the transaction so update the parent's time preemptively. realParent && realParent.touch() - return shim.applySegment(cb, segment, true, this, arguments) + const newContext = context.enterSegment({ segment }) + return shim.applyContext({ + func: cb, + context: newContext, + full: true, + boundThis: this, + args: arguments + }) } }) shim.storeSegment(args[cbIdx], realParent) @@ -1104,7 +1168,8 @@ function getSegment(obj) { */ function getActiveSegment(obj) { const segment = this.getSegment(obj) - if (segment && segment.transaction && segment.transaction.isActive()) { + const transaction = this.tracer.getTransaction() + if (transaction?.isActive()) { return segment } return null @@ -1122,7 +1187,8 @@ function getActiveSegment(obj) { * @returns {TraceSegment} - The segment set as active on the context. */ function setActiveSegment(segment) { - this._contextManager.setContext(segment) + const transaction = this.tracer.getTransaction() + this.tracer.setSegment({ segment, transaction }) return segment } @@ -1144,30 +1210,31 @@ function storeSegment(obj, segment) { } /** - * Sets the given segment as the active one for the duration of the function's - * execution. + * Binds a function to the async context manager with the passed in context. * - * - `applySegment(func, segment, full, context, args[, inContextCB])` + * - `applyContext({ func, context , full, boundThis, args, inContextCB })` * * @memberof Shim.prototype - * @param {Function} func The function to execute in the context of the given segment. - * @param {TraceSegment} segment The segment to make active for the duration of the function. - * @param {boolean} full Indicates if the full lifetime of the segment is bound to this function. - * @param {*} context The `this` argument for the function. - * @param {Array.<*>} args The arguments to be passed into the function. - * @param {Function} [inContextCB] The function used to do more instrumentation work. This function is + * @param {object} params to function + * @param {Function} params.func The function to execute in given async context. + * @param {Context} params.context This context you want to run a function in + * @param {boolean} params.full Indicates if the full lifetime of the segment is bound to this function. + * @param {*} params.boundThis The `this` argument for the function. + * @param {Array.<*>} params.args The arguments to be passed into the function. + * @param {Function} [params.inContextCB] The function used to do more instrumentation work. This function is * guaranteed to be executed with the segment associated with. * @returns {*} Whatever value `func` returned. */ -function applySegment(func, segment, full, context, args, inContextCB) { - // Exist fast for bad arguments. +function applyContext({ func, context, full, boundThis, args, inContextCB }) { + const { segment } = context + // Exit fast for bad arguments. if (!this.isFunction(func)) { return } if (!segment) { this.logger.trace('No segment to apply to function.') - return fnApply.call(func, context, args) + return fnApply.call(func, boundThis, args) } this.logger.trace('Applying segment %s', segment.name) @@ -1183,7 +1250,29 @@ function applySegment(func, segment, full, context, args, inContextCB) { return fnApply.call(func, this, arguments) } - return this.tracer.bindFunction(runInContextCb, segment, full).apply(context, args) + return this.tracer.bindFunction(runInContextCb, context, full).apply(boundThis, args) +} + +/** + * Binds a function to the async context manager with the segment passed in. It'll pull + * the active transaction from the context manager. + * + * - `applySegment(func, segment, full, context, args[, inContextCB])` + * + * @memberof Shim.prototype + * @param {Function} func The function to execute in the context of the given segment. + * @param {TraceSegment} segment The segment to make active for the duration of the function. + * @param {boolean} full Indicates if the full lifetime of the segment is bound to this function. + * @param {*} boundThis The `this` argument for the function. + * @param {Array.<*>} args The arguments to be passed into the function. + * @param {Function} [inContextCB] The function used to do more instrumentation work. This function is + * guaranteed to be executed with the segment associated with. + * @returns {*} Whatever value `func` returned. + */ +function applySegment(func, segment, full, boundThis, args, inContextCB) { + const context = this.tracer.getContext() + const newContext = context.enterSegment({ segment }) + return this.applyContext({ func, context: newContext, full, boundThis, args, inContextCB }) } /** @@ -1205,62 +1294,67 @@ function applySegment(func, segment, full, context, args, inContextCB) { * `null` is returned. */ function createSegment(name, recorder, parent) { - let opts = null + let opts = {} if (this.isString(name)) { // createSegment(name [, recorder] [, parent]) - opts = new specs.SegmentSpec({ name }) + opts.name = name // if the recorder arg is not used, it can either be omitted or null if (this.isFunction(recorder) || this.isNull(recorder)) { // createSegment(name, recorder [, parent]) opts.recorder = recorder - opts.parent = parent } else { // createSegment(name [, parent]) - opts.parent = recorder + parent = recorder } } else { // createSegment(opts) opts = name + parent = opts.parent } - return _rawCreateSegment(this, opts) + const transaction = this.tracer.getTransaction() + parent = parent || this.getActiveSegment() + const spec = new specs.SegmentSpec(opts) + return _rawCreateSegment({ shim: this, spec, parent, transaction }) } /** * @private - * @param {Shim} shim instance of shim - * @param {string|specs.SegmentSpec} opts options for creating segment + * @param {object} params to function + * @param {Shim} params.shim instance of shim + * @param {Transaction} params.transaction active transaction + * @param {TraceSegment} params.parent the segment that will be the parent of the newly created segment + * @param {string|specs.SegmentSpec} params.spec options for creating segment * @returns {?TraceSegment} A new trace segment if a transaction is active, else * `null` is returned. */ -function _rawCreateSegment(shim, opts) { - // Grab parent segment when none in opts so we can check opaqueness. - // Also, saving reference and not assigning to opts to avoid hoisting - // this value to other executions of the same method or a shared spec - // definition - const parent = opts.parent || shim.getActiveSegment() - +function _rawCreateSegment({ shim, spec, parent, transaction }) { // When parent exists and is opaque, no new segment will be created // by tracer.createSegment and the parent will be returned. We bail // out early so we do not risk modifying the parent segment. if (parent?.opaque) { - shim.logger.trace(opts, 'Did not create segment because parent is opaque') + shim.logger.trace(spec, 'Did not create segment because parent is opaque') return parent } - const segment = shim.tracer.createSegment(opts.name, opts.recorder, parent) + const segment = shim.tracer.createSegment({ + name: spec.name, + recorder: spec.recorder, + parent, + transaction + }) if (segment) { - segment.internal = opts.internal - segment.opaque = opts.opaque - segment.shim = shim + segment.internal = spec.internal + segment.opaque = spec.opaque + segment.shimId = shim.id - if (hasOwnProperty(opts, 'parameters')) { - shim.copySegmentParameters(segment, opts.parameters) + if (hasOwnProperty(spec, 'parameters')) { + shim.copySegmentParameters(segment, spec.parameters) } - shim.logger.trace(opts, 'Created segment') + shim.logger.trace(spec, 'Created segment') } else { - shim.logger.debug(opts, 'Failed to create segment') + shim.logger.debug(spec, 'Failed to create segment') } return segment @@ -1753,18 +1847,18 @@ function _wrap(shim, original, name, spec, args) { * @private * @param {Shim} shim * The shim used for the binding. - * @param {Function} fn + * @param {Function} func * The function to be bound to the segment. - * @param {TraceSegment} segment - * The segment the function is bound to. + * @param {Context} context + * The agent context that the function is bound to. * @param {boolean} full * Indicates if the segment's full lifetime is bound to the function. - * @returns {Function} A function which wraps `fn` and makes the given segment + * @returns {Function} A function which wraps `func` and makes the given segment * active for the duration of its execution. */ -function _makeBindWrapper(shim, fn, segment, full) { +function _makeBindWrapper(shim, func, context, full) { return function wrapper() { - return shim.applySegment(fn, segment, full, this, arguments) + return shim.applyContext({ func, context, full, boundThis: this, args: arguments }) } } @@ -1914,8 +2008,10 @@ function wrapStreamEmit({ stream, shim, segment, specEvent, shouldCreateSegment, // Wrap emit such that each event handler is executed within context of this // segment or the event-specific segment. shim.wrap(stream, 'emit', function wrapEmit(shim, emit) { - const tx = segment.transaction - const streamBoundEmit = shim.bindSegment(emit, segment, true) + const context = shim.tracer.getContext() + const tx = context.transaction + const newContext = context.enterSegment({ segment }) + const streamBoundEmit = shim.bindContext({ nodule: emit, context: newContext, full: true }) let eventSegment = null let eventBoundEmit = null let emitCount = 0 @@ -1928,8 +2024,9 @@ function wrapStreamEmit({ stream, shim, segment, specEvent, shouldCreateSegment, let emitToCall = streamBoundEmit if (evnt === specEvent && tx.isActive()) { if (!eventBoundEmit) { - eventSegment = shim.createSegment(segmentName, segment) - eventBoundEmit = shim.bindSegment(emit, eventSegment, true) + eventSegment = shim.createSegment({ name: segmentName, parent: segment }) + const newContext = context.enterSegment({ segment: eventSegment }) + eventBoundEmit = shim.bindContext({ nodule: emit, full: true, context: newContext }) } eventSegment.addAttribute('count', ++emitCount) emitToCall = eventBoundEmit diff --git a/lib/shim/transaction-shim.js b/lib/shim/transaction-shim.js index d54a8b2341..61eb12fff3 100644 --- a/lib/shim/transaction-shim.js +++ b/lib/shim/transaction-shim.js @@ -184,7 +184,7 @@ function setTransactionName(name) { /** * Retrieves whatever CAT headers may be in the given headers. * - * - `handleMqTracingHeaders(headers [, segment [, transportType]])` + * - `handleMqTracingHeaders(headers [, segment ] [, transportType], [, transaction])` * * @memberof TransactionShim.prototype * @@ -197,8 +197,9 @@ function setTransactionName(name) { * provided then the currently active segment is used. * @param {string} [transportType] * The transport type that brought the headers. Usually `HTTP` or `HTTPS`. + * @param {Transaction} transaction active transaction */ -function handleMqTracingHeaders(headers, segment, transportType) { +function handleMqTracingHeaders(headers, segment, transportType, transaction) { // TODO: replace functionality when CAT fully removed. if (!headers) { @@ -215,13 +216,12 @@ function handleMqTracingHeaders(headers, segment, transportType) { // Check that we're in an active transaction. const currentSegment = segment || this.getSegment() - if (!currentSegment || !currentSegment.transaction.isActive()) { + transaction = transaction || this.tracer.getTransaction() + if (!currentSegment || !transaction.isActive()) { this.logger.trace('Not processing headers for CAT or DT, not in an active transaction.') return } - const transaction = currentSegment.transaction - if (config.distributed_tracing.enabled) { transaction.acceptDistributedTraceHeaders(transportType, headers) return @@ -237,7 +237,7 @@ function handleMqTracingHeaders(headers, segment, transportType) { ) cat.assignCatToTransaction(externalId, externalTransaction, transaction) const decodedAppData = cat.parseAppData(config, appData) - cat.assignCatToSegment(decodedAppData, currentSegment) + cat.assignCatToSegment({ appData: decodedAppData, segment: currentSegment, transaction }) // TODO: Handle adding ExternalTransaction metrics for this segment. } @@ -316,11 +316,11 @@ function insertCATReplyHeader(headers, useAlternateHeaderNames) { // Are we in a transaction? const segment = this.getSegment() - if (!segment || !segment.transaction.isActive()) { + const transaction = this.tracer.getTransaction() + if (!segment || !transaction?.isActive()) { this.logger.trace('Not adding CAT reply header, not in an active transaction.') return } - const tx = segment.transaction // Hunt down the content length. // NOTE: In AMQP, content-type and content-encoding are guaranteed fields, but @@ -334,11 +334,16 @@ function insertCATReplyHeader(headers, useAlternateHeaderNames) { } } - const { key, data } = cat.encodeAppData(config, tx, contentLength, useAlternateHeaderNames) + const { key, data } = cat.encodeAppData( + config, + transaction, + contentLength, + useAlternateHeaderNames + ) // Add the header. if (key && data) { headers[key] = data - this.logger.trace('Added outbound response CAT headers for transaction %s', tx.id) + this.logger.trace('Added outbound response CAT headers for transaction %s', transaction.id) } } @@ -351,7 +356,7 @@ function insertCATReplyHeader(headers, useAlternateHeaderNames) { * @private * @param {Shim} shim * The shim used for the binding. - * @param {Function} fn + * @param {Function} func * The function link with the transaction. * @param {string} name * The name of the wrapped function. @@ -360,26 +365,24 @@ function insertCATReplyHeader(headers, useAlternateHeaderNames) { * @returns {Function} A function which wraps `fn` and creates potentially nested * transactions linked to its execution. */ -function _makeNestedTransWrapper(shim, fn, name, spec) { +function _makeNestedTransWrapper(shim, func, name, spec) { return function nestedTransactionWrapper() { if (!shim.agent.canCollectData()) { - return fn.apply(this, arguments) + return func.apply(this, arguments) } - // Reuse existing transactions only if the type matches. - let transaction = shim.tracer.getTransaction() - let segment = shim.getSegment() + let context = shim.tracer.getContext() // Only create a new transaction if we either do not have a current // transaction _or_ the current transaction is not of the type we want. - if (!transaction || spec.type !== transaction.type) { + if (!context?.transaction || spec.type !== context?.transaction?.type) { shim.logger.trace('Creating new nested %s transaction for %s', spec.type, name) - transaction = new Transaction(shim.agent) + const transaction = new Transaction(shim.agent) transaction.type = spec.type - segment = transaction.trace.root + context = context.enterTransaction(transaction) } - return shim.applySegment(fn, segment, false, this, arguments) + return shim.applyContext({ func, context, full: false, boundThis: this, args: arguments }) } } @@ -391,7 +394,7 @@ function _makeNestedTransWrapper(shim, fn, name, spec) { * @private * @param {Shim} shim * The shim used for the binding. - * @param {Function} fn + * @param {Function} func * The function link with the transaction. * @param {string} name * The name of the wrapped function. @@ -400,17 +403,19 @@ function _makeNestedTransWrapper(shim, fn, name, spec) { * @returns {Function} A function which wraps `fn` and potentially creates a new * transaction linked to the function's execution. */ -function _makeTransWrapper(shim, fn, name, spec) { +function _makeTransWrapper(shim, func, name, spec) { return function transactionWrapper() { // Don't nest transactions, reuse existing ones! - const existingTransaction = shim.tracer.getTransaction() + let context = shim.tracer.getContext() + const existingTransaction = context.transaction if (!shim.agent.canCollectData() || existingTransaction) { - return fn.apply(this, arguments) + return func.apply(this, arguments) } shim.logger.trace('Creating new %s transaction for %s', spec.type, name) const transaction = new Transaction(shim.agent) transaction.type = spec.type - return shim.applySegment(fn, transaction.trace.root, false, this, arguments) + context = context.enterTransaction(transaction) + return shim.applyContext({ func, context, full: false, boundThis: this, args: arguments }) } } diff --git a/lib/shim/webframework-shim/middleware.js b/lib/shim/webframework-shim/middleware.js index 33473e7ef5..651f160643 100644 --- a/lib/shim/webframework-shim/middleware.js +++ b/lib/shim/webframework-shim/middleware.js @@ -428,7 +428,7 @@ function wrapNextFn({ shim, txInfo, nextDetails, segment }, nodule, property, is if (isError(shim, err)) { assignError(txInfo, err) } else if (!isFinal && !nextDetails.isErrorWare && nextDetails.appendPath) { - segment.transaction.nameState.popPath(nextDetails.route) + txInfo.transaction.nameState.popPath(nextDetails.route) } // The next call does not signify the end of the segment @@ -446,7 +446,7 @@ function wrapNextFn({ shim, txInfo, nextDetails, segment }, nodule, property, is // more work to do in that scope. return ret.then(function onNextFinish(v) { if (nextDetails.appendPath) { - segment.transaction.nameState.appendPath(nextDetails.route) + txInfo.transaction.nameState.appendPath(nextDetails.route) } txInfo.segmentStack.push(segment) diff --git a/lib/spans/span-event-aggregator.js b/lib/spans/span-event-aggregator.js index f1549dd796..101806adc1 100644 --- a/lib/spans/span-event-aggregator.js +++ b/lib/spans/span-event-aggregator.js @@ -61,23 +61,24 @@ class SpanEventAggregator extends EventAggregator { /** * Attempts to add the given segment to the collection. * - * @param {TraceSegment} segment - The segment to add. - * @param {string} [parentId] - The GUID of the parent span. - * @param isRoot + * @param {object} params to function + * @param {TraceSegment} params.segment segment to add. + * @param {Transaction} params.transaction active transaction + * @param {string} [params.parentId] GUID of the parent span. + * @param {boolean} params.isRoot if segment is root segment * @returns {boolean} True if the segment was added, or false if it was discarded. */ - addSegment(segment, parentId, isRoot) { + addSegment({ segment, transaction, parentId, isRoot }) { // Check if the priority would be accepted before creating the event object. - const tx = segment.transaction - if (tx.priority < this._items.getMinimumPriority()) { + if (transaction.priority < this._items.getMinimumPriority()) { ++this.events.seen this._metrics.getOrCreateMetric(this._metricNames.SEEN).incrementCallCount() return false } - const span = SpanEvent.fromSegment(segment, parentId || null, isRoot) - return this.add(span, tx.priority) + const span = SpanEvent.fromSegment(segment, transaction, parentId || null, isRoot) + return this.add(span, transaction.priority) } /** diff --git a/lib/spans/span-event.js b/lib/spans/span-event.js index fcc04064e9..a193a9ff33 100644 --- a/lib/spans/span-event.js +++ b/lib/spans/span-event.js @@ -92,17 +92,18 @@ class SpanEvent { * The constructed span event will contain extra data depending on the * category of the segment. * - * @param {TraceSegment} segment - The segment to turn into a span event. - * @param {?string} [parentId] - The ID of the segment's parent. - * @param isRoot + * @param {TraceSegment} segment segment to turn into a span event. + * @param {Transaction} transaction active transaction + * @param {?string} [parentId] ID of the segment's parent. + * @param {boolean} isRoot if segment is root segment * @returns {SpanEvent} The constructed event. */ - static fromSegment(segment, parentId = null, isRoot = false) { + static fromSegment(segment, transaction, parentId = null, isRoot = false) { const spanContext = segment.getSpanContext() // Since segments already hold span agent attributes and we want to leverage // filtering, we add to the segment attributes prior to processing. - if (spanContext.hasError && !segment.transaction.hasIgnoredErrorStatusCode()) { + if (spanContext.hasError && !transaction.hasIgnoredErrorStatusCode()) { const details = spanContext.errorDetails segment.addSpanAttribute('error.message', details.message) segment.addSpanAttribute('error.class', details.type) @@ -128,25 +129,23 @@ class SpanEvent { span.intrinsics[key] = value } - const tx = segment.transaction - - span.intrinsics.traceId = tx.traceId + span.intrinsics.traceId = transaction.traceId span.intrinsics.guid = segment.id span.intrinsics.parentId = parentId - span.intrinsics.transactionId = tx.id - span.intrinsics.sampled = tx.sampled - span.intrinsics.priority = tx.priority + span.intrinsics.transactionId = transaction.id + span.intrinsics.sampled = transaction.sampled + span.intrinsics.priority = transaction.priority span.intrinsics.name = segment.name if (isRoot) { - span.intrinsics.trustedParentId = tx.traceContext.trustedParentId - if (tx.traceContext.tracingVendors) { - span.intrinsics.tracingVendors = tx.traceContext.tracingVendors + span.intrinsics.trustedParentId = transaction.traceContext.trustedParentId + if (transaction.traceContext.tracingVendors) { + span.intrinsics.tracingVendors = transaction.traceContext.tracingVendors } } // Only set this if it will be `true`. Must be `null` otherwise. - if (tx.baseSegment === segment) { + if (transaction.baseSegment === segment) { span.intrinsics['nr.entryPoint'] = true } diff --git a/lib/spans/streaming-span-event-aggregator.js b/lib/spans/streaming-span-event-aggregator.js index b8ec9f7543..68665fa2a6 100644 --- a/lib/spans/streaming-span-event-aggregator.js +++ b/lib/spans/streaming-span-event-aggregator.js @@ -98,18 +98,21 @@ class StreamingSpanEventAggregator extends Aggregator { /** * Attempts to add the given segment to the collection. * - * @param {TraceSegment} segment - The segment to add. - * @param {string} [parentId] - The GUID of the parent span. - * @param isRoot + * @param {object} params to function + * @param {TraceSegment} params.segment segment to add. + * @param {string} [parms.parentId] GUID of the parent span. + * @param {Transaction} params.transaction active transaction + * @param {boolean} params.isRoot is segment root segment + * @param params.parentId * @returns {boolean} True if the segment was added, or false if it was discarded. */ - addSegment(segment, parentId, isRoot) { + addSegment({ segment, transaction, parentId, isRoot }) { if (!this.started) { logger.trace('Aggregator has not yet started, dropping span (%s).', segment.name) return } - const span = StreamingSpanEvent.fromSegment(segment, parentId, isRoot) + const span = StreamingSpanEvent.fromSegment(segment, transaction, parentId, isRoot) this.stream.write(span) } diff --git a/lib/spans/streaming-span-event.js b/lib/spans/streaming-span-event.js index 4d435be4e2..da7432bfef 100644 --- a/lib/spans/streaming-span-event.js +++ b/lib/spans/streaming-span-event.js @@ -114,12 +114,12 @@ class StreamingSpanEvent { } } - static fromSegment(segment, parentId = null, isRoot = false) { + static fromSegment(segment, transaction, parentId = null, isRoot = false) { const spanContext = segment.getSpanContext() // Since segments already hold span agent attributes and we want to leverage // filtering, we add to the segment attributes prior to processing. - if (spanContext.hasError && !segment.transaction.hasIgnoredErrorStatusCode()) { + if (spanContext.hasError && !transaction.hasIgnoredErrorStatusCode()) { const details = spanContext.errorDetails segment.addSpanAttribute('error.message', details.message) segment.addSpanAttribute('error.class', details.type) @@ -132,7 +132,6 @@ class StreamingSpanEvent { const customAttributes = spanContext.customAttributes.get(DESTINATIONS.SPAN_EVENT) - const transaction = segment.transaction const traceId = transaction.traceId let span = null diff --git a/lib/transaction/index.js b/lib/transaction/index.js index 80e9d404c4..b3e55f68ec 100644 --- a/lib/transaction/index.js +++ b/lib/transaction/index.js @@ -78,13 +78,6 @@ function Transaction(agent) { throw new Error('every transaction must be bound to the agent') } - this.traceFlag = false - if (agent.config.logging.diagnostics) { - this.traceStacks = [] - } else { - this.traceStacks = null - } - this.agent = agent this.metrics = new Metrics(agent.config.apdex_t, agent.mapper, agent.metricNameNormalizer) @@ -162,24 +155,15 @@ function Transaction(agent) { this.ignoreApdex = false agent.emit('transactionStarted', this) - this.probe('Transaction created', { id: this.id }) } +Transaction.DESTINATIONS = DESTS Transaction.TYPES = TYPES Transaction.TYPES_SET = TYPES_SET Transaction.TRANSPORT_TYPES = TRANSPORT_TYPES Transaction.TRANSPORT_TYPES_SET = TRANSPORT_TYPES_SET Transaction.TRACE_CONTEXT_PARENT_HEADER = TRACE_CONTEXT_PARENT_HEADER -Transaction.prototype.probe = function probe(action, extra) { - if (this.traceStacks) { - this.traceStacks.push({ - stack: new Error(action).stack.split('\n'), - extra - }) - } -} - /** * Add a clear API method for determining whether a transaction is web or * background. @@ -209,12 +193,6 @@ Transaction.prototype.end = function end() { if (!this.timer.isActive()) { return } - if (this.traceFlag) { - logger.warn( - { segment: { name: this.name, stacks: this.traceStacks } }, - 'Flagged transaction ended.' - ) - } if (!this.name) { this.finalizeName(null) // Use existing partial name. @@ -518,7 +496,8 @@ Transaction.prototype._markAsWeb = function _markAsWeb(rawURL) { } } } - this.baseSegment.markAsWeb() + + this.baseSegment.markAsWeb(this) } /** @@ -553,7 +532,7 @@ Transaction.prototype.finalizeName = function finalizeName(name) { this.ignore = this.forceIgnore } - this.baseSegment && this.baseSegment.setNameFromTransaction() + this.baseSegment && this.baseSegment.setNameFromTransaction(this) this._copyNameToActiveSpan(this.name) @@ -698,7 +677,7 @@ Transaction.prototype.addRecorder = function addRecorder(recorder) { Transaction.prototype.record = function record() { const name = this.name for (let i = 0, l = this._recorders.length; i < l; ++i) { - this._recorders[i](name) + this._recorders[i](name, this) } } @@ -1414,4 +1393,15 @@ function addRequestParameters(requestParameters) { } } +/** + * Increments counters used to report details. + * `numSegments` - recorded as supportability metric when transaction ends + * `agent.totalActiveSegments` used as a trace level log value when metrics are harvested + * `agent.segmentsCreatedInHarvest` used as a trace level log value when metrics are harvested + */ +Transaction.prototype.incrementCounters = function incrementCounters() { + ++this.numSegments + this.agent.incrementCounters() +} + module.exports = Transaction diff --git a/lib/transaction/trace/exclusive-time-calculator.js b/lib/transaction/trace/exclusive-time-calculator.js index dbb5b1d685..904cfac75b 100644 --- a/lib/transaction/trace/exclusive-time-calculator.js +++ b/lib/transaction/trace/exclusive-time-calculator.js @@ -6,39 +6,33 @@ 'use strict' class ExclusiveCalculator { - constructor(root) { - this.toProcess = [root] + constructor(root, trace) { + this.node = trace.getNode(root.id) // use a second stack to do a post-order traversal this.parentStack = [] } /** * Kicks off the exclusive duration calculation. This is performed - * using a depth first, postorder traversal over the tree. + * using a depth first, postorder traversal over the tree recursively + * + * @param {Node} node to process duration and children */ - process() { - while (this.toProcess.length) { - const segment = this.toProcess.pop() - const children = segment.getChildren() - // when we hit a leaf, calc the exclusive time and report the time - // range to the parent - if (children.length === 0) { - segment._exclusiveDuration = segment.getDurationInMillis() - if (this.parentStack.length) { - this.finishLeaf(segment.timer.toRange()) - } - } else { - // in the case we are processing an internal node, we just push it on the stack - // and push its children to be processed. all processing will be done after its - // children are all done (i.e. postorder) - this.parentStack.push({ - childrenLeft: children.length, - segment, - childPairs: [] - }) - for (let i = children.length - 1; i >= 0; --i) { - this.toProcess.push(children[i]) - } + process(node = this.node) { + const { children, segment } = node + if (children.length === 0) { + segment._exclusiveDuration = segment.getDurationInMillis() + if (this.parentStack.length) { + this.finishLeaf(segment.timer.toRange()) + } + } else { + this.parentStack.push({ + childrenLeft: children.length, + segment, + childPairs: [] + }) + for (let i = children.length - 1; i >= 0; --i) { + this.process(children[i]) } } } diff --git a/lib/transaction/trace/index.js b/lib/transaction/trace/index.js index 83cb05f86e..810c4a238f 100644 --- a/lib/transaction/trace/index.js +++ b/lib/transaction/trace/index.js @@ -7,16 +7,15 @@ const codec = require('../../util/codec') const urltils = require('../../util/urltils') -const Segment = require('./segment') +const TraceSegment = require('./segment') const { Attributes, MAXIMUM_CUSTOM_ATTRIBUTES } = require('../../attributes') const logger = require('../../logger').child({ component: 'trace' }) - const { DESTINATIONS } = require('../../config/attribute-filter') const FROM_MILLIS = 1e-3 const ATTRIBUTE_SCOPE = 'transaction' - const REQUEST_URI_KEY = 'request.uri' const UNKNOWN_URI_PLACEHOLDER = '/Unknown' +const SegmentTree = require('./segment-tree') /** * A Trace holds the root of the Segment graph and produces the final @@ -31,11 +30,18 @@ function Trace(transaction) { this.transaction = transaction - this.root = new Segment(transaction, 'ROOT') - this.root.start() + const root = new TraceSegment({ + config: transaction.agent.config, + name: 'ROOT', + collect: transaction.collect, + isRoot: true + }) + root.start() + transaction.incrementCounters() this.intrinsics = Object.create(null) - this.segmentsSeen = 0 + this.segments = new SegmentTree(root) + this.root = this.segments.root.segment this.totalTimeCache = null this.custom = new Attributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES) @@ -55,38 +61,33 @@ function Trace(transaction) { /** * End and close the current trace. Triggers metric recording for trace * segments that support recording. + * @param {Node} [node] the node to process the segment and its children */ -Trace.prototype.end = function end() { - const segments = [this.root] +Trace.prototype.end = function end(node = this.segments.root) { + const { children, segment } = node + segment.finalize(this) - while (segments.length) { - const segment = segments.pop() - segment.finalize() - - const children = segment.getChildren() - for (let i = 0; i < children.length; ++i) { - segments.push(children[i]) - } + for (let i = 0; i < children.length; ++i) { + this.end(children[i]) } } /** * Iterates over the trace tree and generates a span event for each segment. + * @param {Node} [node] the node to process the segment and its children */ -Trace.prototype.generateSpanEvents = function generateSpanEvents() { +Trace.prototype.generateSpanEvents = function generateSpanEvents(node = this.segments.root) { const config = this.transaction.agent.config if (!shouldGenerateSpanEvents(config, this.transaction)) { return } - const toProcess = [] + + const { children, segment } = node // Root segment does not become a span, so we need to process it separately. const spanAggregator = this.transaction.agent.spanEventAggregator - - const children = this.root.getChildren() - - if (children.length > 0) { + if (children.length && segment.name === 'ROOT') { // At the point where these attributes are available, we only have a // root span. Adding attributes to first non-root span here. const attributeMap = { @@ -100,36 +101,31 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents() { for (const [key, value] of Object.entries(attributeMap)) { if (value !== null) { - children[0].addSpanAttribute(key, value) + children[0].segment.addSpanAttribute(key, value) } } } - for (let i = 0; i < children.length; ++i) { - toProcess.push(new DTTraceNode(children[i], this.transaction.parentSpanId, true)) - } - - while (toProcess.length) { - const segmentInfo = toProcess.pop() - const segment = segmentInfo.segment - + if (segment.id !== this.root.id) { + const isRoot = segment.parentId === this.root.id + const parentId = isRoot ? this.transaction.parentSpanId : segment.parentId // Even though at some point we might want to stop adding events because all the priorities // should be the same, we need to count the spans as seen. - spanAggregator.addSegment(segment, segmentInfo.parentId, segmentInfo.isRoot) + spanAggregator.addSegment({ + segment, + transaction: this.transaction, + parentId, + isRoot + }) + } - const nodes = segment.getChildren() - for (let i = 0; i < nodes.length; ++i) { - const node = new DTTraceNode(nodes[i], segment.id) - toProcess.push(node) - } + for (let i = 0; i < children.length; ++i) { + this.generateSpanEvents(children[i]) } } function shouldGenerateSpanEvents(config, txn) { - if (!config.distributed_tracing.enabled) { - return false - } - if (!config.span_events.enabled) { + if (!(config.distributed_tracing.enabled && config.span_events.enabled)) { return false } @@ -137,21 +133,23 @@ function shouldGenerateSpanEvents(config, txn) { return infiniteTracingConfigured || txn.sampled } -function DTTraceNode(segment, parentId, isRoot = false) { - this.segment = segment - this.parentId = parentId - this.isRoot = isRoot -} - /** * Add a child to the list of segments. * * @param {string} childName Name for the new segment. * @param {Function} callback Callback function to record metrics related to the trace + * @param {TraceSegment} parent parent of new segment * @returns {TraceSegment} Newly-created segment. */ -Trace.prototype.add = function add(childName, callback) { - return this.root.add(childName, callback) +Trace.prototype.add = function add(childName, callback, parent) { + const { tracer } = this.transaction.agent + parent = parent || this.root + return tracer.createSegment({ + name: childName, + recorder: callback, + parent, + transaction: this.transaction + }) } /** @@ -201,7 +199,7 @@ Trace.prototype.addCustomAttribute = function addCustomAttribute(key, value) { * traces, in milliseconds. */ Trace.prototype.getExclusiveDurationInMillis = function getExclusiveDurationInMillis() { - return this.root.getExclusiveDurationInMillis() + return this.root.getExclusiveDurationInMillis(this) } /** @@ -215,16 +213,22 @@ Trace.prototype.getTotalTimeDurationInMillis = function getTotalTimeDurationInMi if (this.totalTimeCache !== null) { return this.totalTimeCache } - if (this.root.children.length === 0) { + + const rootNode = this.segments.root + const children = [] + children.push(...rootNode.children) + + if (!children.length) { return 0 } - const segments = this.root.getChildren() + let totalTimeInMillis = 0 - while (segments.length !== 0) { - const segment = segments.pop() - totalTimeInMillis += segment.getExclusiveDurationInMillis() - segment.getChildren().forEach((childSegment) => segments.push(childSegment)) + while (children.length !== 0) { + const node = children.pop() + const { segment, children: childChildren } = node + totalTimeInMillis += segment.getExclusiveDurationInMillis(this) + childChildren.forEach((child) => children.push(child)) } if (!this.transaction.isActive()) { @@ -347,6 +351,109 @@ Trace.prototype._getRequestUri = function _getRequestUri() { return requestUri } +Trace.prototype.getNode = function getNode(id) { + return this.segments.find(id) +} + +/** + * Gets all children of a segment that should be collected and not ignored. + * + * @param {Array.} children filters children that are not ignored or `_collect` is false + * @returns {Array.} list of all segments and their children + */ +Trace.prototype.getCollectedChildren = function getCollectedChildren(children) { + return children.filter((child) => child.segment._collect && !child.segment.ignore) +} + +/** + * Gets the parent segment from list of segments on trace by passing in the `parentId` + * and matching on the `segment.id`. Only used in testing + * + * @param {number} parentId id of parent segment you want to retrieve + * @returns {TraceSegment} parent segment + */ +Trace.prototype.getParent = function getParent(parentId) { + const node = this.segments.find(parentId) + return node?.segment +} + +/** + * Gets all children of a segment. This is only used in testing + * + * @param {number} id of segment + * @returns {Array.} list of all segments that have the parentId of the segment + */ +Trace.prototype.getChildren = function getChildren(id) { + const node = this.segments.find(id) + return node?.children.map((child) => child.segment) +} + +/** + * This is perhaps the most poorly-documented element of transaction traces: + * what do each of the segment representations look like prior to encoding? + * Spelunking in the code for the other agents has revealed that each child + * node is an array with the following field in the following order: + * + * 0: entry timestamp relative to transaction start time + * 1: exit timestamp + * 2: metric name + * 3: parameters as a name -> value JSON dictionary + * 4: any child segments + * + * Other agents include further fields in this. I haven't gotten to the bottom + * of all of them (and Ruby, of course, sends marshalled Ruby object), but + * here's what I know so far: + * + * in Java: + * 5: class name + * 6: method name + * + * in Python: + * 5: a "label" + * + * FIXME: I don't know if it makes sense to add custom fields for Node. TBD + */ +Trace.prototype.toJSON = function toJSON() { + // use depth-first search on the segment tree using stack + const resultDest = [] + // array of objects relating a segment and the destination for its + // serialized data. + const segmentsToProcess = [ + { + node: this.segments.root, + destination: resultDest + } + ] + + while (segmentsToProcess.length !== 0) { + const { node, destination } = segmentsToProcess.pop() + const { segment, children } = node + const start = segment.timer.startedRelativeTo(this.root.timer) + const duration = segment.getDurationInMillis() + + const segmentChildren = this.getCollectedChildren(children) + const childArray = [] + + // push serialized data into the specified destination + destination.push([start, start + duration, segment.name, segment.getAttributes(), childArray]) + + if (segmentChildren.length) { + // push the children and the parent's children array into the stack. + // to preserve the chronological order of the children, push them + // onto the stack backwards (so the first one created is on top). + for (let i = segmentChildren.length - 1; i >= 0; --i) { + segmentsToProcess.push({ + node: segmentChildren[i], + destination: childArray + }) + } + } + } + + // pull the result out of the array we serialized it into + return resultDest[0] +} + /** * Serializes the trace into the expected JSON format to be sent. * @@ -360,17 +467,21 @@ Trace.prototype._serializeTrace = function _serializeTrace() { intrinsics: this.intrinsics } - return [ + const trace = [ this.root.timer.start * FROM_MILLIS, {}, // moved to agentAttributes { // hint to RPM for how to display this trace's segments nr_flatten_leading: false }, // moved to userAttributes - this.root.toJSON(), + this.toJSON(), attributes, [] // FIXME: parameter groups ] + + // clear out segments + this.segments = null + return trace } module.exports = Trace diff --git a/lib/transaction/trace/segment-tree.js b/lib/transaction/trace/segment-tree.js new file mode 100644 index 0000000000..cacb66b288 --- /dev/null +++ b/lib/transaction/trace/segment-tree.js @@ -0,0 +1,50 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const defaultLogger = require('../../logger').child({ component: 'segment-tree' }) + +class Node { + constructor(segment) { + this.segment = segment + this.children = [] + } +} + +class SegmentTree { + constructor(root, { logger = defaultLogger } = {}) { + this.logger = logger + this.root = new Node(root) + } + + find(parentId, node = this.root) { + if (parentId === node.segment.id) { + return node + } + + for (const child of node.children) { + const result = this.find(parentId, child) + if (result) { + return result + } + } + + return null + } + + add(segment) { + const node = new Node(segment) + const parent = this.find(segment.parentId) + + if (!parent) { + this.logger.debug('Cannot find parent %s in tree', segment.parentId) + return + } + + parent.children.push(node) + } +} + +module.exports = SegmentTree diff --git a/lib/transaction/trace/segment.js b/lib/transaction/trace/segment.js index b39afb859b..de9d4a8552 100644 --- a/lib/transaction/trace/segment.js +++ b/lib/transaction/trace/segment.js @@ -6,7 +6,6 @@ 'use strict' const { DESTINATIONS } = require('../../config/attribute-filter') -const logger = require('../../logger').child({ component: 'segment' }) const Timer = require('../../timer') const hashes = require('../../util/hashes') @@ -26,61 +25,48 @@ const ATTRIBUTE_SCOPE = 'segment' * * @class * @classdesc - * TraceSegments are inserted to track instrumented function calls. Each one is - * bound to a transaction, given a name (used only internally to the framework + * TraceSegments are inserted to track instrumented function calls. They + * are reported as part of a transaction trace. It has name (used only internally to the framework * for now), and has one or more children (that are also part of the same - * transaction), as well as an associated timer. - * @param {Transaction} transaction - * The transaction to which this segment will be bound. - * @param {string} name - * Human-readable name for this segment (e.g. 'http', 'net', 'express', + * transaction trace), as well as an associated timer. + * @param {object} params to function + * @param {object} params.config agent config + * @param {string} params.name Human-readable name for this segment (e.g. 'http', 'net', 'express', * 'mysql', etc). - * @param {?Function} recorder - * Callback that takes a segment and a scope name as attributes (intended to be - * used to record metrics related to the segment). + * @param {number} params.parentId parent id of segment + * @param {boolean} params.collect flag to collect as part of transaction trace + * @param {TraceSegment} params.root root segment + * @param {boolean} params.isRoot flag to indicate it is the root segment */ -function TraceSegment(transaction, name, recorder) { +function TraceSegment({ config, name, collect, parentId, root, isRoot = false }) { + this.isRoot = isRoot + this.root = root this.name = name - this.transaction = transaction - - ++transaction.numSegments - ++transaction.agent.totalActiveSegments - ++transaction.agent.segmentsCreatedInHarvest - - if (recorder) { - transaction.addRecorder(recorder.bind(null, this)) - } - this.attributes = new Attributes(ATTRIBUTE_SCOPE) - - this.children = [] + this.spansEnabled = config?.distributed_tracing?.enabled && config?.span_events?.enabled // Generate a unique id for use in span events. this.id = hashes.makeId() + this.parentId = parentId this.timer = new Timer() this.internal = false this.opaque = false - this.shim = null + this.shimId = null // hidden class optimization this.partialName = null this._exclusiveDuration = null - this._collect = true + this._collect = collect this.host = null this.port = null this.state = STATE.EXTERNAL this.async = true this.ignore = false - - this.probe('new TraceSegment') } TraceSegment.prototype.getSpanContext = function getSpanContext() { - const config = this.transaction.agent.config - const spansEnabled = config.distributed_tracing.enabled && config.span_events.enabled - - if (!this._spanContext && spansEnabled) { + if (!this._spanContext && this.spansEnabled) { this._spanContext = new SpanContext() } @@ -108,9 +94,7 @@ TraceSegment.prototype.getAttributes = function getAttributes() { } TraceSegment.prototype.getSpanId = function getSpanId() { - const conf = this.transaction.agent.config - const enabled = conf.span_events.enabled && conf.distributed_tracing.enabled - if (enabled) { + if (this.spansEnabled) { return this.id } @@ -125,21 +109,14 @@ TraceSegment.prototype.isInCallbackState = function isInCallbackState() { return this.state === STATE.CALLBACK } -TraceSegment.prototype.probe = function probe(action) { - if (this.transaction.traceStacks) { - this.transaction.probe(action, { segment: this.name }) - } -} - /** * For use when a transaction is ending. The transaction segment should * be named after the transaction it belongs to (which is only known by * the end). + * @param {Transaction} transaction The transaction to which this segment will be bound. */ -TraceSegment.prototype.setNameFromTransaction = function setNameFromTransaction() { - const transaction = this.transaction - - // transaction name and transaciton segment name must match +TraceSegment.prototype.setNameFromTransaction = function setNameFromTransaction(transaction) { + // transaction name and transaction segment name must match this.name = transaction.getFullName() // partialName is used to name apdex metrics when recording @@ -153,10 +130,10 @@ TraceSegment.prototype.setNameFromTransaction = function setNameFromTransaction( * recording, it's also necessary to copy the transaction's partial name. And * finally, marking the trace segment as being a web segment copies the * segment's parameters onto the transaction. + * @param {Transaction} transaction The transaction to which this segment will be bound. */ -TraceSegment.prototype.markAsWeb = function markAsWeb() { - const transaction = this.transaction - this.setNameFromTransaction() +TraceSegment.prototype.markAsWeb = function markAsWeb(transaction) { + this.setNameFromTransaction(transaction) const traceAttrs = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) Object.keys(traceAttrs).forEach((key) => { @@ -172,7 +149,6 @@ TraceSegment.prototype.markAsWeb = function markAsWeb() { * the timer as having a stop time. */ TraceSegment.prototype.touch = function touch() { - this.probe('Touched') this.timer.touch() this._updateRootTimer() } @@ -193,12 +169,11 @@ TraceSegment.prototype.end = function end() { if (!this.timer.isActive()) { return } - this.probe('Ended') this.timer.end() this._updateRootTimer() } -TraceSegment.prototype.finalize = function finalize() { +TraceSegment.prototype.finalize = function finalize(trace) { if (this.timer.softEnd()) { this._updateRootTimer() // timer.softEnd() returns true if the timer was ended prematurely, so @@ -206,15 +181,16 @@ TraceSegment.prototype.finalize = function finalize() { this.name = NAMES.TRUNCATED.PREFIX + this.name } - this.addAttribute('nr_exclusive_duration_millis', this.getExclusiveDurationInMillis()) + this.addAttribute('nr_exclusive_duration_millis', this.getExclusiveDurationInMillis(trace)) } /** * Helper to set the end of the root timer to this segment's root if it is later * in time. */ + TraceSegment.prototype._updateRootTimer = function _updateRootTimer() { - const root = this.transaction.trace.root + const root = this.isRoot ? this : this.root if (this.timer.endsAfter(root.timer)) { const newDuration = this.timer.start + this.getDurationInMillis() - root.timer.start root.overwriteDurationInMillis(newDuration) @@ -230,34 +206,6 @@ TraceSegment.prototype._isEnded = function _isEnded() { return !this.timer.isActive() || this.timer.touched } -/** - * Add a new segment to a scope implicitly bounded by this segment. - * - * @param {string} childName New human-readable name for the segment. - * @param recorder - * @returns {TraceSegment} New nested TraceSegment. - */ -TraceSegment.prototype.add = function add(childName, recorder) { - if (this.opaque) { - logger.trace('Skipping child addition on opaque segment') - return this - } - logger.trace('Adding segment %s to %s in %s', childName, this.name, this.transaction.id) - const segment = new TraceSegment(this.transaction, childName, recorder) - const config = this.transaction.agent.config - - if (this.transaction.trace.segmentsSeen++ >= config.max_trace_segments) { - segment._collect = false - } - this.children.push(segment) - - if (config.debug && config.debug.double_linked_transactions) { - segment.parent = this - } - - return segment -} - /** * Set the duration of the segment explicitly. * @@ -293,138 +241,15 @@ function _setExclusiveDurationInMillis(duration) { */ TraceSegment.prototype.getExclusiveDurationInMillis = getExclusiveDurationInMillis -function getExclusiveDurationInMillis() { +function getExclusiveDurationInMillis(trace) { if (this._exclusiveDuration == null) { // Calculate the exclusive time for the subtree rooted at `this` - const calculator = new ExclusiveCalculator(this) + const calculator = new ExclusiveCalculator(this, trace) calculator.process() } return this._exclusiveDuration } -TraceSegment.prototype.getChildren = function getChildren() { - const children = [] - for (let i = 0, len = this.children.length; i < len; ++i) { - if (!this.children[i].ignore) { - children.push(this.children[i]) - } - } - return children -} - -TraceSegment.prototype.getCollectedChildren = function getCollectedChildren() { - const children = [] - for (let i = 0, len = this.children.length; i < len; ++i) { - if (this.children[i]._collect && !this.children[i].ignore) { - children.push(this.children[i]) - } - } - return children -} - -/** - * Enumerate the timings of this segment's descendants. - * - * @param {number} end The end of this segment, to keep the calculated - * duration from exceeding the duration of the - * parent. Defaults to Infinity. - * @returns {Array} Unsorted list of [start, end] pairs, with no pair - * having an end greater than the passed in end time. - */ -TraceSegment.prototype._getChildPairs = function _getChildPairs(end) { - // quick optimization - if (this.children.length < 1) { - return [] - } - if (!end) { - end = Infinity - } - - let children = this.getChildren() - const childPairs = [] - while (children.length) { - const child = children.pop() - const pair = child.timer.toRange() - - if (pair[0] >= end) { - continue - } - - children = children.concat(child.getChildren()) - - pair[1] = Math.min(pair[1], end) - childPairs.push(pair) - } - - return childPairs -} - -/** - * This is perhaps the most poorly-documented element of transaction traces: - * what do each of the segment representations look like prior to encoding? - * Spelunking in the code for the other agents has revealed that each child - * node is an array with the following field in the following order: - * - * 0: entry timestamp relative to transaction start time - * 1: exit timestamp - * 2: metric name - * 3: parameters as a name -> value JSON dictionary - * 4: any child segments - * - * Other agents include further fields in this. I haven't gotten to the bottom - * of all of them (and Ruby, of course, sends marshalled Ruby object), but - * here's what I know so far: - * - * in Java: - * 5: class name - * 6: method name - * - * in Python: - * 5: a "label" - * - * FIXME: I don't know if it makes sense to add custom fields for Node. TBD - */ -TraceSegment.prototype.toJSON = function toJSON() { - // use depth-first search on the segment tree using stack - const resultDest = [] - // array of objects relating a segment and the destination for its - // serialized data. - const segmentsToProcess = [ - { - segment: this, - destination: resultDest - } - ] - - while (segmentsToProcess.length !== 0) { - const { segment, destination } = segmentsToProcess.pop() - - const start = segment.timer.startedRelativeTo(segment.transaction.trace.root.timer) - const duration = segment.getDurationInMillis() - - const segmentChildren = segment.getCollectedChildren() - const childArray = [] - - // push serialized data into the specified destination - destination.push([start, start + duration, segment.name, segment.getAttributes(), childArray]) - - if (segmentChildren.length) { - // push the children and the parent's children array into the stack. - // to preserve the chronological order of the children, push them - // onto the stack backwards (so the first one created is on top). - for (let i = segmentChildren.length - 1; i >= 0; --i) { - segmentsToProcess.push({ - segment: segmentChildren[i], - destination: childArray - }) - } - } - } - - // pull the result out of the array we serialized it into - return resultDest[0] -} - /** * Adds all the relevant segment attributes for an External http request * diff --git a/lib/transaction/tracer/index.js b/lib/transaction/tracer/index.js index 495862c5bd..ae14d45ff4 100644 --- a/lib/transaction/tracer/index.js +++ b/lib/transaction/tracer/index.js @@ -12,18 +12,21 @@ const INACTIVE_TRANSACTION_MESSAGE = 'Not creating segment "%s" because no trans const SKIP_WRAPPING_FUNCTION_MESSAGE = 'Not wrapping "%s" because it was not a function' const CREATE_SEGMENT_MESSAGE = 'Creating "%s" segment for transaction %s.' const { addCLMAttributes: maybeAddCLMAttributes } = require('../../util/code-level-metrics') +const AsyncLocalContextManager = require('../../context-manager/async-local-context-manager') +const TraceSegment = require('../trace/segment') module.exports = Tracer -function Tracer(agent, contextManager) { +function Tracer(agent) { if (!agent) { throw new Error('Must be initialized with an agent.') } this.agent = agent - this._contextManager = contextManager + this._contextManager = new AsyncLocalContextManager() } +Tracer.prototype.getContext = getContext Tracer.prototype.getTransaction = getTransaction Tracer.prototype.getSegment = getSegment Tracer.prototype.setSegment = setSegment @@ -36,7 +39,6 @@ Tracer.prototype.bindFunction = bindFunction Tracer.prototype.bindEmitter = bindEmitter Tracer.prototype.getOriginal = getOriginal Tracer.prototype.slice = argSlice -Tracer.prototype.wrapFunctionNoSegment = wrapFunctionNoSegment Tracer.prototype.wrapFunctionFirstNoSegment = wrapFunctionFirstNoSegment Tracer.prototype.wrapFunction = wrapFunction Tracer.prototype.wrapFunctionLast = wrapFunctionLast @@ -44,10 +46,14 @@ Tracer.prototype.wrapFunctionFirst = wrapFunctionFirst Tracer.prototype.wrapSyncFunction = wrapSyncFunction Tracer.prototype.wrapCallback = wrapCallback +function getContext() { + return this._contextManager.getContext() +} + function getTransaction() { - const currentSegment = this._contextManager.getContext() - if (currentSegment && currentSegment.transaction && currentSegment.transaction.isActive()) { - return currentSegment.transaction + const context = this.getContext() + if (context?.transaction && context?.transaction?.isActive()) { + return context.transaction } return null @@ -55,11 +61,19 @@ function getTransaction() { // TODO: Remove/replace external uses to tracer.getSegment() function getSegment() { - return this._contextManager.getContext() + const context = this.getContext() + return context?.segment || null } -function setSegment(segment) { - this._contextManager.setContext(segment) +// TODO: update to setNewContext or something like that +function setSegment({ transaction, segment } = {}) { + const context = this.getContext() + const newContext = context.enterSegment({ + transaction: transaction !== undefined ? transaction : context.transaction, + segment: segment !== undefined ? segment : context.segment + }) + + this._contextManager.setContext(newContext) } // TODO: Remove/replace external uses to tracer.getSpanContext() @@ -67,21 +81,47 @@ function getSpanContext() { const currentSegment = this.getSegment() return currentSegment && currentSegment.getSpanContext() } - -function createSegment(name, recorder, _parent) { - const parent = _parent || this.getSegment() - if (!parent || !parent.transaction.isActive()) { +function createSegment({ name, recorder, parent, transaction }) { + if (!parent || !transaction?.isActive()) { logger.trace( { hasParent: !!parent, - transactionActive: parent && parent.transaction.isActive() + transactionActive: transaction?.isActive() }, 'Not creating segment %s, no parent or active transaction available.', name ) return null } - return parent.add(name, recorder) + + if (parent.opaque) { + logger.trace('Skipping child addition on opaque segment') + return parent + } + + logger.trace('Adding segment %s to %s in %s', name, parent.name, transaction.id) + + let collect = true + + if (transaction.numSegments >= this.agent.config.max_trace_segments) { + collect = false + } + transaction.incrementCounters() + + const segment = new TraceSegment({ + config: this.agent.config, + name, + collect, + root: transaction.trace.root, + parentId: parent.id + }) + + if (recorder) { + transaction.addRecorder(recorder.bind(null, segment)) + } + transaction.trace.segments.add(segment) + + return segment } function addSegment(name, recorder, parent, full, task) { @@ -89,10 +129,14 @@ function addSegment(name, recorder, parent, full, task) { throw new Error('task must be a function') } - const segment = this.createSegment(name, recorder, parent) - + const context = this.getContext() + const segment = this.createSegment({ name, recorder, parent, transaction: context.transaction }) + let newContext = context + if (segment) { + newContext = context.enterSegment({ segment }) + } maybeAddCLMAttributes(task, segment) - return this.bindFunction(task, segment, full)(segment) + return this.bindFunction(task, newContext, full)(segment, context?.transaction) } function transactionProxy(handler) { @@ -108,15 +152,13 @@ function transactionProxy(handler) { } // don't nest transactions, reuse existing ones - const segment = tracer.getSegment() + const context = tracer.getContext() + const segment = context?.segment + const currentTx = context?.transaction if (segment) { - if (segment.transaction.traceStacks) { - segment.probe('!!! Nested transaction creation !!!') - segment.transaction.traceFlag = true // Will log the stacks when it ends. - } logger.warn( { - transaction: { id: segment.transaction.id, name: segment.transaction.getName() }, + transaction: { id: currentTx.id, name: currentTx.getName() }, segment: segment.name }, 'Active transaction when creating non-nested transaction' @@ -124,8 +166,10 @@ function transactionProxy(handler) { tracer.agent.recordSupportability('Nodejs/Transactions/Nested') return handler.apply(this, arguments) } + const transaction = new Transaction(tracer.agent) - return tracer.bindFunction(handler, transaction.trace.root, true).apply(this, arguments) + const newContext = context.enterTransaction(transaction) + return tracer.bindFunction(handler, newContext, true).apply(this, arguments) } wrapped[symbols.original] = handler @@ -161,22 +205,21 @@ function transactionNestProxy(type, handler) { } // don't nest transactions, reuse existing ones - let transaction = tracer.getTransaction() - let segment = tracer.getSegment() + let context = tracer.getContext() let createNew = false - if (!transaction || transaction.type !== type) { + if (!context?.transaction || context?.transaction.type !== type) { createNew = true } if (createNew) { - transaction = new Transaction(tracer.agent) + const transaction = new Transaction(tracer.agent) transaction.type = type - segment = transaction.trace.root + context = context.enterTransaction(transaction) } - return tracer.bindFunction(handler, segment).apply(this, arguments) + return tracer.bindFunction(handler, context).apply(this, arguments) } wrapped[symbols.original] = handler @@ -184,39 +227,33 @@ function transactionNestProxy(type, handler) { return wrapped } -function bindFunction(handler, segment, full) { +function bindFunction(handler, context, full) { if (typeof handler !== 'function') { return handler } - return _makeWrapped(this, handler, segment || this.getSegment(), !!full) + return _makeWrapped({ tracer: this, handler, context, full: !!full }) } -function _makeWrapped(tracer, handler, active, full) { +function _makeWrapped({ tracer, handler, context, full }) { + const { segment } = context wrapped[symbols.original] = getOriginal(handler) - wrapped[symbols.segment] = active + wrapped[symbols.segment] = segment return wrapped function wrapped() { - const prev = tracer.getSegment() - - if (active && full) { - active.start() + if (segment && full) { + segment.start() } try { - return tracer._contextManager.runInContext(active, handler, this, arguments) + return tracer._contextManager.runInContext(context, handler, this, arguments) } catch (err) { logger.trace(err, 'Error from wrapped function:') - - if (prev === null && process.domain != null) { - process.domain[symbols.segment] = tracer.getSegment() - } - throw err // Re-throwing application error, this is not an agent error. } finally { - if (active && full) { - active.touch() + if (segment && full) { + segment.touch() } } } @@ -236,7 +273,9 @@ function bindEmitter(emitter, segment) { } const emit = getOriginal(emitter.emit) - emitter.emit = this.bindFunction(emit, segment) + const context = this.getContext() + const newContext = context.enterSegment({ segment }) + emitter.emit = this.bindFunction(emit, newContext) return emitter } @@ -267,35 +306,6 @@ function argSlice(args) { return array } -function wrapFunctionNoSegment(original, name, wrapper) { - if (typeof original !== 'function') { - return original - } - - logger.trace('Wrapping function %s (no segment)', name || original.name || 'anonymous') - const tracer = this - - return wrappedFunction - - function wrappedFunction() { - if (!tracer.getTransaction()) { - return original.apply(this, arguments) - } - let args = tracer.slice(arguments) - - if (wrapper === undefined) { - const last = args.length - 1 - const cb = args[last] - if (typeof cb === 'function') { - args[last] = tracer.bindFunction(cb) - } - } else { - args = wrapper(args) - } - return original.apply(this, args) - } -} - function wrapFunctionFirstNoSegment(original, name) { if (typeof original !== 'function') { return original @@ -310,10 +320,11 @@ function wrapFunctionFirstNoSegment(original, name) { if (!tracer.getTransaction()) { return original.apply(this, arguments) } + const context = tracer.getContext() const args = tracer.slice(arguments) const cb = args[0] if (typeof cb === 'function') { - args[0] = tracer.bindFunction(cb) + args[0] = tracer.bindFunction(cb, context) } return original.apply(this, args) } @@ -331,6 +342,7 @@ function wrapFunctionLast(name, recorder, original) { return wrappedFunction function wrappedFunction() { + const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) @@ -344,14 +356,20 @@ function wrapFunctionLast(name, recorder, original) { if (typeof cb !== 'function') { return original.apply(this, arguments) } - const child = tracer.createSegment(name, recorder) + const child = tracer.createSegment({ + name, + recorder, + parent: context.segment, + transaction: context.transaction + }) args[last] = tracer.wrapCallback(cb, child, function wrappedCallback() { logger.trace('Ending "%s" segment for transaction %s.', name, transaction.id) child.touch() return cb.apply(this, arguments) }) child.start() - return tracer.bindFunction(original, child).apply(this, args) + const newContext = context.enterSegment({ segment: child }) + return tracer.bindFunction(original, newContext).apply(this, args) } } @@ -367,6 +385,7 @@ function wrapFunctionFirst(name, recorder, original) { return wrappedFunction function wrappedFunction() { + const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) @@ -379,14 +398,20 @@ function wrapFunctionFirst(name, recorder, original) { if (typeof cb !== 'function') { return original.apply(this, arguments) } - const child = tracer.createSegment(name, recorder) + const child = tracer.createSegment({ + name, + recorder, + parent: context.segment, + transaction: context.transaction + }) args[0] = tracer.wrapCallback(cb, child, function wrappedCallback() { logger.trace('Ending "%s" segment for transaction %s.', name, transaction.id) child.touch() return cb.apply(this, arguments) }) child.start() - return tracer.bindFunction(original, child).apply(this, args) + const newContext = context.enterSegment({ segment: child }) + return tracer.bindFunction(original, newContext).apply(this, args) } } @@ -403,6 +428,7 @@ function wrapFunction(name, recorder, original, wrapper, resp) { return wrappedFunction function wrappedFunction() { + const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) @@ -411,10 +437,11 @@ function wrapFunction(name, recorder, original, wrapper, resp) { logger.trace(CREATE_SEGMENT_MESSAGE, name, transaction.id) - const child = tracer.createSegment(name, recorder) + const child = tracer.createSegment({ name, recorder, parent: context.segment, transaction }) const args = wrapper.call(this, child, tracer.slice(arguments), bind) child.start() - let result = tracer.bindFunction(original, child).apply(this, args) + const newContext = context.enterSegment({ segment: child }) + let result = tracer.bindFunction(original, newContext).apply(this, args) if (resp) { result = resp.call(this, child, result, bind) } @@ -446,22 +473,25 @@ function wrapSyncFunction(name, recorder, original) { return wrappedFunction function wrappedFunction() { + const context = tracer.getContext() const transaction = tracer.getTransaction() if (!transaction) { logger.trace(INACTIVE_TRANSACTION_MESSAGE, name) return original.apply(this, arguments) } logger.trace('Creating "%s" sync segment for transaction %s.', name, transaction.id) - const child = tracer.createSegment(name, recorder) + const child = tracer.createSegment({ name, recorder, parent: context.segment, transaction }) if (child) { child.async = false } - return tracer.bindFunction(original, child, true).apply(this, arguments) + const newContext = context.enterSegment({ segment: child }) + return tracer.bindFunction(original, newContext, true).apply(this, arguments) } } function wrapCallback(original, segment, wrapped) { const tracer = this + const context = this.getContext() if (typeof original !== 'function') { return original @@ -475,19 +505,20 @@ function wrapCallback(original, segment, wrapped) { wrapped[symbols.original] = original } - const child = tracer.createSegment( - 'Callback: ' + (original.name || 'anonymous'), - null, - segment - ) + const child = tracer.createSegment({ + name: 'Callback: ' + (original.name || 'anonymous'), + parent: segment, + transaction: context.transaction + }) if (child) { child.async = false } - return tracer.bindFunction(wrapped || original, child, true).apply(this, arguments) + const newContext = context.enterSegment({ segment: child }) + return tracer.bindFunction(wrapped || original, newContext, true).apply(this, arguments) }, - segment, + context, false ) } diff --git a/lib/util/cat.js b/lib/util/cat.js index 8c9776ae2c..28d72e341c 100644 --- a/lib/util/cat.js +++ b/lib/util/cat.js @@ -273,11 +273,13 @@ cat.parseAppData = function parseAppData(config, obfAppData) { * Assigns the CAT id, transaction to segment and adds `transaction_guid` when it exists. * It also renames the segment name based on the newly decoded app data when host is present * - * @param {Array} appData decodes CAT app data - * @param {TraceSegment} segment - * @param {string} [host] if host is present it will rename segment with app data and host + * @param {object} params to function + * @param {Array} params.appData decodes CAT app data + * @param {TraceSegment} params.segment segment to assign CAT data + * @param {string} [params.host] if host is present it will rename segment with app data and host + * @param {Transaction} params.transaction active transaction */ -cat.assignCatToSegment = function assignCatToSegment(appData, segment, host) { +cat.assignCatToSegment = function assignCatToSegment({ appData, segment, host, transaction }) { if (!Array.isArray(appData) || typeof appData[0] !== 'string') { logger.trace(`Unknown format for CAT header ${HTTP_CAT_APP_DATA_HEADER}.`) return @@ -297,7 +299,7 @@ cat.assignCatToSegment = function assignCatToSegment(appData, segment, host) { } logger.trace( 'Got inbound response CAT headers in transaction %s from %s', - segment.transaction.id, + transaction.id, transactionGuid ) } diff --git a/lib/util/trace-stacks.js b/lib/util/trace-stacks.js new file mode 100644 index 0000000000..48cd4f8052 --- /dev/null +++ b/lib/util/trace-stacks.js @@ -0,0 +1,25 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = class TraceStacks { + constructor(config) { + this.stack = config?.logging.diagnostics ? [] : null + } + + probe(action, data) { + if (this.stack) { + this.stack.push({ + stack: new Error(action).stack.split('\n'), + extra: data + }) + } + } + + serialize(name) { + return { segment: name, stacks: this.stack } + } +} diff --git a/package.json b/package.json index fbae7715c3..1927781dd4 100644 --- a/package.json +++ b/package.json @@ -198,6 +198,8 @@ "@grpc/grpc-js": "^1.12.2", "@grpc/proto-loader": "^0.7.5", "@newrelic/security-agent": "^2.2.0", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@tyriar/fibonacci-heap": "^2.0.7", "concat-stream": "^2.0.0", "https-proxy-agent": "^7.0.1", @@ -224,6 +226,7 @@ "@newrelic/newrelic-oss-cli": "^0.1.2", "@newrelic/test-utilities": "^9.1.0", "@octokit/rest": "^18.0.15", + "@opentelemetry/sdk-trace-base": "^1.27.0", "@slack/bolt": "^3.7.0", "@smithy/eventstream-codec": "^2.2.0", "@smithy/util-utf8": "^2.3.0", diff --git a/test/benchmark/events/span-event.bench.js b/test/benchmark/events/span-event.bench.js index f1e53bbbe2..6e8420ad87 100644 --- a/test/benchmark/events/span-event.bench.js +++ b/test/benchmark/events/span-event.bench.js @@ -11,16 +11,17 @@ const SpanEvent = require('../../../lib/spans/span-event') const suite = benchmark.createBenchmark({ name: 'SpanEvent' }) let segment = null +let transaction = null suite.add({ name: 'from generic segment', agent: {}, before: (agent) => { - segment = makeSegment(agent) + ;({ segment, transaction } = makeSegment(agent)) segment.name = 'some random segment' }, fn: () => { - return SpanEvent.fromSegment(segment) + return SpanEvent.fromSegment(segment, transaction) } }) @@ -28,11 +29,11 @@ suite.add({ name: 'from external segment', agent: {}, before: (agent) => { - segment = makeSegment(agent) + ;({ segment, transaction } = makeSegment(agent)) segment.name = 'External/www.foobar.com/' }, fn: () => { - return SpanEvent.fromSegment(segment) + return SpanEvent.fromSegment(segment, transaction) } }) @@ -40,30 +41,31 @@ suite.add({ name: 'from db segment', agent: {}, before: (agent) => { - segment = makeSegment(agent) + ;({ segment, transaction } = makeSegment(agent)) segment.name = 'Datastore/statement/SELECT' }, fn: () => { - return SpanEvent.fromSegment(segment) + return SpanEvent.fromSegment(segment, transaction) } }) suite.run() function makeSegment(agent) { - const s = helper.runInTransaction(agent, (tx) => tx.trace.root) - s.addAttribute('foo', 'bar') - s.addAttribute('request.headers.x-customer-header', 'some header value') - s.addAttribute('library', 'my great library') - s.addAttribute('url', 'http://my-site.com') - s.addAttribute('procedure', 'GET') - s.addAttribute('product', 'BestDB') - s.addAttribute('sql', 'SELECT * FROM the_best') - s.addAttribute('database_name', 'users_db') - s.addAttribute('host', '123.123.123.123') - s.addAttribute('port_path_or_id', '3306') - s.end() - s.transaction.end() + const transaction = helper.runInTransaction(agent, (tx) => tx) + const segment = transaction.trace.root + segment.addAttribute('foo', 'bar') + segment.addAttribute('request.headers.x-customer-header', 'some header value') + segment.addAttribute('library', 'my great library') + segment.addAttribute('url', 'http://my-site.com') + segment.addAttribute('procedure', 'GET') + segment.addAttribute('product', 'BestDB') + segment.addAttribute('sql', 'SELECT * FROM the_best') + segment.addAttribute('database_name', 'users_db') + segment.addAttribute('host', '123.123.123.123') + segment.addAttribute('port_path_or_id', '3306') + segment.end() + transaction.end() - return s + return { segment, transaction } } diff --git a/test/benchmark/shim/record.bench.js b/test/benchmark/shim/record.bench.js index db8354c4fc..e7003e3640 100644 --- a/test/benchmark/shim/record.bench.js +++ b/test/benchmark/shim/record.bench.js @@ -41,7 +41,7 @@ const wrapped = shim.record(getTest(), 'func', function () { suite.add({ name: 'wrapper - no transaction', fn: function () { - tracer.setSegment(null) + tracer.setSegment({ segment: null, transaction: null }) wrapped.func(noop) } }) @@ -49,7 +49,7 @@ suite.add({ suite.add({ name: 'wrapper - in transaction', fn: function () { - tracer.setSegment(transaction.trace.root) + tracer.setSegment({ transaction, segment: transaction.trace.root }) wrapped.func(noop) } }) diff --git a/test/benchmark/shim/segments.bench.js b/test/benchmark/shim/segments.bench.js index 095f656539..f747b9bcbb 100644 --- a/test/benchmark/shim/segments.bench.js +++ b/test/benchmark/shim/segments.bench.js @@ -94,7 +94,6 @@ suite.add({ fn: function () { const test = shared.getTest() shim.createSegment('foo', test.func, tx.trace.root) - tx.trace.root.children = [] return test } }) diff --git a/test/benchmark/trace/segment.bench.js b/test/benchmark/trace/segment.bench.js index 514559c8af..579748dca9 100644 --- a/test/benchmark/trace/segment.bench.js +++ b/test/benchmark/trace/segment.bench.js @@ -7,23 +7,20 @@ const helper = require('../../lib/agent_helper') const benchmark = require('../../lib/benchmark') -const Segment = require('../../../lib/transaction/trace/segment') +const Transaction = require('../../../lib/transaction') const agent = helper.loadMockedAgent() -const tx = helper.runInTransaction(agent, (_tx) => _tx) - const suite = benchmark.createBenchmark({ name: 'trace segments' }) -let root - -function addChildren(rootSegment, numChildren) { - const queue = [rootSegment] +let trace +function addChildren(trace, numChildren) { + const queue = [trace.root] for (let numSegments = 1; numSegments < 900; numSegments += numChildren) { const parent = queue.shift() for (let i = 0; i < numChildren; ++i) { - const child = parent.add('child ' + (numSegments + i)) + const child = trace.add('child ' + (numSegments + i), null, parent) child.timer.setDurationInMillis( (0.99 + Math.random() / 100) * parent.timer.durationInMillis, parent.timer.start + 1 @@ -37,12 +34,13 @@ suite.add({ name: 'toJSON flat', before: function buildTree() { - root = new Segment(tx, 'ROOT') - root.timer.setDurationInMillis(10000, Date.now()) - addChildren(root, 899) + const transaction = new Transaction(agent) + trace = transaction.trace + trace.root.timer.setDurationInMillis(10000, Date.now()) + addChildren(trace, 899) }, fn: function () { - return root.toJSON() + return trace.toJSON() } }) @@ -50,12 +48,13 @@ suite.add({ name: 'toJSON linear', before: function buildTree() { - root = new Segment(tx, 'ROOT') - root.timer.setDurationInMillis(10000, Date.now()) - addChildren(root, 1) + const transaction = new Transaction(agent) + trace = transaction.trace + trace.root.timer.setDurationInMillis(10000, Date.now()) + addChildren(trace, 1) }, fn: function () { - return root.toJSON() + return trace.toJSON() } }) @@ -63,12 +62,13 @@ suite.add({ name: 'toJSON binary', before: function buildTree() { - root = new Segment(tx, 'ROOT') - root.timer.setDurationInMillis(10000, Date.now()) - addChildren(root, 2) + const transaction = new Transaction(agent) + trace = transaction.trace + trace.root.timer.setDurationInMillis(10000, Date.now()) + addChildren(trace, 2) }, fn: function () { - return root.toJSON() + return trace.toJSON() } }) @@ -76,12 +76,13 @@ suite.add({ name: 'getExclusiveDurationInMillis flat', before: function buildTree() { - root = new Segment(tx, 'ROOT') - root.timer.setDurationInMillis(10000, Date.now()) - addChildren(root, 899) + const transaction = new Transaction(agent) + trace = transaction.trace + trace.root.timer.setDurationInMillis(10000, Date.now()) + addChildren(trace, 899) }, fn: function () { - return root.getExclusiveDurationInMillis() + return trace.getExclusiveDurationInMillis() } }) @@ -89,12 +90,13 @@ suite.add({ name: 'getExclusiveDurationInMillis linear', before: function buildTree() { - root = new Segment(tx, 'ROOT') - root.timer.setDurationInMillis(10000, Date.now()) - addChildren(root, 1) + const transaction = new Transaction(agent) + trace = transaction.trace + trace.root.timer.setDurationInMillis(10000, Date.now()) + addChildren(trace, 1) }, fn: function () { - return root.getExclusiveDurationInMillis() + return trace.getExclusiveDurationInMillis() } }) @@ -102,12 +104,13 @@ suite.add({ name: 'getExclusiveDurationInMillis binary', before: function buildTree() { - root = new Segment(tx, 'ROOT') - root.timer.setDurationInMillis(10000, Date.now()) - addChildren(root, 2) + const transaction = new Transaction(agent) + trace = transaction.trace + trace.root.timer.setDurationInMillis(10000, Date.now()) + addChildren(trace, 2) }, fn: function () { - return root.getExclusiveDurationInMillis() + return trace.getExclusiveDurationInMillis() } }) diff --git a/test/benchmark/tracer/bindFunction.bench.js b/test/benchmark/tracer/bindFunction.bench.js index 6bad2b1dc3..61193c405e 100644 --- a/test/benchmark/tracer/bindFunction.bench.js +++ b/test/benchmark/tracer/bindFunction.bench.js @@ -14,10 +14,11 @@ const tracer = helper.getTracer() const tx = helper.runInTransaction(s.agent, function (_tx) { return _tx }) -tracer.setSegment(tx.root) +tracer.setSegment({ transaction: tx, segment: tx.trace.root }) preOptBind() -const bound = tracer.bindFunction(shared.getTest().func, tx.root, true) +const ctx = tracer.getContext() +const bound = tracer.bindFunction(shared.getTest().func, ctx, true) setTimeout(function () { suite.add({ @@ -30,16 +31,6 @@ setTimeout(function () { fn: twoParamBind }) - suite.add({ - name: 'just fn', - fn: oneParamBind - }) - - suite.add({ - name: 'null segment', - fn: nullSegmentBind - }) - suite.add({ name: 'mixed', fn: randomBind @@ -57,26 +48,16 @@ setTimeout(function () { function allParamBind() { const test = shared.getTest() - test.func = tracer.bindFunction(test.func, tx.root, Math.random() > 0.5) + const ctx = tracer.getContext() + test.func = tracer.bindFunction(test.func, ctx, Math.random() > 0.5) } function twoParamBind() { const test = shared.getTest() // eslint-disable-next-line no-unused-expressions Math.random() > 0.5 // rand call so all tests perform same amount of work. - test.func = tracer.bindFunction(test.func, tx.root) -} - -function oneParamBind() { - const test = shared.getTest() - // eslint-disable-next-line no-unused-expressions - Math.random() > 0.5 // rand call so all tests perform same amount of work. - test.func = tracer.bindFunction(test.func) -} - -function nullSegmentBind() { - const test = shared.getTest() - test.func = tracer.bindFunction(test.func, null, Math.random() > 0.5) + const ctx = tracer.getContext() + test.func = tracer.bindFunction(test.func, ctx) } function randomBind() { @@ -85,10 +66,6 @@ function randomBind() { allParamBind() } else if (n >= 0.5) { twoParamBind() - } else if (n >= 0.25) { - oneParamBind() - } else { - nullSegmentBind() } } diff --git a/test/benchmark/tracer/segments.bench.js b/test/benchmark/tracer/segments.bench.js index 2d636dfde6..b3834f3e26 100644 --- a/test/benchmark/tracer/segments.bench.js +++ b/test/benchmark/tracer/segments.bench.js @@ -16,7 +16,7 @@ const tx = helper.runInTransaction(s.agent, function (_tx) { return _tx }) -tracer.setSegment(tx.root) +tracer.setSegment({ transaction: tx.root, segment: tx.root }) suite.add({ name: 'tracer.getSegment', diff --git a/test/benchmark/tracer/transaction.bench.js b/test/benchmark/tracer/transaction.bench.js index c2e668db7c..7ce74e470f 100644 --- a/test/benchmark/tracer/transaction.bench.js +++ b/test/benchmark/tracer/transaction.bench.js @@ -15,7 +15,7 @@ const tx = helper.runInTransaction(s.agent, function (_tx) { return _tx }) -tracer.setSegment(tx.root) +tracer.setSegment({ transaction: tx, segment: tx.root }) suite.add({ name: 'tracer.getTransaction', diff --git a/test/benchmark/tracer/utilities.bench.js b/test/benchmark/tracer/utilities.bench.js index 4866e07e95..46e85bcd24 100644 --- a/test/benchmark/tracer/utilities.bench.js +++ b/test/benchmark/tracer/utilities.bench.js @@ -16,7 +16,7 @@ const tx = helper.runInTransaction(s.agent, function (_tx) { return _tx }) -tracer.setSegment(tx.root) +tracer.setSegment({ transaction: tx, segment: tx.root }) suite.add({ name: 'tracer.slice', diff --git a/test/benchmark/tracer/wrapping.bench.js b/test/benchmark/tracer/wrapping.bench.js index 2a420de2bf..21258dc014 100644 --- a/test/benchmark/tracer/wrapping.bench.js +++ b/test/benchmark/tracer/wrapping.bench.js @@ -20,22 +20,16 @@ suite.add({ name: 'tracer.bindFunction', fn: function () { const test = shared.getTest() - return tracer.bindFunction(test.func, tx.root, true) + let ctx = tracer.getContext() + ctx = ctx.enterSegment({ transaction: tx, segment: tx.trace.root }) + return tracer.bindFunction(test.func, ctx, true) } }) suite.add({ name: 'tracer.bindEmitter', fn: function () { - return tracer.bindEmitter(new EventEmitter(), tx.root) - } -}) - -suite.add({ - name: 'tracer.wrapFunctionNoSegment', - fn: function () { - const test = shared.getTest() - return tracer.wrapFunctionNoSegment(test.func, 'func', function () {}) + return tracer.bindEmitter(new EventEmitter(), tx.trace.root) } }) @@ -83,7 +77,7 @@ suite.add({ name: 'tracer.wrapCallback', fn: function () { const test = shared.getTest() - return tracer.wrapCallback(test.func, tx.root, null) + return tracer.wrapCallback(test.func, tx.trace.root, null) } }) diff --git a/test/benchmark/webframework-shim/recordMiddleware.bench.js b/test/benchmark/webframework-shim/recordMiddleware.bench.js index f0d944dc65..7d268cf242 100644 --- a/test/benchmark/webframework-shim/recordMiddleware.bench.js +++ b/test/benchmark/webframework-shim/recordMiddleware.bench.js @@ -59,7 +59,7 @@ function addTests(name, speccer) { suite.add({ name: name + ' - wrapper (no tx) ', fn: function () { - tracer.setSegment(null) + tracer.setSegment({ segment: null, transaction: null }) middleware(getReqd(), {}, noop) } }) @@ -67,7 +67,7 @@ function addTests(name, speccer) { suite.add({ name: name + ' - wrapper (tx) ', fn: function () { - tracer.setSegment(transaction.trace.root) + tracer.setSegment({ transaction, segment: transaction.trace.root }) middleware(getReqd(), {}, noop) } }) diff --git a/test/integration/agent/serverless-harvest.test.js b/test/integration/agent/serverless-harvest.test.js index 9bf6c104c8..f16cd610ee 100644 --- a/test/integration/agent/serverless-harvest.test.js +++ b/test/integration/agent/serverless-harvest.test.js @@ -363,8 +363,13 @@ test('sending sql traces', async (t) => { const expectedSql = 'select pg_sleep(1)' - agent.queries.add(tx.trace.root, 'postgres', expectedSql, 'FAKE STACK') - + agent.queries.add({ + transaction: tx, + segment: tx.trace.root, + type: 'postgres', + query: expectedSql, + trace: 'FAKE STACK' + }) tx.end() agent.once('harvestFinished', () => { const rawPayload = findPayload(t.nr.writeLogs) diff --git a/test/integration/cat/cat.test.js b/test/integration/cat/cat.test.js index d3faf8ec0e..c839171ad6 100644 --- a/test/integration/cat/cat.test.js +++ b/test/integration/cat/cat.test.js @@ -172,8 +172,8 @@ test('cross application tracing full integration', async function (t) { ) // check the external segment for its properties - const externalSegment = - trace.root.children[0].children[trace.root.children[0].children.length - 1] + const [webSegment] = trace.getChildren(trace.root.id) + const [externalSegment] = trace.getChildren(webSegment.id) plan.equal( externalSegment.name.split('/')[0], 'ExternalTransaction', @@ -246,8 +246,8 @@ test('cross application tracing full integration', async function (t) { ) // check the external segment for its properties - const externalSegment = - trace.root.children[0].children[trace.root.children[0].children.length - 1] + const [webSegment] = trace.getChildren(trace.root.id) + const [externalSegment] = trace.getChildren(webSegment.id) plan.equal( externalSegment.name.split('/')[0], 'ExternalTransaction', diff --git a/test/integration/core/crypto.test.js b/test/integration/core/crypto.test.js index 5ebcd35d4d..e404f1c0be 100644 --- a/test/integration/core/crypto.test.js +++ b/test/integration/core/crypto.test.js @@ -48,7 +48,8 @@ test('sync randomBytes', function (t, end) { const bytes = crypto.randomBytes(32) assert.ok(bytes instanceof Buffer) assert.equal(bytes.length, 32) - assert.equal(transaction.trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 0) end() }) }) @@ -72,7 +73,8 @@ test('sync pseudoRandomBytes', function (t, end) { const bytes = crypto.pseudoRandomBytes(32) assert.ok(bytes instanceof Buffer) assert.equal(bytes.length, 32) - assert.equal(transaction.trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 0) end() }) }) @@ -96,7 +98,8 @@ test('sync randomFill', function (t, end) { crypto.randomFillSync(buf) assert.ok(buf instanceof Buffer) assert.equal(buf.length, 10) - assert.equal(transaction.trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 0) end() }) }) @@ -118,7 +121,8 @@ test('scryptSync', (t, end) => { const buf = crypto.scryptSync('secret', 'salt', 10) assert.ok(buf instanceof Buffer) assert.equal(buf.length, 10) - assert.equal(transaction.trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 0) end() }) }) diff --git a/test/integration/core/fs.test.js b/test/integration/core/fs.test.js index f5a5515369..4d170507d2 100644 --- a/test/integration/core/fs.test.js +++ b/test/integration/core/fs.test.js @@ -852,7 +852,8 @@ test('read', async function (t) { plan.equal(len, 12, 'should read correct number of bytes') plan.equal(data.toString('utf8'), content) plan.equal(agent.getTransaction(), trans, 'should preserve transaction') - plan.equal(trans.trace.root.children.length, 0, 'should not create any segments') + const children = trans.trace.getChildren(trans.trace.root.id) + plan.equal(children.length, 0, 'should not create any segments') }) }) @@ -875,7 +876,8 @@ test('write', async function (t) { plan.equal(len, 12, 'should write correct number of bytes') plan.equal(fs.readFileSync(name, 'utf8'), content) plan.equal(agent.getTransaction(), trans, 'should preserve transaction') - plan.equal(trans.trace.root.children.length, 0, 'should not create any segments') + const children = trans.trace.getChildren(trans.trace.root.id) + plan.equal(children.length, 0, 'should not create any segments') }) }) @@ -898,7 +900,8 @@ test('watch (file)', async function (t) { plan.equal(file, 'watch-file', 'should have correct file name') plan.equal(agent.getTransaction(), trans, 'should preserve transaction') - plan.equal(trans.trace.root.children.length, 1, 'should not create any segments') + const children = trans.trace.getChildren(trans.trace.root.id) + plan.equal(children.length, 1, 'should not create any segments') watcher.close() }) fs.writeFile(name, content + 'more', function (err) { @@ -923,7 +926,8 @@ test('watch (dir)', async function (t) { plan.equal(ev, 'rename') plan.equal(file, 'watch-dir') plan.equal(agent.getTransaction(), trans, 'should preserve transaction') - plan.equal(trans.trace.root.children.length, 1, 'should not create any segments') + const children = trans.trace.getChildren(trans.trace.root.id) + plan.equal(children.length, 1, 'should not create any segments') watcher.close() }) fs.writeFile(name, content, function (err) { @@ -951,9 +955,9 @@ test('watch emitter', async function (t) { plan.equal(file, 'watch', 'should be for correct directory') const tx = agent.getTransaction() - const root = trans.trace.root plan.equal(tx && tx.id, trans.id, 'should preserve transaction') - plan.equal(root.children.length, 1, 'should not create any segments') + const children = trans.trace.getChildren(trans.trace.root.id) + plan.equal(children.length, 1, 'should not create any segments') watcher.close() }) @@ -985,7 +989,8 @@ test('watchFile', async function (t) { plan.ok(cur.size > prev.size, 'content modified') plan.equal(agent.getTransaction(), trans, 'should preserve transaction') - plan.equal(trans.trace.root.children.length, 0, 'should not create any segments') + const children = trans.trace.getChildren(trans.trace.root.id) + plan.equal(children.length, 0, 'should not create any segments') fs.unwatchFile(name, onChange) } }) diff --git a/test/integration/core/native-promises.test.js b/test/integration/core/native-promises.test.js index a6c1b4985d..bbab732f70 100644 --- a/test/integration/core/native-promises.test.js +++ b/test/integration/core/native-promises.test.js @@ -165,19 +165,19 @@ test('AsyncLocalStorage based tracking', async (t) => { const plan = tspl(t, { plan: 5 }) const { agent, tracer } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const root = tracer.getSegment() - const aSeg = agent.tracer.createSegment('A') - tracer.setSegment(aSeg) + const aSeg = agent.tracer.createSegment({ name: 'A', parent: root, transaction: tx }) + tracer.setSegment({ segment: aSeg, transaction: tx }) const resA = new TestResource(1) - const bSeg = agent.tracer.createSegment('B') - tracer.setSegment(bSeg) + const bSeg = agent.tracer.createSegment({ name: 'B', parent: root, transaction: tx }) + tracer.setSegment({ segment: bSeg, transaction: tx }) const resB = new TestResource(2) - tracer.setSegment(root) + tracer.setSegment({ segment: root, transaction: tx }) resA.doStuff(() => { plan.equal( @@ -223,20 +223,20 @@ test('AsyncLocalStorage based tracking', async (t) => { helper.runInTransaction(agent, function (txn) { plan.ok(txn, 'transaction should not be null') - const segment = txn.trace.root + const ctx = agent.tracer.getContext() agent.tracer.bindFunction(function one() { return new Promise(executor).then(() => { const tx = agent.getTransaction() plan.equal(tx ? tx.id : null, txn.id) }) - }, segment)() + }, ctx)() const wrapperTwo = agent.tracer.bindFunction(function () { return two() - }, segment) + }, ctx) const wrapperThree = agent.tracer.bindFunction(function () { return three() - }, segment) + }, ctx) function executor(resolve) { setImmediate(() => { @@ -273,20 +273,20 @@ test('AsyncLocalStorage based tracking', async (t) => { helper.runInTransaction(agent, function (txn) { plan.ok(txn, 'transaction should not be null') - const segment = txn.trace.root + const ctx = agent.tracer.getContext() agent.tracer.bindFunction(function one() { return new Promise(executor).then(() => { const tx = agent.getTransaction() plan.equal(tx ? tx.id : null, txn.id) }) - }, segment)() + }, ctx)() const wrapperTwo = agent.tracer.bindFunction(function () { return two() - }, segment) + }, ctx) const wrapperThree = agent.tracer.bindFunction(function () { return three() - }, segment) + }, ctx) function executor(resolve) { process.nextTick(() => { @@ -323,20 +323,20 @@ test('AsyncLocalStorage based tracking', async (t) => { helper.runInTransaction(agent, function (txn) { plan.ok(txn, 'transaction should not be null') - const segment = txn.trace.root + const ctx = agent.tracer.getContext() agent.tracer.bindFunction(function one() { return new Promise(executor).then(() => { const tx = agent.getTransaction() plan.equal(tx ? tx.id : null, txn.id) }) - }, segment)() + }, ctx)() const wrapperTwo = agent.tracer.bindFunction(function () { return two() - }, segment) + }, ctx) const wrapperThree = agent.tracer.bindFunction(function () { return three() - }, segment) + }, ctx) function executor(resolve) { setTimeout(() => { @@ -373,20 +373,20 @@ test('AsyncLocalStorage based tracking', async (t) => { helper.runInTransaction(agent, function (txn) { plan.ok(txn, 'transaction should not be null') - const segment = txn.trace.root + const ctx = agent.tracer.getContext() agent.tracer.bindFunction(function one() { return new Promise(executor).then(() => { const tx = agent.getTransaction() plan.equal(tx ? tx.id : null, txn.id) }) - }, segment)() + }, ctx)() const wrapperTwo = agent.tracer.bindFunction(function () { return two() - }, segment) + }, ctx) const wrapperThree = agent.tracer.bindFunction(function () { return three() - }, segment) + }, ctx) function executor(resolve) { const ref = setInterval(() => { @@ -438,15 +438,15 @@ test('AsyncLocalStorage based tracking', async (t) => { helper.runInTransaction(agent, function (txn) { plan.ok(txn, 'transaction should not be null') - const segment = txn.trace.root - agent.tracer.bindFunction(one, segment)() + const ctx = agent.tracer.getContext() + agent.tracer.bindFunction(one, ctx)() const wrapperTwo = agent.tracer.bindFunction(function () { return two() - }, segment) + }, ctx) const wrapperThree = agent.tracer.bindFunction(function () { return three() - }, segment) + }, ctx) function one() { return new Promise(executor).then(() => { diff --git a/test/integration/core/net.test.js b/test/integration/core/net.test.js index 1e38ce4dec..cb64108707 100644 --- a/test/integration/core/net.test.js +++ b/test/integration/core/net.test.js @@ -31,8 +31,6 @@ test('createServer', function createServerTest(t, end) { const server = net.createServer(handler) server.listen(4123, function listening() { - // leave transaction - tracer.setSegment(null) const socket = net.connect({ port: 4123 }) socket.write('test123') socket.end() @@ -56,16 +54,18 @@ test('createServer', function createServerTest(t, end) { } function onClose() { - const root = agent.getTransaction().trace.root - assert.equal(root.children.length, 1, 'should have a single child') - const child = root.children[0] + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 2, 'should have a single child') + const child = children[1] + const childChildren = transaction.trace.getChildren(child.id) assert.equal(child.name, 'net.Server.onconnection', 'child segment should have correct name') assert.ok(child.timer.touched, 'child should started and ended') - assert.equal(child.children.length, 1, 'child should have a single child segment') - const timeout = child.children[0] + assert.equal(childChildren.length, 1, 'child should have a single child segment') + const timeout = childChildren[0] + const timeoutChildren = transaction.trace.getChildren(timeout.id) assert.equal(timeout.name, 'timers.setTimeout', 'timeout segment should have correct name') assert.ok(timeout.timer.touched, 'timeout should started and ended') - assert.equal(timeout.children.length, 1, 'timeout should have a single callback segment') + assert.equal(timeoutChildren.length, 1, 'timeout should have a single callback segment') end() } }) @@ -110,40 +110,40 @@ test('connect', function connectTest(t, end) { }) function verify() { - const root = agent.getTransaction().trace.root - assert.equal(root.children.length, 1, 'should have a single child') - let connectSegment = root.children[0] + const transaction = agent.getTransaction() + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 1, 'should have a single child') + let connectSegment = children[0] assert.equal( connectSegment.name, 'net.createConnection', 'connect segment should have correct name' ) assert.ok(connectSegment.timer.touched, 'connect should started and ended') + let connectChildren = transaction.trace.getChildren(connectSegment.id) // Depending on the version of Node there may be another connection segment // floating in the trace. - if (connectSegment.children[0].name === 'net.Socket.connect') { - connectSegment = connectSegment.children[0] + if (connectChildren[0].name === 'net.Socket.connect') { + connectSegment = connectChildren[0] } + connectChildren = transaction.trace.getChildren(connectSegment.id) - assert.equal(connectSegment.children.length, 2, 'connect should have a two child segment') - const dnsSegment = connectSegment.children[0] - const timeoutSegment = connectSegment.children[1] + assert.equal(connectChildren.length, 2, 'connect should have a two child segment') + const [dnsSegment, timeoutSegment] = connectChildren assert.equal(dnsSegment.name, 'dns.lookup', 'dns segment should have correct name') assert.ok(dnsSegment.timer.touched, 'dns segment should started and ended') - assert.equal(dnsSegment.children.length, 1, 'dns should have a single callback segment') + const dnsChildren = transaction.trace.getChildren(dnsSegment.id) + assert.equal(dnsChildren.length, 1, 'dns should have a single callback segment') assert.equal( timeoutSegment.name, 'timers.setTimeout', 'timeout segment should have correct name' ) assert.ok(timeoutSegment.timer.touched, 'timeout should started and ended') - assert.equal( - timeoutSegment.children.length, - 1, - 'timeout should have a single callback segment' - ) + const timeoutChildren = transaction.trace.getChildren(timeoutSegment.id) + assert.equal(timeoutChildren.length, 1, 'timeout should have a single callback segment') end() } } @@ -179,34 +179,38 @@ test('createServer and connect', function createServerTest(t, end) { } function onClose() { - const root = agent.getTransaction().trace.root - assert.equal(root.children.length, 2, 'should have 2 children') - let clientSegment = root.children[0] + const transaction = agent.getTransaction() + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 2, 'should have 2 children') + let clientSegment = children[0] assert.equal(clientSegment.name, 'net.connect', 'server segment should have correct name') assert.ok(clientSegment.timer.touched, 'server should started and ended') + let clientChildren = transaction.trace.getChildren(clientSegment.id) // Depending on the version of Node there may be another connection segment // floating in the trace. - if (clientSegment.children[0].name === 'net.Socket.connect') { - clientSegment = clientSegment.children[0] + if (clientChildren[0].name === 'net.Socket.connect') { + clientSegment = clientChildren[0] } + clientChildren = transaction.trace.getChildren(clientSegment.id) - assert.equal(clientSegment.children.length, 1, 'clientSegment should only have one child') - const dnsSegment = clientSegment.children[0] + assert.equal(clientChildren.length, 1, 'clientSegment should only have one child') + const [dnsSegment] = clientChildren if (dnsSegment) { assert.equal(dnsSegment.name, 'dns.lookup', 'dnsSegment is named properly') } else { assert.ok(0, 'did not have children, prevent undefined property lookup') } - const serverSegment = root.children[1] + const serverSegment = children[1] assert.equal( serverSegment.name, 'net.Server.onconnection', 'server segment should have correct name' ) assert.ok(serverSegment.timer.touched, 'server should started and ended') - assert.equal(serverSegment.children.length, 0, 'should not have any server segments') + const serverChildren = transaction.trace.getChildren(serverSegment.id) + assert.equal(serverChildren.length, 0, 'should not have any server segments') end() } }) diff --git a/test/integration/core/timers.test.js b/test/integration/core/timers.test.js index 205378cb20..c85bc06ea5 100644 --- a/test/integration/core/timers.test.js +++ b/test/integration/core/timers.test.js @@ -36,10 +36,9 @@ test('setImmediate: segments', async function (t) { helper.runInTransaction(agent, function transactionWrapper(tx) { timers.setImmediate(function anonymous() { plan.equal(agent.getTransaction().id, tx.id, 'should be in expected transaction') - plan.ok( - !agent.getTransaction().trace.root.children.length, - 'should not have any segment for setImmediate' - ) + const transaction = agent.getTransaction() + const children = transaction.trace.getChildren(transaction.trace.root.id) + plan.equal(children.length, 0) }) }) @@ -118,15 +117,13 @@ test('setImmediate: should not propagate segments for ended transaction', async helper.runInSegment(agent, 'test-segment', () => { const segment = agent.tracer.getSegment() plan.notEqual(segment.name, 'test-segment') - plan.equal(segment.children.length, 0, 'should not propagate segments when transaction ends') + const children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 0, 'should not propagate segments when transaction ends') setImmediate(() => { const segment = agent.tracer.getSegment() plan.notEqual(segment.name, 'test-segment') - plan.equal( - segment.children.length, - 0, - 'should not propagate segments when transaction ends' - ) + const children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 0, 'should not propagate segments when transaction ends') }) }) }) @@ -158,7 +155,8 @@ test('global setImmediate', function testSetImmediate(t, end) { helper.runInTransaction(agent, function transactionWrapper(transaction) { setImmediate(function anonymous() { assert.equal(agent.getTransaction(), transaction) - assert.equal(agent.getTransaction().trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 0) end() }) }) @@ -180,7 +178,8 @@ test('nextTick', async function testNextTick(t) { helper.runInTransaction(agent, function transactionWrapper(transaction) { process.nextTick(function callback() { plan.equal(agent.getTransaction(), transaction) - plan.equal(agent.getTransaction().trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + plan.equal(children.length, 0) }) }) @@ -196,7 +195,8 @@ test('nextTick with extra args', async function testNextTick(t) { process.nextTick( function callback() { plan.equal(agent.getTransaction(), transaction) - plan.equal(agent.getTransaction().trace.root.children.length, 0) + const children = transaction.trace.getChildren(transaction.trace.root.id) + plan.equal(children.length, 0) plan.deepEqual([].slice.call(arguments), [1, 2, 3]) process.nextTick = original }, @@ -225,7 +225,8 @@ test('clearImmediate', (t, end) => { helper.runInTransaction(agent, function transactionWrapper(transaction) { process.nextTick(function callback() { const timer2 = setImmediate(assert.fail) - assert.ok(!transaction.trace.root.children[0]) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 0) clearImmediate(timer2) setImmediate(end) }) @@ -248,7 +249,7 @@ test('clearTimeout should ignore segment created for timer', async (t) => { process.nextTick(function callback() { const timer = setTimeout(plan.fail) - const timerSegment = transaction.trace.root.children[0] + const [timerSegment] = transaction.trace.getChildren(transaction.trace.root.id) plan.equal(timerSegment.name, 'timers.setTimeout') plan.equal(timerSegment.ignore, false) @@ -272,7 +273,7 @@ test('clearTimeout should not ignore parent segment when opaque', async (t) => { segment.opaque = true const timer = setTimeout(plan.fail) - const parentSegment = transaction.trace.root.children[0] + const [parentSegment] = transaction.trace.getChildren(transaction.trace.root.id) plan.equal(parentSegment.name, expectedParentName) plan.equal(parentSegment.ignore, false) @@ -298,7 +299,7 @@ test('clearTimeout should not ignore parent segment when internal', async (t) => const timer = setTimeout(plan.fail) - const parentSegment = transaction.trace.root.children[0] + const [parentSegment] = transaction.trace.getChildren(transaction.trace.root.id) plan.equal(parentSegment.name, expectedParentName) plan.equal(parentSegment.ignore, false) diff --git a/test/integration/core/verify.js b/test/integration/core/verify.js index f2f0a1ea0a..bfcb5c1856 100644 --- a/test/integration/core/verify.js +++ b/test/integration/core/verify.js @@ -8,22 +8,24 @@ module.exports = verifySegments function verifySegments({ agent, name, children = [], end, assert = require('node:assert') }) { - const root = agent.getTransaction().trace.root - assert.equal(root.children.length, 1, 'should have a single child') - const child = root.children[0] + const { trace } = agent.getTransaction() + const traceChildren = trace.getChildren(trace.root.id) + assert.equal(traceChildren.length, 1, 'should have a single child') + const child = traceChildren[0] + const childChildren = trace.getChildren(child.id) assert.equal(child.name, name, 'child segment should have correct name') assert.ok(child.timer.touched, 'child should started and ended') assert.equal( - child.children.length, + childChildren.length, 1 + children.length, 'child should have a single callback segment' ) for (let i = 0; i < children.length; ++i) { - assert.equal(child.children[i].name, children[i]) + assert.equal(childChildren[i].name, children[i]) } - const callback = child.children[child.children.length - 1] + const callback = childChildren[childChildren.length - 1] assert.ok( callback.name === 'Callback: anonymous' || callback.name === 'Callback: ', 'callback segment should have correct name' diff --git a/test/integration/distributed-tracing/trace-context-cross-agent.test.js b/test/integration/distributed-tracing/trace-context-cross-agent.test.js index 547d91a26f..ffbcc84fb1 100644 --- a/test/integration/distributed-tracing/trace-context-cross-agent.test.js +++ b/test/integration/distributed-tracing/trace-context-cross-agent.test.js @@ -341,7 +341,7 @@ async function runTestCase(testCase, parentTest) { const transactionType = testCase.web_transaction ? TYPES.WEB : TYPES.BG helper.runInTransaction(agent, transactionType, function (transaction) { - transaction.baseSegment = transaction.trace.root.add('MyBaseSegment', (segment) => { + transaction.baseSegment = transaction.trace.add('MyBaseSegment', (segment) => { recorder( transaction, testCase.web_transaction ? 'Web' : 'Other', diff --git a/test/integration/instrumentation/fetch.test.js b/test/integration/instrumentation/fetch.test.js index 96ff6e8192..668ef0700d 100644 --- a/test/integration/instrumentation/fetch.test.js +++ b/test/integration/instrumentation/fetch.test.js @@ -79,7 +79,7 @@ test('fetch', async function (t) { }) assert.equal(status, 200) - assertSegments(tx.trace.root, [`External/${HOST}/post`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/post`], { exact: false }) tx.end() }) }) @@ -88,7 +88,7 @@ test('fetch', async function (t) { await helper.runInTransaction(agent, async (tx) => { const { status } = await fetch(`${REQUEST_URL}/get?a=b&c=d`) assert.equal(status, 200) - const segment = metrics.findSegment(tx.trace.root, `External/${HOST}/get`) + const segment = metrics.findSegment(tx.trace, tx.trace.root, `External/${HOST}/get`) const attrs = segment.getAttributes() assert.equal(attrs.url, `${REQUEST_URL}/get`) assert.equal(attrs.procedure, 'GET') @@ -144,7 +144,7 @@ test('fetch', async function (t) { const [{ status }, { status: status2 }] = await Promise.all([req1, req2]) assert.equal(status, 200) assert.equal(status2, 200) - assertSegments(tx.trace.root, [`External/${HOST}/post`, `External/${HOST}/put`], { + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/post`, `External/${HOST}/put`], { exact: false }) tx.end() @@ -159,7 +159,7 @@ test('fetch', async function (t) { }) } catch (err) { assert.equal(err.message, 'fetch failed') - assertSegments(tx.trace.root, ['External/invalidurl/foo'], { exact: false }) + assertSegments(tx.trace, tx.trace.root, ['External/invalidurl/foo'], { exact: false }) assert.equal(tx.exceptions.length, 1) tx.end() } @@ -179,7 +179,7 @@ test('fetch', async function (t) { await req } catch (err) { assert.match(err.message, /This operation was aborted/) - assertSegments(tx.trace.root, [`External/${HOST}/delay/1000`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/delay/1000`], { exact: false }) assert.equal(tx.exceptions.length, 1) assert.equal(tx.exceptions[0].error.name, 'AbortError') tx.end() @@ -206,11 +206,11 @@ test('fetch', async function (t) { await req } catch (error) { assert.match(error.message, /fetch failed/) - assertSegments(transaction.trace.root, [`External/localhost:${port}/`], { + assertSegments(transaction.trace, transaction.trace.root, [`External/localhost:${port}/`], { exact: false }) - const segments = transaction.trace.root.children + const segments = transaction.trace.getChildren(transaction.trace.root.id) const segment = segments[segments.length - 1] assert.ok(segment.timer.start, 'should have started') @@ -225,7 +225,7 @@ test('fetch', async function (t) { await helper.runInTransaction(agent, async (tx) => { const { status } = await fetch(`${REQUEST_URL}/status/400`) assert.equal(status, 400) - assertSegments(tx.trace.root, [`External/${HOST}/status/400`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/status/400`], { exact: false }) tx.end() }) }) diff --git a/test/integration/instrumentation/http-outbound.test.js b/test/integration/instrumentation/http-outbound.test.js index 2dd1e1ddf0..d099fa6df4 100644 --- a/test/integration/instrumentation/http-outbound.test.js +++ b/test/integration/instrumentation/http-outbound.test.js @@ -36,11 +36,11 @@ test('external requests', async function (t) { notVeryReliable.listen(0) - helper.runInTransaction(agent, function inTransaction() { + helper.runInTransaction(agent, function inTransaction(tx) { const req = http.get(notVeryReliable.address()) req.on('error', function onError() { - const segment = agent.tracer.getTransaction().trace.root.children[0] + const [segment] = tx.trace.getChildren(tx.trace.root.id) assert.equal( segment.name, @@ -86,7 +86,7 @@ test('external requests', async function (t) { }) function check(tx) { - const external = tx.trace.root.children[0] + const [external] = tx.trace.getChildren(tx.trace.root.id) assert.equal( external.name, 'External/localhost:' + server.address().port + '/some/path', @@ -94,21 +94,24 @@ test('external requests', async function (t) { ) assert.ok(external.timer.start, 'should have started') assert.ok(external.timer.hasEnd(), 'should have ended') - assert.ok(external.children.length, 'should have children') + const externalChildren = tx.trace.getChildren(external.id) + assert.ok(externalChildren.length, 'should have children') - let connect = external.children[0] + let connect = externalChildren[0] assert.equal(connect.name, 'http.Agent#createConnection', 'should be connect segment') - assert.equal(connect.children.length, 1, 'connect should have 1 child') + let connectChildren = tx.trace.getChildren(connect.id) + assert.equal(connectChildren.length, 1, 'connect should have 1 child') // There is potentially an extra layer of create/connect segments. - if (connect.children[0].name === 'net.Socket.connect') { - connect = connect.children[0] + if (connectChildren[0].name === 'net.Socket.connect') { + connect = connectChildren[0] } + connectChildren = tx.trace.getChildren(connect.id) - const dnsLookup = connect.children[0] + const dnsLookup = connectChildren[0] assert.equal(dnsLookup.name, 'dns.lookup', 'should be dns.lookup segment') - const callback = external.children[external.children.length - 1] + const callback = externalChildren[externalChildren.length - 1] assert.equal(callback.name, 'timers.setTimeout', 'should have timeout segment') end() @@ -138,7 +141,8 @@ test('external requests', async function (t) { const req = http.get(opts, function onResponse(res) { res.resume() res.once('end', function () { - const segment = agent.tracer.getTransaction().trace.root.children[0] + const { trace } = agent.tracer.getTransaction() + const [segment] = trace.getChildren(trace.root.id) assert.equal( segment.name, 'External/www.google.com/proxy/path', @@ -167,15 +171,16 @@ test('external requests', async function (t) { }) function check() { - const root = agent.tracer.getTransaction().trace.root - const segment = root.children[0] + const tx = agent.getTransaction() + const [segment] = tx.trace.getChildren(tx.trace.root.id) assert.equal(segment.name, 'External/example.com/', 'should be named') assert.ok(segment.timer.start, 'should have started') assert.ok(segment.timer.hasEnd(), 'should have ended') - assert.equal(segment.children.length, 1, 'should have 1 child') + const segmentChildren = tx.trace.getChildren(segment.id) + assert.equal(segmentChildren.length, 1, 'should have 1 child') - const notDuped = segment.children[0] + const notDuped = segmentChildren[0] assert.notEqual( notDuped.name, segment.name, @@ -214,7 +219,7 @@ test('external requests', async function (t) { http.get('http://example.com', (res) => { res.resume() res.on('end', () => { - const segment = tx.trace.root.children[0] + const [segment] = tx.trace.getChildren(tx.trace.root.id) assert.equal(segment.name, 'External/example.com/', 'should create external segment') end() }) @@ -229,7 +234,7 @@ test('external requests', async function (t) { const req = http.get('http://example.com', (res) => { res.resume() res.on('end', () => { - const segment = tx.trace.root.children[0] + const [segment] = tx.trace.getChildren(tx.trace.root.id) const attrs = segment.getAttributes() assert.deepEqual(attrs, { url: 'http://example.com/', diff --git a/test/integration/response-handling-utils.js b/test/integration/response-handling-utils.js index a89beb5a35..b298026cff 100644 --- a/test/integration/response-handling-utils.js +++ b/test/integration/response-handling-utils.js @@ -58,7 +58,13 @@ function createTestData(agent, callback) { helper.runInTransaction(agent, (transaction) => { const segment = transaction.trace.add('MySegment') segment.overwriteDurationInMillis(1) - agent.queries.add(segment, 'mysql', 'select * from foo', new Error().stack) + agent.queries.add({ + segment, + transaction, + type: 'mysql', + query: 'select * from foo', + trace: new Error().stack + }) transaction.finalizeNameFromUri('/some/test/url', 200) transaction.end() diff --git a/test/integration/transaction/total-time.test.js b/test/integration/transaction/total-time.test.js index cd4ddf4522..4e0cbd197b 100644 --- a/test/integration/transaction/total-time.test.js +++ b/test/integration/transaction/total-time.test.js @@ -22,9 +22,7 @@ test('totaltime: single segment', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const only = root.add('only') + const only = transaction.trace.add('only') only.timer.setDurationInMillis(1000, start) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 1000) @@ -36,12 +34,10 @@ test('totaltime: parent with child not overlapping', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const child = parent.add('child') + const child = transaction.trace.add('child') child.timer.setDurationInMillis(1000, start + 1000) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 2000) @@ -53,12 +49,11 @@ test('totaltime: parent with a child overlapping by 500ms', function (t, end) { const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const child = parent.add('child') + const child = transaction.trace.add('child', null, parent) child.timer.setDurationInMillis(1000, start + 500) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 1500) @@ -70,15 +65,13 @@ test('totaltime: 1 parent, 2 parallel equal children no overlap with parent', (t const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const first = parent.add('first') + const first = transaction.trace.add('first', null, parent) first.timer.setDurationInMillis(1000, start + 1000) - const second = parent.add('second') + const second = transaction.trace.add('second', null, parent) second.timer.setDurationInMillis(1000, start + 1000) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 3000) @@ -90,15 +83,13 @@ test('totaltime: 1 parent, 2 parallel equal children one overlaps with parent by const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const first = parent.add('first') + const first = transaction.trace.add('first', null, parent) first.timer.setDurationInMillis(1000, start + 1000) - const second = parent.add('second') + const second = transaction.trace.add('second', null, parent) second.timer.setDurationInMillis(1000, start + 500) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 2500) @@ -110,15 +101,13 @@ test('totaltime: 1 parent, 1 child, 1 grand child, all at same time', function ( const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const child = parent.add('child') + const child = transaction.trace.add('child', null, parent) child.timer.setDurationInMillis(1000, start) - const grandchild = child.add('grandchild') + const grandchild = transaction.trace.add('grandchild', null, child) grandchild.timer.setDurationInMillis(1000, start) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 1000) @@ -130,15 +119,13 @@ test('totaltime: 1 parent, 1 child, 1 grand child, 500ms at each step', function const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const child = parent.add('child') + const child = transaction.trace.add('child', null, parent) child.timer.setDurationInMillis(1000, start + 500) - const grandchild = child.add('grandchild') + const grandchild = transaction.trace.add('grandchild', null, child) grandchild.timer.setDurationInMillis(1000, start + 1000) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 2000) @@ -150,15 +137,14 @@ test('totaltime: 1 parent, 1 child, 1 grand child, 250ms after previous start', const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const child = parent.add('child') + const child = transaction.trace.add('child', null, parent) child.timer.setDurationInMillis(1000, start + 250) - const grandchild = child.add('grandchild') + const grandchild = transaction.trace.add('grandchild', null, child) grandchild.timer.setDurationInMillis(1000, start + 500) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 1500) @@ -170,15 +156,13 @@ test('totaltime: 1 child ending before parent, 1 grand child ending after parent const { agent } = t.nr helper.runInTransaction(agent, function (transaction) { const start = Date.now() - const root = transaction.trace.root - - const parent = root.add('parent') + const parent = transaction.trace.add('parent') parent.timer.setDurationInMillis(1000, start) - const child = parent.add('child') + const child = transaction.trace.add('child', null, parent) child.timer.setDurationInMillis(200, start + 100) - const grandchild = child.add('grandchild') + const grandchild = transaction.trace.add('grandchild', null, child) grandchild.timer.setDurationInMillis(1000, start + 200) assert.equal(transaction.trace.getTotalTimeDurationInMillis(), 1200) diff --git a/test/integration/transaction/tracer.test.js b/test/integration/transaction/tracer.test.js index 695c5e506d..141d206f02 100644 --- a/test/integration/transaction/tracer.test.js +++ b/test/integration/transaction/tracer.test.js @@ -11,6 +11,7 @@ const helper = require('../../lib/agent_helper') const { EventEmitter } = require('events') const symbols = require('../../../lib/symbols') const tempRemoveListeners = require('../../lib/temp-remove-listeners') +const Context = require('../../../lib/context-manager/context') test.beforeEach((ctx) => { const agent = helper.instrumentMockedAgent() @@ -33,24 +34,26 @@ test('bind in transaction', async function testBind(t) { helper.runInTransaction(agent, function inTrans(transaction) { const root = transaction.trace.root - const other = tracer.createSegment('other') + const ctx = tracer.getContext() + const other = tracer.createSegment({ name: 'other', parent: root, transaction }) + const otherCtx = ctx.enterSegment({ segment: other }) plan.equal(tracer.getTransaction(), transaction, 'should start in transaction') plan.equal(tracer.getSegment(), root, 'should start at root segment') - let bound = tracer.bindFunction(compare) + let bound = tracer.bindFunction(compare, ctx) - tracer.setSegment(null) + tracer.setSegment({ transaction: null, segment: null }) bound.call(context, root) plan.equal(tracer.getSegment(), null, 'should reset segment after being called') - bound = tracer.bindFunction(compare, other) + bound = tracer.bindFunction(compare, otherCtx) bound.call(context, other) - tracer.setSegment(root) - bound = tracer.bindFunction(compare, null) + tracer.setSegment({ transaction, segment: root }) + bound = tracer.bindFunction(compare, new Context()) plan.equal(tracer.getSegment(), root, 'should be back to root segment') - bound.call(context, root) + bound.call(context, null) function compare(expected) { plan.equal(this, context, 'should pass through context') @@ -65,18 +68,21 @@ test('bind outside transaction', async function testBind(t) { const { agent, tracer } = t.nr let root + let rootCtx const plan = tspl(t, { plan: 5 }) - let bound = tracer.bindFunction(compare) + const context = tracer.getContext() + let bound = tracer.bindFunction(compare, context) compare(null) bound(null) helper.runInTransaction(agent, function inTrans(transaction) { + rootCtx = tracer.getContext() root = transaction.trace.root bound(null) }) - bound = tracer.bindFunction(compare, root) + bound = tracer.bindFunction(compare, rootCtx) bound(root) compare(null) @@ -95,12 +101,13 @@ test('bind + throw', async function testThrows(t) { helper.runInTransaction(agent, function inTrans(transaction) { const root = transaction.trace.root - compare(dangerous(root, root), root) - compare(dangerous(null, root), root) + const rootCtx = tracer.getContext() + compare(dangerous(rootCtx, root), root) + compare(dangerous(new Context(), null), root) - tracer.setSegment(null) - compare(dangerous(root, root), null) - compare(dangerous(null, null), null) + tracer.setSegment({ transaction: null, segment: null }) + compare(dangerous(rootCtx, root), null) + compare(dangerous(new Context(), null), null) }) function compare(run, expected) { @@ -112,11 +119,11 @@ test('bind + throw', async function testThrows(t) { } } - function dangerous(segment, expected) { + function dangerous(ctx, expected) { return tracer.bindFunction(function bound() { plan.equal(tracer.getSegment(), expected, 'should have expected segment') throw error - }, segment) + }, ctx) } await plan.completed }) @@ -133,7 +140,13 @@ test('bind + capture error', async function testThrows(t) { }) function inTrans(transaction) { - const other = tracer.createSegment('other') + const other = tracer.createSegment({ + name: 'other', + transaction, + parent: transaction.trace.root + }) + const context = tracer.getContext() + const otherCtx = context.enterSegment({ segment: other }) transaction.name = name process.once('uncaughtException', function onUncaughtException(err) { const logged = agent.errors.traceAggregator.errors[0] @@ -147,14 +160,14 @@ test('bind + capture error', async function testThrows(t) { plan.notEqual(name, logged[1], 'should not have a transaction with the error') plan.equal(error.message, logged[2], 'should have the error message') }) - dangerous(other)() + dangerous(otherCtx, other)() } - function dangerous(segment) { + function dangerous(ctx, segment) { return tracer.bindFunction(function bound() { plan.equal(tracer.getSegment(), segment) throw error - }, segment) + }, ctx) } await plan.completed }) @@ -164,16 +177,27 @@ test('bind + full', async function testThrows(t) { const plan = tspl(t, { plan: 10 }) - helper.runInTransaction(agent, function inTrans() { - const segment = tracer.createSegment('segment') - const notStarted = tracer.createSegment('notStarted') - let bound = tracer.bindFunction(check, segment, true) + helper.runInTransaction(agent, function inTrans(transaction) { + const segment = tracer.createSegment({ + name: 'segment', + transaction, + parent: transaction.trace.root + }) + const context = tracer.getContext() + const ctx = context.enterSegment({ segment }) + const notStarted = tracer.createSegment({ + name: 'notStarted', + transaction, + parent: transaction.trace.root + }) + const notStartedCtx = ctx.enterSegment({ segment: notStarted }) + let bound = tracer.bindFunction(check, ctx, true) plan.ok(!segment.timer.hrstart) bound() plan.ok(segment.timer.hrDuration) - bound = tracer.bindFunction(checkNotStarted, notStarted, false) + bound = tracer.bindFunction(checkNotStarted, notStartedCtx, false) plan.ok(!notStarted.timer.hrstart) bound() @@ -219,9 +243,10 @@ test('bind + args', async function testThrows(t) { const plan = tspl(t, { plan: 1 }) helper.runInTransaction(agent, function inTrans() { + const ctx = tracer.getContext() bound = tracer.bindFunction(function withArgs() { plan.deepEqual([].slice.call(arguments), [1, 2, 3]) - }) + }, ctx) }) bound(1, 2, 3) @@ -231,16 +256,14 @@ test('bind + args', async function testThrows(t) { test('getTransaction', async function testGetTransaction(t) { const { agent, tracer } = t.nr - const plan = tspl(t, { plan: 5 }) + const plan = tspl(t, { plan: 3 }) plan.ok(!tracer.getTransaction()) helper.runInTransaction(agent, function inTrans(transaction) { plan.equal(tracer.getTransaction(), transaction) - plan.equal(tracer.getSegment().transaction, transaction) transaction.end() plan.ok(!tracer.getTransaction()) - plan.equal(tracer.getSegment().transaction, transaction) }) await plan.completed @@ -249,18 +272,20 @@ test('getTransaction', async function testGetTransaction(t) { test('getSegment', async function testGetTransaction(t) { const { agent, tracer } = t.nr - const plan = tspl(t, { plan: 5 }) + const plan = tspl(t, { plan: 4 }) plan.ok(!tracer.getSegment()) helper.runInTransaction(agent, function inTrans(transaction) { const root = transaction.trace.root plan.equal(tracer.getSegment(), root) - plan.equal(tracer.getSegment().transaction, transaction) setTimeout(function onTimeout() { - const segment = root.children[0].children[0] - plan.equal(tracer.getSegment(), segment) - plan.equal(tracer.getSegment().name, 'Callback: onTimeout') + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + const [setTimeoutCb] = transaction.trace.getChildren(child.id) + const segment = tracer.getSegment() + + plan.equal(segment, setTimeoutCb) + plan.equal(segment.name, 'Callback: onTimeout') }, 0) }) @@ -273,11 +298,17 @@ test('createSegment', async function testCreateSegment(t) { let root const plan = tspl(t, { plan: 10 }) - const noSegment = tracer.createSegment('outside transaction') + const noSegment = tracer.createSegment({ name: 'outside transaction' }) plan.equal(noSegment, null) helper.runInTransaction(agent, function inTrans(transaction) { - const segment = tracer.createSegment('inside transaction') + const segment = tracer.createSegment({ + name: 'inside transaction', + transaction, + parent: transaction.trace.root + }) + const context = tracer.getContext() + const ctx = context.enterSegment({ segment }) root = transaction.trace.root plan.equal(tracer.getSegment(), root) plan.equal(segment.name, 'inside transaction') @@ -286,17 +317,21 @@ test('createSegment', async function testCreateSegment(t) { plan.equal(segment.timer.hrstart, null) plan.equal(segment.timer.hrDuration, null) plan.equal(tracer.getSegment(), segment) - }, segment)() - }) - - const outerSegment = tracer.createSegment('outside with parent', null, root) + }, ctx)() - tracer.bindFunction(function bound() { - plan.equal(outerSegment.name, 'outside with parent') - plan.equal(outerSegment.timer.hrstart, null) - plan.equal(outerSegment.timer.hrDuration, null) - plan.equal(tracer.getSegment(), outerSegment) - }, outerSegment)() + const outerSegment = tracer.createSegment({ + name: 'outside with parent', + transaction, + parent: root + }) + const outerCtx = context.enterSegment({ segment: outerSegment }) + tracer.bindFunction(function bound() { + plan.equal(outerSegment.name, 'outside with parent') + plan.equal(outerSegment.timer.hrstart, null) + plan.equal(outerSegment.timer.hrDuration, null) + plan.equal(tracer.getSegment(), outerSegment) + }, outerCtx)() + }) await plan.completed }) @@ -307,7 +342,12 @@ test('createSegment + recorder', async function testCreateSegment(t) { const plan = tspl(t, { plan: 2 }) helper.runInTransaction(agent, function inTrans(transaction) { - const segment = tracer.createSegment('inside transaction', recorder) + const segment = tracer.createSegment({ + name: 'inside transaction', + recorder, + transaction, + parent: transaction.trace.root + }) plan.equal(segment.name, 'inside transaction') transaction.end() @@ -328,17 +368,19 @@ test('addSegment', async function addSegmentTest(t) { plan.equal(tracer.addSegment('outside', null, null, false, check), null) helper.runInTransaction(agent, function inTrans(transaction) { - const segment = tracer.addSegment('inside', null, null, false, check) + const segment = tracer.addSegment('inside', null, transaction.trace.root, false, check) plan.equal(segment.name, 'inside') root = transaction.trace.root - plan.equal(root.children[0], segment) - }) + let [child] = transaction.trace.getChildren(root.id) + plan.equal(child, segment) - const outside = tracer.addSegment('outside', null, root, false, check) + const outside = tracer.addSegment('outside', null, root, false, check) + ;[, child] = transaction.trace.getChildren(root.id) - plan.equal(outside.name, 'outside') - plan.equal(root.children[1], outside) + plan.equal(outside.name, 'outside') + plan.equal(child, outside) + }) function check(segment) { plan.equal(segment, tracer.getSegment()) @@ -355,12 +397,14 @@ test('addSegment + recorder', async function addSegmentTest(t) { const plan = tspl(t, { plan: 7 }) helper.runInTransaction(agent, function inTrans(transaction) { - segment = tracer.addSegment('inside', record, null, false, check) + segment = tracer.addSegment('inside', record, transaction.trace.root, false, check) const root = transaction.trace.root plan.equal(segment.name, 'inside') plan.equal(segment.timer.hrDuration, null) - plan.equal(root.children[0], segment) + const [child] = transaction.trace.getChildren(root.id) + plan.equal(child, segment) + transaction.end() }) @@ -385,12 +429,14 @@ test('addSegment + full', async function addSegmentTest(t) { const plan = tspl(t, { plan: 6 }) helper.runInTransaction(agent, function inTrans(transaction) { - const segment = tracer.addSegment('inside', null, null, true, check) + const segment = tracer.addSegment('inside', null, transaction.trace.root, true, check) const root = transaction.trace.root plan.equal(segment.name, 'inside') plan.ok(segment.timer.hrDuration) - plan.equal(root.children[0], segment) + const [child] = transaction.trace.getChildren(root.id) + plan.equal(child, segment) + transaction.end() }) @@ -506,7 +552,7 @@ test('bindEmitter', async function testbindEmitter(t) { emitter.emit('after', data) emitter2.on('before', check(root)) - tracer.bindEmitter(emitter2) + tracer.bindEmitter(emitter2, root) emitter2.on('after', check(root)) emitter2.emit('before', data) emitter2.emit('after', data) @@ -549,46 +595,6 @@ test('tracer.slice', async function testSlice(t) { await plan.completed }) -test('wrapFunctionNoSegment', async function testwrapFunctionNoSegment(t) { - const { agent, tracer } = t.nr - - const outer = {} - const inner = {} - let segment = null - - const plan = tspl(t, { plan: 15 }) - - const wrapped = tracer.wrapFunctionNoSegment(doSomething) - - wrapped.call(outer, segment, [4], 4, check) - - helper.runInTransaction(agent, function runInTransaction(transaction) { - segment = transaction.trace.root - wrapped.call(outer, segment, [1, 2, 3], 1, 2, 3, check) - }) - - wrapped.call(outer, null, [4, 5], 4, 5, check) - - function doSomething(seg) { - const args = tracer.slice(arguments) - const callback = args.pop() - plan.equal(this, outer) - plan.equal(tracer.getSegment(), seg) - process.nextTick(function next() { - tracer.setSegment(null) - callback.apply(inner, args) - }) - } - - function check(seg, expected) { - plan.deepEqual([].slice.call(arguments, 2), expected) - plan.equal(tracer.getSegment(), seg) - plan.equal(this, inner) - } - - await plan.completed -}) - test('wrapFunction', async function testwrapFunction(t) { const { agent, tracer } = t.nr @@ -613,12 +619,14 @@ test('wrapFunction', async function testwrapFunction(t) { function makeCallback(val) { return function callback(parent, arg) { const segment = tracer.getSegment() + const transaction = tracer.getTransaction() plan.equal(arg, val) plan.equal(this, inner) if (parent) { + const children = transaction.trace.getChildren(parent.id) plan.ok(segment.timer.hrstart) plan.ok(!segment.timer.hrDuration) - plan.notEqual(parent.children.indexOf(segment), -1) + plan.notEqual(children.indexOf(segment), -1) } return val @@ -627,6 +635,7 @@ test('wrapFunction', async function testwrapFunction(t) { function callAll(name, a, b, c) { const segment = tracer.getSegment() + const transaction = tracer.getTransaction() if (name) { plan.equal(segment.name, name) @@ -638,8 +647,10 @@ test('wrapFunction', async function testwrapFunction(t) { plan.equal(this, outer) process.nextTick(function next() { + let children if (segment) { - plan.equal(segment.children.length, 0) + children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 0) } plan.equal(a.call(inner, segment, 'a'), 'a') @@ -647,12 +658,13 @@ test('wrapFunction', async function testwrapFunction(t) { plan.equal(c.call(inner, segment, 'c'), 'c') if (segment) { - segment.children.forEach(function (child) { + children = transaction.trace.getChildren(segment.id) + children.forEach(function (child) { plan.ok(child.timer.hrstart) plan.ok(child.timer.hrDuration) }) plan.ok(segment.timer.hrDuration) - segment.transaction.end() + transaction.end() } }) @@ -706,13 +718,15 @@ test('wrapFunctionLast', async function testwrapFunctionLast(t) { function callback(parent, callbackArgs) { const segment = tracer.getSegment() + const transaction = tracer.getTransaction() plan.deepEqual(callbackArgs, [1, 2, 3]) plan.equal(this, inner) if (parent) { plan.ok(segment.timer.hrstart) plan.ok(!segment.timer.hrDuration) - plan.equal(parent.children[0], segment) + const [child] = transaction.trace.getChildren(parent.id) + plan.equal(child, segment) } return innerReturn @@ -720,6 +734,7 @@ test('wrapFunctionLast', async function testwrapFunctionLast(t) { function takesCallback(name) { const segment = tracer.getSegment() + const transaction = tracer.getTransaction() const cbArgs = [].slice.call(arguments, 1, -1) const cb = arguments[arguments.length - 1] @@ -733,18 +748,21 @@ test('wrapFunctionLast', async function testwrapFunctionLast(t) { plan.equal(this, outer) process.nextTick(function next() { + let children if (segment) { - plan.equal(segment.children.length, 0) + children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 0) } plan.equal(cb.call(inner, segment, cbArgs), innerReturn) if (segment) { - plan.equal(segment.children.length, 1) - plan.ok(segment.children[0].timer.hrstart) - plan.ok(segment.children[0].timer.hrDuration) + children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 1) + plan.ok(children[0].timer.hrstart) + plan.ok(children[0].timer.hrDuration) plan.ok(segment.timer.hrDuration) - segment.transaction.end() + transaction.end() } }) @@ -780,13 +798,15 @@ test('wrapFunctionFirst', async function testwrapFunctionFirst(t) { function callback(parent, args) { const segment = tracer.getSegment() + const transaction = tracer.getTransaction() plan.deepEqual(args, [1, 2, 3]) plan.equal(this, inner) if (parent) { plan.ok(segment.timer.hrstart) plan.ok(!segment.timer.hrDuration) - plan.equal(parent.children[0], segment) + const [child] = transaction.trace.getChildren(parent.id) + plan.equal(child, segment) } return innerReturn @@ -794,6 +814,7 @@ test('wrapFunctionFirst', async function testwrapFunctionFirst(t) { function takesCallback(cb, name) { const segment = tracer.getSegment() + const transaction = tracer.getTransaction() const args = [].slice.call(arguments, 2) if (name) { @@ -806,18 +827,21 @@ test('wrapFunctionFirst', async function testwrapFunctionFirst(t) { plan.equal(this, outer) process.nextTick(function next() { + let children if (segment) { - plan.equal(segment.children.length, 0) + children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 0) } plan.equal(cb.call(inner, segment, args), innerReturn) if (segment) { - plan.equal(segment.children.length, 1) - plan.ok(segment.children[0].timer.hrstart) - plan.ok(segment.children[0].timer.hrDuration) + children = transaction.trace.getChildren(segment.id) + plan.equal(children.length, 1) + plan.ok(children[0].timer.hrstart) + plan.ok(children[0].timer.hrDuration) plan.ok(segment.timer.hrDuration) - segment.transaction.end() + transaction.end() } }) @@ -841,8 +865,9 @@ test('wrapSyncFunction', async function testwrapSyncFunction(t) { helper.runInTransaction(agent, function inTrans(transaction) { wrapped(transaction, [4], 4) - plan.ok(transaction.trace.root.children[0].timer.hrstart) - plan.ok(transaction.trace.root.children[0].timer.hrDuration) + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + plan.ok(child.timer.hrstart) + plan.ok(child.timer.hrDuration) transaction.end() }) @@ -856,8 +881,9 @@ test('wrapSyncFunction', async function testwrapSyncFunction(t) { } } - function record(segment) { - plan.equal(segment, segment.transaction.trace.root.children[0]) + function record(segment, scope, transaction) { + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + plan.equal(segment, child) plan.equal(segment.name, 'my segment') } }) diff --git a/test/integration/transaction/wide-segment-tree.js b/test/integration/transaction/wide-segment-tree.js index e155255c9a..ce69b61d31 100644 --- a/test/integration/transaction/wide-segment-tree.js +++ b/test/integration/transaction/wide-segment-tree.js @@ -31,14 +31,12 @@ const distributedTracingConfig = { const agent = helper.loadMockedAgent(distributedTracingConfig) helper.runInTransaction(agent, function (transaction) { - const root = transaction.trace.root - // Avoid special casing of root that can result in avoiding // bugs deeper in the tree with wide segment trees - const child = root.add('child1') + const child = transaction.trace.add('child1') for (let index = 0; index < DANGEROUS_SEGMENT_WIDTH; index++) { - child.add('segment: ' + index) + transaction.trace.add('segment: ' + index, null, child) } try { diff --git a/test/lib/agent_helper.js b/test/lib/agent_helper.js index 3145d98f8b..470cd493bf 100644 --- a/test/lib/agent_helper.js +++ b/test/lib/agent_helper.js @@ -84,14 +84,6 @@ helper.loadMockedAgent = function loadMockedAgent(conf, setState = true) { // agent needs a 'real' configuration const configurator = require('../../lib/config') const config = configurator.createInstance(conf) - - if (!config.debug) { - config.debug = {} - } - - // adds link to parents node in traces for easier testing - config.debug.double_linked_transactions = true - // stub applications config.applications = () => ['New Relic for Node.js tests'] @@ -319,8 +311,9 @@ helper.runInNamedTransaction = (agent, type, callback) => { helper.runInSegment = (agent, name, callback) => { const tracer = agent.tracer + const parent = tracer.getSegment() - return tracer.addSegment(name, null, null, null, callback) + return tracer.addSegment(name, null, parent, null, callback) } /** diff --git a/test/lib/custom-assertions/assert-segments.js b/test/lib/custom-assertions/assert-segments.js index 0bdaa7aac1..b42bf2b3ec 100644 --- a/test/lib/custom-assertions/assert-segments.js +++ b/test/lib/custom-assertions/assert-segments.js @@ -63,6 +63,7 @@ * followed by an array of strings indicates that the first string is a parent * element, and the subsequent array of strings is its child elements. * + * @param {Trace} trace Transaction trace * @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 @@ -77,12 +78,9 @@ * directly under test. Only used when `exact` is true. * @param {object} [deps] Injected dependencies. * @param {object} [deps.assert] Assertion library to use. - * @param options - * @param root0 - * @param root0.assert - * @param options.assert */ module.exports = function assertSegments( // eslint-disable-line sonarjs/cognitive-complexity + trace, parent, expected, options, @@ -100,7 +98,8 @@ module.exports = function assertSegments( // eslint-disable-line sonarjs/cogniti } function getChildren(_parent) { - return _parent.children.filter(function (item) { + const children = trace.getChildren(_parent.id) + return children.filter(function (item) { if (exact && options && options.exclude) { return options.exclude.indexOf(item.name) === -1 } @@ -135,7 +134,7 @@ module.exports = function assertSegments( // eslint-disable-line sonarjs/cogniti ) } } else if (typeof sequenceItem === 'object') { - assertSegments(child, sequenceItem, options, { assert }) + assertSegments(trace, child, sequenceItem, options, { assert }) } } @@ -147,14 +146,14 @@ module.exports = function assertSegments( // eslint-disable-line sonarjs/cogniti 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] + for (let j = 0; j < children.length; j++) { + if (children[j].name === sequenceItem) { + child = 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 }) + assertSegments(trace, child, expected[i + 1], { exact }, { assert }) } } } diff --git a/test/lib/custom-assertions/compare-segments.js b/test/lib/custom-assertions/compare-segments.js index fde765b060..690707de37 100644 --- a/test/lib/custom-assertions/compare-segments.js +++ b/test/lib/custom-assertions/compare-segments.js @@ -9,18 +9,21 @@ * 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. + * @param {object} params to function + * @param {TraceSegment} params.parent segment + * @param {Array} params.segments list of expected segments + * @param {Trace} params.trace transaction trace + * @param {object} [params.assert] Assertion library to use. */ -module.exports = function compareSegments( +module.exports = function compareSegments({ parent, segments, - { assert = require('node:assert') } = {} -) { - assert.ok(parent.children.length, segments.length, 'should be the same amount of children') + trace, + assert = require('node:assert') +}) { + const parentChildren = trace.getChildren(parent.id) + assert.ok(parentChildren.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') + assert.equal(parentChildren[index].id, segment.id, 'should have same ids') }) } diff --git a/test/lib/metrics_helper.js b/test/lib/metrics_helper.js index 9feba35598..1853dc0d16 100644 --- a/test/lib/metrics_helper.js +++ b/test/lib/metrics_helper.js @@ -10,13 +10,14 @@ const urltils = require('../../lib/util/urltils') exports.findSegment = findSegment exports.getMetricHostName = getMetricHostName -function findSegment(root, name) { +function findSegment(trace, root, name) { + const children = trace.getChildren(root.id) if (root.name === name) { return root - } else if (root.children && root.children.length) { - for (let i = 0; i < root.children.length; i++) { - const child = root.children[i] - const found = findSegment(child, name) + } else if (children.length) { + for (let i = 0; i < children.length; i++) { + const child = children[i] + const found = findSegment(trace, child, name) if (found) { return found } diff --git a/test/smoke/client-s3.test.js b/test/smoke/client-s3.test.js index 13e5f3b4ce..2b38eb0516 100644 --- a/test/smoke/client-s3.test.js +++ b/test/smoke/client-s3.test.js @@ -41,8 +41,8 @@ test('@aws-sdk/client-s3 functionality', async (t) => { transaction.end() - const { url, procedure, ...awsAttributes } = - transaction.trace.root.children[1].attributes.get(TRANS_SEGMENT) + const [, child] = transaction.trace.getChildren(transaction.trace.root.id) + const { url, procedure, ...awsAttributes } = child.attributes.get(TRANS_SEGMENT) delete awsAttributes.nr_exclusive_duration_millis diff --git a/test/unit/agent/agent.test.js b/test/unit/agent/agent.test.js index 85f802f120..5c704021a8 100644 --- a/test/unit/agent/agent.test.js +++ b/test/unit/agent/agent.test.js @@ -101,11 +101,6 @@ test('when loaded with defaults', async (t) => { const { agent } = t.nr assert.throws(() => agent.setState('bogus'), /Invalid state bogus/) }) - - await t.test('has some debugging configuration by default', (t) => { - const { agent } = t.nr - assert.equal(Object.hasOwn(agent.config, 'debug'), true) - }) }) test('should load naming rules when configured', () => { @@ -654,8 +649,15 @@ test('when connected', async (t) => { agent.logs.add([{ key: 'bar' }]) const tx = new helper.FakeTransaction(agent, '/path/to/fake') tx.metrics = { apdexT: 0 } - const segment = new helper.FakeSegment(tx, 2_000) - agent.queries.add(segment, 'mysql', 'select * from foo', 'Stack\nFrames') + const segment = tx.trace.add('FakeSegment') + segment.setDurationInMillis(2000) + agent.queries.add({ + transaction: tx, + segment, + type: 'mysql', + query: 'select * from foo', + trace: 'Stack\nFrames' + }) agent.spanEventAggregator.add(segment) agent.transactionEventAggregator.add(tx) agent.customEventAggregator.add({ key: 'value' }) @@ -693,8 +695,15 @@ test('when connected', async (t) => { agent.logs.add([{ key: 'bar' }]) const tx = new helper.FakeTransaction(agent, '/path/to/fake') tx.metrics = { apdexT: 0 } - const segment = new helper.FakeSegment(tx, 2_000) - agent.queries.add(segment, 'mysql', 'select * from foo', 'Stack\nFrames') + const segment = tx.trace.add('FakeSegment') + segment.setDurationInMillis(2000) + agent.queries.add({ + transaction: tx, + segment, + type: 'mysql', + query: 'select * from foo', + trace: 'Stack\nFrames' + }) agent.spanEventAggregator.add(segment) agent.transactionEventAggregator.add(tx) agent.customEventAggregator.add({ key: 'value' }) diff --git a/test/unit/api/api-set-controller-name.test.js b/test/unit/api/api-set-controller-name.test.js index 46b7323b6a..96d12f40e5 100644 --- a/test/unit/api/api-set-controller-name.test.js +++ b/test/unit/api/api-set-controller-name.test.js @@ -125,7 +125,7 @@ function goldenPathRenameControllerInTransaction({ agent, api }) { return new Promise((resolve) => { agent.on('transactionFinished', function (finishedTransaction) { finishedTransaction.finalizeNameFromUri(TEST_URL, 200) - segment.markAsWeb(TEST_URL) + segment.markAsWeb(finishedTransaction) resolve({ transaction: finishedTransaction, segment }) }) diff --git a/test/unit/api/api-set-transaction-name.test.js b/test/unit/api/api-set-transaction-name.test.js index 644b7e96be..cc28fcca5b 100644 --- a/test/unit/api/api-set-transaction-name.test.js +++ b/test/unit/api/api-set-transaction-name.test.js @@ -80,7 +80,7 @@ function setTranasactionNameGoldenPath({ agent, api }) { return new Promise((resolve) => { agent.on('transactionFinished', function (finishedTransaction) { finishedTransaction.finalizeNameFromUri(TEST_URL, 200) - segment.markAsWeb(TEST_URL) + segment.markAsWeb(finishedTransaction) resolve({ transaction: finishedTransaction, segment }) }) diff --git a/test/unit/api/api-start-background-transaction.test.js b/test/unit/api/api-start-background-transaction.test.js index bbbfda97d5..2479e4fc1e 100644 --- a/test/unit/api/api-start-background-transaction.test.js +++ b/test/unit/api/api-start-background-transaction.test.js @@ -51,7 +51,7 @@ test('Agent API - startBackgroundTransaction', async (t) => { assert.ok(transaction.isActive()) const currentSegment = tracer.getSegment() - const nestedSegment = currentSegment.children[0] + const [nestedSegment] = transaction.trace.getChildren(currentSegment.id) assert.equal(nestedSegment.name, 'Nodejs/nested') }) @@ -182,6 +182,31 @@ test('Agent API - startBackgroundTransaction', async (t) => { }) }) + await t.test('should record metrics', (t, end) => { + const { agent, api } = t.nr + let transaction + api.startBackgroundTransaction('test', function () { + transaction = agent.tracer.getTransaction() + }) + + transaction.end() + const metrics = transaction.metrics.unscoped + ;[ + 'OtherTransaction/Nodejs/test', + 'OtherTransactionTotalTime/Nodejs/test', + 'OtherTransaction/all', + 'OtherTransactionTotalTime', + 'OtherTransactionTotalTime', + 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/all', + 'DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther' + ].forEach((metric) => { + assert.ok(metrics[metric].total, `${metric} has total`) + assert.ok(metrics[metric].totalExclusive, `${metric} has totalExclusive`) + }) + + end() + }) + await t.test('should not throw when no handler is supplied', (t, end) => { const { api } = t.nr assert.doesNotThrow(() => api.startBackgroundTransaction('test')) @@ -221,7 +246,8 @@ test('Agent API - startBackgroundTransaction', async (t) => { api.startBackgroundTransaction('nested-clm-test', function () { nested({ api }) const currentSegment = tracer.getSegment() - const nestedSegment = currentSegment.children[0] + const transaction = agent.tracer.getTransaction() + const [nestedSegment] = transaction.trace.getChildren(currentSegment.id) assertCLMAttrs({ segments: [ { diff --git a/test/unit/api/api-start-segment.test.js b/test/unit/api/api-start-segment.test.js index b8efac74b4..503ccf8300 100644 --- a/test/unit/api/api-start-segment.test.js +++ b/test/unit/api/api-start-segment.test.js @@ -88,9 +88,13 @@ test('Agent API - startSegment', async (t) => { const transactionScopedCustomMetric = transactionNameMetric['Custom/foobar'] assert.ok(transactionScopedCustomMetric) + assert.ok(transactionScopedCustomMetric.total) + assert.ok(transactionScopedCustomMetric.totalExclusive) const unscopedCustomMetric = tx.metrics.unscoped['Custom/foobar'] assert.ok(unscopedCustomMetric) + assert.ok(unscopedCustomMetric.total) + assert.ok(unscopedCustomMetric.totalExclusive) end() }) diff --git a/test/unit/api/api-start-web-transaction.test.js b/test/unit/api/api-start-web-transaction.test.js index 95d1dc839c..7eb39274fa 100644 --- a/test/unit/api/api-start-web-transaction.test.js +++ b/test/unit/api/api-start-web-transaction.test.js @@ -49,7 +49,7 @@ test('Agent API - startWebTransaction', async (t) => { assert.ok(transaction.isActive()) const currentSegment = tracer.getSegment() - const nestedSegment = currentSegment.children[0] + const [nestedSegment] = transaction.trace.getChildren(currentSegment.id) assert.equal(nestedSegment.name, 'nested') }) @@ -174,9 +174,10 @@ test('Agent API - startWebTransaction', async (t) => { const { agent, api, tracer } = t.nr agent.config.code_level_metrics.enabled = enabled api.startWebTransaction('clm-nested-test', function () { + const tx = agent.tracer.getTransaction() nested({ api }) const currentSegment = tracer.getSegment() - const nestedSegment = currentSegment.children[0] + const [nestedSegment] = tx.trace.getChildren(currentSegment.id) assertCLMAttrs({ segments: [ { diff --git a/test/unit/context-manager/async-local-context-manager.test.js b/test/unit/context-manager/async-local-context-manager.test.js index a935bc30fb..a68dc17d42 100644 --- a/test/unit/context-manager/async-local-context-manager.test.js +++ b/test/unit/context-manager/async-local-context-manager.test.js @@ -8,20 +8,22 @@ const test = require('node:test') const assert = require('node:assert') const AsyncLocalContextManager = require('../../../lib/context-manager/async-local-context-manager') +const Context = require('../../../lib/context-manager/context') test('Should default to null context', () => { - const contextManager = new AsyncLocalContextManager({}) + const contextManager = new AsyncLocalContextManager() const context = contextManager.getContext() - assert.equal(context, null) + assert.ok(context instanceof Context) + assert.equal(context.transaction, null) + assert.equal(context.segment, null) }) test('setContext should update the current context', () => { - const contextManager = new AsyncLocalContextManager({}) - - const expectedContext = { name: 'new context' } + const contextManager = new AsyncLocalContextManager() + const expectedContext = new Context('tx', 'new context') contextManager.setContext(expectedContext) const context = contextManager.getContext() @@ -30,10 +32,11 @@ test('setContext should update the current context', () => { test('runInContext()', async (t) => { await t.test('should execute callback synchronously', () => { - const contextManager = new AsyncLocalContextManager({}) + const contextManager = new AsyncLocalContextManager() + const context = contextManager.getContext() let callbackCalled = false - contextManager.runInContext({}, () => { + contextManager.runInContext(context, () => { callbackCalled = true }) @@ -41,12 +44,13 @@ test('runInContext()', async (t) => { }) await t.test('should set context to active for life of callback', (t, end) => { - const contextManager = new AsyncLocalContextManager({}) + const contextManager = new AsyncLocalContextManager() - const previousContext = { name: 'previous' } + const context = contextManager.getContext() + const previousContext = context.enterSegment({ segment: 'previous', transaction: 'tx' }) contextManager.setContext(previousContext) - const newContext = { name: 'new' } + const newContext = context.enterSegment({ segment: 'new', transaction: 'tx1' }) contextManager.runInContext(newContext, () => { const context = contextManager.getContext() @@ -57,26 +61,40 @@ test('runInContext()', async (t) => { }) await t.test('should restore previous context when callback completes', () => { - const contextManager = new AsyncLocalContextManager({}) + const contextManager = new AsyncLocalContextManager() - const previousContext = { name: 'previous' } + const context = contextManager.getContext() + const previousContext = context.enterSegment({ segment: 'previous', transaction: 'tx' }) contextManager.setContext(previousContext) - const newContext = { name: 'new' } + const newContext = context.enterSegment({ segment: 'new', transaction: 'tx1' }) contextManager.runInContext(newContext, () => {}) - const context = contextManager.getContext() + assert.deepEqual(contextManager.getContext(), previousContext) + }) + + await t.test('should run a function in a transaction', () => { + const contextManager = new AsyncLocalContextManager() - assert.equal(context, previousContext) + let context = contextManager.getContext() + const transaction = { name: 'tx', trace: { root: { name: 'foo' } } } + context = context.enterTransaction(transaction) + + contextManager.runInContext(context, () => { + const curContext = contextManager.getContext() + assert.equal(curContext.transaction, transaction) + assert.equal(curContext.segment, transaction.trace.root) + }) }) await t.test('should restore previous context on exception', () => { const contextManager = new AsyncLocalContextManager({}) - const previousContext = { name: 'previous' } + const context = contextManager.getContext() + const previousContext = context.enterSegment({ segment: 'previous', transaction: 'tx' }) contextManager.setContext(previousContext) - const newContext = { name: 'new' } + const newContext = context.enterSegment({ segment: 'new', transaction: 'tx1' }) try { contextManager.runInContext(newContext, () => { @@ -87,21 +105,16 @@ test('runInContext()', async (t) => { // swallowing error } - const context = contextManager.getContext() - - assert.equal(context, previousContext) + assert.deepEqual(contextManager.getContext(), previousContext) }) await t.test('should apply `cbThis` arg to execution', (t, end) => { const contextManager = new AsyncLocalContextManager({}) - const previousContext = { name: 'previous' } - contextManager.setContext(previousContext) - - const newContext = { name: 'new' } + const context = contextManager.getContext() const expectedThis = () => {} - contextManager.runInContext(newContext, functionRunInContext, expectedThis) + contextManager.runInContext(context, functionRunInContext, expectedThis) function functionRunInContext() { assert.equal(this, expectedThis) @@ -112,15 +125,12 @@ test('runInContext()', async (t) => { await t.test('should apply args array to execution', (t, end) => { const contextManager = new AsyncLocalContextManager({}) - const previousContext = { name: 'previous' } - contextManager.setContext(previousContext) - - const newContext = { name: 'new' } + const context = contextManager.getContext() const expectedArg1 = 'first arg' const expectedArg2 = 'second arg' const args = [expectedArg1, expectedArg2] - contextManager.runInContext(newContext, functionRunInContext, null, args) + contextManager.runInContext(context, functionRunInContext, null, args) function functionRunInContext(arg1, arg2) { assert.equal(arg1, expectedArg1) @@ -131,11 +141,7 @@ test('runInContext()', async (t) => { await t.test('should apply arguments construct to execution', (t, end) => { const contextManager = new AsyncLocalContextManager({}) - - const previousContext = { name: 'previous' } - contextManager.setContext(previousContext) - - const newContext = { name: 'new' } + const context = contextManager.getContext() const expectedArg1 = 'first arg' const expectedArg2 = 'second arg' @@ -143,7 +149,7 @@ test('runInContext()', async (t) => { function executingFunction() { contextManager.runInContext( - newContext, + context, function functionRunInContext(arg1, arg2) { assert.equal(arg1, expectedArg1) assert.equal(arg2, expectedArg2) diff --git a/test/unit/context-manager/create-context-manager.test.js b/test/unit/context-manager/create-context-manager.test.js deleted file mode 100644 index 542ff11127..0000000000 --- a/test/unit/context-manager/create-context-manager.test.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2021 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 createImplementation = require('../../../lib/context-manager/create-context-manager') -const AsyncLocalContextManager = require('../../../lib/context-manager/async-local-context-manager') - -test('Should return AsyncLocalContextManager by default', () => { - const contextManager = createImplementation({ - logging: {}, - feature_flag: {} - }) - - assert.equal(contextManager instanceof AsyncLocalContextManager, true) -}) diff --git a/test/unit/instrumentation/elasticsearch.test.js b/test/unit/db/query-parsers/elasticsearch.test.js similarity index 97% rename from test/unit/instrumentation/elasticsearch.test.js rename to test/unit/db/query-parsers/elasticsearch.test.js index 3ce5341fba..2b44d86dd6 100644 --- a/test/unit/instrumentation/elasticsearch.test.js +++ b/test/unit/db/query-parsers/elasticsearch.test.js @@ -7,7 +7,7 @@ const test = require('node:test') const assert = require('node:assert') -const { parsePath, queryParser } = require('../../../lib/instrumentation/@elastic/elasticsearch') +const { parsePath, queryParser } = require('../../../../lib/db/query-parsers/elasticsearch') const methods = [ { name: 'GET', expected: 'get' }, { name: 'PUT', expected: 'create' }, diff --git a/test/unit/db/query-sample.test.js b/test/unit/db/query-sample.test.js index 93b85793b1..1a522cd8d5 100644 --- a/test/unit/db/query-sample.test.js +++ b/test/unit/db/query-sample.test.js @@ -83,9 +83,8 @@ test('Query Sample', async (t) => { } } const fakeSample = { - segment: { - transaction: {} - } + transaction: {}, + segment: {} } let codecCalled = false @@ -118,13 +117,12 @@ test('Query Sample', async (t) => { let getFullNameCalled = false const fakeSample = { - segment: { - transaction: { - getFullName: () => { - getFullNameCalled = true - } + transaction: { + getFullName: () => { + getFullNameCalled = true } - } + }, + segment: {} } const clock = sinon.useFakeTimers({ @@ -162,11 +160,11 @@ test('Query Sample', async (t) => { const fakeGetAttributes = () => expectedParams const fakeSample = { trace: {}, + transaction: { + addDistributedTraceIntrinsics: () => {} + }, segment: { - getAttributes: fakeGetAttributes, - transaction: { - addDistributedTraceIntrinsics: () => {} - } + getAttributes: fakeGetAttributes } } @@ -190,13 +188,13 @@ test('Query Sample', async (t) => { } const fakeSample = { trace: {}, - segment: { - getAttributes: () => ({}), - transaction: { - addDistributedTraceIntrinsics: () => { - addDtIntrinsicsCalled = true - } + transaction: { + addDistributedTraceIntrinsics: () => { + addDtIntrinsicsCalled = true } + }, + segment: { + getAttributes: () => ({}) } } @@ -218,13 +216,13 @@ test('Query Sample', async (t) => { } const fakeSample = { trace: {}, - segment: { - getAttributes: () => ({}), - transaction: { - addDistributedTraceIntrinsics: () => { - addDtIntrinsicsCalled = true - } + transaction: { + addDistributedTraceIntrinsics: () => { + addDtIntrinsicsCalled = true } + }, + segment: { + getAttributes: () => ({}) } } diff --git a/test/unit/db/query-trace-aggregator.test.js b/test/unit/db/query-trace-aggregator.test.js index cf932ce0bb..a80804c3d2 100644 --- a/test/unit/db/query-trace-aggregator.test.js +++ b/test/unit/db/query-trace-aggregator.test.js @@ -1097,7 +1097,13 @@ function addQuery(queries, duration, url, query) { const transaction = new FakeTransaction(null, url) const segment = new FakeSegment(transaction, duration) - queries.add(segment, 'mysql', query || 'select * from foo where a=2', FAKE_STACK) + queries.add({ + transaction, + segment, + type: 'mysql', + query: query || 'select * from foo where a=2', + trace: FAKE_STACK + }) return segment } diff --git a/test/unit/db/trace.test.js b/test/unit/db/trace.test.js index 558dcc5a33..02ec285df9 100644 --- a/test/unit/db/trace.test.js +++ b/test/unit/db/trace.test.js @@ -39,7 +39,13 @@ test('SQL trace attributes', async (t) => { const payload = tx._createDistributedTracePayload().text() tx.isDistributedTrace = null tx._acceptDistributedTracePayload(payload) - agent.queries.add(tx.trace.root, 'postgres', 'select pg_sleep(1)', 'FAKE STACK') + agent.queries.add({ + transaction: tx, + segment: tx.trace.root, + type: 'postgres', + query: 'select pg_sleep(1)', + trace: 'FAKE STACK' + }) agent.queries.prepareJSON((err, samples) => { assert.ifError(err) const sample = samples[0] @@ -63,7 +69,13 @@ test('SQL trace attributes', async (t) => { const { agent } = t.nr helper.runInTransaction(agent, function (tx) { const query = 'select pg_sleep(1)' - agent.queries.add(tx.trace.root, 'postgres', query, 'FAKE STACK') + agent.queries.add({ + transaction: tx, + segment: tx.trace.root, + type: 'postgres', + query, + trace: 'FAKE STACK' + }) const sampleObj = agent.queries.samples.values().next().value const sample = agent.queries.prepareJSONSync()[0] assert.equal(sample[0], tx.getFullName()) @@ -86,7 +98,13 @@ test('SQL trace attributes', async (t) => { agent.config.account_id = 1 agent.config.simple_compression = true helper.runInTransaction(agent, function (tx) { - agent.queries.add(tx.trace.root, 'postgres', 'select pg_sleep(1)', 'FAKE STACK') + agent.queries.add({ + transaction: tx, + segment: tx.trace.root, + type: 'postgres', + query: 'select pg_sleep(1)', + trace: 'FAKE STACK' + }) agent.queries.prepareJSON((err, samples) => { assert.ifError(err) const sample = samples[0] diff --git a/test/unit/distributed_tracing/dt-cats.test.js b/test/unit/distributed_tracing/dt-cats.test.js index c16570fbba..c6f8e15807 100644 --- a/test/unit/distributed_tracing/dt-cats.test.js +++ b/test/unit/distributed_tracing/dt-cats.test.js @@ -36,7 +36,7 @@ test('distributed tracing', async function (t) { agent.config.span_events.enabled = testCase.span_events_enabled helper.runInTransaction(agent, (tx) => { tx.type = testCase.web_transaction ? 'web' : 'bg' - tx.baseSegment = tx.trace.root.add('MyBaseSegment', (segment) => { + tx.baseSegment = tx.trace.add('MyBaseSegment', (segment) => { recorder( tx, testCase.web_transaction ? 'Web' : 'Other', @@ -44,6 +44,7 @@ test('distributed tracing', async function (t) { segment.getExclusiveDurationInMillis() ) }) + agent.tracer.setSegment({ segment: tx.baseSegment }) if (!Array.isArray(testCase.inbound_payloads)) { testCase.inbound_payloads = [testCase.inbound_payloads] diff --git a/test/unit/distributed_tracing/tracecontext.test.js b/test/unit/distributed_tracing/tracecontext.test.js index 93098b0937..24d6b91573 100644 --- a/test/unit/distributed_tracing/tracecontext.test.js +++ b/test/unit/distributed_tracing/tracecontext.test.js @@ -97,6 +97,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() txn.acceptTraceContextPayload(traceparent, undefined) @@ -120,6 +121,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() txn.acceptTraceContextPayload(traceparent, tracestate) @@ -367,6 +369,7 @@ test('TraceContext', async function (t) { const { agent } = ctx.nr helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() const tp1 = txn.traceContext.createTraceparent() @@ -390,6 +393,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() const headers = getTraceContextHeaders(txn) @@ -422,6 +426,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() txn.traceContext.acceptTraceContextPayload(traceparent, duplicateAcctTraceState) @@ -470,6 +475,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() txn.acceptTraceContextPayload(traceparent, tracestate) @@ -691,6 +697,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() txn.acceptTraceContextPayload(incomingTraceparent, incomingTracestate) @@ -732,6 +739,7 @@ test('TraceContext', async function (t) { helper.runInTransaction(agent, function (txn) { const childSegment = txn.trace.add('child') + agent.tracer.setSegment({ segment: childSegment }) childSegment.start() txn.acceptTraceContextPayload(incomingTraceparent, incomingTracestate) diff --git a/test/unit/instrumentation/core/promises.test.js b/test/unit/instrumentation/core/promises.test.js index 73074d322b..89cc6fad25 100644 --- a/test/unit/instrumentation/core/promises.test.js +++ b/test/unit/instrumentation/core/promises.test.js @@ -158,7 +158,7 @@ function checkTrace(t, tx) { const expectedSegment = tracer.getSegment() const segment = tx.trace.root assert.equal(segment.name, 'a') - assert.equal(segment.children.length, 0) + assert.equal(tx.trace.getChildren(segment.id).length, 0) // verify current segment is same as trace root assert.deepEqual(segment.name, expectedSegment.name, 'current segment is same as one in tracer') return Promise.resolve() diff --git a/test/unit/instrumentation/http/http.test.js b/test/unit/instrumentation/http/http.test.js index 4124426434..8835acb26b 100644 --- a/test/unit/instrumentation/http/http.test.js +++ b/test/unit/instrumentation/http/http.test.js @@ -20,7 +20,11 @@ const encKey = 'gringletoes' function addSegment({ agent }) { const transaction = agent.getTransaction() transaction.type = 'web' - transaction.baseSegment = new Segment(transaction, 'base-segment') + transaction.baseSegment = new Segment({ + config: agent.config, + name: 'base-segment', + root: transaction.trace.root + }) } test('built-in http module instrumentation', async (t) => { diff --git a/test/unit/instrumentation/http/outbound.test.js b/test/unit/instrumentation/http/outbound.test.js index db8e95b6a2..82d4deadd4 100644 --- a/test/unit/instrumentation/http/outbound.test.js +++ b/test/unit/instrumentation/http/outbound.test.js @@ -24,7 +24,11 @@ const testSignatures = require('./outbound-utils') function addSegment({ agent }) { const transaction = agent.getTransaction() transaction.type = 'web' - transaction.baseSegment = new Segment(transaction, 'base-segment') + transaction.baseSegment = new Segment({ + config: agent.config, + name: 'base-segment', + root: transaction.trace.root + }) } test('instrumentOutbound', async (t) => { @@ -45,7 +49,8 @@ test('instrumentOutbound', async (t) => { const req = new events.EventEmitter() helper.runInTransaction(agent, function (transaction) { instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) - assert.deepEqual(transaction.trace.root.children[0].getAttributes(), {}) + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + assert.deepEqual(child.getAttributes(), {}) function makeFakeRequest() { req.path = '/asdf?a=b&another=yourself&thing&grownup=true' @@ -62,7 +67,8 @@ test('instrumentOutbound', async (t) => { const req = new events.EventEmitter() helper.runInTransaction(agent, function (transaction) { instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) - assert.deepEqual(transaction.trace.root.children[0].getAttributes(), { + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + assert.deepEqual(child.getAttributes(), { procedure: 'GET', url: `http://${HOSTNAME}:${PORT}/asdf` }) @@ -87,7 +93,8 @@ test('instrumentOutbound', async (t) => { const req = new events.EventEmitter() helper.runInTransaction(agent, function (transaction) { instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) - assert.deepEqual(transaction.trace.root.children[0].getAttributes(), { + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + assert.deepEqual(child.getAttributes(), { procedure: 'GET', url: `http://${HOSTNAME}:${PORT}/***` }) @@ -108,7 +115,8 @@ test('instrumentOutbound', async (t) => { const name = NAMES.EXTERNAL.PREFIX + HOSTNAME + ':' + PORT + path instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) - assert.equal(transaction.trace.root.children[0].name, name) + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(child.name, name) function makeFakeRequest() { req.path = '/asdf?a=b&another=yourself&thing&grownup=true' @@ -124,8 +132,9 @@ test('instrumentOutbound', async (t) => { helper.runInTransaction(agent, function (transaction) { agent.config.attributes.enabled = true instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) + const [child] = transaction.trace.getChildren(transaction.trace.root.id) assert.deepEqual( - transaction.trace.root.children[0].attributes.get(DESTINATIONS.SPAN_EVENT), + child.attributes.get(DESTINATIONS.SPAN_EVENT), { hostname: HOSTNAME, port: PORT, @@ -171,7 +180,8 @@ test('instrumentOutbound', async (t) => { const name = NAMES.EXTERNAL.PREFIX + HOSTNAME + ':' + PORT + path req.path = path instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) - assert.equal(transaction.trace.root.children[0].name, name) + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(child.name, name) end() }) @@ -189,7 +199,8 @@ test('instrumentOutbound', async (t) => { const name = NAMES.EXTERNAL.PREFIX + HOSTNAME + ':' + PORT + '/newrelic' req.path = path instrumentOutbound(agent, { host: HOSTNAME, port: PORT }, makeFakeRequest) - assert.equal(transaction.trace.root.children[0].name, name) + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(child.name, name) }) function makeFakeRequest() { @@ -343,7 +354,7 @@ test('should add data from cat header to segment', async (t) => { const port = server.address().port http .get({ host: 'localhost', port }, function (res) { - const segment = agent.tracer.getTransaction().trace.root.children[0] + const segment = agent.tracer.getSegment() assert.equal(segment.catId, '123#456') assert.equal(segment.catTransaction, 'abc') @@ -365,7 +376,7 @@ test('should add data from cat header to segment', async (t) => { const port = server.address().port http .get({ host: 'localhost', port }, function (res) { - const segment = agent.tracer.getTransaction().trace.root.children[0] + const segment = agent.tracer.getSegment() assert.equal(segment.catId, '123#456') assert.equal(segment.catTransaction, 'abc') @@ -469,7 +480,8 @@ test('when working with http.request', async (t) => { opts.method = 'POST' const req = http.request(opts, function (res) { - const attributes = transaction.trace.root.children[0].getAttributes() + const [child] = transaction.trace.getChildren(transaction.trace.root.id) + const attributes = child.getAttributes() assert.equal(attributes.url, 'http://www.google.com/index.html') assert.equal(attributes.procedure, 'POST') res.resume() @@ -513,10 +525,14 @@ test('when working with http.request', async (t) => { nock(host).get(path).reply(200, 'Hello from Google') helper.runInTransaction(agent, (transaction) => { - const parentSegment = agent.tracer.createSegment('ParentSegment') + const parentSegment = agent.tracer.createSegment({ + name: 'ParentSegment', + parent: transaction.trace.root, + transaction + }) parentSegment.opaque = true - tracer.setSegment(parentSegment) // make the current active segment + tracer.setSegment({ transaction, segment: parentSegment }) // make the current active segment http.get(`${host}${path}`, (res) => { const segment = tracer.getSegment() diff --git a/test/unit/instrumentation/opensearch.test.js b/test/unit/instrumentation/opensearch.test.js deleted file mode 100644 index bc3cbb3530..0000000000 --- a/test/unit/instrumentation/opensearch.test.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2023 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 { - parsePath, - queryParser -} = require('../../../lib/instrumentation/@opensearch-project/opensearch') -const instrumentation = require('../../../lib/instrumentation/@opensearch-project/opensearch') -const methods = [ - { name: 'GET', expected: 'get' }, - { name: 'PUT', expected: 'create' }, - { name: 'POST', expected: 'create' }, - { name: 'DELETE', expected: 'delete' }, - { name: 'HEAD', expected: 'exists' } -] - -test('should log warning if version is not supported', async () => { - const shim = { - pkgVersion: '2.0.0', - logger: { - debug(msg) { - assert.equal( - msg, - 'Opensearch support is for versions 2.1.0 and above. Not instrumenting 2.0.0.' - ) - } - } - } - instrumentation({}, {}, '@opensearch-project/opensearch', shim) -}) -test('parsePath should behave as expected', async (t) => { - await t.test('indices', async function () { - const path = '/indexName' - methods.forEach((m) => { - const { collection, operation } = parsePath(path, m.name) - const expectedOp = `index.${m.expected}` - assert.equal(collection, 'indexName', "index should be 'indexName'") - assert.equal(operation, expectedOp, 'operation should include index and method') - }) - }) - await t.test('search of one index', async function () { - const path = '/indexName/_search' - methods.forEach((m) => { - const { collection, operation } = parsePath(path, m.name) - const expectedOp = 'search' - assert.equal(collection, 'indexName', "index should be 'indexName'") - assert.equal(operation, expectedOp, "operation should be 'search'") - }) - }) - await t.test('search of all indices', async function () { - const path = '/_search/' - methods.forEach((m) => { - if (m.name === 'PUT') { - // skip PUT - return - } - const { collection, operation } = parsePath(path, m.name) - const expectedOp = 'search' - assert.equal(collection, 'any', 'index should be `any`') - assert.equal(operation, expectedOp, `operation should match ${expectedOp}`) - }) - }) - await t.test('doc', async function () { - const path = '/indexName/_doc/testKey' - methods.forEach((m) => { - const { collection, operation } = parsePath(path, m.name) - const expectedOp = `doc.${m.expected}` - assert.equal(collection, 'indexName', "index should be 'indexName'") - assert.equal(operation, expectedOp, `operation should match ${expectedOp}`) - }) - }) - await t.test('path is /', async function () { - const path = '/' - methods.forEach((m) => { - const { collection, operation } = parsePath(path, m.name) - const expectedOp = `index.${m.expected}` - assert.equal(collection, 'any', 'index should be `any`') - assert.equal(operation, expectedOp, `operation should match ${expectedOp}`) - }) - }) - await t.test( - 'should provide sensible defaults when path is {} and parser encounters an error', - function () { - const path = {} - methods.forEach((m) => { - const { collection, operation } = parsePath(path, m.name) - const expectedOp = 'unknown' - assert.equal(collection, 'any', 'index should be `any`') - assert.equal(operation, expectedOp, `operation should match '${expectedOp}'`) - }) - } - ) -}) - -test('queryParser should behave as expected', async (t) => { - await t.test('given a querystring, it should use that for query', () => { - const params = JSON.stringify({ - path: '/_search', - method: 'GET', - querystring: { q: 'searchterm' } - }) - const expected = { - collection: 'any', - operation: 'search', - query: JSON.stringify({ q: 'searchterm' }) - } - const parseParams = queryParser(params) - assert.deepEqual(parseParams, expected, 'queryParser should handle query strings') - }) - await t.test('given a body, it should use that for query', () => { - const params = JSON.stringify({ - path: '/_search', - method: 'POST', - body: { match: { body: 'document' } } - }) - const expected = { - collection: 'any', - operation: 'search', - query: JSON.stringify({ match: { body: 'document' } }) - } - const parseParams = queryParser(params) - assert.deepEqual(parseParams, expected, 'queryParser should handle query body') - }) - await t.test('given a bulkBody, it should use that for query', () => { - const params = JSON.stringify({ - path: '/_msearch', - method: 'POST', - bulkBody: [ - {}, // cross-index searches have can have an empty metadata section - { query: { match: { body: 'sixth' } } }, - {}, - { query: { match: { body: 'bulk' } } } - ] - }) - const expected = { - collection: 'any', - operation: 'msearch.create', - query: JSON.stringify([ - {}, // cross-index searches have can have an empty metadata section - { query: { match: { body: 'sixth' } } }, - {}, - { query: { match: { body: 'bulk' } } } - ]) - } - const parseParams = queryParser(params) - assert.deepEqual(parseParams, expected, 'queryParser should handle query body') - }) -}) diff --git a/test/unit/instrumentation/prisma-client.test.js b/test/unit/instrumentation/prisma-client.test.js index 4101d0e06f..dd299cbecf 100644 --- a/test/unit/instrumentation/prisma-client.test.js +++ b/test/unit/instrumentation/prisma-client.test.js @@ -127,7 +127,7 @@ test('PrismaClient unit.tests', async (t) => { args: { query: 'select test from schema.unit-test;' }, action: 'executeRaw' }) - const { children } = tx.trace.root + const children = tx.trace.getChildren(tx.trace.root.id) assert.equal(children.length, 3, 'should have 3 segments') const [firstSegment, secondSegment, thirdSegment] = children assert.equal(firstSegment.name, 'Datastore/statement/Prisma/user/create') @@ -180,7 +180,7 @@ test('PrismaClient unit.tests', async (t) => { helper.runInTransaction(agent, async (tx) => { await client._executeRequest({ action: 'executeRaw' }) - const { children } = tx.trace.root + const children = tx.trace.getChildren(tx.trace.root.id) const [firstSegment] = children assert.equal(firstSegment.name, 'Datastore/statement/Prisma/other/other') end() @@ -228,7 +228,7 @@ test('PrismaClient unit.tests', async (t) => { args: [['select test from unit-test;']], action: 'executeRaw' }) - const { children } = tx.trace.root + const children = tx.trace.getChildren(tx.trace.root.id) assert.equal(children.length, 2, 'should have 3 segments') const [firstSegment, secondSegment] = children assert.equal(firstSegment.name, 'Datastore/statement/Prisma/user/create') diff --git a/test/unit/instrumentation/redis.test.js b/test/unit/instrumentation/redis.test.js index 36fa840b83..5f53feefdf 100644 --- a/test/unit/instrumentation/redis.test.js +++ b/test/unit/instrumentation/redis.test.js @@ -184,7 +184,7 @@ test('createClient saves connection options', async function (t) { helper.runInTransaction(agent, async function (tx) { await client.queue.addCommand(['test', 'key', 'value']) await client2.queue.addCommand(['test2', 'key2', 'value2']) - const [redisSegment, redisSegment2] = tx.trace.root.children + const [redisSegment, redisSegment2] = tx.trace.getChildren(tx.trace.root.id) const attrs = redisSegment.getAttributes() assert.deepEqual( attrs, diff --git a/test/unit/instrumentation/undici.test.js b/test/unit/instrumentation/undici.test.js index 95d81eb356..8ee1db302c 100644 --- a/test/unit/instrumentation/undici.test.js +++ b/test/unit/instrumentation/undici.test.js @@ -88,6 +88,7 @@ test('undici instrumentation', async function (t) { } channels.create.publish({ request }) assert.ok(request[symbols.parentSegment]) + assert.ok(request[symbols.transaction]) assert.equal(request.addHeader.callCount, 2) assert.deepEqual(request.addHeader.args[0], ['x-newrelic-synthetics', 'synthHeader']) assert.deepEqual(request.addHeader.args[1], [ @@ -146,6 +147,11 @@ test('undici instrumentation', async function (t) { request2[symbols.parentSegment].id, 'parent segment should be same' ) + assert.equal( + request[symbols.transaction].id, + request2[symbols.transaction].id, + 'tx should be same' + ) tx.end() end() }) @@ -165,6 +171,7 @@ test('undici instrumentation', async function (t) { const request2 = { path: '/request2', addHeader: sandbox.stub(), origin: HOST } channels.create.publish({ request: request2 }) assert.notEqual(request[symbols.parentSegment], request2[symbols.parentSegment]) + assert.equal(request[symbols.transaction], request2[symbols.transaction]) tx.end() end() }) @@ -190,6 +197,11 @@ test('undici instrumentation', async function (t) { request2[symbols.parentSegment].name, 'parent segment should not be same' ) + assert.equal( + request[symbols.transaction].id, + request2[symbols.transaction].id, + 'tx should be the same' + ) tx.end() end() }) @@ -205,7 +217,10 @@ test('undici instrumentation', async function (t) { origin: 'https://unittesting.com', path: '/foo?a=b&c=d' } - request[symbols.parentSegment] = shim.createSegment('parent') + request[symbols.parentSegment] = shim.createSegment({ + name: 'parent', + parent: tx.trace.root + }) channels.create.publish({ request }) assert.ok(request[symbols.segment]) const segment = shim.getSegment() @@ -228,7 +243,10 @@ test('undici instrumentation', async function (t) { origin: 'http://unittesting.com', path: '/http' } - request[symbols.parentSegment] = shim.createSegment('parent') + request[symbols.parentSegment] = shim.createSegment({ + name: 'parent', + parent: tx.trace.root + }) channels.create.publish({ request }) const segment = shim.getSegment() assert.equal(segment.name, 'External/unittesting.com/http') @@ -246,7 +264,10 @@ test('undici instrumentation', async function (t) { method: 'POST', path: '/port-https' } - request[symbols.parentSegment] = shim.createSegment('parent') + request[symbols.parentSegment] = shim.createSegment({ + name: 'parent', + parent: tx.trace.root + }) channels.create.publish({ request }) const segment = shim.getSegment() assert.equal(segment.name, 'External/unittesting.com:9999/port-https') @@ -264,7 +285,10 @@ test('undici instrumentation', async function (t) { method: 'POST', path: '/port-http' } - request[symbols.parentSegment] = shim.createSegment('parent') + request[symbols.parentSegment] = shim.createSegment({ + name: 'parent', + parent: tx.trace.root + }) channels.create.publish({ request }) const segment = shim.getSegment() assert.equal(segment.name, 'External/unittesting.com:8080/port-http') @@ -282,7 +306,10 @@ test('undici instrumentation', async function (t) { method: 'POST', path: '/port-http' } - request[symbols.parentSegment] = shim.createSegment('parent') + request[symbols.parentSegment] = shim.createSegment({ + name: 'parent', + parent: tx.trace.root + }) channels.create.publish({ request }) const segment = shim.getSegment() assert.equal(segment.name, 'ROOT', 'should not create a new segment if URL fails to parse') @@ -312,7 +339,7 @@ test('undici instrumentation', async function (t) { await t.test('should add statusCode and statusText from response', function (t, end) { helper.runInTransaction(agent, function (tx) { - const segment = shim.createSegment('active') + const segment = shim.createSegment({ name: 'active', parent: tx.trace.root }) const request = { [symbols.segment]: segment } @@ -334,7 +361,7 @@ test('undici instrumentation', async function (t) { agent.config.encoding_key = 'testing-key' agent.config.trusted_account_ids = [111] helper.runInTransaction(agent, function (tx) { - const segment = shim.createSegment('active') + const segment = shim.createSegment({ name: 'active', parent: tx.trace.root }) segment.addAttribute('url', 'https://www.unittesting.com/path') const request = { [symbols.segment]: segment @@ -360,12 +387,13 @@ test('undici instrumentation', async function (t) { await t.test('request:trailers', async function (t) { await t.test('should end current segment and restore to parent', function (t, end) { helper.runInTransaction(agent, function (tx) { - const parentSegment = shim.createSegment('parent') - const segment = shim.createSegment('active') + const parentSegment = shim.createSegment({ name: 'parent', parent: tx.trace.root }) + const segment = shim.createSegment({ name: 'active', parent: tx.trace.root }) shim.setActiveSegment(segment) const request = { [symbols.parentSegment]: parentSegment, - [symbols.segment]: segment + [symbols.segment]: segment, + [symbols.transaction]: tx } channels.send.publish({ request }) assert.equal(segment.timer.state, 3, 'previous active segment timer should be stopped') @@ -382,13 +410,14 @@ test('undici instrumentation', async function (t) { function (t, end) { helper.runInTransaction(agent, function (tx) { sandbox.stub(tx.agent.errors, 'add') - const parentSegment = shim.createSegment('parent') - const segment = shim.createSegment('active') + const parentSegment = shim.createSegment({ name: 'parent', parent: tx.trace.root }) + const segment = shim.createSegment({ name: 'active', parent: tx.trace.root }) shim.setActiveSegment(segment) const error = new Error('request failed') const request = { [symbols.parentSegment]: parentSegment, - [symbols.segment]: segment + [symbols.segment]: segment, + [symbols.transaction]: tx } channels.error.publish({ request, error }) assert.equal(segment.timer.state, 3, 'previous active segment timer should be stopped') diff --git a/test/unit/lib/otel/consumer.test.js b/test/unit/lib/otel/consumer.test.js new file mode 100644 index 0000000000..e4351dd060 --- /dev/null +++ b/test/unit/lib/otel/consumer.test.js @@ -0,0 +1,71 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +// Tests to verify that we can map OTEL "consumer" spans to NR segments. + +const test = require('node:test') +const assert = require('node:assert') + +const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base') +const { SpanKind } = require('@opentelemetry/api') +const { + SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_MESSAGING_DESTINATION, + SEMATTRS_MESSAGING_DESTINATION_KIND +} = require('@opentelemetry/semantic-conventions') + +const { DESTINATIONS } = require('../../../../lib/transaction') +const helper = require('../../../lib/agent_helper') +const createSpan = require('./fixtures/span') +const SegmentSynthesizer = require('../../../../lib/otel/segment-synthesis') + +test.beforeEach((ctx) => { + const logs = [] + const logger = { + debug(...args) { + logs.push(args) + } + } + const agent = helper.loadMockedAgent() + const synth = new SegmentSynthesizer(agent, { logger }) + const tracer = new BasicTracerProvider().getTracer('default') + + ctx.nr = { + agent, + logger, + logs, + synth, + tracer + } +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) +}) + +test('should create consumer segment from otel span', (t) => { + const { synth, tracer } = t.nr + const span = createSpan({ tracer, kind: SpanKind.CONSUMER }) + span.setAttribute('messaging.operation', 'receive') + span.setAttribute(SEMATTRS_MESSAGING_SYSTEM, 'msgqueuer') + span.setAttribute(SEMATTRS_MESSAGING_DESTINATION, 'dest1') + span.setAttribute(SEMATTRS_MESSAGING_DESTINATION_KIND, 'topic1') + + const expectedName = 'OtherTransaction/Message/msgqueuer/topic1/Named/dest1' + const { segment, transaction } = synth.synthesize(span) + assert.equal(segment.name, expectedName) + assert.equal(segment.parentId, segment.root.id) + assert.equal(transaction.name, expectedName) + assert.equal(transaction.type, 'bg') + assert.equal(transaction.baseSegment, segment) + assert.equal( + transaction.trace.attributes.get(DESTINATIONS.TRANS_SCOPE)['message.queueName'], + 'dest1' + ) + + transaction.end() +}) diff --git a/test/unit/lib/otel/fixtures/db-sql.js b/test/unit/lib/otel/fixtures/db-sql.js new file mode 100644 index 0000000000..77a95de656 --- /dev/null +++ b/test/unit/lib/otel/fixtures/db-sql.js @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { + SEMATTRS_DB_SYSTEM, + SEMATTRS_DB_SQL_TABLE, + SEMATTRS_DB_OPERATION, + SEMATTRS_DB_STATEMENT, + DbSystemValues, + SEMATTRS_DB_MONGODB_COLLECTION +} = require('@opentelemetry/semantic-conventions') +const { SpanKind } = require('@opentelemetry/api') +const createSpan = require('./span') + +function createDbClientSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.CLIENT, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_DB_SYSTEM, 'custom-db') + span.setAttribute(SEMATTRS_DB_SQL_TABLE, 'test-table') + span.setAttribute(SEMATTRS_DB_OPERATION, 'select') + return span +} + +function createDbStatementSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.CLIENT, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_DB_SYSTEM, 'custom-db') + span.setAttribute(SEMATTRS_DB_STATEMENT, 'select * from test-table') + return span +} + +function createMemcachedDbSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.CLIENT, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_DB_SYSTEM, DbSystemValues.MEMCACHED) + span.setAttribute(SEMATTRS_DB_OPERATION, 'set') + return span +} + +function createMongoDbSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.CLIENT, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_DB_SYSTEM, DbSystemValues.MONGODB) + span.setAttribute(SEMATTRS_DB_OPERATION, 'insert') + span.setAttribute(SEMATTRS_DB_MONGODB_COLLECTION, 'test-collection') + return span +} + +function createRedisDbSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.CLIENT, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_DB_SYSTEM, DbSystemValues.REDIS) + span.setAttribute(SEMATTRS_DB_STATEMENT, 'hset hash random random') + return span +} + +module.exports = { + createDbClientSpan, + createDbStatementSpan, + createMemcachedDbSpan, + createMongoDbSpan, + createRedisDbSpan +} diff --git a/test/unit/lib/otel/fixtures/http-client.js b/test/unit/lib/otel/fixtures/http-client.js new file mode 100644 index 0000000000..372b0debcc --- /dev/null +++ b/test/unit/lib/otel/fixtures/http-client.js @@ -0,0 +1,16 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD } = require('@opentelemetry/semantic-conventions') +const { SpanKind } = require('@opentelemetry/api') +const createSpan = require('./span') + +module.exports = function createHttpClientSpan({ parentId, tracer, tx }) { + const span = createSpan({ name: 'test-span', kind: SpanKind.CLIENT, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_HTTP_METHOD, 'GET') + span.setAttribute(SEMATTRS_HTTP_HOST, 'newrelic.com') + return span +} diff --git a/test/unit/lib/otel/fixtures/index.js b/test/unit/lib/otel/fixtures/index.js new file mode 100644 index 0000000000..8d8899099c --- /dev/null +++ b/test/unit/lib/otel/fixtures/index.js @@ -0,0 +1,32 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { + createDbClientSpan, + createDbStatementSpan, + createMemcachedDbSpan, + createMongoDbSpan, + createRedisDbSpan +} = require('./db-sql') +const createSpan = require('./span') +const createHttpClientSpan = require('./http-client') +const { createRpcServerSpan, createHttpServerSpan, createBaseHttpSpan } = require('./server') +const { createQueueProducerSpan, createTopicProducerSpan } = require('./producer') + +module.exports = { + createBaseHttpSpan, + createDbClientSpan, + createDbStatementSpan, + createHttpClientSpan, + createHttpServerSpan, + createMemcachedDbSpan, + createMongoDbSpan, + createQueueProducerSpan, + createRedisDbSpan, + createRpcServerSpan, + createSpan, + createTopicProducerSpan +} diff --git a/test/unit/lib/otel/fixtures/producer.js b/test/unit/lib/otel/fixtures/producer.js new file mode 100644 index 0000000000..581e682718 --- /dev/null +++ b/test/unit/lib/otel/fixtures/producer.js @@ -0,0 +1,35 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { + MessagingDestinationKindValues, + SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_MESSAGING_DESTINATION, + SEMATTRS_MESSAGING_DESTINATION_KIND +} = require('@opentelemetry/semantic-conventions') +const { SpanKind } = require('@opentelemetry/api') +const createSpan = require('./span') + +function createTopicProducerSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.PRODUCER, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_MESSAGING_SYSTEM, 'messaging-lib') + span.setAttribute(SEMATTRS_MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.TOPIC) + span.setAttribute(SEMATTRS_MESSAGING_DESTINATION, 'test-topic') + return span +} + +function createQueueProducerSpan({ parentId, tracer, tx, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.PRODUCER, parentId, tracer, tx }) + span.setAttribute(SEMATTRS_MESSAGING_SYSTEM, 'messaging-lib') + span.setAttribute(SEMATTRS_MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.QUEUE) + span.setAttribute(SEMATTRS_MESSAGING_DESTINATION, 'test-queue') + return span +} + +module.exports = { + createQueueProducerSpan, + createTopicProducerSpan +} diff --git a/test/unit/lib/otel/fixtures/server.js b/test/unit/lib/otel/fixtures/server.js new file mode 100644 index 0000000000..fb9b6f8be0 --- /dev/null +++ b/test/unit/lib/otel/fixtures/server.js @@ -0,0 +1,43 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_ROUTE, + SEMATTRS_HTTP_URL, + SEMATTRS_RPC_SYSTEM, + SEMATTRS_RPC_SERVICE, + SEMATTRS_RPC_METHOD +} = require('@opentelemetry/semantic-conventions') + +const { SpanKind } = require('@opentelemetry/api') +const createSpan = require('./span') + +function createRpcServerSpan({ tracer, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.SERVER, tracer }) + span.setAttribute(SEMATTRS_RPC_SYSTEM, 'grpc') + span.setAttribute(SEMATTRS_RPC_METHOD, 'findUser') + span.setAttribute(SEMATTRS_RPC_SERVICE, 'TestService') + return span +} + +function createHttpServerSpan({ tracer, name = 'test-span' }) { + const span = createSpan({ name, kind: SpanKind.SERVER, tracer }) + span.setAttribute(SEMATTRS_HTTP_METHOD, 'PUT') + span.setAttribute(SEMATTRS_HTTP_ROUTE, '/user/:id') + span.setAttribute(SEMATTRS_HTTP_URL, '/user/1') + return span +} + +function createBaseHttpSpan({ tracer, name = 'test-span' }) { + return createSpan({ name, kind: SpanKind.SERVER, tracer }) +} + +module.exports = { + createBaseHttpSpan, + createHttpServerSpan, + createRpcServerSpan +} diff --git a/test/unit/lib/otel/fixtures/span.js b/test/unit/lib/otel/fixtures/span.js new file mode 100644 index 0000000000..6405679134 --- /dev/null +++ b/test/unit/lib/otel/fixtures/span.js @@ -0,0 +1,17 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const { ROOT_CONTEXT, TraceFlags } = require('@opentelemetry/api') +const { Span } = require('@opentelemetry/sdk-trace-base') + +module.exports = function createSpan({ parentId, tracer, tx, kind, name }) { + const spanContext = { + traceId: tx?.trace?.id, + spanId: tx?.trace?.root?.id, + traceFlags: TraceFlags.SAMPLED + } + return new Span(tracer, ROOT_CONTEXT, name, spanContext, kind, parentId) +} diff --git a/test/unit/lib/otel/rules.test.js b/test/unit/lib/otel/rules.test.js new file mode 100644 index 0000000000..57ade4938f --- /dev/null +++ b/test/unit/lib/otel/rules.test.js @@ -0,0 +1,89 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +// Based upon https://github.com/open-telemetry/opentelemetry-js/blob/8fc76896595aac912bf9e15d4f19c167317844c8/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts#L851 + +const test = require('node:test') +const assert = require('node:assert') + +const { ROOT_CONTEXT, SpanKind, TraceFlags } = require('@opentelemetry/api') +const { BasicTracerProvider, Span } = require('@opentelemetry/sdk-trace-base') +const { RulesEngine } = require('../../../../lib/otel/rules.js') + +const tracer = new BasicTracerProvider().getTracer('default') +const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED +} +const parentId = '5c1c63257de34c67' + +test('engine returns correct matching rule', () => { + const engine = new RulesEngine() + const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.SERVER, parentId) + span.setAttribute('http.request.method', 'GET') + span.end() + + const rule = engine.test(span) + assert.notEqual(rule, undefined) + assert.equal(rule.name, 'OtelHttpServer1_23') +}) + +test('consumer does not match fallback rule', () => { + const engine = new RulesEngine() + const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CONSUMER, parentId) + span.setAttribute('messaging.operation', 'create') + span.end() + + const rule = engine.test(span) + assert.notEqual(rule, undefined) + assert.equal(rule.name, 'OtelMessagingConsumer1_24') +}) + +test('fallback server rule is met', () => { + const engine = new RulesEngine() + const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.SERVER, parentId) + span.setAttribute('foo.bar', 'baz') + span.end() + + const rule = engine.test(span) + assert.notEqual(rule, undefined) + assert.equal(rule.name, 'FallbackServer') +}) + +test('fallback client rule is met', () => { + const engine = new RulesEngine() + const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CLIENT, parentId) + span.setAttribute('foo.bar', 'baz') + span.end() + + const rule = engine.test(span) + assert.notEqual(rule, undefined) + assert.equal(rule.name, 'FallbackClient') +}) + +test('fallback producer rule is met', () => { + const engine = new RulesEngine() + const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.PRODUCER, parentId) + span.setAttribute('foo.bar', 'baz') + span.end() + + const rule = engine.test(span) + assert.notEqual(rule, undefined) + assert.equal(rule.name, 'FallbackProducer') +}) + +test('fallback internal rule is met', () => { + const engine = new RulesEngine() + const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.INTERNAL, parentId) + span.setAttribute('foo.bar', 'baz') + span.end() + + const rule = engine.test(span) + assert.notEqual(rule, undefined) + assert.equal(rule.name, 'Fallback') +}) diff --git a/test/unit/lib/otel/segment-synthesizer.test.js b/test/unit/lib/otel/segment-synthesizer.test.js new file mode 100644 index 0000000000..39c0903bb0 --- /dev/null +++ b/test/unit/lib/otel/segment-synthesizer.test.js @@ -0,0 +1,251 @@ +/* + * 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 helper = require('../../../lib/agent_helper') +const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base') +const SegmentSynthesizer = require('../../../../lib/otel/segment-synthesis') +const createMockLogger = require('../../mocks/logger') +const { + createBaseHttpSpan, + createDbClientSpan, + createSpan, + createHttpClientSpan, + createHttpServerSpan, + createDbStatementSpan, + createMongoDbSpan, + createRedisDbSpan, + createRpcServerSpan, + createMemcachedDbSpan, + createTopicProducerSpan, + createQueueProducerSpan +} = require('./fixtures') +const { SEMATTRS_DB_SYSTEM } = require('@opentelemetry/semantic-conventions') +const { SpanKind } = require('@opentelemetry/api') +const { DESTINATIONS } = require('../../../../lib/config/attribute-filter') + +test.beforeEach((ctx) => { + const loggerMock = createMockLogger() + const agent = helper.loadMockedAgent() + const synthesizer = new SegmentSynthesizer(agent, { logger: loggerMock }) + const tracer = new BasicTracerProvider().getTracer('default') + const parentId = '5c1c63257de34c67' + ctx.nr = { + agent, + loggerMock, + parentId, + synthesizer, + tracer + } +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) +}) + +test('should create http external segment from otel http client span', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createHttpClientSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'External/newrelic.com') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create db segment', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createDbClientSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Datastore/statement/custom-db/test-table/select') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create db segment and get operation and table from db.statement', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createDbStatementSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Datastore/statement/custom-db/test-table/select') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create db segment and get collection from db.mongodb.collection', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createMongoDbSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Datastore/statement/mongodb/test-collection/insert') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create db segment and get operation from db.statement when system is redis', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createRedisDbSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Datastore/operation/redis/hset') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create db segment and get operation from db.operation when system is memcached', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createMemcachedDbSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Datastore/operation/memcached/set') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should log table and operation as unknown when the db.system, db.sql.table and db.operation to not exist as span attributes', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createSpan({ name: 'test-span', kind: SpanKind.CLIENT, parentId, tx, tracer }) + span.setAttribute(SEMATTRS_DB_SYSTEM, 'test-db') + + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Datastore/statement/test-db/Unknown/Unknown') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create rpc segment', (t) => { + const { synthesizer, tracer } = t.nr + const span = createRpcServerSpan({ tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + const expectedName = 'WebTransaction/WebFrameworkUri/grpc/TestService.findUser' + assert.equal(segment.name, expectedName) + assert.equal(segment.parentId, segment.root.id) + assert.ok(transaction) + assert.equal(transaction.name, expectedName) + const segmentAttrs = segment.getAttributes() + assert.equal(segmentAttrs.component, 'grpc') + assert.equal(transaction.url, expectedName) + assert.equal(transaction.baseSegment.name, segment.name) + const attrs = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal(attrs['request.method'], 'findUser') + assert.equal(attrs['request.uri'], expectedName) +}) + +test('should create http server segment', (t) => { + const { synthesizer, tracer } = t.nr + const span = createHttpServerSpan({ tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + const expectedName = 'WebTransaction/Nodejs/PUT//user/:id' + assert.equal(segment.name, expectedName) + assert.equal(segment.parentId, segment.root.id) + assert.ok(transaction) + assert.equal(transaction.name, expectedName) + assert.equal(transaction.url, '/user/1') + assert.equal(transaction.baseSegment.name, segment.name) + const attrs = transaction.trace.attributes.get(DESTINATIONS.TRANS_TRACE) + assert.equal(attrs['request.method'], 'PUT') + assert.equal(attrs['request.uri'], '/user/1') + transaction.end() +}) + +test('should create base http server segment', (t) => { + const { synthesizer, tracer } = t.nr + const span = createBaseHttpSpan({ tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + const expectedName = 'WebTransaction/NormalizedUri/*' + assert.equal(segment.name, expectedName) + assert.equal(segment.parentId, segment.root.id) + assert.equal(transaction.baseSegment.name, segment.name) + assert.ok(transaction) + assert.equal(transaction.name, expectedName) +}) + +test('should create topic producer segment', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createTopicProducerSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'MessageBroker/messaging-lib/topic/Produce/Named/test-topic') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create queue producer segment', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createQueueProducerSpan({ tx, parentId, tracer }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'MessageBroker/messaging-lib/queue/Produce/Named/test-queue') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should create internal custom segment', (t, end) => { + const { agent, synthesizer, parentId, tracer } = t.nr + helper.runInTransaction(agent, (tx) => { + const span = createSpan({ + name: 'doer-of-stuff', + kind: SpanKind.INTERNAL, + parentId, + tx, + tracer + }) + const { segment, transaction } = synthesizer.synthesize(span) + assert.equal(tx.id, transaction.id) + assert.equal(segment.name, 'Custom/doer-of-stuff') + assert.equal(segment.parentId, tx.trace.root.id) + tx.end() + end() + }) +}) + +test('should log warning span does not match a rule', (t, end) => { + const { agent, synthesizer, loggerMock, parentId, tracer } = t.nr + + helper.runInTransaction(agent, (tx) => { + const span = createSpan({ name: 'test-span', kind: 'bogus', parentId, tx, tracer }) + const data = synthesizer.synthesize(span) + assert.ok(!data) + assert.deepEqual(loggerMock.debug.args[0], [ + 'Cannot match a rule to span name: %s, kind %s', + 'test-span', + 'bogus' + ]) + tx.end() + end() + }) +}) diff --git a/test/unit/llm-events/aws-bedrock/chat-completion-message.test.js b/test/unit/llm-events/aws-bedrock/chat-completion-message.test.js index 9dcc95f41a..f0af45c441 100644 --- a/test/unit/llm-events/aws-bedrock/chat-completion-message.test.js +++ b/test/unit/llm-events/aws-bedrock/chat-completion-message.test.js @@ -49,12 +49,12 @@ test.beforeEach((ctx) => { ctx.nr.content = 'a prompt' + ctx.nr.transaction = { + id: 'tx-1', + traceId: 'trace-1' + } ctx.nr.segment = { - id: 'segment-1', - transaction: { - id: 'tx-1', - traceId: 'trace-1' - } + id: 'segment-1' } ctx.nr.bedrockResponse = { diff --git a/test/unit/llm-events/aws-bedrock/chat-completion-summary.test.js b/test/unit/llm-events/aws-bedrock/chat-completion-summary.test.js index 81d111cb16..cd37b0d020 100644 --- a/test/unit/llm-events/aws-bedrock/chat-completion-summary.test.js +++ b/test/unit/llm-events/aws-bedrock/chat-completion-summary.test.js @@ -38,10 +38,10 @@ test.beforeEach((ctx) => { } } + ctx.nr.transaction = { + id: 'tx-1' + } ctx.nr.segment = { - transaction: { - id: 'tx-1' - }, getDurationInMillis() { return 100 } diff --git a/test/unit/llm-events/aws-bedrock/embedding.test.js b/test/unit/llm-events/aws-bedrock/embedding.test.js index 35a4e1d4d7..b2908401a6 100644 --- a/test/unit/llm-events/aws-bedrock/embedding.test.js +++ b/test/unit/llm-events/aws-bedrock/embedding.test.js @@ -54,8 +54,10 @@ test.beforeEach((ctx) => { 'x-amzn-requestid': 'request-1' } } + ctx.nr.transaction = { + traceId: 'id' + } ctx.nr.segment = { - transaction: { traceId: 'id' }, getDurationInMillis() { return 1.008 } diff --git a/test/unit/llm-events/aws-bedrock/event.test.js b/test/unit/llm-events/aws-bedrock/event.test.js index 1703c007a5..e1a19eae78 100644 --- a/test/unit/llm-events/aws-bedrock/event.test.js +++ b/test/unit/llm-events/aws-bedrock/event.test.js @@ -39,11 +39,11 @@ test.beforeEach((ctx) => { } } + ctx.nr.transaction = { + traceId: 'trace-1' + } ctx.nr.segment = { - id: 'segment-1', - transaction: { - traceId: 'trace-1' - } + id: 'segment-1' } ctx.nr.bedrockResponse = { diff --git a/test/unit/llm-events/langchain/chat-completion-message.test.js b/test/unit/llm-events/langchain/chat-completion-message.test.js index 78dfe19f9b..698960b996 100644 --- a/test/unit/llm-events/langchain/chat-completion-message.test.js +++ b/test/unit/llm-events/langchain/chat-completion-message.test.js @@ -11,7 +11,8 @@ const LangChainCompletionMessage = require('../../../../lib/llm-events/langchain test.beforeEach((ctx) => { ctx.nr = {} - ctx.nr._tx = { + ctx.nr.transaction = { + traceId: 'trace-1', trace: { custom: { get() { @@ -36,16 +37,13 @@ test.beforeEach((ctx) => { }, tracer: { getTransaction() { - return ctx.nr._tx + return ctx.nr.transaction } } } ctx.nr.segment = { - id: 'segment-1', - transaction: { - traceId: 'trace-1' - } + id: 'segment-1' } ctx.nr.runId = 'run-1' diff --git a/test/unit/llm-events/langchain/chat-completion-summary.test.js b/test/unit/llm-events/langchain/chat-completion-summary.test.js index 83d770fcad..37a0246990 100644 --- a/test/unit/llm-events/langchain/chat-completion-summary.test.js +++ b/test/unit/llm-events/langchain/chat-completion-summary.test.js @@ -11,7 +11,8 @@ const LangChainCompletionSummary = require('../../../../lib/llm-events/langchain test.beforeEach((ctx) => { ctx.nr = {} - ctx.nr._tx = { + ctx.nr.transaction = { + traceId: 'trace-1', trace: { custom: { get() { @@ -31,16 +32,13 @@ test.beforeEach((ctx) => { }, tracer: { getTransaction() { - return ctx.nr._tx + return ctx.nr.transaction } } } ctx.nr.segment = { id: 'segment-1', - transaction: { - traceId: 'trace-1' - }, getDurationInMillis() { return 42 } diff --git a/test/unit/llm-events/langchain/event.test.js b/test/unit/llm-events/langchain/event.test.js index 6199deb7b9..c9b470abd9 100644 --- a/test/unit/llm-events/langchain/event.test.js +++ b/test/unit/llm-events/langchain/event.test.js @@ -11,7 +11,8 @@ const LangChainEvent = require('../../../../lib/llm-events/langchain/event') test.beforeEach((ctx) => { ctx.nr = {} - ctx.nr._tx = { + ctx.nr.transaction = { + traceId: 'trace-1', trace: { custom: { get() { @@ -34,16 +35,13 @@ test.beforeEach((ctx) => { }, tracer: { getTransaction() { - return ctx.nr._tx + return ctx.nr.transaction } } } ctx.nr.segment = { - id: 'segment-1', - transaction: { - traceId: 'trace-1' - } + id: 'segment-1' } ctx.nr.runId = 'run-1' diff --git a/test/unit/llm-events/langchain/tool.test.js b/test/unit/llm-events/langchain/tool.test.js index 639a4a7b22..049c2ecee4 100644 --- a/test/unit/llm-events/langchain/tool.test.js +++ b/test/unit/llm-events/langchain/tool.test.js @@ -11,18 +11,6 @@ const LangChainTool = require('../../../../lib/llm-events/langchain/tool') test.beforeEach((ctx) => { ctx.nr = {} - ctx.nr._tx = { - trace: { - custom: { - get() { - return { - 'llm.conversation_id': 'test-conversation' - } - } - } - } - } - ctx.nr.agent = { config: { ai_monitoring: { @@ -36,7 +24,20 @@ test.beforeEach((ctx) => { }, tracer: { getTransaction() { - return ctx.nr._tx + return ctx.nr.transaction + } + } + } + + ctx.nr.transaction = { + traceId: 'trace-1', + trace: { + custom: { + get() { + return { + 'llm.conversation_id': 'test-conversation' + } + } } } } @@ -45,10 +46,7 @@ test.beforeEach((ctx) => { getDurationInMillis() { return 1.01 }, - id: 'segment-1', - transaction: { - traceId: 'trace-1' - } + id: 'segment-1' } ctx.nr.runId = 'run-1' diff --git a/test/unit/llm-events/langchain/vector-search-result.test.js b/test/unit/llm-events/langchain/vector-search-result.test.js index 7189d32d90..794489e560 100644 --- a/test/unit/llm-events/langchain/vector-search-result.test.js +++ b/test/unit/llm-events/langchain/vector-search-result.test.js @@ -12,18 +12,6 @@ const LangChainVectorSearch = require('../../../../lib/llm-events/langchain/vect test.beforeEach((ctx) => { ctx.nr = {} - ctx.nr._tx = { - trace: { - custom: { - get() { - return { - 'llm.conversation_id': 'test-conversation' - } - } - } - } - } - ctx.nr.agent = { config: { ai_monitoring: { @@ -37,16 +25,25 @@ test.beforeEach((ctx) => { }, tracer: { getTransaction() { - return ctx.nr._tx + return ctx.nr.transaction } } } + ctx.nr.transaction = { + traceId: 'trace-1', + trace: { + custom: { + get() { + return { + 'llm.conversation_id': 'test-conversation' + } + } + } + } + } ctx.nr.segment = { id: 'segment-1', - transaction: { - traceId: 'trace-1' - }, getDurationInMillis() { return 42 } diff --git a/test/unit/llm-events/langchain/vector-search.test.js b/test/unit/llm-events/langchain/vector-search.test.js index 73c04a938f..771f118308 100644 --- a/test/unit/llm-events/langchain/vector-search.test.js +++ b/test/unit/llm-events/langchain/vector-search.test.js @@ -41,11 +41,11 @@ test.beforeEach((ctx) => { } } + ctx.nr.transaction = { + traceId: 'trace-1' + } ctx.nr.segment = { id: 'segment-1', - transaction: { - traceId: 'trace-1' - }, getDurationInMillis() { return 42 } diff --git a/test/unit/llm-events/openai/chat-completion-message.test.js b/test/unit/llm-events/openai/chat-completion-message.test.js index f727cd35d7..53b5df2a8a 100644 --- a/test/unit/llm-events/openai/chat-completion-message.test.js +++ b/test/unit/llm-events/openai/chat-completion-message.test.js @@ -28,6 +28,7 @@ test('should create a LlmChatCompletionMessage event', (t, end) => { const segment = api.shim.getActiveSegment() const summaryId = 'chat-summary-id' const chatMessageEvent = new LlmChatCompletionMessage({ + transaction: tx, agent, segment, request: req, @@ -51,6 +52,7 @@ test('should create a LlmChatCompletionMessage from response choices', (t, end) const segment = api.shim.getActiveSegment() const summaryId = 'chat-summary-id' const chatMessageEvent = new LlmChatCompletionMessage({ + transaction: tx, agent, segment, request: req, @@ -77,6 +79,7 @@ test('should set conversation_id from custom attributes', (t, end) => { helper.runInTransaction(agent, () => { api.addCustomAttribute('llm.conversation_id', conversationId) const chatMessageEvent = new LlmChatCompletionMessage({ + transaction: {}, agent, segment: {}, request: {}, @@ -98,6 +101,7 @@ test('respects record_content', (t, end) => { const chatMessageEvent = new LlmChatCompletionMessage({ agent, segment: {}, + transaction: {}, request: {}, response: {} }) @@ -116,12 +120,13 @@ test('should use token_count from tokenCountCallback for prompt message', (t, en return expectedCount } api.setLlmTokenCountCallback(cb) - helper.runInTransaction(agent, () => { + helper.runInTransaction(agent, (tx) => { api.startSegment('fakeSegment', false, () => { const segment = api.shim.getActiveSegment() const summaryId = 'chat-summary-id' delete chatRes.usage const chatMessageEvent = new LlmChatCompletionMessage({ + transaction: tx, agent, segment, request: req, @@ -146,7 +151,7 @@ test('should use token_count from tokenCountCallback for completion messages', ( return expectedCount } api.setLlmTokenCountCallback(cb) - helper.runInTransaction(agent, () => { + helper.runInTransaction(agent, (tx) => { api.startSegment('fakeSegment', false, () => { const segment = api.shim.getActiveSegment() const summaryId = 'chat-summary-id' @@ -154,6 +159,7 @@ test('should use token_count from tokenCountCallback for completion messages', ( const chatMessageEvent = new LlmChatCompletionMessage({ agent, segment, + transaction: tx, request: req, response: chatRes, completionId: summaryId, @@ -169,7 +175,7 @@ test('should use token_count from tokenCountCallback for completion messages', ( test('should not set token_count if not set in usage nor a callback registered', (t, end) => { const { agent } = t.nr const api = helper.getAgentApi() - helper.runInTransaction(agent, () => { + helper.runInTransaction(agent, (tx) => { api.startSegment('fakeSegment', false, () => { const segment = api.shim.getActiveSegment() const summaryId = 'chat-summary-id' @@ -177,6 +183,7 @@ test('should not set token_count if not set in usage nor a callback registered', const chatMessageEvent = new LlmChatCompletionMessage({ agent, segment, + transaction: tx, request: req, response: chatRes, completionId: summaryId, @@ -196,7 +203,7 @@ test('should not set token_count if not set in usage nor a callback registered r // empty cb } api.setLlmTokenCountCallback(cb) - helper.runInTransaction(agent, () => { + helper.runInTransaction(agent, (tx) => { api.startSegment('fakeSegment', false, () => { const segment = api.shim.getActiveSegment() const summaryId = 'chat-summary-id' @@ -204,6 +211,7 @@ test('should not set token_count if not set in usage nor a callback registered r const chatMessageEvent = new LlmChatCompletionMessage({ agent, segment, + transaction: tx, request: req, response: chatRes, completionId: summaryId, diff --git a/test/unit/llm-events/openai/chat-completion-summary.test.js b/test/unit/llm-events/openai/chat-completion-summary.test.js index ca9e24823a..984cabe802 100644 --- a/test/unit/llm-events/openai/chat-completion-summary.test.js +++ b/test/unit/llm-events/openai/chat-completion-summary.test.js @@ -30,6 +30,7 @@ test('should properly create a LlmChatCompletionSummary event', (t, end) => { const chatSummaryEvent = new LlmChatCompletionSummary({ agent, segment, + transaction: tx, request: req, response: chatRes }) @@ -45,6 +46,7 @@ test('should set error to true', (ctx, end) => { helper.runInTransaction(agent, () => { const chatSummaryEvent = new LlmChatCompletionSummary({ agent, + transaction: null, segment: null, request: {}, response: {}, @@ -67,6 +69,7 @@ test('should set `llm.` attributes from custom attributes', (t, end) => { const chatSummaryEvent = new LlmChatCompletionSummary({ agent, segment: null, + transaction: null, request: {}, response: {} }) diff --git a/test/unit/llm-events/openai/common.js b/test/unit/llm-events/openai/common.js index dcf655bbd8..2f828c92b7 100644 --- a/test/unit/llm-events/openai/common.js +++ b/test/unit/llm-events/openai/common.js @@ -45,18 +45,20 @@ const req = { function getExpectedResult(tx, event, type, completionId) { const trace = tx.trace.root + const [child] = tx.trace.getChildren(trace.id) + const spanId = child.id let expected = { id: event.id, appName: 'New Relic for Node.js tests', request_id: 'req-id', trace_id: tx.traceId, - span_id: trace.children[0].id, + span_id: spanId, 'response.model': 'gpt-3.5-turbo-0613', vendor: 'openai', ingest_source: 'Node' } const resKeys = { - duration: trace.children[0].getDurationInMillis(), + duration: child.getDurationInMillis(), 'request.model': 'gpt-3.5-turbo-0613', 'response.organization': 'new-relic', 'response.headers.llmVersion': '1.0.0', diff --git a/test/unit/llm-events/openai/embedding.test.js b/test/unit/llm-events/openai/embedding.test.js index ca8f3d75ae..eee4b5fc6f 100644 --- a/test/unit/llm-events/openai/embedding.test.js +++ b/test/unit/llm-events/openai/embedding.test.js @@ -32,7 +32,13 @@ test('should properly create a LlmEmbedding event', (t, end) => { api.startSegment('fakeSegment', false, () => { const segment = api.shim.getActiveSegment() segment.end() - const embeddingEvent = new LlmEmbedding({ agent, segment, request: req, response: res }) + const embeddingEvent = new LlmEmbedding({ + agent, + segment, + transaction: tx, + request: req, + response: res + }) const expected = getExpectedResult(tx, embeddingEvent, 'embedding') assert.deepEqual(embeddingEvent, expected) end() @@ -62,6 +68,7 @@ test('should properly create a LlmEmbedding event', (t, end) => { const embeddingEvent = new LlmEmbedding({ agent, segment: null, + transaction: null, request: { input: value }, response: {} }) diff --git a/test/unit/metrics-recorder/database-metrics-recorder.test.js b/test/unit/metrics-recorder/database-metrics-recorder.test.js index 6625c3cf6d..4757e26765 100644 --- a/test/unit/metrics-recorder/database-metrics-recorder.test.js +++ b/test/unit/metrics-recorder/database-metrics-recorder.test.js @@ -33,7 +33,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - recordQueryMetrics.bind(ps)(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST', transaction) transaction.end() ctx.nr.metrics = transaction.metrics @@ -101,7 +101,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - recordQueryMetrics.bind(ps)(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST', transaction) transaction.end() ctx.nr.metrics = transaction.metrics @@ -166,7 +166,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - recordQueryMetrics.bind(ps)(segment, null) + recordQueryMetrics.bind(ps)(segment, null, transaction) transaction.end() ctx.nr.metrics = transaction.metrics @@ -229,7 +229,7 @@ test('recording database metrics', async (t) => { transaction.type = Transaction.TYPES.BG segment.setDurationInMillis(333) - recordQueryMetrics.bind(ps)(segment, null) + recordQueryMetrics.bind(ps)(segment, null, transaction) transaction.end() ctx.nr.metrics = transaction.metrics @@ -298,13 +298,13 @@ test('recording slow queries', async (t) => { ctx.nr.segment = segment segment.setDurationInMillis(503) - recordQueryMetrics.bind(ps)(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST', transaction) const ps2 = new ParsedStatement('MySql', 'select', 'foo', 'select * from foo where b=2') const segment2 = transaction.trace.add('test') segment2.setDurationInMillis(501) - recordQueryMetrics.bind(ps2)(segment2, 'TEST') + recordQueryMetrics.bind(ps2)(segment2, 'TEST', transaction) transaction.end() }) @@ -359,13 +359,13 @@ test('recording slow queries', async (t) => { ctx.nr.segment = segment segment.setDurationInMillis(503) - recordQueryMetrics.bind(ps)(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST', transaction) const ps2 = new ParsedStatement('MySql', 'select', null, 'select * from foo where b=2') const segment2 = transaction.trace.add('test') segment2.setDurationInMillis(501) - recordQueryMetrics.bind(ps2)(segment2, 'TEST') + recordQueryMetrics.bind(ps2)(segment2, 'TEST', transaction) transaction.end() }) @@ -428,13 +428,13 @@ test('recording slow queries', async (t) => { ctx.nr.segment = segment segment.setDurationInMillis(503) - recordQueryMetrics.bind(ps)(segment, 'TEST') + recordQueryMetrics.bind(ps)(segment, 'TEST', transaction) const ps2 = new ParsedStatement('MySql', 'select', null, null) const segment2 = transaction.trace.add('test') segment2.setDurationInMillis(501) - recordQueryMetrics.bind(ps2)(segment2, 'TEST') + recordQueryMetrics.bind(ps2)(segment2, 'TEST', transaction) transaction.end() }) diff --git a/test/unit/metrics-recorder/distributed-trace.test.js b/test/unit/metrics-recorder/distributed-trace.test.js index b343a3e43e..0e6ea51478 100644 --- a/test/unit/metrics-recorder/distributed-trace.test.js +++ b/test/unit/metrics-recorder/distributed-trace.test.js @@ -12,7 +12,7 @@ const recordDistributedTrace = require('../../../lib/metrics/recorders/distribut const Transaction = require('../../../lib/transaction') const makeSegment = (opts) => { - const segment = opts.tx.trace.root.add('placeholder') + const segment = opts.tx.trace.add('placeholder') segment.setDurationInMillis(opts.duration) segment._setExclusiveDurationInMillis(opts.exclusive) diff --git a/test/unit/metrics-recorder/generic.test.js b/test/unit/metrics-recorder/generic.test.js index 35d9e5d687..42c91e9000 100644 --- a/test/unit/metrics-recorder/generic.test.js +++ b/test/unit/metrics-recorder/generic.test.js @@ -11,7 +11,7 @@ const recordGeneric = require('../../../lib/metrics/recorders/generic') const Transaction = require('../../../lib/transaction') function makeSegment(options) { - const segment = options.transaction.trace.root.add('placeholder') + const segment = options.transaction.trace.add('placeholder') segment.setDurationInMillis(options.duration) segment._setExclusiveDurationInMillis(options.exclusive) @@ -27,7 +27,7 @@ function record(options) { const transaction = options.transaction transaction.finalizeNameFromUri(options.url, options.code) - recordGeneric(segment, options.transaction.name) + recordGeneric(segment, options.transaction.name, options.transaction) } test('recordGeneric', async function (t) { @@ -50,7 +50,7 @@ test('recordGeneric', async function (t) { exclusive: 0 }) assert.doesNotThrow(function () { - recordGeneric(segment, undefined) + recordGeneric(segment, undefined, trans) }) }) @@ -61,7 +61,7 @@ test('recordGeneric', async function (t) { duration: 5, exclusive: 5 }) - recordGeneric(segment, undefined) + recordGeneric(segment, undefined, trans) const result = [[{ name: 'placeholder' }, [1, 0.005, 0.005, 0.005, 0.005, 0.000025]]] @@ -93,9 +93,9 @@ test('recordGeneric', async function (t) { await t.test('should report exclusive time correctly', function (t) { const { trans } = t.nr const root = trans.trace.root - const parent = root.add('Test/Parent', recordGeneric) - const child1 = parent.add('Test/Child/1', recordGeneric) - const child2 = parent.add('Test/Child/2', recordGeneric) + const parent = trans.trace.add('Test/Parent', recordGeneric) + const child1 = trans.trace.add('Test/Child/1', recordGeneric, parent) + const child2 = trans.trace.add('Test/Child/2', recordGeneric, parent) root.setDurationInMillis(30, 0) parent.setDurationInMillis(30, 0) diff --git a/test/unit/metrics-recorder/http-external.test.js b/test/unit/metrics-recorder/http-external.test.js index 8ccdcc52ee..d99ae9e3c4 100644 --- a/test/unit/metrics-recorder/http-external.test.js +++ b/test/unit/metrics-recorder/http-external.test.js @@ -10,12 +10,12 @@ const helper = require('../../lib/agent_helper') const generateRecorder = require('../../../lib/metrics/recorders/http_external') const Transaction = require('../../../lib/transaction') -function recordExternal(segment, scope) { - return generateRecorder('test.example.com', 'http')(segment, scope) +function recordExternal(segment, scope, transaction) { + return generateRecorder('test.example.com', 'http')(segment, scope, transaction) } function makeSegment(options) { - const segment = options.transaction.trace.root.add('placeholder') + const segment = options.transaction.trace.add('placeholder') segment.setDurationInMillis(options.duration) segment._setExclusiveDurationInMillis(options.exclusive) @@ -31,7 +31,7 @@ function record(options) { const transaction = options.transaction transaction.finalizeNameFromUri(options.url, options.code) - recordExternal(segment, options.transaction.name) + recordExternal(segment, options.transaction.name, options.transaction) } test('recordExternal', async function (t) { @@ -56,7 +56,7 @@ test('recordExternal', async function (t) { exclusive: 0 }) assert.doesNotThrow(function () { - recordExternal(segment, undefined) + recordExternal(segment, undefined, trans) }) }) @@ -67,7 +67,7 @@ test('recordExternal', async function (t) { duration: 0, exclusive: 0 }) - recordExternal(segment, undefined) + recordExternal(segment, undefined, trans) const result = [ [{ name: 'External/test.example.com/http' }, [1, 0, 0, 0, 0, 0]], @@ -108,9 +108,13 @@ test('recordExternal', async function (t) { await t.test('should report exclusive time correctly', function (t) { const { trans } = t.nr const root = trans.trace.root - const parent = root.add('/parent', recordExternal) - const child1 = parent.add('/child1', generateRecorder('api.twitter.com', 'https')) - const child2 = parent.add('/child2', generateRecorder('oauth.facebook.com', 'http')) + const parent = trans.trace.add('/parent', recordExternal) + const child1 = trans.trace.add('/child1', generateRecorder('api.twitter.com', 'https'), parent) + const child2 = trans.trace.add( + '/child2', + generateRecorder('oauth.facebook.com', 'http'), + parent + ) root.setDurationInMillis(32, 0) parent.setDurationInMillis(32, 0) diff --git a/test/unit/metrics-recorder/http.test.js b/test/unit/metrics-recorder/http.test.js index b9b988e485..6b245c481b 100644 --- a/test/unit/metrics-recorder/http.test.js +++ b/test/unit/metrics-recorder/http.test.js @@ -12,7 +12,7 @@ const recordWeb = require('../../../lib/metrics/recorders/http') const Transaction = require('../../../lib/transaction') function makeSegment(options) { - const segment = options.transaction.trace.root.add('placeholder') + const segment = options.transaction.trace.add('placeholder') segment.setDurationInMillis(options.duration) segment._setExclusiveDurationInMillis(options.exclusive) @@ -28,8 +28,8 @@ function record(options) { const transaction = options.transaction transaction.finalizeNameFromUri(options.url, options.code) - segment.markAsWeb(options.url) - recordWeb(segment, options.transaction.name) + segment.markAsWeb(transaction) + recordWeb(segment, options.transaction.name, options.transaction) } function beforeEach(ctx) { diff --git a/test/unit/metrics-recorder/queue-time-http.test.js b/test/unit/metrics-recorder/queue-time-http.test.js index adb9aa16e8..c4d007d4bb 100644 --- a/test/unit/metrics-recorder/queue-time-http.test.js +++ b/test/unit/metrics-recorder/queue-time-http.test.js @@ -12,7 +12,7 @@ const recordWeb = require('../../../lib/metrics/recorders/http') const Transaction = require('../../../lib/transaction') function makeSegment(options) { - const segment = options.transaction.trace.root.add('placeholder') + const segment = options.transaction.trace.add('placeholder') segment.setDurationInMillis(options.duration) segment._setExclusiveDurationInMillis(options.exclusive) @@ -29,8 +29,8 @@ function record(options) { transaction.finalizeNameFromUri(options.url, options.code) transaction.queueTime = options.queueTime - segment.markAsWeb(options.url) - recordWeb(segment, options.transaction.name) + segment.markAsWeb(transaction) + recordWeb(segment, options.transaction.name, options.transaction) } test('when recording queueTime', async (t) => { diff --git a/test/unit/shim/datastore-shim.test.js b/test/unit/shim/datastore-shim.test.js index 42388805a4..83c96766b8 100644 --- a/test/unit/shim/datastore-shim.test.js +++ b/test/unit/shim/datastore-shim.test.js @@ -33,9 +33,9 @@ test('DatastoreShim', async function (t) { return agent.tracer.getSegment() }, withNested: function () { + const tx = agent.tracer.getTransaction() const segment = agent.tracer.getSegment() - segment.add('ChildSegment') - + tx.trace.add('ChildSegment', null, segment) return segment } } @@ -323,11 +323,10 @@ test('DatastoreShim', async function (t) { name: 'getActiveSegment' }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'getActiveSegment') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -344,11 +343,10 @@ test('DatastoreShim', async function (t) { name: 'getActiveSegment' }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/operation/Cassandra/getActiveSegment') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -362,11 +360,10 @@ test('DatastoreShim', async function (t) { const { agent, shim, wrappable } = t.nr shim.recordOperation(wrappable, 'getActiveSegment', { name: 'getActiveSegment' }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/operation/Cassandra/getActiveSegment') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -383,10 +380,11 @@ test('DatastoreShim', async function (t) { const startingSegment = agent.tracer.getSegment() const segment = wrappable.withNested() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/operation/Cassandra/test') - assert.equal(segment.children.length, 1) - const [childSegment] = segment.children + + const children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 1) + const [childSegment] = children assert.equal(childSegment.name, 'ChildSegment') end() }) @@ -401,9 +399,9 @@ test('DatastoreShim', async function (t) { const startingSegment = agent.tracer.getSegment() const segment = wrappable.withNested() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/operation/Cassandra/test') - assert.equal(segment.children.length, 0) + const children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 0) end() }) }) @@ -646,11 +644,10 @@ test('DatastoreShim', async function (t) { }) ) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment(query) assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'getActiveSegment') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -666,11 +663,10 @@ test('DatastoreShim', async function (t) { new QuerySpec({ query: shim.FIRST, record: true }) ) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment(query) assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/statement/Cassandra/my_table/select') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -683,11 +679,10 @@ test('DatastoreShim', async function (t) { const { agent, shim, wrappable } = t.nr shim.recordQuery(wrappable, 'getActiveSegment', new QuerySpec({ query: shim.FIRST })) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment(query) assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/statement/Cassandra/my_table/select') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -781,7 +776,8 @@ test('DatastoreShim', async function (t) { helper.runInTransaction(agent, (tx) => { wrappable.bar() const rootSegment = agent.tracer.getSegment() - const attrs = rootSegment.children[0].getAttributes() + const [child] = tx.trace.getChildren(rootSegment.id) + const attrs = child.getAttributes() assert.equal( attrs['test-attr'], 'unit-test', @@ -841,11 +837,10 @@ test('DatastoreShim', async function (t) { const { agent, shim, wrappable } = t.nr shim.recordBatchQuery(wrappable, 'getActiveSegment', new QuerySpec({ query: shim.FIRST })) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment(query) assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'Datastore/statement/Cassandra/my_table/select/batch') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -927,7 +922,7 @@ test('DatastoreShim', async function (t) { await t.test('should create a new segment on the first call', function (t, end) { const { agent, shim, wrappable } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const args = [1, 2, wrappable.getActiveSegment] shim.bindRowCallbackSegment(args, shim.LAST) @@ -935,14 +930,15 @@ test('DatastoreShim', async function (t) { const segment = shim.getSegment() const cbSegment = args[2]() assert.notEqual(cbSegment, segment) - assert.ok(segment.children.includes(cbSegment)) + const children = tx.trace.getChildren(segment.id) + assert.ok(children.includes(cbSegment)) end() }) }) await t.test('should not create a new segment for calls after the first', function (t, end) { const { agent, shim, wrappable } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const args = [1, 2, wrappable.getActiveSegment] shim.bindRowCallbackSegment(args, shim.LAST) @@ -950,13 +946,15 @@ test('DatastoreShim', async function (t) { const segment = shim.getSegment() const cbSegment = args[2]() assert.notEqual(cbSegment, segment) - assert.ok(segment.children.includes(cbSegment)) - assert.equal(segment.children.length, 1) + let children = tx.trace.getChildren(segment.id) + assert.ok(children.includes(cbSegment)) + assert.equal(children.length, 1) // Call it a second time and see if we have the same segment. const cbSegment2 = args[2]() assert.equal(cbSegment2, cbSegment) - assert.equal(segment.children.length, 1) + children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 1) end() }) }) diff --git a/test/unit/shim/message-shim.test.js b/test/unit/shim/message-shim.test.js index 9e1120264c..cffc0687df 100644 --- a/test/unit/shim/message-shim.test.js +++ b/test/unit/shim/message-shim.test.js @@ -42,9 +42,9 @@ test('MessageShim', async function (t) { }, sendMessages: function () {}, withNested: function () { + const transaction = agent.tracer.getTransaction() const segment = agent.tracer.getSegment() - segment.add('ChildSegment') - + transaction.trace.add('ChildSegment', null, segment) return segment } } @@ -177,11 +177,10 @@ test('MessageShim', async function (t) { return new MessageSpec({ destinationName: 'foobar' }) }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Produce/Named/foobar') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -327,11 +326,11 @@ test('MessageShim', async function (t) { helper.runInTransaction(agent, (tx) => { const segment = wrappable.withNested() - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Produce/Named/foobar') - assert.equal(segment.children.length, 1) - const [childSegment] = segment.children + const children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 1) + const [childSegment] = children assert.equal(childSegment.name, 'ChildSegment') end() }) @@ -345,10 +344,10 @@ test('MessageShim', async function (t) { helper.runInTransaction(agent, (tx) => { const segment = wrappable.withNested() - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Produce/Named/foobar') - assert.equal(segment.children.length, 0) + const children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 0) end() }) }) @@ -497,11 +496,10 @@ test('MessageShim', async function (t) { return new MessageSpec({ destinationName: 'foobar' }) }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Consume/Named/foobar') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -526,11 +524,10 @@ test('MessageShim', async function (t) { destinationType: shim.EXCHANGE }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment('fizzbang') assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Consume/Named/fizzbang') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -625,11 +622,11 @@ test('MessageShim', async function (t) { helper.runInTransaction(agent, function (tx) { const segment = wrappable.withNested() - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Consume/Named/foobar') - assert.equal(segment.children.length, 1) - const [childSegment] = segment.children + const children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 1) + const [childSegment] = children assert.equal(childSegment.name, 'ChildSegment') end() }) @@ -643,9 +640,9 @@ test('MessageShim', async function (t) { helper.runInTransaction(agent, function (tx) { const segment = wrappable.withNested() - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Exchange/Consume/Named/foobar') - assert.equal(segment.children.length, 0) + const children = tx.trace.getChildren(segment.id) + assert.equal(children.length, 0) end() }) }) @@ -721,11 +718,10 @@ test('MessageShim', async function (t) { const { agent, shim, wrappable } = t.nr shim.recordPurgeQueue(wrappable, 'getActiveSegment', new MessageSpec({ queue: shim.FIRST })) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = agent.tracer.getSegment() const segment = wrappable.getActiveSegment('foobar') assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'MessageBroker/RabbitMQ/Queue/Purge/Named/foobar') assert.equal(agent.tracer.getSegment(), startingSegment) end() @@ -948,7 +944,8 @@ test('MessageShim', async function (t) { const parent = wrapped('my.queue', function consumer() { const segment = shim.getSegment() assert.notEqual(segment.name, 'Callback: consumer') - assert.equal(segment.transaction.type, 'message') + const transaction = shim.tracer.getTransaction() + assert.equal(transaction.type, 'message') end() }) @@ -958,7 +955,7 @@ test('MessageShim', async function (t) { await t.test('should end the transaction immediately if not handled', function (t, end) { const { shim, wrapped } = t.nr wrapped('my.queue', function consumer() { - const tx = shim.getSegment().transaction + const tx = shim.tracer.getTransaction() assert.equal(tx.isActive(), true) setTimeout(function () { assert.equal(tx.isActive(), false) @@ -974,7 +971,7 @@ test('MessageShim', async function (t) { } wrapped('my.queue', function consumer() { - const tx = shim.getSegment().transaction + const tx = shim.tracer.getTransaction() assert.equal(tx.isActive(), true) return new Promise(function (resolve) { @@ -1017,7 +1014,7 @@ test('MessageShim', async function (t) { const api = new API(agent) wrapped('my.queue', function consumer() { - const tx = shim.getSegment().transaction + const tx = shim.tracer.getTransaction() const handle = api.getTransaction() assert.equal(tx.isActive(), true) @@ -1045,8 +1042,7 @@ test('MessageShim', async function (t) { await t.test('should add agent attributes (e.g. routing key)', function (t, end) { const { shim, wrapped } = t.nr wrapped('my.queue', function consumer() { - const segment = shim.getSegment() - const tx = segment.transaction + const tx = shim.tracer.getTransaction() const traceParams = tx.trace.attributes.get(DESTINATIONS.TRANS_TRACE) assert.equal(traceParams['message.routingKey'], 'routing.key') @@ -1184,7 +1180,7 @@ test('MessageShim', async function (t) { } wrapped('my.queue', function consumer() { - const tx = shim.getSegment().transaction + const tx = shim.tracer.getTransaction() assert.equal(tx.incomingCatId, '9876#id') assert.equal(tx.referringTransactionGuid, 'trans id') @@ -1216,11 +1212,12 @@ test('MessageShim', async function (t) { await t.test('should bind the subscribe callback', function (t, end) { const { agent, shim, wrapped } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { + const { trace } = tx const parent = wrapped('my.queue', null, function subCb() { const segment = shim.getSegment() assert.equal(segment.name, 'Callback: subCb') - compareSegments(parent, [segment]) + compareSegments({ parent, segments: [segment], trace }) end() }) assert.ok(parent) @@ -1229,12 +1226,13 @@ test('MessageShim', async function (t) { await t.test('should still start a new transaction in the consumer', function (t, end) { const { agent, shim, wrapped } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const parent = wrapped('my.queue', function consumer() { + const childTx = shim.tracer.getTransaction() const segment = shim.getSegment() assert.notEqual(segment.name, 'Callback: consumer') - assert.ok(segment.transaction.id) - assert.notEqual(segment.transaction.id, parent.transaction.id) + assert.ok(childTx.id) + assert.notEqual(tx.id, childTx.id) end() }) assert.ok(parent) diff --git a/test/unit/shim/shim.test.js b/test/unit/shim/shim.test.js index 2d5bbf8171..d55fea157f 100644 --- a/test/unit/shim/shim.test.js +++ b/test/unit/shim/shim.test.js @@ -20,6 +20,8 @@ const { const promiseResolvers = require('../../lib/promise-resolvers') const { tspl } = require('@matteo.collina/tspl') const tempOverrideUncaught = require('../../lib/temp-override-uncaught') +const Transaction = require('../../../lib/transaction') +const TraceSegment = require('../../../lib/transaction/trace/segment') test('Shim', async function (t) { function beforeEach(ctx) { @@ -360,21 +362,12 @@ test('Shim', async function (t) { t.beforeEach(function (ctx) { beforeEach(ctx) - ctx.nr.segment = { - started: false, - touched: false, - probed: false, - start: function () { - this.started = true - }, - touch: function () { - this.touched = true - }, - probe: function () { - this.probed = true - } - } - + const transaction = new Transaction(ctx.nr.agent) + ctx.nr.segment = new TraceSegment({ + config: ctx.nr.agent.config, + name: 'test', + root: transaction.trace.root + }) ctx.nr.startingSegment = ctx.nr.tracer.getSegment() }) @@ -444,9 +437,9 @@ test('Shim', async function (t) { // no segment is passed in. To get around this we set the // active segment to an object known not to be null then do the // wrapping. - tracer.setSegment(segment) + tracer.setSegment({ segment }) const wrapped = shim.bindSegment(wrappable.getActiveSegment) - tracer.setSegment(startingSegment) + tracer.setSegment({ segment: startingSegment }) assert.equal(wrapped(), segment) assert.equal(tracer.getSegment(), startingSegment) @@ -457,8 +450,8 @@ test('Shim', async function (t) { shim.bindSegment(wrappable, 'getActiveSegment', segment) wrappable.getActiveSegment() - assert.equal(segment.started, false) - assert.equal(segment.touched, false) + assert.equal(segment.timer.state, 1) + assert.equal(segment.timer.touched, false) }) await t.test('should start and touch the segment if `full` is `true`', function (t) { @@ -466,13 +459,13 @@ test('Shim', async function (t) { shim.bindSegment(wrappable, 'getActiveSegment', segment, true) wrappable.getActiveSegment() - assert.equal(segment.started, true) - assert.equal(segment.touched, true) + assert.equal(segment.timer.state, 2) + assert.equal(segment.timer.touched, true) }) await t.test('should default to the current segment', function (t) { const { tracer, segment, shim, wrappable } = t.nr - tracer.setSegment(segment) + tracer.setSegment({ segment }) shim.bindSegment(wrappable, 'getActiveSegment') const activeSegment = wrappable.getActiveSegment() assert.equal(activeSegment, segment) @@ -882,13 +875,12 @@ test('Shim', async function (t) { return new RecorderSpec({ name: 'internal test segment', internal: true }) }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = tracer.getSegment() startingSegment.internal = true - startingSegment.shim = shim + startingSegment.shimId = shim.id const segment = wrappable.getActiveSegment() assert.equal(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'ROOT') assert.equal(tracer.getSegment(), startingSegment) end() @@ -944,7 +936,7 @@ test('Shim', async function (t) { 'should call after hook on record when function is done executing', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { function testAfter() { return 'result' } @@ -953,13 +945,14 @@ test('Shim', async function (t) { name: 'test segment', callback: shim.LAST, after(args) { - assert.equal(Object.keys(args).length, 6, 'should have 6 args to after hook') - const { fn, name, error, result, segment } = args + assert.equal(Object.keys(args).length, 7, 'should have 7 args to after hook') + const { fn, name, error, result, segment, transaction } = args assert.equal(segment.name, 'test segment') assert.equal(error, undefined) assert.deepEqual(fn, testAfter) assert.equal(name, testAfter.name) assert.equal(result, 'result') + assert.equal(tx.id, transaction.id) } }) }) @@ -976,7 +969,7 @@ test('Shim', async function (t) { function (t, end) { const { agent, shim } = t.nr const err = new Error('test err') - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { function testAfter() { throw err } @@ -985,13 +978,14 @@ test('Shim', async function (t) { name: 'test segment', callback: shim.LAST, after(args) { - assert.equal(Object.keys(args).length, 6, 'should have 6 args to after hook') - const { fn, name, error, result, segment } = args + assert.equal(Object.keys(args).length, 7, 'should have 7 args to after hook') + const { fn, name, error, result, segment, transaction } = args assert.equal(segment.name, 'test segment') assert.deepEqual(error, err) assert.equal(result, undefined) assert.deepEqual(fn, testAfter) assert.equal(name, testAfter.name) + assert.equal(tx.id, transaction.id) } }) }) @@ -1117,17 +1111,19 @@ test('Shim', async function (t) { return new RecorderSpec({ name: 'test segment', stream: 'foobar' }) }) - helper.runInTransaction(agent, function () { - const ret = wrapped() - assert.equal(ret, stream) - }) - stream.on('foobar', function () { const emitSegment = shim.getSegment() - assert.equal(emitSegment.parent, stream.segment) + const tx = agent.tracer.getTransaction() + const children = tx.trace.getChildren(stream.segment.id) + assert.ok(children.includes(emitSegment)) end() }) - stream.emit('foobar') + + helper.runInTransaction(agent, function () { + const ret = wrapped() + assert.equal(ret, stream) + stream.emit('foobar') + }) }) await t.test('should create an event segment if an event name is given', function (t) { @@ -1136,31 +1132,34 @@ test('Shim', async function (t) { return new RecorderSpec({ name: 'test segment', stream: 'foobar' }) }) - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const ret = wrapped() assert.equal(ret, stream) + // Emit the event and check the segment name. + let children = tx.trace.getChildren(stream.segment.id) + assert.equal(children.length, 0) + stream.emit('foobar') + children = tx.trace.getChildren(stream.segment.id) + assert.equal(children.length, 1) + + const [eventSegment] = children + assert.match(eventSegment.name, /Event callback: foobar/) + assert.equal(eventSegment.getAttributes().count, 1) + + // Emit it again and see if the name updated. + stream.emit('foobar') + children = tx.trace.getChildren(stream.segment.id) + assert.equal(children.length, 1) + assert.equal(children[0], eventSegment) + assert.equal(eventSegment.getAttributes().count, 2) + + // Emit it once more and see if the name updated again. + stream.emit('foobar') + children = tx.trace.getChildren(stream.segment.id) + assert.equal(children.length, 1) + assert.equal(children[0], eventSegment) + assert.equal(eventSegment.getAttributes().count, 3) }) - - // Emit the event and check the segment name. - assert.equal(stream.segment.children.length, 0) - stream.emit('foobar') - assert.equal(stream.segment.children.length, 1) - - const [eventSegment] = stream.segment.children - assert.match(eventSegment.name, /Event callback: foobar/) - assert.equal(eventSegment.getAttributes().count, 1) - - // Emit it again and see if the name updated. - stream.emit('foobar') - assert.equal(stream.segment.children.length, 1) - assert.equal(stream.segment.children[0], eventSegment) - assert.equal(eventSegment.getAttributes().count, 2) - - // Emit it once more and see if the name updated again. - stream.emit('foobar') - assert.equal(stream.segment.children.length, 1) - assert.equal(stream.segment.children[0], eventSegment) - assert.equal(eventSegment.getAttributes().count, 3) }) }) @@ -1329,7 +1328,7 @@ test('Shim', async function (t) { name: segmentName, promise: true, after(args) { - plan.equal(Object.keys(args).length, 6, 'should have 6 args to after hook') + plan.equal(Object.keys(args).length, 7, 'should have 7 args to after hook') const { fn, name, error, result, segment } = args plan.deepEqual(fn, toWrap) plan.equal(name, toWrap.name) @@ -1362,7 +1361,7 @@ test('Shim', async function (t) { name: segmentName, promise: true, after(args) { - plan.equal(Object.keys(args).length, 5, 'should have 6 args to after hook') + plan.equal(Object.keys(args).length, 6, 'should have 6 args to after hook') const { fn, name, error, segment } = args plan.deepEqual(fn, toWrap) plan.equal(name, toWrap.name) @@ -1457,11 +1456,10 @@ test('Shim', async function (t) { return new RecorderSpec({ name: 'test segment' }) }) - helper.runInTransaction(agent, function (tx) { + helper.runInTransaction(agent, function () { const startingSegment = tracer.getSegment() const segment = wrappable.getActiveSegment() assert.notEqual(segment, startingSegment) - assert.equal(segment.transaction, tx) assert.equal(segment.name, 'test segment') assert.equal(tracer.getSegment(), startingSegment) end() @@ -1571,12 +1569,14 @@ test('Shim', async function (t) { }) }) - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const parentSegment = shim.getSegment() const resultingSegment = wrapped(cb) assert.notEqual(resultingSegment, parentSegment) - assert.ok(parentSegment.children.includes(resultingSegment)) + + const children = tx.trace.getChildren(parentSegment.id) + assert.ok(children.includes(resultingSegment)) end() }) }) @@ -1597,12 +1597,13 @@ test('Shim', async function (t) { }) }) - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const parentSegment = shim.getSegment() const resultingSegment = wrapped() assert.equal(resultingSegment, parentSegment) - assert.ok(!parentSegment.children.includes(resultingSegment)) + const children = tx.trace.getChildren(parentSegment.id) + assert.ok(!children.includes(resultingSegment)) end() }) }) @@ -1896,7 +1897,12 @@ test('Shim', async function (t) { await t.test('#getSegment', async function (t) { t.beforeEach(function (ctx) { beforeEach(ctx) - ctx.nr.segment = { probe: function () {} } + const transaction = new Transaction(ctx.nr.agent) + ctx.nr.segment = new TraceSegment({ + config: ctx.nr.agent.config, + name: 'test', + root: transaction.trace.root + }) }) t.afterEach(afterEach) @@ -1908,7 +1914,7 @@ test('Shim', async function (t) { await t.test('should return the current segment if the function is not bound', function (t) { const { tracer, segment, shim } = t.nr - tracer.setSegment(segment) + tracer.setSegment({ segment }) assert.equal( shim.getSegment(function () {}), segment @@ -1917,7 +1923,7 @@ test('Shim', async function (t) { await t.test('should return the current segment if no object is provided', function (t) { const { tracer, segment, shim } = t.nr - tracer.setSegment(segment) + tracer.setSegment({ segment }) assert.equal(shim.getSegment(), segment) }) }) @@ -1925,22 +1931,21 @@ test('Shim', async function (t) { await t.test('#getActiveSegment', async function (t) { t.beforeEach(function (ctx) { beforeEach(ctx) - ctx.nr.segment = { - probe: function () {}, - transaction: { - active: true, - isActive: function () { - return this.active - } - } - } + const transaction = new Transaction(ctx.nr.agent) + ctx.nr.segment = new TraceSegment({ + config: ctx.nr.agent.config, + name: 'test', + root: transaction.trace.root + }) + ctx.nr.transaction = transaction }) t.afterEach(afterEach) await t.test( 'should return the segment a function is bound to when transaction is active', function (t) { - const { segment, shim } = t.nr + const { segment, shim, transaction, tracer } = t.nr + tracer.setSegment({ transaction }) const bound = shim.bindSegment(function () {}, segment) assert.equal(shim.getActiveSegment(bound), segment) } @@ -1949,8 +1954,8 @@ test('Shim', async function (t) { await t.test( 'should return the current segment if the function is not bound when transaction is active', function (t) { - const { tracer, segment, shim } = t.nr - tracer.setSegment(segment) + const { segment, shim, tracer, transaction } = t.nr + tracer.setSegment({ segment, transaction }) assert.equal( shim.getActiveSegment(function () {}), segment @@ -1961,8 +1966,8 @@ test('Shim', async function (t) { await t.test( 'should return the current segment if no object is provided when transaction is active', function (t) { - const { tracer, segment, shim } = t.nr - tracer.setSegment(segment) + const { segment, shim, tracer, transaction } = t.nr + tracer.setSegment({ segment, transaction }) assert.equal(shim.getActiveSegment(), segment) } ) @@ -1970,9 +1975,10 @@ test('Shim', async function (t) { await t.test( 'should return null for a bound function when transaction is not active', function (t) { - const { segment, shim } = t.nr - segment.transaction.active = false - const bound = shim.bindSegment(function () {}, segment) + const { segment, shim, transaction, tracer } = t.nr + transaction.timer.state = 3 + tracer.setSegment({ transaction }) + const bound = shim.bindSegment(function () {}, { segment }) assert.equal(shim.getActiveSegment(bound), null) } ) @@ -1980,9 +1986,9 @@ test('Shim', async function (t) { await t.test( 'should return null if the function is not bound when transaction is not active', function (t) { - const { tracer, segment, shim } = t.nr - segment.transaction.active = false - tracer.setSegment(segment) + const { tracer, segment, shim, transaction } = t.nr + transaction.timer.state = 3 + tracer.setSegment({ segment, transaction }) assert.equal( shim.getActiveSegment(function () {}), null @@ -1993,9 +1999,9 @@ test('Shim', async function (t) { await t.test( 'should return null if no object is provided when transaction is not active', function (t) { - const { tracer, segment, shim } = t.nr - segment.transaction.active = false - tracer.setSegment(segment) + const { tracer, segment, shim, transaction } = t.nr + transaction.timer.state = 3 + tracer.setSegment({ segment, transaction }) assert.equal(shim.getActiveSegment(), null) } ) @@ -2015,7 +2021,7 @@ test('Shim', async function (t) { await t.test('should default to the current segment', function (t) { const { tracer, shim, wrappable } = t.nr const segment = { probe: function () {} } - tracer.setSegment(segment) + tracer.setSegment({ segment }) shim.storeSegment(wrappable) assert.equal(shim.getSegment(wrappable), segment) }) @@ -2104,31 +2110,31 @@ test('Shim', async function (t) { await t.test('should create a new segment', function (t, end) { const { agent, shim, wrappable } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const args = [wrappable.getActiveSegment] const segment = wrappable.getActiveSegment() - const parent = shim.createSegment('test segment') + const parent = shim.createSegment('test-segment') shim.bindCallbackSegment({}, args, shim.LAST, parent) const cbSegment = args[0]() assert.notEqual(cbSegment, segment) assert.notEqual(cbSegment, parent) - compareSegments(parent, [cbSegment]) + compareSegments({ parent, segments: [cbSegment], trace: tx.trace }) end() }) }) await t.test('should make the `parentSegment` translucent after running', function (t, end) { const { agent, shim, wrappable } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const args = [wrappable.getActiveSegment] - const parent = shim.createSegment('test segment') + const parent = shim.createSegment({ name: 'test segment', parent: tx.trace.root }) parent.opaque = true shim.bindCallbackSegment({}, args, shim.LAST, parent) const cbSegment = args[0]() assert.notEqual(cbSegment, parent) - compareSegments(parent, [cbSegment]) + compareSegments({ parent, segments: [cbSegment], trace: tx.trace }) assert.equal(parent.opaque, false) end() }) @@ -2136,14 +2142,14 @@ test('Shim', async function (t) { await t.test('should default the `parentSegment` to the current one', function (t, end) { const { agent, shim, wrappable } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const args = [wrappable.getActiveSegment] const segment = wrappable.getActiveSegment() shim.bindCallbackSegment({}, args, shim.LAST) const cbSegment = args[0]() assert.notEqual(cbSegment, segment) - compareSegments(segment, [cbSegment]) + compareSegments({ parent: segment, segments: [cbSegment], trace: tx.trace }) end() }) }) @@ -2156,14 +2162,14 @@ test('Shim', async function (t) { executed = true } } - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const args = [wrappable.getActiveSegment] const segment = wrappable.getActiveSegment() shim.bindCallbackSegment(spec, args, shim.LAST) const cbSegment = args[0]() assert.notEqual(cbSegment, segment) - compareSegments(segment, [cbSegment]) + compareSegments({ parent: segment, segments: [cbSegment], trace: tx.trace }) assert.equal(executed, true) end() }) @@ -2173,20 +2179,12 @@ test('Shim', async function (t) { await t.test('#applySegment', async function (t) { t.beforeEach(function (ctx) { beforeEach(ctx) - ctx.nr.segment = { - name: 'segment', - started: false, - touched: false, - start: function () { - this.started = true - }, - touch: function () { - this.touched = true - }, - probe: function () { - this.probed = true - } - } + const transaction = new Transaction(ctx.nr.agent) + ctx.nr.segment = new TraceSegment({ + config: ctx.nr.agent.config, + name: 'test', + root: transaction.trace.root + }) }) t.afterEach(afterEach) @@ -2234,25 +2232,25 @@ test('Shim', async function (t) { await t.test('should make the segment active for the duration of execution', function (t) { const { tracer, segment, shim, wrappable } = t.nr const prevSegment = { name: 'prevSegment', probe: function () {} } - tracer.setSegment(prevSegment) + tracer.setSegment({ segment: prevSegment }) const activeSegment = shim.applySegment(wrappable.getActiveSegment, segment) assert.equal(tracer.getSegment(), prevSegment) assert.equal(activeSegment, segment) - assert.equal(segment.touched, false) - assert.equal(segment.started, false) + assert.equal(segment.timer.touched, false) + assert.equal(segment.timer.state, 1) }) await t.test('should start and touch the segment if `full` is `true`', function (t) { const { segment, shim, wrappable } = t.nr shim.applySegment(wrappable.getActiveSegment, segment, true) - assert.equal(segment.touched, true) - assert.equal(segment.started, true) + assert.equal(segment.timer.touched, true) + assert.equal(segment.timer.state, 2) }) await t.test('should not change the active segment if `segment` is `null`', function (t) { const { tracer, segment, shim, wrappable } = t.nr - tracer.setSegment(segment) + tracer.setSegment({ segment }) let activeSegment = null assert.doesNotThrow(function () { activeSegment = shim.applySegment(wrappable.getActiveSegment, null) @@ -2298,7 +2296,7 @@ test('Shim', async function (t) { throw new Error('test error') } const prevSegment = { name: 'prevSegment', probe: function () {} } - tracer.setSegment(prevSegment) + tracer.setSegment({ segment: prevSegment }) assert.throws(function () { shim.applySegment(func, segment) @@ -2318,7 +2316,7 @@ test('Shim', async function (t) { shim.applySegment(func, segment, true) }, 'Error: test error') - assert.equal(segment.touched, true) + assert.equal(segment.timer.touched, true) } ) }) @@ -2328,8 +2326,8 @@ test('Shim', async function (t) { t.afterEach(afterEach) await t.test('should create a segment with the correct name', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { - const segment = shim.createSegment('foobar') + helper.runInTransaction(agent, function (tx) { + const segment = shim.createSegment({ name: 'foobar', parent: tx.trace.root }) assert.equal(segment.name, 'foobar') end() }) @@ -2337,42 +2335,43 @@ test('Shim', async function (t) { await t.test('should allow `recorder` to be omitted', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { - const parent = shim.createSegment('parent') + helper.runInTransaction(agent, function (tx) { + const parent = shim.createSegment({ name: 'parent', parent: tx.trace.root }) const child = shim.createSegment('child', parent) assert.equal(child.name, 'child') - compareSegments(parent, [child]) + compareSegments({ parent, segments: [child], trace: tx.trace }) end() }) }) await t.test('should allow `recorder` to be null', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { - const parent = shim.createSegment('parent') + helper.runInTransaction(agent, function (tx) { + const parent = shim.createSegment('parent', tx.trace.root) const child = shim.createSegment('child', null, parent) assert.equal(child.name, 'child') - compareSegments(parent, [child]) + compareSegments({ parent, segments: [child], trace: tx.trace }) end() }) }) await t.test('should not create children for opaque segments', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { - const parent = shim.createSegment('parent') + helper.runInTransaction(agent, function (tx) { + const parent = shim.createSegment('parent', tx.trace.root) parent.opaque = true const child = shim.createSegment('child', parent) assert.equal(child.name, 'parent') - assert.deepEqual(parent.children, []) + const children = tx.trace.getChildren(parent.id) + assert.deepEqual(children, []) end() }) }) await t.test('should not modify returned parent for opaque segments', (t, end) => { const { agent, shim } = t.nr - helper.runInTransaction(agent, () => { - const parent = shim.createSegment('parent') + helper.runInTransaction(agent, (tx) => { + const parent = shim.createSegment('parent', tx.trace.root) parent.opaque = true parent.internal = true @@ -2387,24 +2386,22 @@ test('Shim', async function (t) { await t.test('should default to the current segment as the parent', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const parent = shim.getSegment() - const child = shim.createSegment('child') - compareSegments(parent, [child]) + const child = shim.createSegment('child', parent) + compareSegments({ parent, segments: [child], trace: tx.trace }) end() }) }) await t.test('should not modify returned parent for opaque segments', (t, end) => { const { agent, shim } = t.nr - helper.runInTransaction(agent, () => { - const parent = shim.createSegment('parent') + helper.runInTransaction(agent, (tx) => { + const parent = shim.createSegment({ name: 'parent', parent: tx.trace.root }) parent.opaque = true parent.internal = true - shim.setActiveSegment(parent) - - const child = shim.createSegment('child') + const child = shim.createSegment('child', parent) assert.equal(child, parent) assert.equal(parent.opaque, true) @@ -2415,11 +2412,11 @@ test('Shim', async function (t) { await t.test('should work with all parameters in an object', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { - const parent = shim.createSegment('parent') + helper.runInTransaction(agent, function (tx) { + const parent = shim.createSegment('parent', tx.trace.root) const child = shim.createSegment({ name: 'child', parent }) assert.equal(child.name, 'child') - compareSegments(parent, [child]) + compareSegments({ parent, segments: [child], trace: tx.trace }) end() }) }) @@ -2441,8 +2438,8 @@ test('Shim', async function (t) { agent.config.attributes.exclude = ['ignore_me', 'host', 'port_path_or_id', 'database_name'] agent.config.emit('attributes.exclude') agent.config.attributes.enabled = true - helper.runInTransaction(agent, function () { - ctx.nr.segment = shim.createSegment({ name: 'child', parameters }) + helper.runInTransaction(agent, function (tx) { + ctx.nr.segment = shim.createSegment({ name: 'child', parameters, parent: tx.trace.root }) }) ctx.nr.parameters = parameters }) @@ -2480,8 +2477,8 @@ test('Shim', async function (t) { const { agent, parameters, shim } = t.nr let segment agent.config.attributes.enabled = false - helper.runInTransaction(agent, function () { - segment = shim.createSegment({ name: 'child', parameters }) + helper.runInTransaction(agent, function (tx) { + segment = shim.createSegment({ name: 'child', parameters, parent: tx.trace.root }) }) assert.ok(segment.attributes) const attributes = segment.getAttributes() diff --git a/test/unit/shim/transaction-shim.test.js b/test/unit/shim/transaction-shim.test.js index 66cabc5d61..0521262fe4 100644 --- a/test/unit/shim/transaction-shim.test.js +++ b/test/unit/shim/transaction-shim.test.js @@ -63,8 +63,8 @@ test('TransactionShim', async function (t) { return 'fiz' }, anony: function () {}, - getActiveSegment: function () { - return agent.tracer.getSegment() + getActiveTransaction: function () { + return agent.tracer.getTransaction() } } @@ -202,20 +202,20 @@ test('TransactionShim', async function (t) { const { shim, wrappable } = t.nr shim.bindCreateTransaction( wrappable, - 'getActiveSegment', + 'getActiveTransaction', new TransactionSpec({ type: shim.WEB }) ) - const segment = wrappable.getActiveSegment() - assert.equal(segment.transaction.type, shim.WEB) + const tx = wrappable.getActiveTransaction() + assert.equal(tx.type, shim.WEB) - shim.unwrap(wrappable, 'getActiveSegment') + shim.unwrap(wrappable, 'getActiveTransaction') shim.bindCreateTransaction( wrappable, - 'getActiveSegment', + 'getActiveTransaction', new TransactionSpec({ type: shim.BG }) ) - const bgSegment = wrappable.getActiveSegment() - assert.equal(bgSegment.transaction.type, shim.BG) + const bgTx = wrappable.getActiveTransaction() + assert.equal(bgTx.type, shim.BG) }) await t.test('should not create a nested transaction when `spec.nest` is false', function (t) { @@ -226,11 +226,11 @@ test('TransactionShim', async function (t) { let bgCalled = false const bg = shim.bindCreateTransaction(function () { bgCalled = true - bgTx = shim.getSegment().transaction + bgTx = shim.tracer.getTransaction() }, new TransactionSpec({ type: shim.BG })) const web = shim.bindCreateTransaction(function () { webCalled = true - webTx = shim.getSegment().transaction + webTx = shim.tracer.getTransaction() bg() }, new TransactionSpec({ type: shim.WEB })) @@ -266,14 +266,14 @@ test('TransactionShim', async function (t) { const { shim } = ctx.nr ctx.nr.transactions = [] ctx.nr.web = shim.bindCreateTransaction(function (cb) { - ctx.nr.transactions.push(shim.getSegment().transaction) + ctx.nr.transactions.push(shim.tracer.getTransaction()) if (cb) { cb() } }, new TransactionSpec({ type: shim.WEB, nest: true })) ctx.nr.bg = shim.bindCreateTransaction(function (cb) { - ctx.nr.transactions.push(shim.getSegment().transaction) + ctx.nr.transactions.push(shim.tracer.getTransaction()) if (cb) { cb() } @@ -441,7 +441,7 @@ test('TransactionShim', async function (t) { assert.ok(!segment.catTransaction) assert.ok(!segment.getAttributes().transaction_guid) - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) assert.ok(!tx.incomingCatId) assert.ok(!tx.referringTransactionGuid) @@ -465,7 +465,7 @@ test('TransactionShim', async function (t) { assert.ok(!segment.catTransaction) assert.ok(!segment.getAttributes().transaction_guid) - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) assert.ok(!tx.incomingCatId) assert.ok(!tx.referringTransactionGuid) @@ -488,7 +488,7 @@ test('TransactionShim', async function (t) { assert.ok(!segment.getAttributes().transaction_guid) assert.doesNotThrow(function () { - shim.handleMqTracingHeaders(null, segment) + shim.handleMqTracingHeaders(null, segment, null, tx) }) assert.ok(!tx.incomingCatId) @@ -516,7 +516,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, shim.BG, function (tx2) { assert.notEqual(tx2, tx) - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) }) assert.equal(tx.incomingCatId, '9876#id') @@ -568,7 +568,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, shim.BG, function (tx2) { assert.notEqual(tx2, tx) - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) }) assert.equal(tx.incomingCatId, '9876#id') @@ -615,7 +615,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, function (tx) { const headers = { traceparent } const segment = shim.getSegment() - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) const outboundHeaders = {} tx.insertDistributedTraceHeaders(outboundHeaders) @@ -638,7 +638,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, function (tx) { const headers = { traceparent, tracestate } const segment = shim.getSegment() - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) const outboundHeaders = {} tx.insertDistributedTraceHeaders(outboundHeaders) @@ -660,7 +660,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, function (tx) { const headers = { traceparent, tracestate } const segment = shim.getSegment() - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx) const outboundHeaders = {} tx.insertDistributedTraceHeaders(outboundHeaders) @@ -687,7 +687,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, shim.BG, function (tx2) { assert.notEqual(tx2, tx) - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx2) }) assert.equal(segment.catId, '6789#app') @@ -702,7 +702,7 @@ test('TransactionShim', async function (t) { 'should attach the CAT info to current segment if not provided - DT disabled, app data is provided', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const headers = createCATHeaders(agent.config) const segment = shim.getSegment() delete headers['X-NewRelic-Id'] @@ -712,7 +712,7 @@ test('TransactionShim', async function (t) { assert.ok(!segment.catTransaction) assert.ok(!segment.getAttributes().transaction_guid) - shim.handleMqTracingHeaders(headers) + shim.handleMqTracingHeaders(headers, null, null, tx) assert.equal(segment.catId, '6789#app') assert.equal(segment.catTransaction, 'app data transaction name') @@ -738,7 +738,7 @@ test('TransactionShim', async function (t) { helper.runInTransaction(agent, shim.BG, function (tx2) { assert.notEqual(tx2, tx) - shim.handleMqTracingHeaders(headers, segment) + shim.handleMqTracingHeaders(headers, segment, null, tx2) }) assert.equal(segment.catId, '6789#app') @@ -753,7 +753,7 @@ test('TransactionShim', async function (t) { 'should not attach any CAT data to the segment, app data is for an untrusted application', function (t, end) { const { agent, shim } = t.nr - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { const headers = createCATHeaders(agent.config) const segment = shim.getSegment() delete headers['X-NewRelic-Id'] @@ -764,7 +764,7 @@ test('TransactionShim', async function (t) { assert.ok(!segment.catTransaction) assert.ok(!segment.getAttributes().transaction_guid) - shim.handleMqTracingHeaders(headers) + shim.handleMqTracingHeaders(headers, null, null, tx) assert.ok(!segment.catId) assert.ok(!segment.catTransaction) diff --git a/test/unit/shim/webframework-shim.test.js b/test/unit/shim/webframework-shim.test.js index 39163be032..c92f4532e3 100644 --- a/test/unit/shim/webframework-shim.test.js +++ b/test/unit/shim/webframework-shim.test.js @@ -625,31 +625,6 @@ test('WebFrameworkShim', async function (t) { } }) - await t.test('should reinstate its own context', function (t, end) { - const { agent, req, shim, txInfo, wrappable } = t.nr - testType(shim.MIDDLEWARE, 'Nodejs/Middleware/Restify/getActiveSegment') - - function testType(type, expectedName) { - const wrapped = shim.recordMiddleware( - wrappable.getActiveSegment, - new MiddlewareSpec({ - type, - route: '' - }) - ) - const tx = helper.runInTransaction(agent, function (_tx) { - return _tx - }) - txInfo.transaction = tx - txInfo.segmentStack.push(tx.trace.root) - - const segment = wrapped(req) - - assert.equal(segment.name, expectedName) - end() - } - }) - await t.test('should capture route parameters when high_security is off', function (t, end) { const { agent, req, shim, txInfo, wrappable } = t.nr agent.config.high_security = false diff --git a/test/unit/shimmer.test.js b/test/unit/shimmer.test.js index 6892039777..87169fb519 100644 --- a/test/unit/shimmer.test.js +++ b/test/unit/shimmer.test.js @@ -440,13 +440,14 @@ test('shimmer', async function (t) { transactions[i] = current ids[i] = current.id + const ctx = agent.tracer.getContext() process.nextTick( agent.tracer.bindFunction(function bindFunctionCb() { const lookup = agent.getTransaction() plan.equal(lookup, current) synchronizer.emit('inner', lookup, i) - }) + }, ctx) ) }) wrapped() @@ -479,13 +480,14 @@ test('shimmer', async function (t) { transactions[i] = current ids[i] = current.id + const ctx = agent.tracer.getContext() setTimeout( agent.tracer.bindFunction(function bindFunctionCb() { const lookup = agent.getTransaction() plan.equal(lookup, current) synchronizer.emit('inner', lookup, i) - }), + }, ctx), 1 ) }) @@ -524,6 +526,7 @@ test('shimmer', async function (t) { transactions[j] = current ids[j] = id + const ctx = agent.tracer.getContext() eventer.on( name, agent.tracer.bindFunction(function bindFunctionCb() { @@ -532,7 +535,7 @@ test('shimmer', async function (t) { plan.equal(lookup.id, id) eventer.emit('inner', lookup, j) - }) + }, ctx) ) eventer.emit(name) @@ -604,11 +607,12 @@ test('shimmer', async function (t) { verify(j, 'createTicker', current) + const ctx = agent.tracer.getContext() process.nextTick( agent.tracer.bindFunction(function bindFunctionCb() { verify(j, 'nextTick', current) createTimer(current, j) - }) + }, ctx) ) }) } diff --git a/test/unit/spans/span-event-aggregator.test.js b/test/unit/spans/span-event-aggregator.test.js index 1055e90de4..574ad56203 100644 --- a/test/unit/spans/span-event-aggregator.test.js +++ b/test/unit/spans/span-event-aggregator.test.js @@ -60,7 +60,7 @@ test('SpanAggregator', async (t) => { assert.equal(spanEventAggregator.length, 0) - spanEventAggregator.addSegment(segment, 'p') + spanEventAggregator.addSegment({ segment, transaction: tx, parentId: 'p' }) assert.equal(spanEventAggregator.length, 1) const event = spanEventAggregator.getEvents()[0] @@ -84,7 +84,7 @@ test('SpanAggregator', async (t) => { const segment = agent.tracer.getSegment() assert.equal(spanEventAggregator.length, 0) - spanEventAggregator.addSegment(segment) + spanEventAggregator.addSegment({ segment, transaction: tx }) assert.equal(spanEventAggregator.length, 1) const event = spanEventAggregator.getEvents()[0] @@ -134,20 +134,20 @@ test('SpanAggregator', async (t) => { assert.equal(spanEventAggregator.seen, 0) // First segment is added regardless of priority. - assert.equal(spanEventAggregator.addSegment(segment), true) + assert.equal(spanEventAggregator.addSegment({ segment, transaction: tx }), true) assert.equal(spanEventAggregator.length, 1) assert.equal(spanEventAggregator.seen, 1) // Higher priority should be added. tx.priority = 100 - assert.equal(spanEventAggregator.addSegment(segment), true) + assert.equal(spanEventAggregator.addSegment({ segment, transaction: tx }), true) assert.equal(spanEventAggregator.length, 1) assert.equal(spanEventAggregator.seen, 2) const event1 = spanEventAggregator.getEvents()[0] // Lower priority should not be added. tx.priority = 1 - assert.equal(spanEventAggregator.addSegment(segment), false) + assert.equal(spanEventAggregator.addSegment({ segment, transaction: tx }), false) assert.equal(spanEventAggregator.length, 1) assert.equal(spanEventAggregator.seen, 3) const event2 = spanEventAggregator.getEvents()[0] @@ -173,7 +173,7 @@ test('SpanAggregator', async (t) => { setTimeout(() => { const segment = agent.tracer.getSegment() - spanEventAggregator.addSegment(segment) + spanEventAggregator.addSegment({ segment, transaction: tx }) const payload = spanEventAggregator._toPayloadSync() diff --git a/test/unit/spans/span-event.test.js b/test/unit/spans/span-event.test.js index 2629bf42e8..be81e50fe8 100644 --- a/test/unit/spans/span-event.test.js +++ b/test/unit/spans/span-event.test.js @@ -62,7 +62,8 @@ test('fromSegment()', async (t) => { transaction.priority = 42 setTimeout(() => { - const segment = agent.tracer.getTransaction().trace.root.children[0] + const tx = agent.tracer.getTransaction() + const [segment] = tx.trace.getChildren(tx.trace.root.id) segment.addSpanAttribute('SpiderSpan', 'web') segment.addSpanAttribute('host', 'my-host') segment.addSpanAttribute('port', 222) @@ -70,7 +71,7 @@ test('fromSegment()', async (t) => { const spanContext = segment.getSpanContext() spanContext.addCustomAttribute('Span Lee', 'no prize') - const span = SpanEvent.fromSegment(segment, 'parent') + const span = SpanEvent.fromSegment(segment, transaction, 'parent') // Should have all the normal properties. assert.ok(span) @@ -135,8 +136,9 @@ test('fromSegment()', async (t) => { https.get('https://example.com?foo=bar', (res) => { res.resume() res.on('end', () => { - const segment = agent.tracer.getTransaction().trace.root.children[0] - const span = SpanEvent.fromSegment(segment, 'parent') + const tx = agent.tracer.getTransaction() + const [segment] = tx.trace.getChildren(tx.trace.root.id) + const span = SpanEvent.fromSegment(segment, transaction, 'parent') // Should have all the normal properties. assert.ok(span) @@ -237,8 +239,8 @@ test('fromSegment()', async (t) => { dsConn.myDbOp(longQuery, () => { transaction.end() - const segment = transaction.trace.root.children[0] - const span = SpanEvent.fromSegment(segment, 'parent') + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) + const span = SpanEvent.fromSegment(segment, transaction, 'parent') // Should have all the normal properties. assert.ok(span) @@ -303,7 +305,7 @@ test('fromSegment()', async (t) => { setTimeout(() => { const segment = agent.tracer.getSegment() - const span = SpanEvent.fromSegment(segment, 'parent') + const span = SpanEvent.fromSegment(segment, transaction, 'parent') const serializedSpan = span.toJSON() const [intrinsics] = serializedSpan @@ -336,7 +338,7 @@ test('fromSegment()', async (t) => { spanContext.addIntrinsicAttribute('intrinsic.1', 1) spanContext.addIntrinsicAttribute('intrinsic.2', 2) - const span = SpanEvent.fromSegment(segment, 'parent') + const span = SpanEvent.fromSegment(segment, transaction, 'parent') const serializedSpan = span.toJSON() const [intrinsics] = serializedSpan @@ -357,10 +359,10 @@ test('fromSegment()', async (t) => { res.resume() res.on('end', () => { - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) assert.ok(segment.name.startsWith('Truncated')) - const span = SpanEvent.fromSegment(segment) + const span = SpanEvent.fromSegment(segment, transaction) assert.ok(span) assert.ok(span instanceof SpanEvent) assert.ok(span instanceof SpanEvent.HttpSpanEvent) @@ -374,12 +376,12 @@ test('fromSegment()', async (t) => { await t.test('should handle truncated datastore spans', (t, end) => { const { agent } = t.nr helper.runInTransaction(agent, (transaction) => { - const segment = transaction.trace.root.add('Datastore/operation/something') + const segment = transaction.trace.add('Datastore/operation/something') transaction.end() // end before segment to trigger truncate assert.ok(segment.name.startsWith('Truncated')) - const span = SpanEvent.fromSegment(segment) + const span = SpanEvent.fromSegment(segment, transaction) assert.ok(span) assert.ok(span instanceof SpanEvent) assert.ok(span instanceof SpanEvent.DatastoreSpanEvent) diff --git a/test/unit/spans/streaming-span-event.test.js b/test/unit/spans/streaming-span-event.test.js index 4c63ce144c..a92380ce71 100644 --- a/test/unit/spans/streaming-span-event.test.js +++ b/test/unit/spans/streaming-span-event.test.js @@ -60,13 +60,14 @@ test('fromSegment()', async (t) => { transaction.priority = 42 setTimeout(() => { - const segment = agent.tracer.getTransaction().trace.root.children[0] + const tx = agent.tracer.getTransaction() + const [segment] = tx.trace.getChildren(tx.trace.root.id) const spanContext = segment.getSpanContext() spanContext.addCustomAttribute('Span Lee', 'no prize') segment.addSpanAttribute('host', 'my-host') segment.addSpanAttribute('port', 22) - const span = StreamingSpanEvent.fromSegment(segment, 'parent') + const span = StreamingSpanEvent.fromSegment(segment, transaction, 'parent') // Should have all the normal properties. assert.ok(span) @@ -130,8 +131,9 @@ test('fromSegment()', async (t) => { https.get('https://example.com?foo=bar', (res) => { res.resume() res.on('end', () => { - const segment = agent.tracer.getTransaction().trace.root.children[0] - const span = StreamingSpanEvent.fromSegment(segment, 'parent') + const tx = agent.tracer.getTransaction() + const [segment] = tx.trace.getChildren(tx.trace.root.id) + const span = StreamingSpanEvent.fromSegment(segment, transaction, 'parent') // Should have all the normal properties. assert.ok(span) @@ -237,8 +239,8 @@ test('fromSegment()', async (t) => { dsConn.myDbOp(longQuery, () => { transaction.end() - const segment = transaction.trace.root.children[0] - const span = StreamingSpanEvent.fromSegment(segment, 'parent') + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) + const span = StreamingSpanEvent.fromSegment(segment, transaction, 'parent') // Should have all the normal properties. assert.ok(span) @@ -329,7 +331,7 @@ test('fromSegment()', async (t) => { const spanContext = agent.tracer.getSpanContext() spanContext.addCustomAttribute('customKey', 'customValue') - const span = StreamingSpanEvent.fromSegment(segment, 'parent') + const span = StreamingSpanEvent.fromSegment(segment, transaction, 'parent') const serializedSpan = span.toStreamingFormat() const { @@ -366,7 +368,7 @@ test('fromSegment()', async (t) => { spanContext.addIntrinsicAttribute('intrinsic.1', 1) spanContext.addIntrinsicAttribute('intrinsic.2', 2) - const span = StreamingSpanEvent.fromSegment(segment, 'parent') + const span = StreamingSpanEvent.fromSegment(segment, transaction, 'parent') const serializedSpan = span.toStreamingFormat() const { intrinsics } = serializedSpan @@ -387,10 +389,10 @@ test('fromSegment()', async (t) => { res.resume() res.on('end', () => { - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) assert.ok(segment.name.startsWith('Truncated')) - const span = StreamingSpanEvent.fromSegment(segment) + const span = StreamingSpanEvent.fromSegment(segment, transaction) assert.ok(span) assert.ok(span instanceof StreamingSpanEvent) @@ -407,12 +409,12 @@ test('fromSegment()', async (t) => { await t.test('should handle truncated datastore spans', (t, end) => { const { agent } = t.nr helper.runInTransaction(agent, (transaction) => { - const segment = transaction.trace.root.add('Datastore/operation/something') + const segment = transaction.trace.add('Datastore/operation/something') transaction.end() // end before segment to trigger truncate assert.ok(segment.name.startsWith('Truncated')) - const span = StreamingSpanEvent.fromSegment(segment) + const span = StreamingSpanEvent.fromSegment(segment, transaction) assert.ok(span) assert.ok(span instanceof StreamingSpanEvent) diff --git a/test/unit/transaction.test.js b/test/unit/transaction.test.js index 562d3485cd..054b7de749 100644 --- a/test/unit/transaction.test.js +++ b/test/unit/transaction.test.js @@ -1142,11 +1142,11 @@ test('_createDistributedTracePayload', async (t) => { await t.test('adds the current span id as the parent span id', (t) => { const { agent, txn, tracer } = t.nr agent.config.span_events.enabled = true - tracer.setSegment(txn.trace.root) + tracer.setSegment({ segment: txn.trace.root, transaction: txn }) txn.sampled = true const payload = JSON.parse(txn._createDistributedTracePayload().text()) assert.equal(payload.d.id, txn.trace.root.id) - tracer.setSegment(null) + tracer.setSegment({ segment: null, transaction: null }) agent.config.span_events.enabled = false }) @@ -1155,10 +1155,10 @@ test('_createDistributedTracePayload', async (t) => { agent.config.span_events.enabled = true txn._calculatePriority() txn.sampled = false - tracer.setSegment(txn.trace.root) + tracer.setSegment({ segment: txn.trace.root, transaction: txn }) const payload = JSON.parse(txn._createDistributedTracePayload().text()) assert.equal(payload.d.id, undefined) - tracer.setSegment(null) + tracer.setSegment({ segment: null, transaction: null }) agent.config.span_events.enabled = false }) @@ -1458,7 +1458,7 @@ test('insertDistributedTraceHeaders', async (t) => { const txn = new Transaction(agent) - tracer.setSegment(txn.trace.root) + tracer.setSegment({ transaction: txn, segment: txn.trace.root }) const outboundHeaders = createHeadersAndInsertTrace(txn) const traceparent = outboundHeaders.traceparent @@ -1485,7 +1485,7 @@ test('insertDistributedTraceHeaders', async (t) => { const txn = new Transaction(agent) const lowercaseHexRegex = /^[a-f0-9]+/ - tracer.setSegment(txn.trace.root) + tracer.setSegment({ transaction: txn, segment: txn.trace.root }) const outboundHeaders = createHeadersAndInsertTrace(txn) const traceparent = outboundHeaders.traceparent @@ -1504,7 +1504,7 @@ test('insertDistributedTraceHeaders', async (t) => { const txn = new Transaction(agent) - tracer.setSegment(txn.trace.root) + tracer.setSegment({ transaction: txn, segment: txn.trace.root }) txn.sampled = true const outboundHeaders = createHeadersAndInsertTrace(txn) @@ -1526,7 +1526,7 @@ test('insertDistributedTraceHeaders', async (t) => { txn.acceptTraceContextPayload(traceparent, tracestate) - tracer.setSegment(txn.trace.root) + tracer.setSegment({ transaction: txn, segment: txn.trace.root }) const outboundHeaders = createHeadersAndInsertTrace(txn) const traceparentParts = outboundHeaders.traceparent.split('-') @@ -2027,7 +2027,7 @@ test('when being named with finalizeName', async (t) => { }) function setupNameState(transaction) { - transaction.baseSegment = transaction.trace.root.add('basesegment') + transaction.baseSegment = transaction.trace.add('basesegment') transaction.nameState.setPrefix('Restify') transaction.nameState.setVerb('COOL') transaction.nameState.setDelimiter('/') @@ -2053,8 +2053,12 @@ function createHeadersAndInsertTrace(transaction) { } function addSegmentInContext(tracer, transaction, name) { - const segment = new Segment(transaction, name) - tracer.setSegment(segment) + const segment = new Segment({ + config: transaction.agent.config, + name, + root: transaction.trace.root + }) + tracer.setSegment({ transaction, segment }) return segment } diff --git a/test/unit/transaction/trace/index.test.js b/test/unit/transaction/trace/index.test.js index a6082140e5..32ccbae12b 100644 --- a/test/unit/transaction/trace/index.test.js +++ b/test/unit/transaction/trace/index.test.js @@ -113,13 +113,12 @@ test('Trace', async (t) => { const trace = transaction.trace const child1 = (transaction.baseSegment = trace.add('test')) child1.start() - const child2 = child1.add('nested') + const child2 = trace.add('nested', null, child1) child2.start() child1.end() child2.end() trace.root.end() transaction.end() - trace.generateSpanEvents() const events = agent.spanEventAggregator.getEvents() const nested = events[0] @@ -171,7 +170,7 @@ test('Trace', async (t) => { const trace = transaction.trace const child1 = trace.add('test') child1.start() - const child2 = child1.add('nested') + const child2 = trace.add('nested', null, child1) child2.start() child1.end() child2.end() @@ -192,35 +191,11 @@ test('Trace', async (t) => { const trace = transaction.trace const child1 = trace.add('test') child1.start() - const child2 = child1.add('nested') - child2.start() - child1.end() - child2.end() - trace.root.end() - transaction.end() - - const events = agent.spanEventAggregator.getEvents() - assert.equal(events.length, 0) - }) - - await t.test('should not generate span events on end if transaction is not sampled', (t) => { - const { agent } = t.nr - agent.config.span_events.enabled = true - agent.config.distributed_tracing.enabled = false - - const transaction = new Transaction(agent) - - const trace = transaction.trace - const child1 = trace.add('test') - child1.start() - const child2 = child1.add('nested') + const child2 = trace.add('nested', null, child1) child2.start() child1.end() child2.end() trace.root.end() - - transaction.priority = 0 - transaction.sampled = false transaction.end() const events = agent.spanEventAggregator.getEvents() @@ -256,8 +231,9 @@ test('Trace', async (t) => { transaction.acceptDistributedTraceHeaders('HTTP', headers) // Create at least one segment - const trace = new Trace(transaction) + const trace = transaction.trace const child = (transaction.baseSegment = trace.add('test')) + child.start() child.end() @@ -295,10 +271,11 @@ test('Trace', async (t) => { const transaction = new Transaction(agent) transaction.sampled = true - const trace = new Trace(transaction) + const trace = transaction.trace // add a child segment const child = (transaction.baseSegment = trace.add('test')) + child.start() child.end() @@ -333,7 +310,9 @@ test('when serializing synchronously', async (t) => { await t.test('should produce a transaction trace in the expected format', async (t) => { const { details } = t.nr + assert.ok(details.trace.segments) const traceJSON = details.trace.generateJSONSync() + assert.ok(!details.trace.segments) const reconstituted = await codecDecodeAsync(traceJSON[4]) assert.deepEqual(traceJSON, details.expectedEncoding, 'full trace JSON') @@ -404,7 +383,9 @@ test('when serializing asynchronously', async (t) => { await t.test('should produce a transaction trace in the expected format', async (t) => { const { details } = t.nr + assert.ok(details.trace.segments) const traceJSON = await details.trace.generateJSONAsync() + assert.ok(!details.trace.segments) const reconstituted = await codecDecodeAsync(traceJSON[4]) assert.deepEqual(traceJSON, details.expectedEncoding, 'full trace JSON') @@ -503,14 +484,15 @@ test('when inserting segments', async (t) => { const { trace } = t.nr trace.setDurationInMillis(40, 0) const child = trace.add('Custom/Test18/Child1') + child.setDurationInMillis(27, 0) - let seg = child.add('UnitTest') + let seg = trace.add('UnitTest', null, child) seg.setDurationInMillis(9, 1) - seg = child.add('UnitTest1') + seg = trace.add('UnitTest1', null, child) seg.setDurationInMillis(13, 1) - seg = child.add('UnitTest2') + seg = trace.add('UnitTest2', null, child) seg.setDurationInMillis(9, 16) - seg = child.add('UnitTest2') + seg = trace.add('UnitTest2', null, child) seg.setDurationInMillis(14, 16) assert.equal(trace.getTotalTimeDurationInMillis(), 48) }) @@ -518,15 +500,15 @@ test('when inserting segments', async (t) => { await t.test('should report total time on branched traces', (t) => { const { trace } = t.nr trace.setDurationInMillis(40, 0) - const child = trace.add('Custom/Test18/Child1') + const child = trace.add('Custom/Test18/Child1', null, trace.root) child.setDurationInMillis(27, 0) - const seg1 = child.add('UnitTest') + const seg1 = trace.add('UnitTest', null, child) seg1.setDurationInMillis(9, 1) - let seg = child.add('UnitTest1') + let seg = trace.add('UnitTest1', null, child) seg.setDurationInMillis(13, 1) - seg = seg1.add('UnitTest2') + seg = trace.add('UnitTest2', null, seg1) seg.setDurationInMillis(9, 16) - seg = seg1.add('UnitTest2') + seg = trace.add('UnitTest2', null, seg1) seg.setDurationInMillis(14, 16) assert.equal(trace.getTotalTimeDurationInMillis(), 48) }) @@ -535,101 +517,122 @@ test('when inserting segments', async (t) => { const { trace } = t.nr const expectedTrace = [ 0, - 27, - 'Root', - { nr_exclusive_duration_millis: 3 }, + 40, + 'ROOT', + { nr_exclusive_duration_millis: 10 }, [ [ - 1, - 10, - 'first', - { nr_exclusive_duration_millis: 9 }, - [[16, 25, 'first-first', { nr_exclusive_duration_millis: 9 }, []]] - ], - [ - 1, - 14, - 'second', - { nr_exclusive_duration_millis: 13 }, + 0, + 27, + 'Root', + { nr_exclusive_duration_millis: 3 }, [ - [16, 25, 'second-first', { nr_exclusive_duration_millis: 9 }, []], - [16, 25, 'second-second', { nr_exclusive_duration_millis: 9 }, []] + [ + 1, + 10, + 'first', + { nr_exclusive_duration_millis: 9 }, + [[16, 25, 'first-first', { nr_exclusive_duration_millis: 9 }, []]] + ], + [ + 1, + 14, + 'second', + { nr_exclusive_duration_millis: 13 }, + [ + [16, 25, 'second-first', { nr_exclusive_duration_millis: 9 }, []], + [16, 25, 'second-second', { nr_exclusive_duration_millis: 9 }, []] + ] + ] ] ] ] ] trace.setDurationInMillis(40, 0) - const child = trace.add('Root') + const child = trace.add('Root', null, trace.root) + child.setDurationInMillis(27, 0) - const seg1 = child.add('first') + const seg1 = trace.add('first', null, child) + seg1.setDurationInMillis(9, 1) - const seg2 = child.add('second') + const seg2 = trace.add('second', null, child) seg2.setDurationInMillis(13, 1) - let seg = seg1.add('first-first') + let seg = trace.add('first-first', null, seg1) seg.setDurationInMillis(9, 16) - seg = seg1.add('first-second') + seg = trace.add('first-second', null, seg1) seg.setDurationInMillis(14, 16) seg._collect = false - seg = seg2.add('second-first') + seg = trace.add('second-first', null, seg2) seg.setDurationInMillis(9, 16) - seg = seg2.add('second-second') + seg = trace.add('second-second', null, seg2) seg.setDurationInMillis(9, 16) trace.end() - assert.deepEqual(child.toJSON(), expectedTrace) + assert.deepEqual(trace.toJSON(), expectedTrace) }) await t.test('should report the expected trees for branched trees', (t) => { const { trace } = t.nr const expectedTrace = [ 0, - 27, - 'Root', - { nr_exclusive_duration_millis: 3 }, + 40, + 'ROOT', + { nr_exclusive_duration_millis: 10 }, [ [ - 1, - 10, - 'first', - { nr_exclusive_duration_millis: 9 }, - [ - [16, 25, 'first-first', { nr_exclusive_duration_millis: 9 }, []], - [16, 30, 'first-second', { nr_exclusive_duration_millis: 14 }, []] - ] - ], - [ - 1, - 14, - 'second', - { nr_exclusive_duration_millis: 13 }, + 0, + 27, + 'Root', + { nr_exclusive_duration_millis: 3 }, [ - [16, 25, 'second-first', { nr_exclusive_duration_millis: 9 }, []], - [16, 25, 'second-second', { nr_exclusive_duration_millis: 9 }, []] + [ + 1, + 10, + 'first', + { nr_exclusive_duration_millis: 9 }, + [ + [16, 25, 'first-first', { nr_exclusive_duration_millis: 9 }, []], + [16, 30, 'first-second', { nr_exclusive_duration_millis: 14 }, []] + ] + ], + [ + 1, + 14, + 'second', + { nr_exclusive_duration_millis: 13 }, + [ + [16, 25, 'second-first', { nr_exclusive_duration_millis: 9 }, []], + [16, 25, 'second-second', { nr_exclusive_duration_millis: 9 }, []] + ] + ] ] ] ] ] + trace.setDurationInMillis(40, 0) - const child = trace.add('Root') + const child = trace.add('Root', null, trace.root) + child.setDurationInMillis(27, 0) - const seg1 = child.add('first') + const seg1 = trace.add('first', null, child) + seg1.setDurationInMillis(9, 1) - const seg2 = child.add('second') + const seg2 = trace.add('second', null, child) seg2.setDurationInMillis(13, 1) - let seg = seg1.add('first-first') + let seg = trace.add('first-first', null, seg1) seg.setDurationInMillis(9, 16) - seg = seg1.add('first-second') + seg = trace.add('first-second', null, seg1) seg.setDurationInMillis(14, 16) - seg = seg2.add('second-first') + seg = trace.add('second-first', null, seg2) seg.setDurationInMillis(9, 16) - seg = seg2.add('second-second') + seg = trace.add('second-second', null, seg2) seg.setDurationInMillis(9, 16) trace.end() - assert.deepEqual(child.toJSON(), expectedTrace) + assert.deepEqual(trace.toJSON(), expectedTrace) }) await t.test('should measure exclusive time vs total time at each level of the graph', (t) => { @@ -657,10 +660,12 @@ test('when inserting segments', async (t) => { // add another that starts within the first range but that extends beyond const child3 = trace.add('Custom/Test19/Child3') + child3.setDurationInMillis(22, now + 11) // add a final child that's entirely disjoint const child4 = trace.add('Custom/Test19/Child4') + child4.setDurationInMillis(4, now + 35) assert.equal(trace.getExclusiveDurationInMillis(), 5) @@ -674,24 +679,25 @@ test('when inserting segments', async (t) => { // create a long child on its own const child1 = trace.add('Custom/Test20/Child1') + child1.setDurationInMillis(33, now) // add another, short child as a sibling - const child2 = child1.add('Custom/Test20/Child2') + const child2 = trace.add('Custom/Test20/Child2', null, child1) child2.setDurationInMillis(5, now) // add two disjoint children of the second segment encompassed by the first segment - const child3 = child2.add('Custom/Test20/Child3') + const child3 = trace.add('Custom/Test20/Child3', null, child2) child3.setDurationInMillis(11, now) - const child4 = child2.add('Custom/Test20/Child3') + const child4 = trace.add('Custom/Test20/Child3', null, child2) child4.setDurationInMillis(11, now + 16) assert.equal(trace.getExclusiveDurationInMillis(), 9) - assert.equal(child4.getExclusiveDurationInMillis(), 11) - assert.equal(child3.getExclusiveDurationInMillis(), 11) - assert.equal(child2.getExclusiveDurationInMillis(), 0) - assert.equal(child1.getExclusiveDurationInMillis(), 11) + assert.equal(child4.getExclusiveDurationInMillis(trace), 11) + assert.equal(child3.getExclusiveDurationInMillis(trace), 11) + assert.equal(child2.getExclusiveDurationInMillis(trace), 0) + assert.equal(child1.getExclusiveDurationInMillis(trace), 11) }) await t.test('should accurately sum partially overlapping segments', (t) => { @@ -737,21 +743,68 @@ test('when inserting segments', async (t) => { // They will be tagged as _collect = false after the limit runs out. for (let i = 0; i < 950; ++i) { const segment = trace.add(i.toString(), noop) - if (i < 900) { + // root segment is already added + if (i < 899) { assert.equal(segment._collect, true, `segment ${i} should be collected`) } else { assert.equal(segment._collect, false, `segment ${i} should not be collected`) } } - assert.equal(trace.root.children.length, 950) + // 950 + root segment + assert.equal(transaction.numSegments, 951) assert.equal(transaction._recorders.length, 950) - trace.segmentCount = 0 - trace.root.children = [] - trace.recorders = [] - + trace.end() function noop() {} }) + + await t.test('should not cause a stack overflow', { timeout: 30000 }, (t) => { + const { trace } = t.nr + for (let i = 0; i < 9000; ++i) { + trace.add(`Child ${i}`) + } + + assert.doesNotThrow(function () { + trace.toJSON() + }) + }) + + await t.test('should get all children for a segment', (t) => { + const { trace } = t.nr + assert.deepEqual(trace.segments.root.children, []) + const segment = trace.add('base') + const segment2 = trace.add('1', null, segment) + const segment3 = trace.add('2', null, segment) + const children = trace.getChildren(segment.id) + assert.ok(children.length, 2) + assert.deepEqual(children, [segment2, segment3]) + }) + + await t.test('should get all collected children for a segment', (t) => { + const { trace } = t.nr + const segment = trace.add('base') + const segment2 = trace.add('1', null, segment) + const segment3 = trace.add('2', null, segment) + const segment4 = trace.add('3', null, segment) + segment4._collect = false + const segment5 = trace.add('4', null, segment) + segment5.ignore = true + let { children } = trace.getNode(segment.id) + children = trace.getCollectedChildren(children) + assert.ok(children.length, 2) + assert.deepEqual(children[0].segment, segment2) + assert.deepEqual(children[0].children, []) + assert.deepEqual(children[1].segment, segment3) + assert.deepEqual(children[1].children, []) + }) + + await t.test('should get parent segment for a segment', (t) => { + const { trace } = t.nr + const segment = trace.add('base') + const segment2 = trace.add('1', null, segment) + const parent = trace.getParent(segment2.parentId) + assert.equal(parent, segment) + }) }) test('should set URI to null when request.uri attribute is excluded globally', async (t) => { @@ -925,7 +978,7 @@ function addTwoSegments(transaction) { const trace = transaction.trace const child1 = (transaction.baseSegment = trace.add('test')) child1.start() - const child2 = child1.add('nested') + const child2 = trace.add('nested', null, child1) child2.start() child1.end() child2.end() @@ -944,8 +997,6 @@ async function makeTrace(agent) { transaction.url = URL transaction.verb = 'GET' - transaction.timer.setDurationInMillis(DURATION) - const trace = transaction.trace // promisifying `trace.generateJSON` so tests do not have to call done @@ -955,18 +1006,19 @@ async function makeTrace(agent) { assert.ok(start > 0, "root segment's start time") trace.setDurationInMillis(DURATION, 0) - const web = trace.root.add(URL) + const web = trace.add(URL) transaction.baseSegment = web transaction.finalizeNameFromUri(URL, 200) // top-level element will share a duration with the quasi-ROOT node web.setDurationInMillis(DURATION, 0) - const db = web.add('Database/statement/AntiSQL/select/getSome') + const db = trace.add('Database/statement/AntiSQL/select/getSome', null, web) db.setDurationInMillis(14, 3) - const memcache = web.add('Datastore/operation/Memcache/lookup') + const memcache = trace.add('Datastore/operation/Memcache/lookup', null, web) memcache.setDurationInMillis(20, 8) + transaction.timer.setDurationInMillis(DURATION) trace.end() /* @@ -974,6 +1026,21 @@ async function makeTrace(agent) { * outermost version having its scope always set to 'ROOT'. The null bits * are parameters, which are optional, and so far, unimplemented for Node. */ + const dbSegment = [ + 3, + 17, + 'Database/statement/AntiSQL/select/getSome', + { nr_exclusive_duration_millis: 14 }, + [] + ] + const memcacheSegment = [ + 8, + 28, + 'Datastore/operation/Memcache/lookup', + { nr_exclusive_duration_millis: 20 }, + [] + ] + const rootSegment = [ 0, DURATION, @@ -989,15 +1056,10 @@ async function makeTrace(agent) { 'request.parameters.test': 'value', nr_exclusive_duration_millis: 8 }, - [ - // TODO: ensure that the ordering is correct WRT start time - db.toJSON(), - memcache.toJSON() - ] + [dbSegment, memcacheSegment] ] ] ] - const rootNode = [ trace.root.timer.start / 1000, {}, @@ -1018,7 +1080,6 @@ async function makeTrace(agent) { return { transaction, trace, - rootSegment, rootNode, expectedEncoding: [ 0, diff --git a/test/unit/transaction/trace/segment-tree.test.js b/test/unit/transaction/trace/segment-tree.test.js new file mode 100644 index 0000000000..d808ec639d --- /dev/null +++ b/test/unit/transaction/trace/segment-tree.test.js @@ -0,0 +1,55 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const assert = require('node:assert') +const test = require('node:test') +const sinon = require('sinon') +const createLoggerMock = require('../../mocks/logger') +const SegmentTree = require('../../../../lib/transaction/trace/segment-tree') + +test('should add root to segment tree', () => { + const segment = { id: '1', name: 'ROOT' } + const tree = new SegmentTree(segment) + assert.deepEqual(tree.root, { segment, children: [] }) +}) + +test('should find the proper parent node', () => { + const segment = { id: '1', name: 'ROOT', parentId: '0' } + const tree = new SegmentTree(segment) + const segment2 = { id: '2', parentId: '1', name: 'segment2' } + tree.add(segment2) + const segment3 = { id: '3', parentId: '2', name: 'segment3' } + const segment4 = { id: '4', parentId: '2', name: 'segment4' } + tree.add(segment3) + tree.add(segment4) + const segment5 = { id: '5', parentId: '1', name: 'segment5' } + tree.add(segment5) + const segment6 = { id: '6', parentId: '4', name: 'segment6' } + tree.add(segment6) + + let parent = tree.find(segment6.parentId) + assert.deepEqual(parent.segment, segment4) + parent = tree.find(segment5.parentId) + assert.deepEqual(parent.segment, segment) + parent = tree.find(segment4.parentId) + assert.deepEqual(parent.segment, segment2) + parent = tree.find(segment3.parentId) + assert.deepEqual(parent.segment, segment2) + parent = tree.find(segment2.parentId) + assert.deepEqual(parent.segment, segment) + parent = tree.find(segment.parentId) + assert.ok(!parent) +}) + +test('should not add child if parent cannot be found', () => { + const segment = { id: '1', name: 'ROOT' } + const loggerStub = createLoggerMock(sinon) + const tree = new SegmentTree(segment, { logger: loggerStub }) + const segment2 = { id: '2', parentId: '0', name: 'segment2' } + tree.add(segment2) + assert.deepEqual(tree.root.children, []) + assert.deepEqual(loggerStub.debug.args[0], ['Cannot find parent %s in tree', '0']) +}) diff --git a/test/unit/transaction/trace/segment.test.js b/test/unit/transaction/trace/segment.test.js index 33aa97ead0..32e1d459a2 100644 --- a/test/unit/transaction/trace/segment.test.js +++ b/test/unit/transaction/trace/segment.test.js @@ -16,6 +16,7 @@ const Transaction = require('../../../../lib/transaction') function beforeEach(ctx) { ctx.nr = {} ctx.nr.agent = helper.loadMockedAgent() + ctx.nr.agent.config.logging.diagnostics = true } function afterEach(ctx) { @@ -26,101 +27,71 @@ test('TraceSegment', async (t) => { t.beforeEach(beforeEach) t.afterEach(afterEach) - await t.test('should be bound to a Trace', (t) => { - const { agent } = t.nr - let segment = null - const trans = new Transaction(agent) - assert.throws(function noTrace() { - segment = new TraceSegment(null, 'UnitTest') - }) - assert.equal(segment, null) - - const success = new TraceSegment(trans, 'UnitTest') - assert.equal(success.transaction, trans) - trans.end() - }) - - await t.test('should not add new children when marked as opaque', (t) => { - const { agent } = t.nr - const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') - assert.ok(!segment.opaque) - segment.opaque = true - segment.add('child') - assert.equal(segment.children.length, 0) - segment.opaque = false - segment.add('child') - assert.equal(segment.children.length, 1) - trans.end() - }) - - await t.test('should call an optional callback function', (t, end) => { - const { agent } = t.nr - const trans = new Transaction(agent) - assert.doesNotThrow(function noCallback() { - // eslint-disable-next-line no-new - new TraceSegment(trans, 'UnitTest') - }) - const working = new TraceSegment(trans, 'UnitTest', function () { - end() - }) - working.end() - trans.end() - }) - await t.test('has a name', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const success = new TraceSegment(trans, 'UnitTest') + const root = trans.trace.root + const success = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root + }) assert.equal(success.name, 'UnitTest') }) - await t.test('is created with no children', (t) => { - const { agent } = t.nr - const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') - assert.equal(segment.children.length, 0) - }) - await t.test('has a timer', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') + const root = trans.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root + }) assert.ok(segment.timer) }) await t.test('does not start its timer on creation', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') + const root = trans.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root + }) assert.equal(segment.timer.isRunning(), false) }) await t.test('allows the timer to be updated without ending it', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') + const root = trans.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root + }) segment.start() segment.touch() assert.equal(segment.timer.isRunning(), true) assert.ok(segment.getDurationInMillis() > 0) }) - await t.test('accepts a callback that records metrics for this segment', (t, end) => { - const { agent } = t.nr - const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'Test', (insider) => { - assert.equal(insider, segment) - end() - }) - segment.end() - trans.end() - }) - await t.test('should return the segment id when dt and spans are enabled', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'Test') + const root = trans.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'Test', + collect: true, + root + }) agent.config.distributed_tracing.enabled = true agent.config.span_events.enabled = true assert.equal(segment.getSpanId(), segment.id) @@ -129,18 +100,30 @@ test('TraceSegment', async (t) => { await t.test('should return null when dt is disabled', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'Test') + const root = trans.trace.root agent.config.distributed_tracing.enabled = false agent.config.span_events.enabled = true + const segment = new TraceSegment({ + config: agent.config, + name: 'Test', + collect: true, + root + }) assert.equal(segment.getSpanId(), null) }) await t.test('should return null when spans are disabled', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'Test') + const root = trans.trace.root agent.config.distributed_tracing.enabled = true agent.config.span_events.enabled = false + const segment = new TraceSegment({ + config: agent.config, + name: 'Test', + collect: true, + root + }) assert.ok(segment.getSpanId() === null) }) @@ -148,7 +131,13 @@ test('TraceSegment', async (t) => { const { agent } = t.nr const trans = new Transaction(agent) const trace = trans.trace - const segment = new TraceSegment(trans, 'Test') + const root = trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'Test', + collect: true, + root + }) segment.setDurationInMillis(10, 0) @@ -160,52 +149,30 @@ test('TraceSegment', async (t) => { }, 10) }) - await t.test('properly tracks the number of active or harvested segments', (t, end) => { - const { agent } = t.nr - assert.equal(agent.activeTransactions, 0) - assert.equal(agent.totalActiveSegments, 0) - assert.equal(agent.segmentsCreatedInHarvest, 0) - - const tx = new Transaction(agent) - assert.equal(agent.totalActiveSegments, 1) - assert.equal(agent.segmentsCreatedInHarvest, 1) - assert.equal(tx.numSegments, 1) - assert.equal(agent.activeTransactions, 1) - - // eslint-disable-next-line sonarjs/no-unused-vars, sonarjs/no-dead-store, no-unused-vars - const segment = new TraceSegment(tx, 'Test') - assert.equal(agent.totalActiveSegments, 2) - assert.equal(agent.segmentsCreatedInHarvest, 2) - assert.equal(tx.numSegments, 2) - tx.end() - - assert.equal(agent.activeTransactions, 0) - - setTimeout(function () { - assert.equal(agent.totalActiveSegments, 0) - assert.equal(agent.segmentsClearedInHarvest, 2) - - agent.forceHarvestAll(() => { - assert.equal(agent.totalActiveSegments, 0) - assert.equal(agent.segmentsClearedInHarvest, 0) - assert.equal(agent.segmentsCreatedInHarvest, 0) - end() - }) - }, 10) - }) - await t.test('toJSON should not modify attributes', (t) => { const { agent } = t.nr const transaction = new Transaction(agent) - const segment = new TraceSegment(transaction, 'TestSegment') - segment.toJSON() + const root = transaction.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'TestSegment', + collect: true, + root + }) + transaction.trace.toJSON() assert.deepEqual(segment.getAttributes(), {}) }) await t.test('when ended stops its timer', (t) => { const { agent } = t.nr const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') + const root = trans.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root + }) segment.end() assert.equal(segment.timer.isRunning(), false) }) @@ -221,8 +188,9 @@ test('TraceSegment', async (t) => { trace.end() - // See documentation on TraceSegment.toJSON for what goes in which field. - assert.deepEqual(segment.toJSON(), [ + // get serialized segment from trace + const serializedSegment = trace.toJSON()[4][0] + assert.deepEqual(serializedSegment, [ 3, 17, 'DB/select/getSome', @@ -234,13 +202,21 @@ test('TraceSegment', async (t) => { await t.test('#finalize should add nr_exclusive_duration_millis attribute', (t) => { const { agent } = t.nr const transaction = new Transaction(agent) - const segment = new TraceSegment(transaction, 'TestSegment') + const root = transaction.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'TestSegment', + collect: true, + root, + parentId: root.id + }) + transaction.trace.segments.add(segment) segment._setExclusiveDurationInMillis(1) assert.deepEqual(segment.getAttributes(), {}) - segment.finalize() + segment.finalize(transaction.trace) assert.equal(segment.getAttributes()['nr_exclusive_duration_millis'], 1) }) @@ -250,20 +226,27 @@ test('TraceSegment', async (t) => { const segmentName = 'TestSegment' const transaction = new Transaction(agent) - const segment = new TraceSegment(transaction, segmentName) + const root = transaction.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: segmentName, + collect: true, + root, + parentId: root.id + }) + + transaction.trace.segments.add(segment) // Force truncation sinon.stub(segment.timer, 'softEnd').returns(true) sinon.stub(segment.timer, 'endsAfter').returns(true) - const root = transaction.trace.root - // Make root duration calculation predictable root.timer.start = 1000 segment.timer.start = 1001 segment.overwriteDurationInMillis(3) - segment.finalize() + segment.finalize(transaction.trace) assert.equal(segment.name, `Truncated/${segmentName}`) assert.equal(root.getDurationInMillis(), 4) @@ -279,11 +262,9 @@ test('with children created from URLs', async (t) => { const transaction = new Transaction(ctx.nr.agent) const trace = transaction.trace - const segment = trace.add('UnitTest') - const url = '/test?test1=value1&test2&test3=50&test4=' - const webChild = segment.add(url) + const webChild = trace.add(url) transaction.baseSegment = webChild transaction.finalizeNameFromUri(url, 200) @@ -292,6 +273,7 @@ test('with children created from URLs', async (t) => { trace.end() ctx.nr.webChild = webChild + ctx.nr.trace = trace }) t.afterEach(afterEach) @@ -324,8 +306,10 @@ test('with children created from URLs', async (t) => { }) await t.test('should serialize the segment with the parameters', (t) => { - const { webChild } = t.nr - assert.deepEqual(webChild.toJSON(), [ + const { trace } = t.nr + // get serialized segment from trace + const serializedSegment = trace.toJSON()[4][0] + assert.deepEqual(serializedSegment, [ 0, 1, 'WebTransaction/NormalizedUri/*', @@ -350,8 +334,6 @@ test('with parameters parsed out by framework', async (t) => { const trace = transaction.trace trace.mer = 6 - const segment = trace.add('UnitTest') - const url = '/test' const params = {} @@ -360,7 +342,7 @@ test('with parameters parsed out by framework', async (t) => { params[1] = 'another' params.test3 = '50' - const webChild = segment.add(url) + const webChild = trace.add(url) transaction.trace.attributes.addAttributes(DESTINATIONS.TRANS_SCOPE, params) transaction.baseSegment = webChild transaction.finalizeNameFromUri(url, 200) @@ -397,7 +379,7 @@ test('with parameters parsed out by framework', async (t) => { }) await t.test('should serialize the segment with the parameters', (t) => { - const { webChild } = t.nr + const { trace } = t.nr const expected = [ 0, 1, @@ -410,7 +392,9 @@ test('with parameters parsed out by framework', async (t) => { }, [] ] - assert.deepEqual(webChild.toJSON(), expected) + // get serialized segment from trace + const serializedSegment = trace.toJSON()[4][0] + assert.deepEqual(serializedSegment, expected) }) }) @@ -421,10 +405,9 @@ test('with attributes.enabled set to false', async (t) => { const transaction = new Transaction(ctx.nr.agent) const trace = transaction.trace - const segment = new TraceSegment(transaction, 'UnitTest') const url = '/test?test1=value1&test2&test3=50&test4=' - const webChild = segment.add(url) + const webChild = trace.add(url) webChild.addAttribute('test', 'non-null value') transaction.baseSegment = webChild transaction.finalizeNameFromUri(url, 200) @@ -432,6 +415,7 @@ test('with attributes.enabled set to false', async (t) => { trace.setDurationInMillis(1, 0) webChild.setDurationInMillis(1, 0) ctx.nr.webChild = webChild + ctx.nr.trace = trace }) t.afterEach(afterEach) @@ -446,9 +430,11 @@ test('with attributes.enabled set to false', async (t) => { }) await t.test('should serialize the segment without the parameters', (t) => { - const { webChild } = t.nr + const { trace } = t.nr const expected = [0, 1, 'WebTransaction/NormalizedUri/*', {}, []] - assert.deepEqual(webChild.toJSON(), expected) + // get serialized segment from trace + const serializedSegment = trace.toJSON()[4][0] + assert.deepEqual(serializedSegment, expected) }) }) @@ -465,14 +451,12 @@ test('with attributes.enabled set', async (t) => { const transaction = new Transaction(ctx.nr.agent) const trace = transaction.trace - const segment = trace.add('UnitTest') - const url = '/test?test1=value1&test2&test3=50&test4=' - const webChild = segment.add(url) + const webChild = trace.add(url) transaction.baseSegment = webChild transaction.finalizeNameFromUri(url, 200) - webChild.markAsWeb(url) + webChild.markAsWeb(transaction) trace.setDurationInMillis(1, 0) webChild.setDurationInMillis(1, 0) @@ -480,6 +464,7 @@ test('with attributes.enabled set', async (t) => { ctx.nr.webChild = webChild trace.end() + ctx.nr.trace = trace }) t.afterEach(afterEach) @@ -512,8 +497,10 @@ test('with attributes.enabled set', async (t) => { }) await t.test('should serialize the segment with the parameters', (t) => { - const { webChild } = t.nr - assert.deepEqual(webChild.toJSON(), [ + const { trace } = t.nr + // get the specific segment from serialized trace + const serializedSegment = trace.toJSON()[4][0] + assert.deepEqual(serializedSegment, [ 0, 1, 'WebTransaction/NormalizedUri/*', @@ -530,13 +517,18 @@ test('with attributes.enabled set', async (t) => { test('when serialized', async (t) => { t.beforeEach((ctx) => { const agent = helper.loadMockedAgent() - const trans = new Transaction(agent) - const segment = new TraceSegment(trans, 'UnitTest') + const transaction = new Transaction(agent) + const root = transaction.trace.root + const segment = transaction.trace.add('UnitTest') + ctx.nr = { agent, segment, - trans + transaction, + root } + + ctx.nr.agent.config.logging.diagnostics = true }) t.afterEach((ctx) => { @@ -544,9 +536,9 @@ test('when serialized', async (t) => { }) await t.test('should create a plain JS array', (t) => { - const { segment } = t.nr + const { segment, transaction } = t.nr segment.end() - const js = segment.toJSON() + const js = transaction.trace.toJSON()[4][0] assert.ok(Array.isArray(js)) assert.equal(typeof js[0], 'number') @@ -559,20 +551,6 @@ test('when serialized', async (t) => { assert.ok(Array.isArray(js[4])) assert.equal(js[4].length, 0) }) - - await t.test('should not cause a stack overflow', { timeout: 30000 }, (t) => { - const { segment, trans } = t.nr - let parent = segment - for (let i = 0; i < 9000; ++i) { - const child = new TraceSegment(trans, 'Child ' + i) - parent.children.push(child) - parent = child - } - - assert.doesNotThrow(function () { - segment.toJSON() - }) - }) }) test('getSpanContext', async (t) => { @@ -583,7 +561,13 @@ test('getSpanContext', async (t) => { } }) const transaction = new Transaction(agent) - const segment = new TraceSegment(transaction, 'UnitTest') + const root = transaction.trace.root + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root + }) ctx.nr = { agent, segment, @@ -607,15 +591,27 @@ test('getSpanContext', async (t) => { }) await t.test('should not create a new context when empty and DT disabled', (t) => { - const { agent, segment } = t.nr + const { agent, transaction } = t.nr agent.config.distributed_tracing.enabled = false + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root: transaction.trace.root + }) const spanContext = segment.getSpanContext() assert.ok(!spanContext) }) await t.test('should not create a new context when empty and Spans disabled', (t) => { - const { agent, segment } = t.nr + const { agent, transaction } = t.nr agent.config.span_events.enabled = false + const segment = new TraceSegment({ + config: agent.config, + name: 'UnitTest', + collect: true, + root: transaction.trace.root + }) const spanContext = segment.getSpanContext() assert.ok(!spanContext) }) diff --git a/test/unit/transaction/tracer.test.js b/test/unit/transaction/tracer.test.js index bbc6ea2465..15138e5d57 100644 --- a/test/unit/transaction/tracer.test.js +++ b/test/unit/transaction/tracer.test.js @@ -8,6 +8,7 @@ const assert = require('node:assert') const test = require('node:test') const helper = require('../../lib/agent_helper') const Segment = require('../../../lib/transaction/trace/segment') +const Transaction = require('../../../lib/transaction') const notRunningStates = ['stopped', 'stopping', 'errored'] function beforeEach(ctx) { @@ -118,7 +119,10 @@ test('Tracer', async function (t) { } assert.throws(() => { - const fn = tracer.bindFunction(wrapMe, new Segment(trans, 'name')) + const segment = new Segment({ name: 'name', isRoot: false, root: trans.trace.root }) + let context = tracer.getContext() + context = context.enterSegment({ segment }) + const fn = tracer.bindFunction(wrapMe, context) fn() }, /Error: FIREBOMB/) end() @@ -142,4 +146,104 @@ test('Tracer', async function (t) { } ) }) + + await t.test('Optional callback', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + + await t.test('should call an optional callback function', (t, end) => { + const { agent } = t.nr + const trans = new Transaction(agent) + const trace = trans.trace + + assert.doesNotThrow(function noCallback() { + trace.add('UnitTest', null, null) + }) + + const working = trace.add( + 'UnitTest', + function () { + end() + }, + null, + false, + function () {} + ) + + working.end() + trans.end() + }) + + await t.test('accepts a callback that records metrics for this segment', (t, end) => { + const { agent } = t.nr + const trans = new Transaction(agent) + const trace = trans.trace + + const segment = trace.add( + 'Test', + (insider) => { + assert.equal(insider, segment) + end() + }, + null, + false, + function () {} + ) + segment.end() + trans.end() + }) + }) + + await t.test('increment segments', async (t) => { + t.beforeEach(beforeEach) + t.afterEach(afterEach) + await t.test('properly tracks the number of active or harvested segments', (t, end) => { + const { agent, tracer } = t.nr + assert.equal(agent.activeTransactions, 0) + assert.equal(agent.totalActiveSegments, 0) + assert.equal(agent.segmentsCreatedInHarvest, 0) + + const tx = new Transaction(agent) + tracer.setSegment({ transaction: tx, segment: tx.trace.root }) + assert.equal(agent.totalActiveSegments, 1) + assert.equal(agent.segmentsCreatedInHarvest, 1) + assert.equal(tx.numSegments, 1) + assert.equal(agent.activeTransactions, 1) + assert.equal(tx.trace.segments.root.children.length, 0) + + tracer.createSegment({ name: 'Test', parent: tx.trace.root, transaction: tx }) + assert.equal(agent.totalActiveSegments, 2) + assert.equal(agent.segmentsCreatedInHarvest, 2) + assert.equal(tx.numSegments, 2) + assert.equal(tx.trace.segments.root.children.length, 1) + tx.end() + + assert.equal(agent.activeTransactions, 0) + + setTimeout(function () { + assert.equal(agent.totalActiveSegments, 0) + assert.equal(agent.segmentsClearedInHarvest, 2) + + agent.forceHarvestAll(() => { + assert.equal(agent.totalActiveSegments, 0) + assert.equal(agent.segmentsClearedInHarvest, 0) + assert.equal(agent.segmentsCreatedInHarvest, 0) + end() + }) + }, 10) + }) + await t.test('skip adding children when parent is opaque', (t) => { + const { agent, tracer } = t.nr + const tx = new Transaction(agent) + tracer.setSegment({ transaction: tx, segment: tx.trace.root }) + const segment = tracer.createSegment({ name: 'Test', parent: tx.trace.root, transaction: tx }) + segment.opaque = true + const segment2 = tracer.createSegment({ name: 'Test1', parent: segment, transaction: tx }) + const segment3 = tracer.createSegment({ name: 'Test2', parent: segment, transaction: tx }) + assert.equal(segment2.id, segment.id) + assert.equal(segment3.id, segment.id) + assert.equal(tx.trace.segments.root.children.length, 1) + tx.end() + }) + }) }) diff --git a/test/versioned-external/external-repos.js b/test/versioned-external/external-repos.js index 82b0601eac..ac3ab1c73a 100644 --- a/test/versioned-external/external-repos.js +++ b/test/versioned-external/external-repos.js @@ -15,7 +15,7 @@ const repos = [ { name: 'apollo-server', repository: 'https://github.com/newrelic/newrelic-node-apollo-server-plugin.git', - branch: 'main', + branch: 'remove-child-segments', additionalFiles: [ 'tests/lib', ] diff --git a/test/versioned/amqplib-esm/issue-2663.test.mjs b/test/versioned/amqplib-esm/issue-2663.test.mjs index 0e790198a0..e0ef1c30f4 100644 --- a/test/versioned/amqplib-esm/issue-2663.test.mjs +++ b/test/versioned/amqplib-esm/issue-2663.test.mjs @@ -46,7 +46,7 @@ test('esm import does instrumentation', async () => { const body = msg.content.toString('utf8') assert.equal(body, 'hello', 'should receive expected body') - amqpUtils.verifyTransaction(tx, 'get') + amqpUtils.verifyTransaction(agent, tx, 'get') channel.ack(msg) tx.end() amqpUtils.verifyGet({ diff --git a/test/versioned/amqplib/amqp-utils.js b/test/versioned/amqplib/amqp-utils.js index 121697fdd0..4c1c4b1e15 100644 --- a/test/versioned/amqplib/amqp-utils.js +++ b/test/versioned/amqplib/amqp-utils.js @@ -27,7 +27,7 @@ exports.verifyTransaction = verifyTransaction exports.getChannel = getChannel function verifySubscribe(tx, exchange, routingKey) { - const isCallback = !!metrics.findSegment(tx.trace.root, 'Callback: ') + const isCallback = !!metrics.findSegment(tx.trace, tx.trace.root, 'Callback: ') let segments = [] @@ -39,7 +39,7 @@ function verifySubscribe(tx, exchange, routingKey) { segments = ['MessageBroker/RabbitMQ/Exchange/Produce/Named/' + exchange] } - assertSegments(tx.trace.root, segments) + assertSegments(tx.trace, tx.trace.root, segments) assertMetrics( tx.metrics, @@ -51,6 +51,7 @@ function verifySubscribe(tx, exchange, routingKey) { assert.equal(tx.getFullName(), null, 'should not set transaction name') const consume = metrics.findSegment( + tx.trace, tx.trace.root, 'MessageBroker/RabbitMQ/Exchange/Produce/Named/' + exchange ) @@ -91,7 +92,7 @@ function verifyDistributedTrace(produceTransaction, consumeTransaction) { consumeTransaction.traceId, 'should have proper trace id' ) - const produceSegment = produceTransaction.trace.root.children[0] + const [produceSegment] = produceTransaction.trace.getChildren(produceTransaction.trace.root.id) assert.equal( produceSegment.id, consumeTransaction.parentSpanId, @@ -121,6 +122,7 @@ function verifyConsumeTransaction(tx, exchange, queue, routingKey) { ) const consume = metrics.findSegment( + tx.trace, tx.trace.root, 'OtherTransaction/Message/RabbitMQ/Exchange/Named/' + exchange ) @@ -143,7 +145,7 @@ function verifyConsumeTransaction(tx, exchange, queue, routingKey) { } function verifySendToQueue(tx) { - assertSegments(tx.trace.root, ['MessageBroker/RabbitMQ/Exchange/Produce/Named/Default']) + assertSegments(tx.trace, tx.trace.root, ['MessageBroker/RabbitMQ/Exchange/Produce/Named/Default']) assertMetrics( tx.metrics, @@ -153,6 +155,7 @@ function verifySendToQueue(tx) { ) const segment = metrics.findSegment( + tx.trace, tx.trace.root, 'MessageBroker/RabbitMQ/Exchange/Produce/Named/Default' ) @@ -165,7 +168,7 @@ function verifySendToQueue(tx) { } function verifyProduce(tx, exchangeName, routingKey) { - const isCallback = !!metrics.findSegment(tx.trace.root, 'Callback: ') + const isCallback = !!metrics.findSegment(tx.trace, tx.trace.root, 'Callback: ') let segments = [] if (isCallback) { @@ -197,7 +200,7 @@ function verifyProduce(tx, exchangeName, routingKey) { ] } - assertSegments(tx.trace.root, segments, 'should have expected segments') + assertSegments(tx.trace, tx.trace.root, segments, 'should have expected segments') assertMetrics( tx.metrics, @@ -207,6 +210,7 @@ function verifyProduce(tx, exchangeName, routingKey) { ) const segment = metrics.findSegment( + tx.trace, tx.trace.root, 'MessageBroker/RabbitMQ/Exchange/Produce/Named/' + exchangeName ) @@ -222,17 +226,17 @@ function verifyProduce(tx, exchangeName, routingKey) { } function verifyGet({ tx, exchangeName, routingKey, queue, assertAttr }) { - const isCallback = !!metrics.findSegment(tx.trace.root, 'Callback: ') + const isCallback = !!metrics.findSegment(tx.trace, tx.trace.root, 'Callback: ') const produceName = 'MessageBroker/RabbitMQ/Exchange/Produce/Named/' + exchangeName const consumeName = 'MessageBroker/RabbitMQ/Exchange/Consume/Named/' + queue if (isCallback) { - assertSegments(tx.trace.root, [produceName, consumeName, ['Callback: ']]) + assertSegments(tx.trace, tx.trace.root, [produceName, consumeName, ['Callback: ']]) } else { - assertSegments(tx.trace.root, [produceName, consumeName]) + assertSegments(tx.trace, tx.trace.root, [produceName, consumeName]) } assertMetrics(tx.metrics, [[{ name: produceName }], [{ name: consumeName }]], false, false) if (assertAttr) { - const segment = metrics.findSegment(tx.trace.root, consumeName) + const segment = metrics.findSegment(tx.trace, tx.trace.root, consumeName) const attributes = segment.getAttributes() assert.equal(attributes.host, params.rabbitmq_host, 'should have host on segment') assert.equal(attributes.port, params.rabbitmq_port, 'should have port on segment') @@ -241,7 +245,7 @@ function verifyGet({ tx, exchangeName, routingKey, queue, assertAttr }) { } function verifyPurge(tx) { - const isCallback = !!metrics.findSegment(tx.trace.root, 'Callback: ') + const isCallback = !!metrics.findSegment(tx.trace, tx.trace.root, 'Callback: ') let segments = [] if (isCallback) { @@ -272,16 +276,14 @@ function verifyPurge(tx) { 'MessageBroker/RabbitMQ/Queue/Purge/Temp' ] } - assertSegments(tx.trace.root, segments, 'should have expected segments') + assertSegments(tx.trace, tx.trace.root, segments, 'should have expected segments') assertMetrics(tx.metrics, [[{ name: 'MessageBroker/RabbitMQ/Queue/Purge/Temp' }]], false, false) } -function verifyTransaction(tx, msg) { - const seg = tx.agent.tracer.getSegment() - if (seg) { - assert.equal(seg.transaction.id, tx.id, 'should have correct transaction in ' + msg) - } +function verifyTransaction(agent, tx, msg) { + const transaction = agent.getTransaction() + assert.equal(transaction.id, tx.id, 'should have correct transaction in ' + msg) } function getChannel(amqplib, cb) { diff --git a/test/versioned/amqplib/callback.test.js b/test/versioned/amqplib/callback.test.js index d0f3a9ab86..f14f635125 100644 --- a/test/versioned/amqplib/callback.test.js +++ b/test/versioned/amqplib/callback.test.js @@ -74,7 +74,7 @@ test('amqplib callback instrumentation', async function (t) { helper.runInTransaction(agent, function (tx) { amqplib.connect(amqpUtils.CON_STRING, null, function (err, _conn) { assert.ok(!err, 'should not break connection') - const [segment] = tx.trace.root.children + const [segment] = tx.trace.getChildren(tx.trace.root.id) assert.equal(segment.name, 'amqplib.connect') const attrs = segment.getAttributes() assert.equal(attrs.host, 'localhost') @@ -113,16 +113,16 @@ test('amqplib callback instrumentation', async function (t) { assert.ok(agent.tracer.getSegment(), 'should start in transaction') channel.assertExchange(exchange, 'fanout', null, function (err) { assert.ok(!err, 'should not error asserting exchange') - amqpUtils.verifyTransaction(tx, 'assertExchange') + amqpUtils.verifyTransaction(agent, tx, 'assertExchange') channel.assertQueue('', { exclusive: true }, function (err, result) { assert.ok(!err, 'should not error asserting queue') - amqpUtils.verifyTransaction(tx, 'assertQueue') + amqpUtils.verifyTransaction(agent, tx, 'assertQueue') const queueName = result.queue channel.bindQueue(queueName, exchange, '', null, function (err) { assert.ok(!err, 'should not error binding queue') - amqpUtils.verifyTransaction(tx, 'bindQueue') + amqpUtils.verifyTransaction(agent, tx, 'bindQueue') channel.publish(exchange, '', Buffer.from('hello')) setImmediate(function () { tx.end() @@ -145,16 +145,16 @@ test('amqplib callback instrumentation', async function (t) { helper.runInTransaction(agent, function (tx) { channel.assertExchange(exchange, 'direct', null, function (err) { assert.ok(!err, 'should not error asserting exchange') - amqpUtils.verifyTransaction(tx, 'assertExchange') + amqpUtils.verifyTransaction(agent, tx, 'assertExchange') channel.assertQueue('', { exclusive: true }, function (err, result) { assert.ok(!err, 'should not error asserting queue') - amqpUtils.verifyTransaction(tx, 'assertQueue') + amqpUtils.verifyTransaction(agent, tx, 'assertQueue') const queueName = result.queue channel.bindQueue(queueName, exchange, 'key1', null, function (err) { assert.ok(!err, 'should not error binding queue') - amqpUtils.verifyTransaction(tx, 'bindQueue') + amqpUtils.verifyTransaction(agent, tx, 'bindQueue') channel.publish(exchange, 'key1', Buffer.from('hello')) setImmediate(function () { tx.end() @@ -178,16 +178,16 @@ test('amqplib callback instrumentation', async function (t) { helper.runInTransaction(agent, function (tx) { channel.assertExchange(exchange, 'direct', null, function (err) { assert.ok(!err, 'should not error asserting exchange') - amqpUtils.verifyTransaction(tx, 'assertExchange') + amqpUtils.verifyTransaction(agent, tx, 'assertExchange') channel.assertQueue('', { exclusive: true }, function (err, result) { assert.ok(!err, 'should not error asserting queue') - amqpUtils.verifyTransaction(tx, 'assertQueue') + amqpUtils.verifyTransaction(agent, tx, 'assertQueue') queueName = result.queue channel.bindQueue(queueName, exchange, 'key1', null, function (err) { assert.ok(!err, 'should not error binding queue') - amqpUtils.verifyTransaction(tx, 'bindQueue') + amqpUtils.verifyTransaction(agent, tx, 'bindQueue') channel.purgeQueue(queueName, function (err) { assert.ok(!err, 'should not error purging queue') setImmediate(function () { @@ -221,7 +221,7 @@ test('amqplib callback instrumentation', async function (t) { assert.ok(!err, 'should not cause an error') assert.ok(msg, 'should receive a message') - amqpUtils.verifyTransaction(tx, 'get') + amqpUtils.verifyTransaction(agent, tx, 'get') const body = msg.content.toString('utf8') assert.equal(body, 'hello', 'should receive expected body') @@ -266,7 +266,7 @@ test('amqplib callback instrumentation', async function (t) { assert.ok(!err, 'should not cause an error') assert.ok(msg, 'should receive a message') - amqpUtils.verifyTransaction(tx, 'get') + amqpUtils.verifyTransaction(agent, tx, 'get') const body = msg.content.toString('utf8') assert.equal(body, 'hello', 'should receive expected body') @@ -321,7 +321,7 @@ test('amqplib callback instrumentation', async function (t) { }) helper.runInTransaction(agent, function (tx) { produceTx = tx - amqpUtils.verifyTransaction(tx, 'consume') + amqpUtils.verifyTransaction(agent, tx, 'consume') channel.publish(exchange, 'consume-tx-key', Buffer.from('hello')) }) }) @@ -373,7 +373,7 @@ test('amqplib callback instrumentation', async function (t) { helper.runInTransaction(agent, function (tx) { produceTx = tx assert.ok(!err, 'should not error subscribing consumer') - amqpUtils.verifyTransaction(tx, 'consume') + amqpUtils.verifyTransaction(agent, tx, 'consume') channel.publish(exchange, 'consume-tx-key', Buffer.from('hello')) }) diff --git a/test/versioned/amqplib/promises.test.js b/test/versioned/amqplib/promises.test.js index 1314e80582..3ab386cdf7 100644 --- a/test/versioned/amqplib/promises.test.js +++ b/test/versioned/amqplib/promises.test.js @@ -65,7 +65,7 @@ test('amqplib promise instrumentation', async function (t) { const { agent, amqplib } = t.nr await helper.runInTransaction(agent, async function (tx) { const _conn = await amqplib.connect(amqpUtils.CON_STRING) - const [segment] = tx.trace.root.children + const [segment] = tx.trace.getChildren(tx.trace.root.id) assert.equal(segment.name, 'amqplib.connect') const attrs = segment.getAttributes() assert.equal(attrs.host, 'localhost') @@ -95,12 +95,12 @@ test('amqplib promise instrumentation', async function (t) { await helper.runInTransaction(agent, async function (tx) { assert.ok(agent.tracer.getSegment(), 'should start in transaction') await channel.assertExchange(amqpUtils.FANOUT_EXCHANGE, 'fanout') - amqpUtils.verifyTransaction(tx, 'assertExchange') + amqpUtils.verifyTransaction(agent, tx, 'assertExchange') const result = await channel.assertQueue('', { exclusive: true }) - amqpUtils.verifyTransaction(tx, 'assertQueue') + amqpUtils.verifyTransaction(agent, tx, 'assertQueue') const queueName = result.queue await channel.bindQueue(queueName, amqpUtils.FANOUT_EXCHANGE) - amqpUtils.verifyTransaction(tx, 'bindQueue') + amqpUtils.verifyTransaction(agent, tx, 'bindQueue') channel.publish(amqpUtils.FANOUT_EXCHANGE, '', Buffer.from('hello')) tx.end() amqpUtils.verifyProduce(tx, amqpUtils.FANOUT_EXCHANGE) @@ -111,12 +111,12 @@ test('amqplib promise instrumentation', async function (t) { const { agent, channel } = t.nr await helper.runInTransaction(agent, async function (tx) { await channel.assertExchange(amqpUtils.DIRECT_EXCHANGE, 'direct') - amqpUtils.verifyTransaction(tx, 'assertExchange') + amqpUtils.verifyTransaction(agent, tx, 'assertExchange') const result = await channel.assertQueue('', { exclusive: true }) - amqpUtils.verifyTransaction(tx, 'assertQueue') + amqpUtils.verifyTransaction(agent, tx, 'assertQueue') const queueName = result.queue await channel.bindQueue(queueName, amqpUtils.DIRECT_EXCHANGE, 'key1') - amqpUtils.verifyTransaction(tx, 'bindQueue') + amqpUtils.verifyTransaction(agent, tx, 'bindQueue') channel.publish(amqpUtils.DIRECT_EXCHANGE, 'key1', Buffer.from('hello')) tx.end() amqpUtils.verifyProduce(tx, amqpUtils.DIRECT_EXCHANGE, 'key1') @@ -128,14 +128,14 @@ test('amqplib promise instrumentation', async function (t) { await helper.runInTransaction(agent, async function (tx) { await channel.assertExchange(amqpUtils.DIRECT_EXCHANGE, 'direct') - amqpUtils.verifyTransaction(tx, 'assertExchange') + amqpUtils.verifyTransaction(agent, tx, 'assertExchange') const result = await channel.assertQueue('', { exclusive: true }) - amqpUtils.verifyTransaction(tx, 'assertQueue') + amqpUtils.verifyTransaction(agent, tx, 'assertQueue') const queueName = result.queue await channel.bindQueue(queueName, amqpUtils.DIRECT_EXCHANGE, 'key1') - amqpUtils.verifyTransaction(tx, 'bindQueue') + amqpUtils.verifyTransaction(agent, tx, 'bindQueue') await channel.purgeQueue(queueName) - amqpUtils.verifyTransaction(tx, 'purgeQueue') + amqpUtils.verifyTransaction(agent, tx, 'purgeQueue') tx.end() amqpUtils.verifyPurge(tx) }) @@ -155,7 +155,7 @@ test('amqplib promise instrumentation', async function (t) { const body = msg.content.toString('utf8') assert.equal(body, 'hello', 'should receive expected body') - amqpUtils.verifyTransaction(tx, 'get') + amqpUtils.verifyTransaction(agent, tx, 'get') channel.ack(msg) tx.end() amqpUtils.verifyGet({ @@ -184,7 +184,7 @@ test('amqplib promise instrumentation', async function (t) { const body = msg.content.toString('utf8') assert.equal(body, 'hello', 'should receive expected body') - amqpUtils.verifyTransaction(tx, 'get') + amqpUtils.verifyTransaction(agent, tx, 'get') channel.ack(msg) tx.end() amqpUtils.verifyGet({ @@ -223,7 +223,7 @@ test('amqplib promise instrumentation', async function (t) { }) await helper.runInTransaction(agent, async function (tx) { publishTx = tx - amqpUtils.verifyTransaction(tx, 'consume') + amqpUtils.verifyTransaction(agent, tx, 'consume') channel.publish(exchange, 'consume-tx-key', Buffer.from('hello')) }) await promise @@ -262,7 +262,7 @@ test('amqplib promise instrumentation', async function (t) { }) await helper.runInTransaction(agent, async function (tx) { publishTx = tx - amqpUtils.verifyTransaction(tx, 'consume') + amqpUtils.verifyTransaction(agent, tx, 'consume') channel.publish(exchange, 'consume-tx-key', Buffer.from('hello')) }) await promise diff --git a/test/versioned/aws-sdk-v2/amazon-dax-client.test.js b/test/versioned/aws-sdk-v2/amazon-dax-client.test.js index 6702f44e9c..169c46a4ac 100644 --- a/test/versioned/aws-sdk-v2/amazon-dax-client.test.js +++ b/test/versioned/aws-sdk-v2/amazon-dax-client.test.js @@ -61,11 +61,19 @@ test('amazon-dax-client', async (t) => { const root = transaction.trace.root // Won't have the attributes cause not making web request... - const segments = common.getMatchingSegments(root, common.DATASTORE_PATTERN) + const segments = common.getMatchingSegments({ + trace: transaction.trace, + segment: root, + pattern: common.DATASTORE_PATTERN + }) assert.equal(segments.length, 1) - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: transaction.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') const segment = segments[0] diff --git a/test/versioned/aws-sdk-v2/dynamodb.test.js b/test/versioned/aws-sdk-v2/dynamodb.test.js index f9a06c1e1e..eabaf38b3b 100644 --- a/test/versioned/aws-sdk-v2/dynamodb.test.js +++ b/test/versioned/aws-sdk-v2/dynamodb.test.js @@ -92,11 +92,19 @@ test('DynamoDB', async (t) => { function finish(end, tests, tx) { const root = tx.trace.root - const segments = common.checkAWSAttributes(root, common.DATASTORE_PATTERN) + const segments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.DATASTORE_PATTERN + }) assert.equal(segments.length, tests.length, `should have ${tests.length} aws datastore segments`) - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') segments.forEach((segment, i) => { diff --git a/test/versioned/aws-sdk-v2/http-services.test.js b/test/versioned/aws-sdk-v2/http-services.test.js index fef4215c6d..d469799ddf 100644 --- a/test/versioned/aws-sdk-v2/http-services.test.js +++ b/test/versioned/aws-sdk-v2/http-services.test.js @@ -238,7 +238,11 @@ test('AWS HTTP Services', async (t) => { }) function finish(end, service, operation, tx) { - const externals = common.checkAWSAttributes(tx.trace.root, common.EXTERN_PATTERN) + const externals = common.checkAWSAttributes({ + trace: tx.trace, + segment: tx.trace.root, + pattern: common.EXTERN_PATTERN + }) if (assert.equal(externals.length, 1, 'should have an aws external')) { const attrs = externals[0].attributes.get(common.SEGMENT_DESTINATION) match(attrs, { diff --git a/test/versioned/aws-sdk-v2/s3.test.js b/test/versioned/aws-sdk-v2/s3.test.js index 50a2fe6d84..69a45ae9e5 100644 --- a/test/versioned/aws-sdk-v2/s3.test.js +++ b/test/versioned/aws-sdk-v2/s3.test.js @@ -82,7 +82,11 @@ test('S3 buckets', async (t) => { }) function finish(end, tx) { - const externals = common.checkAWSAttributes(tx.trace.root, common.EXTERN_PATTERN) + const externals = common.checkAWSAttributes({ + trace: tx.trace, + segment: tx.trace.root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externals.length, 3, 'should have 3 aws externals') const [head, create, del] = externals checkAttrs(head, 'headBucket') diff --git a/test/versioned/aws-sdk-v2/sns.test.js b/test/versioned/aws-sdk-v2/sns.test.js index 5122db0c84..8ab9e9e38e 100644 --- a/test/versioned/aws-sdk-v2/sns.test.js +++ b/test/versioned/aws-sdk-v2/sns.test.js @@ -72,10 +72,18 @@ test('SNS', async (t) => { function finish(end, tx) { const root = tx.trace.root - const messages = common.checkAWSAttributes(root, common.SNS_PATTERN) + const messages = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.SNS_PATTERN + }) assert.equal(messages.length, 1, 'should have 1 message broker segment') - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') const attrs = messages[0].attributes.get(common.SEGMENT_DESTINATION) diff --git a/test/versioned/aws-sdk-v2/sqs.test.js b/test/versioned/aws-sdk-v2/sqs.test.js index db711a4db9..02226e3b6c 100644 --- a/test/versioned/aws-sdk-v2/sqs.test.js +++ b/test/versioned/aws-sdk-v2/sqs.test.js @@ -154,7 +154,11 @@ function finish({ const expectedSegmentCount = 3 const root = transaction.trace.root - const segments = common.checkAWSAttributes(root, common.SQS_PATTERN) + const segments = common.checkAWSAttributes({ + trace: transaction.trace, + segment: root, + pattern: common.SQS_PATTERN + }) assert.equal( segments.length, @@ -162,7 +166,11 @@ function finish({ `should have ${expectedSegmentCount} AWS MessageBroker/SQS segments` ) - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: transaction.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') const [sendMessage, sendMessageBatch, receiveMessage] = segments diff --git a/test/versioned/aws-sdk-v3/bedrock-chat-completions.test.js b/test/versioned/aws-sdk-v3/bedrock-chat-completions.test.js index 119189d0a9..e7dc8dcce7 100644 --- a/test/versioned/aws-sdk-v3/bedrock-chat-completions.test.js +++ b/test/versioned/aws-sdk-v3/bedrock-chat-completions.test.js @@ -110,6 +110,7 @@ test.afterEach(afterEach) assert.equal(response.$metadata.requestId, expected.headers['x-amzn-requestid']) assert.deepEqual(body, expected.body) assertSegments( + tx.trace, tx.trace.root, ['Llm/completion/Bedrock/InvokeModelCommand', [expectedExternalPath(modelId)]], { exact: false } @@ -301,6 +302,7 @@ test.afterEach(afterEach) }) assertSegments( + tx.trace, tx.trace.root, ['Llm/completion/Bedrock/InvokeModelCommand', [expectedExternalPath(modelId)]], { exact: false } @@ -438,6 +440,7 @@ test('ai21: should properly create errors on create completion (streamed)', asyn }) assertSegments( + tx.trace, tx.trace.root, [ 'Llm/completion/Bedrock/InvokeModelWithResponseStreamCommand', @@ -501,6 +504,7 @@ test('models that do not support streaming should be handled', async (t) => { }) assertSegments( + tx.trace, tx.trace.root, [ 'Llm/embedding/Bedrock/InvokeModelWithResponseStreamCommand', diff --git a/test/versioned/aws-sdk-v3/bedrock-embeddings.test.js b/test/versioned/aws-sdk-v3/bedrock-embeddings.test.js index 99c96ab7e0..5db0c2c898 100644 --- a/test/versioned/aws-sdk-v3/bedrock-embeddings.test.js +++ b/test/versioned/aws-sdk-v3/bedrock-embeddings.test.js @@ -68,6 +68,7 @@ test.afterEach(afterEach) assert.equal(response.$metadata.requestId, expected.headers['x-amzn-requestid']) assert.deepEqual(body, expected.body) assertSegments( + tx.trace, tx.trace.root, ['Llm/embedding/Bedrock/InvokeModelCommand', [expectedExternalPath(modelId)]], { exact: false } @@ -87,17 +88,18 @@ test.afterEach(afterEach) const events = agent.customEventAggregator.events.toArray() assert.equal(events.length, 1) const embedding = events.filter(([{ type }]) => type === 'LlmEmbedding')[0] + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedEmbedding = { id: /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/, appName: 'New Relic for Node.js tests', request_id: '743dd35b-744b-4ddf-b5c6-c0f3de2e3142', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': modelId, vendor: 'bedrock', ingest_source: 'Node', 'request.model': modelId, - duration: tx.trace.root.children[0].getDurationInMillis(), + duration: segment.getDurationInMillis(), input: prompt, error: false } @@ -157,6 +159,7 @@ test.afterEach(afterEach) }) assertSegments( + tx.trace, tx.trace.root, ['Llm/embedding/Bedrock/InvokeModelCommand', [expectedExternalPath(modelId)]], { exact: false } @@ -164,17 +167,18 @@ test.afterEach(afterEach) const events = agent.customEventAggregator.events.toArray() assert.equal(events.length, 1) const embedding = events.filter(([{ type }]) => type === 'LlmEmbedding')[0] + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedEmbedding = { id: /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/, appName: 'New Relic for Node.js tests', request_id: '743dd35b-744b-4ddf-b5c6-c0f3de2e3142', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': modelId, vendor: 'bedrock', ingest_source: 'Node', 'request.model': modelId, - duration: tx.trace.root.children[0].getDurationInMillis(), + duration: segment.getDurationInMillis(), input: prompt, error: true } diff --git a/test/versioned/aws-sdk-v3/client-dynamodb.test.js b/test/versioned/aws-sdk-v3/client-dynamodb.test.js index 587f9bb619..fb4ba40eb3 100644 --- a/test/versioned/aws-sdk-v3/client-dynamodb.test.js +++ b/test/versioned/aws-sdk-v3/client-dynamodb.test.js @@ -116,7 +116,11 @@ test('DynamoDB', async (t) => { } tx.end() const root = tx.trace.root - const segments = common.checkAWSAttributes(root, common.DATASTORE_PATTERN) + const segments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.DATASTORE_PATTERN + }) segments.forEach((segment) => { const attrs = segment.attributes.get(common.SEGMENT_DESTINATION) @@ -186,7 +190,11 @@ function createCommands({ lib, tableName }) { function finish({ commands, tx, setDatastoreSpy }) { const root = tx.trace.root - const segments = common.checkAWSAttributes(root, common.DATASTORE_PATTERN) + const segments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.DATASTORE_PATTERN + }) assert.equal( segments.length, @@ -194,7 +202,11 @@ function finish({ commands, tx, setDatastoreSpy }) { `should have ${commands.length} AWS datastore segments` ) - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') segments.forEach((segment, i) => { diff --git a/test/versioned/aws-sdk-v3/common.js b/test/versioned/aws-sdk-v3/common.js index 92259c8193..f1aa64eb08 100644 --- a/test/versioned/aws-sdk-v3/common.js +++ b/test/versioned/aws-sdk-v3/common.js @@ -17,7 +17,7 @@ const assert = require('node:assert') const SEGMENT_DESTINATION = TRANS_SEGMENT const helper = require('../../lib/agent_helper') -function checkAWSAttributes(segment, pattern, markedSegments = []) { +function checkAWSAttributes({ trace, segment, pattern, markedSegments = [] }) { const expectedAttrs = { 'aws.operation': String, 'aws.service': String, @@ -30,27 +30,33 @@ function checkAWSAttributes(segment, pattern, markedSegments = []) { const attrs = segment.attributes.get(TRANS_SEGMENT) match(attrs, expectedAttrs) } - segment.children.forEach((child) => { - checkAWSAttributes(child, pattern, markedSegments) + const children = trace.getChildren(segment.id) + children.forEach((child) => { + checkAWSAttributes({ trace, segment: child, pattern, markedSegments }) }) return markedSegments } -function getMatchingSegments(segment, pattern, markedSegments = []) { +function getMatchingSegments({ trace, segment, pattern, markedSegments = [] }) { if (pattern.test(segment.name)) { markedSegments.push(segment) } - segment.children.forEach((child) => { - getMatchingSegments(child, pattern, markedSegments) + const children = trace.getChildren(segment.id) + children.forEach((child) => { + getMatchingSegments({ trace, segment: child, pattern, markedSegments }) }) return markedSegments } function checkExternals({ service, operations, tx, end }) { - const externals = checkAWSAttributes(tx.trace.root, EXTERN_PATTERN) + const externals = checkAWSAttributes({ + trace: tx.trace, + segment: tx.trace.root, + pattern: EXTERN_PATTERN + }) assert.equal( externals.length, operations.length, @@ -71,11 +77,12 @@ function checkExternals({ service, operations, tx, end }) { } function assertChatCompletionMessages({ tx, chatMsgs, expectedId, modelId, prompt, resContent }) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const baseMsg = { appName: 'New Relic for Node.js tests', request_id: 'eda0760a-c3f0-4fc1-9a1e-75559d642866', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': modelId, vendor: 'bedrock', ingest_source: 'Node', @@ -111,18 +118,19 @@ function assertChatCompletionMessages({ tx, chatMsgs, expectedId, modelId, promp } function assertChatCompletionSummary({ tx, modelId, chatSummary, error = false, numMsgs = 2 }) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedChatSummary = { id: /\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/, appName: 'New Relic for Node.js tests', request_id: 'eda0760a-c3f0-4fc1-9a1e-75559d642866', 'llm.conversation_id': 'convo-id', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': modelId, vendor: 'bedrock', ingest_source: 'Node', 'request.model': modelId, - duration: tx.trace.root.children[0].getDurationInMillis(), + duration: segment.getDurationInMillis(), 'response.number_of_messages': error ? 1 : numMsgs, 'response.choices.finish_reason': error ? undefined : 'endoftext', 'request.temperature': 0.5, diff --git a/test/versioned/aws-sdk-v3/lambda.test.js b/test/versioned/aws-sdk-v3/lambda.test.js index c1f012443f..fc0c369705 100644 --- a/test/versioned/aws-sdk-v3/lambda.test.js +++ b/test/versioned/aws-sdk-v3/lambda.test.js @@ -21,7 +21,7 @@ const { match } = require('../../lib/custom-assertions') function checkEntityLinkingSegments({ operations, tx, end }) { const root = tx.trace.root - const segments = checkAWSAttributes(root, EXTERN_PATTERN) + const segments = checkAWSAttributes({ trace: tx.trace, segment: root, pattern: EXTERN_PATTERN }) const accountId = tx.agent.config.cloud.aws.account_id const testFunctionName = 'funcName' @@ -47,7 +47,7 @@ function checkNonLinkableSegments({ operations, tx, end }) { // When no account ID or ARN is available, make sure not to set cloud resource id or platform const root = tx.trace.root - const segments = checkAWSAttributes(root, EXTERN_PATTERN) + const segments = checkAWSAttributes({ trace: tx.trace, segment: root, pattern: EXTERN_PATTERN }) const accountId = tx.agent.config?.cloud?.aws?.account_id assert(segments.length > 0, 'should have segments') diff --git a/test/versioned/aws-sdk-v3/lib-dynamodb.test.js b/test/versioned/aws-sdk-v3/lib-dynamodb.test.js index c85bf1da3a..dd5507c3b1 100644 --- a/test/versioned/aws-sdk-v3/lib-dynamodb.test.js +++ b/test/versioned/aws-sdk-v3/lib-dynamodb.test.js @@ -136,11 +136,19 @@ test('DynamoDB', async (t) => { function finish(end, tests, tx) { const root = tx.trace.root - const segments = common.checkAWSAttributes(root, common.DATASTORE_PATTERN) + const segments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.DATASTORE_PATTERN + }) assert.equal(segments.length, tests.length, `should have ${tests.length} aws datastore segments`) - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') const accountId = tx.agent.config.cloud.aws.account_id diff --git a/test/versioned/aws-sdk-v3/sns.test.js b/test/versioned/aws-sdk-v3/sns.test.js index 3e58a839f8..9b2af8d465 100644 --- a/test/versioned/aws-sdk-v3/sns.test.js +++ b/test/versioned/aws-sdk-v3/sns.test.js @@ -207,11 +207,19 @@ test('SNS', async (t) => { function finish(end, tx, destName, setLibrarySpy) { const root = tx.trace.root - const messages = common.checkAWSAttributes(root, common.SNS_PATTERN) + const messages = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.SNS_PATTERN + }) assert.equal(messages.length, 1, 'should have 1 message broker segment') assert.ok(messages[0].name.endsWith(destName), 'should have appropriate destination') - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: tx.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') const attrs = messages[0].attributes.get(common.SEGMENT_DESTINATION) diff --git a/test/versioned/aws-sdk-v3/sqs.test.js b/test/versioned/aws-sdk-v3/sqs.test.js index 8f4b523fab..05471b00ad 100644 --- a/test/versioned/aws-sdk-v3/sqs.test.js +++ b/test/versioned/aws-sdk-v3/sqs.test.js @@ -91,7 +91,11 @@ function finish({ transaction, queueName, setLibrarySpy }) { const expectedSegmentCount = 3 const root = transaction.trace.root - const segments = common.checkAWSAttributes(root, common.SQS_PATTERN) + const segments = common.checkAWSAttributes({ + trace: transaction.trace, + segment: root, + pattern: common.SQS_PATTERN + }) assert.equal( segments.length, @@ -99,7 +103,11 @@ function finish({ transaction, queueName, setLibrarySpy }) { `should have ${expectedSegmentCount} AWS MessageBroker/SQS segments` ) - const externalSegments = common.checkAWSAttributes(root, common.EXTERN_PATTERN) + const externalSegments = common.checkAWSAttributes({ + trace: transaction.trace, + segment: root, + pattern: common.EXTERN_PATTERN + }) assert.equal(externalSegments.length, 0, 'should not have any External segments') const [sendMessage, sendMessageBatch, receiveMessage] = segments diff --git a/test/versioned/cassandra-driver/query.test.js b/test/versioned/cassandra-driver/query.test.js index cf0f61da58..dd1545f6b9 100644 --- a/test/versioned/cassandra-driver/query.test.js +++ b/test/versioned/cassandra-driver/query.test.js @@ -111,11 +111,8 @@ test('executeBatch - callback style', (t, end) => { 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' - ) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 1, 'there should be only one child of the root') verifyTrace(agent, transaction.trace, `${KS}.${FAM}`) transaction.end() checkMetric(agent) @@ -137,17 +134,14 @@ test('executeBatch - promise style', (t, end) => { client .batch(insArr, { hints }) .then(() => { + assert.ok(agent.getTransaction(), 'transaction still should be visible') 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' - ) + const children = transaction.trace.getChildren(transaction.trace.root.id) + assert.equal(children.length, 2, 'there should be two children of the root') verifyTrace(agent, transaction.trace, `${KS}.${FAM}`) transaction.end() checkMetric(agent) @@ -232,6 +226,7 @@ function verifyTrace(agent, trace, table) { assert.ok(trace.root, 'root element should exist') const setSegment = findSegment( + trace, trace.root, 'Datastore/statement/Cassandra/' + table + '/insert/batch' ) @@ -241,18 +236,22 @@ function verifyTrace(agent, trace, table) { if (setSegment) { verifyTraceSegment(agent, setSegment, 'insert/batch') + const children = trace.getChildren(setSegment.id) assert.ok( - setSegment.children.length >= 2, + 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') + const getSegment = findSegment( + trace, + trace.root, + 'Datastore/statement/Cassandra/' + table + '/select' + ) assert.ok(getSegment, 'trace segment for select should exist') if (getSegment) { + const getChildren = trace.getChildren(getSegment.id) verifyTraceSegment(agent, getSegment, 'select') - - assert.ok(getSegment.children.length >= 1, 'get should have a callback/promise segment') + assert.ok(getChildren.length >= 1, 'get should have a callback/promise segment') assert.ok(getSegment.timer.hrDuration, 'trace segment should have ended') } } diff --git a/test/versioned/connect/route.test.js b/test/versioned/connect/route.test.js index d6525ce692..977fc14762 100644 --- a/test/versioned/connect/route.test.js +++ b/test/versioned/connect/route.test.js @@ -39,7 +39,7 @@ test('should properly name transaction from route name', async (t) => { plan.equal(tx.verb, 'GET', 'HTTP method is GET') plan.equal(tx.statusCode, 200, 'status code is OK') plan.ok(tx.trace, 'transaction has trace') - const web = tx.trace.root.children[0] + const [web] = tx.trace.getChildren(tx.trace.root.id) plan.ok(web, 'trace has web segment') plan.equal(web.name, tx.name, 'segment name and transaction name match') plan.equal(web.partialName, 'Connect/GET//foo', 'should have partial name for apdex') @@ -73,7 +73,7 @@ test('should default to `/` when no route is specified', async (t) => { plan.equal(tx.verb, 'GET', 'HTTP method is GET') plan.equal(tx.statusCode, 200, 'status code is OK') plan.ok(tx.trace, 'transaction has trace') - const web = tx.trace.root.children[0] + const [web] = tx.trace.getChildren(tx.trace.root.id) plan.ok(web, 'trace has web segment') plan.equal(web.name, tx.name, 'segment name and transaction name match') plan.equal(web.partialName, 'Connect/GET//', 'should have partial name for apdex') diff --git a/test/versioned/disabled-instrumentation/disabled-express.test.js b/test/versioned/disabled-instrumentation/disabled-express.test.js index 83763685c4..fd67eeabda 100644 --- a/test/versioned/disabled-instrumentation/disabled-express.test.js +++ b/test/versioned/disabled-instrumentation/disabled-express.test.js @@ -36,7 +36,7 @@ test('should still record child segments if express instrumentation is disabled' assert.equal(tx.name, 'WebTransaction/NormalizedUri/*', 'should not name transactions') const rootSegment = tx.trace.root const expectedSegments = ['WebTransaction/NormalizedUri/*', ['Datastore/operation/Redis/get']] - assertSegments(rootSegment, expectedSegments) + assertSegments(tx.trace, rootSegment, expectedSegments) resolve() }) }) diff --git a/test/versioned/disabled-instrumentation/disabled-ioredis.test.js b/test/versioned/disabled-instrumentation/disabled-ioredis.test.js index c433d08789..684e217ce6 100644 --- a/test/versioned/disabled-instrumentation/disabled-ioredis.test.js +++ b/test/versioned/disabled-instrumentation/disabled-ioredis.test.js @@ -48,7 +48,7 @@ test('Disabled PG scenarios', async (t) => { await collection.countDocuments() await redisClient.get('bar') tx.end() - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'Datastore/statement/MongoDB/disabled-inst-test/aggregate', 'Datastore/statement/MongoDB/disabled-inst-test/next' ]) @@ -65,7 +65,7 @@ test('Disabled PG scenarios', async (t) => { redisClient.get('bar', (innerErr) => { tx.end() assert.equal(innerErr, null) - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'Datastore/statement/MongoDB/disabled-inst-test/aggregate', 'Datastore/statement/MongoDB/disabled-inst-test/next' ]) diff --git a/test/versioned/elastic/elasticsearch.test.js b/test/versioned/elastic/elasticsearch.test.js index 74bbed80ee..a50d45f203 100644 --- a/test/versioned/elastic/elasticsearch.test.js +++ b/test/versioned/elastic/elasticsearch.test.js @@ -104,8 +104,7 @@ test('Elasticsearch instrumentation', async (t) => { assert.ok(transaction, 'transaction should be visible') await client.indices.create({ index }) const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, `Datastore/statement/ElasticSearch/${index}/index.create`, @@ -120,8 +119,7 @@ test('Elasticsearch instrumentation', async (t) => { await bulkInsert({ client, pkgVersion }) assert.ok(transaction, 'transaction should still be visible after bulk create') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/any/bulk.create', @@ -145,10 +143,9 @@ test('Elasticsearch instrumentation', async (t) => { }) assert.ok(transaction, 'transaction should still be visible after bulk create') const trace = transaction.trace - 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] + const [firstChild, secondChild] = trace.getChildren(trace.root.id) + assert.ok(firstChild) assert.equal( secondChild.name, 'Datastore/statement/ElasticSearch/any/bulk.create', @@ -169,8 +166,7 @@ test('Elasticsearch instrumentation', async (t) => { assert.ok(search, 'search should return a result') assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, `Datastore/statement/ElasticSearch/${DB_INDEX_2}/search`, @@ -205,8 +201,7 @@ test('Elasticsearch instrumentation', async (t) => { assert.ok(search, 'search should return a result') assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, `Datastore/statement/ElasticSearch/${DB_INDEX}/search`, @@ -244,8 +239,7 @@ test('Elasticsearch instrumentation', async (t) => { assert.ok(search, 'search should return a result') assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/any/search', @@ -293,8 +287,7 @@ test('Elasticsearch instrumentation', async (t) => { 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 - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/any/msearch.create', @@ -335,8 +328,7 @@ test('Elasticsearch instrumentation', async (t) => { 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 - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'timers.setTimeout', @@ -423,7 +415,7 @@ test('Elasticsearch instrumentation', async (t) => { ...documentProp }) - const createSegment = transaction.trace.root.children[0] + const [createSegment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = createSegment.getAttributes() assert.equal(attributes.host, undefined, 'should not have host attribute') assert.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') @@ -446,7 +438,7 @@ test('Elasticsearch instrumentation', async (t) => { } catch (e) { assert.ok(e, 'should not be able to create an index named _search') } - const firstChild = transaction?.trace?.root?.children[0] + const [firstChild] = transaction.trace.getChildren(transaction.trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/ElasticSearch/_search/index.create', diff --git a/test/versioned/elastic/elasticsearchNoop.test.js b/test/versioned/elastic/elasticsearchNoop.test.js index 1ffd758f6b..c31bfb0686 100644 --- a/test/versioned/elastic/elasticsearchNoop.test.js +++ b/test/versioned/elastic/elasticsearchNoop.test.js @@ -46,7 +46,7 @@ test('Elasticsearch instrumentation', async (t) => { } catch (e) { assert.ok(!e, 'should not error') } - const firstChild = transaction?.trace?.root?.children[0] + const [firstChild] = transaction.trace.getChildren(transaction.trace.root.id) assert.equal( firstChild.name, `External/localhost:9200/${DB_INDEX}`, diff --git a/test/versioned/express-esm/segments.test.mjs b/test/versioned/express-esm/segments.test.mjs index b200e66de3..665e82580e 100644 --- a/test/versioned/express-esm/segments.test.mjs +++ b/test/versioned/express-esm/segments.test.mjs @@ -58,6 +58,7 @@ test('first two segments are built-in Express middleware', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], assertSegmentsOptions @@ -87,6 +88,7 @@ test('segments for route handler', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], assertSegmentsOptions @@ -104,6 +106,7 @@ test('route function names are in segment names', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions @@ -121,6 +124,7 @@ test('middleware mounted on a path should produce correct names', async (t) => { const { transaction } = await runTest({ agent, server, endpoint: '/test/1' }) const routeSegment = findSegment( + transaction.trace, transaction.trace.root, NAMES.EXPRESS.MIDDLEWARE + 'handler//test/:id' ) @@ -144,6 +148,7 @@ test('each handler in route has its own segment', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Route Path: /test', @@ -170,6 +175,7 @@ test('segments for routers', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/router1/test' }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router1', @@ -202,6 +208,7 @@ test('two root routers', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /', @@ -238,6 +245,7 @@ test('router mounted as a route handler', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, [ `Expressjs/Route Path: ${segmentPath}`, @@ -268,6 +276,7 @@ test('segments for routers', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/router1/test' }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router1', @@ -300,6 +309,7 @@ test('segments for sub-app', async (t) => { : 'Expressjs/Mounted App: /subapp1' assertSegments( + transaction.trace, rootSegment, [firstSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]], assertSegmentsOptions @@ -338,6 +348,7 @@ test('segments for sub-app router', async (t) => { : 'Expressjs/Mounted App: /subapp1' assertSegments( + transaction.trace, rootSegment, [ firstSegment, @@ -375,6 +386,7 @@ test('segments for wildcard', async (t) => { : 'Expressjs/Mounted App: /subapp1' assertSegments( + transaction.trace, rootSegment, [firstSegment, ['Expressjs/Route Path: /:app', [NAMES.EXPRESS.MIDDLEWARE + '']]], assertSegmentsOptions @@ -409,6 +421,7 @@ test('router with subapp', async (t) => { : 'Expressjs/Mounted App: /subapp1' assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router1', @@ -432,7 +445,12 @@ test('mounted middleware', async (t) => { }) const { rootSegment, transaction } = await runTest({ agent, server }) - assertSegments(rootSegment, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test'], assertSegmentsOptions) + assertSegments( + transaction.trace, + rootSegment, + [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test'], + assertSegmentsOptions + ) checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test']) }) @@ -450,6 +468,7 @@ test('error middleware', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Route Path: /test', @@ -488,6 +507,7 @@ test('error handler in router', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router', @@ -531,6 +551,7 @@ test('error handler in second router', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router1', @@ -574,6 +595,7 @@ test('error handler outside of router', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router', @@ -614,6 +636,7 @@ test('error handler outside of two routers', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint }) assertSegments( + transaction.trace, rootSegment, [ 'Expressjs/Router: /router1', @@ -645,6 +668,7 @@ test('when using a route variable', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/a/b' }) assertSegments( + transaction.trace, rootSegment, ['Expressjs/Route Path: /:foo/:bar', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions @@ -667,6 +691,7 @@ test('when using a string pattern in path', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/abcd' }) assertSegments( + transaction.trace, rootSegment, [`Expressjs/Route Path: ${path}`, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions @@ -684,6 +709,7 @@ test('when using a regular expression in path', async (t) => { const { rootSegment, transaction } = await runTest({ agent, server, endpoint: '/a' }) assertSegments( + transaction.trace, rootSegment, ['Expressjs/Route Path: /a/', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions @@ -694,7 +720,7 @@ test('when using a regular expression in path', async (t) => { async function runTest({ agent, server, endpoint = '/test', errors = 0 }) { const transaction = await makeRequestAndFinishTransaction({ server, agent, endpoint }) - const rootSegment = transaction.trace.root.children[0] + const [rootSegment] = transaction.trace.getChildren(transaction.trace.root.id) assert.equal(agent.errors.traceAggregator.errors.length, errors, `should have ${errors} errors`) return { rootSegment, transaction } diff --git a/test/versioned/express-esm/transaction-naming.test.mjs b/test/versioned/express-esm/transaction-naming.test.mjs index 78f196e1dc..4a998177ff 100644 --- a/test/versioned/express-esm/transaction-naming.test.mjs +++ b/test/versioned/express-esm/transaction-naming.test.mjs @@ -401,7 +401,8 @@ test('Express transaction names are unaffected by errorware', async (t) => { const promise = new Promise((resolve) => { transactionHandler = function (tx) { const expected = 'WebTransaction/Expressjs/GET//test' - plan.equal(tx.trace.root.children[0].name, expected) + const [baseSegment] = tx.trace.getChildren(tx.trace.root.id) + plan.equal(baseSegment.name, expected) resolve() } }) diff --git a/test/versioned/express/async-handlers.test.js b/test/versioned/express/async-handlers.test.js index da6cb2e24b..1a0bf31103 100644 --- a/test/versioned/express/async-handlers.test.js +++ b/test/versioned/express/async-handlers.test.js @@ -36,8 +36,8 @@ test('async handlers', { skip: !isExpress5() }, async (t) => { }) const tx = await runTest(t, '/test') - const [children] = tx.trace.root.children - const [mw, handler] = children.children + const [child] = tx.trace.getChildren(tx.trace.root.id) + const [mw, handler] = tx.trace.getChildren(child.id) assert.ok( Math.ceil(mw.getDurationInMillis()) >= mwTimeout, `should be at least ${mwTimeout} for middleware segment` diff --git a/test/versioned/express/bare-router.test.js b/test/versioned/express/bare-router.test.js index 1c9712f070..73459f38b6 100644 --- a/test/versioned/express/bare-router.test.js +++ b/test/versioned/express/bare-router.test.js @@ -35,7 +35,7 @@ test('Express router introspection', async function (t) { plan.equal(transaction.verb, 'GET', 'HTTP method is GET') plan.ok(transaction.trace, 'transaction has trace') - const web = transaction.trace.root.children[0] + const [web] = transaction.trace.getChildren(transaction.trace.root.id) plan.ok(web, 'trace has web segment') plan.equal(web.name, transaction.name, 'segment name and transaction name match') diff --git a/test/versioned/express/client-disconnect.test.js b/test/versioned/express/client-disconnect.test.js index 6536ba5779..860d099806 100644 --- a/test/versioned/express/client-disconnect.test.js +++ b/test/versioned/express/client-disconnect.test.js @@ -44,6 +44,7 @@ test('Client Premature Disconnection', { timeout: 3000 }, (t, end) => { agent.on('transactionFinished', (transaction) => { assertSegments( + transaction.trace, transaction.trace.root, [ 'WebTransaction/Expressjs/POST//test', diff --git a/test/versioned/express/router-params.test.js b/test/versioned/express/router-params.test.js index a273a9c84b..a4aec2f475 100644 --- a/test/versioned/express/router-params.test.js +++ b/test/versioned/express/router-params.test.js @@ -46,7 +46,7 @@ test('Express router introspection', async function (t) { plan.equal(transaction.verb, 'GET', 'HTTP method is GET') plan.ok(transaction.trace, 'transaction has trace') - const web = transaction.trace.root.children[0] + const [web] = transaction.trace.getChildren(transaction.trace.root.id) plan.ok(web, 'trace has web segment') plan.equal(web.name, transaction.name, 'segment name and transaction name match') plan.equal( diff --git a/test/versioned/express/segments.test.js b/test/versioned/express/segments.test.js index 7eb98b0bd5..3758a56a28 100644 --- a/test/versioned/express/segments.test.js +++ b/test/versioned/express/segments.test.js @@ -36,10 +36,11 @@ test('first two segments are built-in Express middlewares', function (t, end) { res.end() }) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { // TODO: check for different HTTP methods assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], assertSegmentsOptions ) @@ -59,7 +60,7 @@ test('middleware with child segment gets named correctly', function (t, end) { }, 1) }) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { checkMetrics(transaction.metrics, [NAMES.EXPRESS.MIDDLEWARE + '//test']) end() @@ -73,9 +74,10 @@ test('segments for route handler', function (t, end) { res.end() }) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], assertSegmentsOptions ) @@ -93,9 +95,10 @@ test('route function names are in segment names', function (t, end) { res.end() }) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions ) @@ -113,9 +116,10 @@ test('middleware mounted on a path should produce correct names', function (t, e res.send() }) - runTest(t, '/test/1', function (segments, transaction) { + runTest(t, '/test/1', function (root, transaction) { const segment = findSegment( - transaction.trace.root, + transaction.trace, + root, NAMES.EXPRESS.MIDDLEWARE + 'handler//test/:id' ) assert.ok(segment) @@ -139,9 +143,10 @@ test('each handler in route has its own segment', function (t, end) { } ) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + 'handler1', NAMES.EXPRESS.MIDDLEWARE + 'handler2'] @@ -168,9 +173,10 @@ test('segments for routers', function (t, end) { app.use('/router1', router) - runTest(t, '/router1/test', function (segments, transaction) { + runTest(t, '/router1/test', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router1', ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] @@ -203,9 +209,10 @@ test('two root routers', function (t, end) { }) app.use('/', router2) - runTest(t, '/test', function (segments, transaction) { + runTest(t, '/test', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /', 'Expressjs/Router: /', @@ -241,9 +248,10 @@ test('router mounted as a route handler', function (t, end) { } app.get(path, router1) - runTest(t, '/test', function (segments, transaction) { + runTest(t, '/test', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ `Expressjs/Route Path: ${segmentPath}`, [ @@ -274,9 +282,10 @@ test('segments for routers', function (t, end) { app.use('/router1', router) - runTest(t, '/router1/test', function (segments, transaction) { + runTest(t, '/router1/test', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router1', ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']] @@ -304,14 +313,15 @@ test('segments for sub-app', function (t, end) { app.use('/subapp1', subapp) - runTest(t, '/subapp1/test', function (segments, transaction) { + runTest(t, '/subapp1/test', function (root, transaction) { // express 5 no longer handles child routers as mounted applications const firstSegment = isExpress5 ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' : 'Expressjs/Mounted App: /subapp1' assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [firstSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]], assertSegmentsOptions ) @@ -345,13 +355,14 @@ test('segments for sub-app router', function (t, end) { app.use('/subapp1', subapp) - runTest(t, '/subapp1/test', function (segments, transaction) { + runTest(t, '/subapp1/test', function (root, transaction) { // express 5 no longer handles child routers as mounted applications const firstSegment = isExpress5 ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' : 'Expressjs/Mounted App: /subapp1' assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ firstSegment, [ @@ -384,13 +395,14 @@ test('segments for wildcard', function (t, end) { app.use('/subapp1', subapp) - runTest(t, '/subapp1/test', function (segments, transaction) { + runTest(t, '/subapp1/test', function (root, transaction) { // express 5 no longer handles child routers as mounted applications const firstSegment = isExpress5 ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' : 'Expressjs/Mounted App: /subapp1' assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [firstSegment, ['Expressjs/Route Path: /:app', [NAMES.EXPRESS.MIDDLEWARE + '']]], assertSegmentsOptions ) @@ -416,13 +428,14 @@ test('router with subapp', function (t, end) { router.use('/subapp1', subapp) app.use('/router1', router) - runTest(t, '/router1/subapp1/test', function (segments, transaction) { + runTest(t, '/router1/subapp1/test', function (root, transaction) { // express 5 no longer handles child routers as mounted applications const subAppSegment = isExpress5 ? NAMES.EXPRESS.MIDDLEWARE + 'app//subapp1' : 'Expressjs/Mounted App: /subapp1' assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router1', [subAppSegment, ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']]] @@ -447,9 +460,10 @@ test('mounted middleware', function (t, end) { res.end() }) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler//test'], assertSegmentsOptions ) @@ -471,9 +485,10 @@ test('error middleware', function (t, end) { res.end() }) - runTest(t, function (segments, transaction) { + runTest(t, function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + ''], @@ -518,9 +533,10 @@ test('error handler in router', function (t, end) { endpoint, errors: 0 }, - function (segments, transaction) { + function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router', [ @@ -571,9 +587,10 @@ test('error handler in second router', function (t, end) { endpoint, errors: 0 }, - function (segments, transaction) { + function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router1', [ @@ -624,9 +641,10 @@ test('error handler outside of router', function (t, end) { endpoint, errors: 0 }, - function (segments, transaction) { + function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router', ['Expressjs/Route Path: /test', [NAMES.EXPRESS.MIDDLEWARE + '']], @@ -674,9 +692,10 @@ test('error handler outside of two routers', function (t, end) { endpoint, errors: 0 }, - function (segments, transaction) { + function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, [ 'Expressjs/Router: /router1', [ @@ -709,9 +728,10 @@ test('when using a route variable', function (t, end) { res.end() }) - runTest(t, '/a/b', function (segments, transaction) { + runTest(t, '/a/b', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, ['Expressjs/Route Path: /:foo/:bar', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions ) @@ -734,9 +754,10 @@ test('when using a string pattern in path', function (t, end) { res.end() }) - runTest(t, '/abcd', function (segments, transaction) { + runTest(t, '/abcd', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, ['Expressjs/Route Path: ' + path, [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions ) @@ -754,9 +775,10 @@ test('when using a regular expression in path', function (t, end) { res.end() }) - runTest(t, '/a', function (segments, transaction) { + runTest(t, '/a', function (root, transaction) { assertSegments( - transaction.trace.root.children[0], + transaction.trace, + root, ['Expressjs/Route Path: /a/', [NAMES.EXPRESS.MIDDLEWARE + 'myHandler']], assertSegmentsOptions ) @@ -785,9 +807,15 @@ for (const enabled of codeLevelMetrics) { res.end() }) - runTest(t, '/chained', function (segments, transaction) { - const routeSegment = findSegment(transaction.trace.root, 'Expressjs/Route Path: /chained') - const [mw1Segment, mw2Segment, handlerSegment] = routeSegment.children + runTest(t, '/chained', function (root, transaction) { + const routeSegment = findSegment( + transaction.trace, + transaction.trace.root, + 'Expressjs/Route Path: /chained' + ) + const [mw1Segment, mw2Segment, handlerSegment] = transaction.trace.getChildren( + routeSegment.id + ) const defaultPath = 'test/versioned/express/segments.test.js' assertCLMAttrs({ segments: [ @@ -832,11 +860,11 @@ function runTest(t, options, callback) { } agent.on('transactionFinished', function (tx) { - const baseSegment = tx.trace.root.children[0] + const [baseSegment] = tx.trace.getChildren(tx.trace.root.id) assert.equal(agent.errors.traceAggregator.errors.length, errors, 'should have errors') - callback(baseSegment.children, tx) + callback(baseSegment, tx) }) makeRequest(port, endpoint, function (response) { diff --git a/test/versioned/express/transaction-naming.test.js b/test/versioned/express/transaction-naming.test.js index dade6c9fc2..e9eb3f8058 100644 --- a/test/versioned/express/transaction-naming.test.js +++ b/test/versioned/express/transaction-naming.test.js @@ -382,7 +382,8 @@ test('Express transaction names are unaffected by errorware', async function (t) agent.on('transactionFinished', function (tx) { const expected = 'WebTransaction/Expressjs/GET//test' - plan.equal(tx.trace.root.children[0].name, expected) + const [baseSegment] = tx.trace.getChildren(tx.trace.root.id) + plan.equal(baseSegment.name, expected) }) app.use('/test', function () { diff --git a/test/versioned/fastify/add-hook.test.js b/test/versioned/fastify/add-hook.test.js index 97020e9e5a..f0d1bbf260 100644 --- a/test/versioned/fastify/add-hook.test.js +++ b/test/versioned/fastify/add-hook.test.js @@ -108,7 +108,7 @@ test('non-error hooks', async (t) => { ] ] } - assertSegments(transaction.trace.root, expectedSegments) + assertSegments(transaction.trace, transaction.trace.root, expectedSegments) txPassed = true }) @@ -168,7 +168,7 @@ test('error hook', async function errorHookTest(t) { ] } - assertSegments(transaction.trace.root, expectedSegments) + assertSegments(transaction.trace, transaction.trace.root, expectedSegments) txPassed = true }) diff --git a/test/versioned/fastify/code-level-metrics-hooks.test.js b/test/versioned/fastify/code-level-metrics-hooks.test.js index 31ad174960..cafcf13d00 100644 --- a/test/versioned/fastify/code-level-metrics-hooks.test.js +++ b/test/versioned/fastify/code-level-metrics-hooks.test.js @@ -43,11 +43,12 @@ async function performTest(t) { let txPassed = false agent.on('transactionFinished', (transaction) => { - const baseSegment = transaction.trace.root.children - const [onRequestSegment, handlerSegment] = helper.isSecurityAgentEnabled(agent) - ? baseSegment[0].children[0].children - : baseSegment[0].children - const onSendSegment = handlerSegment.children[0] + const [baseSegment] = transaction.trace.getChildren(transaction.trace.root.id) + let [onRequestSegment, handlerSegment] = transaction.trace.getChildren(baseSegment.id) + if (helper.isSecurityAgentEnabled(agent)) { + ;[onRequestSegment, handlerSegment] = transaction.trace.getChildren(onRequestSegment.id) + } + const [onSendSegment] = transaction.trace.getChildren(handlerSegment.id) assertCLMAttrs({ segments: [ { diff --git a/test/versioned/fastify/code-level-metrics-middleware.test.js b/test/versioned/fastify/code-level-metrics-middleware.test.js index 0449d88310..b112ce2c2a 100644 --- a/test/versioned/fastify/code-level-metrics-middleware.test.js +++ b/test/versioned/fastify/code-level-metrics-middleware.test.js @@ -53,9 +53,12 @@ async function setup(t, config) { } } -function assertSegments(testContext, baseSegment, isCLMEnabled) { - const { agent } = testContext.nr - const { children } = helper.isSecurityAgentEnabled(agent) ? baseSegment.children[0] : baseSegment +function assertSegments({ t, trace, baseSegment, isCLMEnabled }) { + const { agent } = t.nr + let children = trace.getChildren(baseSegment.id) + if (helper.isSecurityAgentEnabled(agent)) { + children = trace.getChildren(children[0].id) + } // TODO: once we drop v2 support, this function can be removed and assert inline in test below if (semver.satisfies(pkgVersion, '>=3')) { const [middieSegment, handlerSegment] = children @@ -105,7 +108,13 @@ async function performTest(t) { agent.on('transactionFinished', (transaction) => { calls.test++ - assertSegments(t, transaction.trace.root.children[0], agent.config.code_level_metrics.enabled) + const [baseSegment] = transaction.trace.getChildren(transaction.trace.root.id) + assertSegments({ + t, + baseSegment, + trace: transaction.trace, + isCLMEnabled: agent.config.code_level_metrics.enabled + }) }) await fastify.listen({ port: 0 }) diff --git a/test/versioned/fastify/naming-common.js b/test/versioned/fastify/naming-common.js index d7ef4a51ad..efb21e6da7 100644 --- a/test/versioned/fastify/naming-common.js +++ b/test/versioned/fastify/naming-common.js @@ -43,7 +43,7 @@ module.exports = async function runTests(t, getExpectedSegments) { ] } - assertSegments(transaction.trace.root, expectedSegments) + assertSegments(transaction.trace, transaction.trace.root, expectedSegments) }) await fastify.listen({ port: 0 }) diff --git a/test/versioned/grpc/util.cjs b/test/versioned/grpc/util.cjs index 0e8205d0ec..8425326753 100644 --- a/test/versioned/grpc/util.cjs +++ b/test/versioned/grpc/util.cjs @@ -151,8 +151,8 @@ util.assertExternalSegment = function assertExternalSegment( ) { const methodName = util.getRPCName(fnName) const segmentName = `${EXTERNAL.PREFIX}${CLIENT_ADDR}:${port}${methodName}` - assertSegments(tx.trace.root, [segmentName], { exact: false }, { assert }) - const segment = metricsHelpers.findSegment(tx.trace.root, segmentName) + assertSegments(tx.trace, tx.trace.root, [segmentName], { exact: false }, { assert }) + const segment = metricsHelpers.findSegment(tx.trace, tx.trace.root, segmentName) const attributes = segment.getAttributes() assert.equal( attributes.url, diff --git a/test/versioned/hapi/render.test.js b/test/versioned/hapi/render.test.js index 592ac1731c..7500b15c76 100644 --- a/test/versioned/hapi/render.test.js +++ b/test/versioned/hapi/render.test.js @@ -103,13 +103,14 @@ test('using EJS templates', { timeout: 2000 }, (t, end) => { }) function verifyEnded(root, tx) { - for (let i = 0, len = root.children.length; i < len; i++) { - const segment = root.children[i] + const children = tx.trace.getChildren(root.id) + for (let i = 0, len = children.length; i < len; i++) { + const segment = children[i] assert.ok( segment.timer.hasEnd(), util.format('verify %s (%s) has ended', segment.name, tx.id) ) - if (segment.children) { + if (tx.trace.getChildren(segment.id)) { verifyEnded(segment, tx) } } diff --git a/test/versioned/hapi/router.test.js b/test/versioned/hapi/router.test.js index 2e23fcacc9..7b4682b17c 100644 --- a/test/versioned/hapi/router.test.js +++ b/test/versioned/hapi/router.test.js @@ -42,7 +42,7 @@ function verifier(verb = 'GET') { assert.equal(transaction.verb, verb, 'HTTP method is ' + verb) assert.ok(transaction.trace, 'transaction has trace') - const web = transaction.trace.root.children[0] + const [web] = transaction.trace.getChildren(transaction.trace.root.id) assert.ok(web, 'trace has web segment') assert.equal(web.name, transaction.name, 'segment name and transaction name match') @@ -269,8 +269,9 @@ test('using custom handler defaults', (t, end) => { test('404 transaction is named correctly', (t, end) => { const { agent, server } = t.nr agent.on('transactionFinished', function (tx) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) assert.equal( - tx.trace.root.children[0].name, + segment.name, 'WebTransaction/Nodejs/GET/(not found)', '404 segment has standardized name' ) diff --git a/test/versioned/hapi/segments.test.js b/test/versioned/hapi/segments.test.js index f814609a1d..0cefaa6805 100644 --- a/test/versioned/hapi/segments.test.js +++ b/test/versioned/hapi/segments.test.js @@ -30,8 +30,8 @@ test.afterEach((ctx) => { function runTest(agent, server, callback) { agent.on('transactionFinished', function (tx) { - const baseSegment = tx.trace.root.children[0] - callback(baseSegment.children, tx) + const [baseSegment] = tx.trace.getChildren(tx.trace.root.id) + callback(baseSegment, tx) }) server.start().then(function () { @@ -78,9 +78,9 @@ test('route handler is recorded as middleware', (t, end) => { } }) - runTest(agent, server, function (segments, transaction) { + runTest(agent, server, function (baseSegment, transaction) { checkMetrics(transaction.metrics, [NAMES.HAPI.MIDDLEWARE + 'myHandler//test']) - assertSegments(transaction.trace.root.children[0], [NAMES.HAPI.MIDDLEWARE + 'myHandler//test']) + assertSegments(transaction.trace, baseSegment, [NAMES.HAPI.MIDDLEWARE + 'myHandler//test']) end() }) }) @@ -100,11 +100,9 @@ test('custom handler type is recorded as middleware', (t, end) => { handler: { customHandler: { key1: 'val1' } } }) - runTest(agent, server, function (segments, transaction) { + runTest(agent, server, function (baseSegment, transaction) { checkMetrics(transaction.metrics, [NAMES.HAPI.MIDDLEWARE + 'customHandler//test']) - assertSegments(transaction.trace.root.children[0], [ - NAMES.HAPI.MIDDLEWARE + 'customHandler//test' - ]) + assertSegments(transaction.trace, baseSegment, [NAMES.HAPI.MIDDLEWARE + 'customHandler//test']) end() }) }) @@ -124,12 +122,12 @@ test('extensions are recorded as middleware', (t, end) => { } }) - runTest(agent, server, function (segments, transaction) { + runTest(agent, server, function (baseSegment, transaction) { checkMetrics(transaction.metrics, [ NAMES.HAPI.MIDDLEWARE + '//onRequest', NAMES.HAPI.MIDDLEWARE + 'myHandler//test' ]) - assertSegments(transaction.trace.root.children[0], [ + assertSegments(transaction.trace, baseSegment, [ NAMES.HAPI.MIDDLEWARE + '//onRequest', NAMES.HAPI.MIDDLEWARE + 'myHandler//test' ]) @@ -156,12 +154,12 @@ test('custom route handler and extension recorded as middleware', (t, end) => { handler: { customHandler: { key1: 'val1' } } }) - runTest(agent, server, function (segments, transaction) { + runTest(agent, server, function (baseSegment, transaction) { checkMetrics(transaction.metrics, [ NAMES.HAPI.MIDDLEWARE + '//onRequest', NAMES.HAPI.MIDDLEWARE + 'customHandler//test' ]) - assertSegments(transaction.trace.root.children[0], [ + assertSegments(transaction.trace, baseSegment, [ NAMES.HAPI.MIDDLEWARE + '//onRequest', NAMES.HAPI.MIDDLEWARE + 'customHandler//test' ]) @@ -191,8 +189,8 @@ for (const clmEnabled of [true, false]) { } }) - runTest(agent, server, function (segments) { - const [onRequestSegment, handlerSegment] = segments + runTest(agent, server, function (baseSegment, transaction) { + const [onRequestSegment, handlerSegment] = transaction.trace.getChildren(baseSegment.id) assertClmAttrs({ segments: [ { @@ -232,7 +230,8 @@ for (const clmEnabled of [true, false]) { handler: { customHandler: { key1: 'val1' } } }) - runTest(agent, server, function ([customHandlerSegment]) { + runTest(agent, server, function (baseSegment, transaction) { + const [customHandlerSegment] = transaction.trace.getChildren(baseSegment.id) assertClmAttrs({ segments: [ { @@ -269,7 +268,8 @@ for (const clmEnabled of [true, false]) { } server.register(plugin).then(() => { - runTest(agent, server, function ([pluginHandlerSegment]) { + runTest(agent, server, function (baseSegment, transaction) { + const [pluginHandlerSegment] = transaction.trace.getChildren(baseSegment.id) assertClmAttrs({ segments: [ { diff --git a/test/versioned/ioredis/ioredis.test.js b/test/versioned/ioredis/ioredis.test.js index 60c5870b37..ccdfdd6b39 100644 --- a/test/versioned/ioredis/ioredis.test.js +++ b/test/versioned/ioredis/ioredis.test.js @@ -77,16 +77,18 @@ test('ioredis instrumentation', async (t) => { agent.on('transactionFinished', function (tx) { const root = tx.trace.root - plan.equal(root.children.length, 2, 'root has two children') + const children = tx.trace.getChildren(root.id) + plan.equal(children.length, 2, 'root has two children') + + const [setSegment, getSegment] = children - const setSegment = root.children[0] plan.equal(setSegment.name, 'Datastore/operation/Redis/set') // ioredis operations return promise, any 'then' callbacks will be sibling segments // of the original redis call - const getSegment = root.children[1] plan.equal(getSegment.name, 'Datastore/operation/Redis/get') - plan.equal(getSegment.children.length, 0, 'should not contain any segments') + const getChildren = tx.trace.getChildren(getSegment.id) + plan.equal(getChildren.length, 0, 'should not contain any segments') }) helper.runInTransaction(agent, async (transaction) => { diff --git a/test/versioned/kafkajs/kafka.test.js b/test/versioned/kafkajs/kafka.test.js index 63f1b1572a..779f48c6f7 100644 --- a/test/versioned/kafkajs/kafka.test.js +++ b/test/versioned/kafkajs/kafka.test.js @@ -61,27 +61,26 @@ test('send records correctly', async (t) => { const expectedName = 'produce-tx' agent.on('transactionFinished', (tx) => { - if (tx.name !== expectedName) { - return - } - - const name = `MessageBroker/Kafka/Topic/Produce/Named/${topic}` - const segment = tx.agent.tracer.getSegment() + if (tx.name === expectedName) { + const name = `MessageBroker/Kafka/Topic/Produce/Named/${topic}` + const segment = tx.agent.tracer.getSegment() + const children = tx.trace.getChildren(segment.id) - const foundSegment = segment.children.find((s) => s.name.endsWith(topic)) - plan.equal(foundSegment.name, name) + const foundSegment = children.find((s) => s.name.endsWith(topic)) + plan.equal(foundSegment.name, name) - const metric = tx.metrics.getMetric(name) - plan.equal(metric.callCount, 1) - const sendMetric = agent.metrics.getMetric( - 'Supportability/Features/Instrumentation/kafkajs/send' - ) - plan.equal(sendMetric.callCount, 1) + const metric = tx.metrics.getMetric(name) + plan.equal(metric.callCount, 1) + const sendMetric = agent.metrics.getMetric( + 'Supportability/Features/Instrumentation/kafkajs/send' + ) + plan.equal(sendMetric.callCount, 1) - const produceTrackingMetric = agent.metrics.getMetric( - `MessageBroker/Kafka/Nodes/${broker}/Produce/${topic}` - ) - plan.equal(produceTrackingMetric.callCount, 1) + const produceTrackingMetric = agent.metrics.getMetric( + `MessageBroker/Kafka/Nodes/${broker}/Produce/${topic}` + ) + plan.equal(produceTrackingMetric.callCount, 1) + } }) helper.runInTransaction(agent, async (tx) => { @@ -192,8 +191,9 @@ test('sendBatch records correctly', async (t) => { if (tx.name === expectedName) { const name = `MessageBroker/Kafka/Topic/Produce/Named/${topic}` const segment = tx.agent.tracer.getSegment() + const children = tx.trace.getChildren(segment.id) - const foundSegment = segment.children.find((s) => s.name.endsWith(topic)) + const foundSegment = children.find((s) => s.name.endsWith(topic)) plan.equal(foundSegment.name, name) const metric = tx.metrics.getMetric(name) @@ -306,6 +306,7 @@ test('consume inside of a transaction', async (t) => { txCount++ if (tx.name === expectedName) { assertSegments( + tx.trace, tx.trace.root, [`${SEGMENT_PREFIX}subscribe`, `${SEGMENT_PREFIX}run`], { @@ -362,6 +363,7 @@ test('consume batch inside of a transaction', async (t) => { const txPromise = new Promise((resolve) => { agent.on('transactionFinished', (tx) => { assertSegments( + tx.trace, tx.trace.root, [`${SEGMENT_PREFIX}subscribe`, `${SEGMENT_PREFIX}run`], { exact: false }, diff --git a/test/versioned/kafkajs/utils.js b/test/versioned/kafkajs/utils.js index 7602663ad1..0e964eb433 100644 --- a/test/versioned/kafkajs/utils.js +++ b/test/versioned/kafkajs/utils.js @@ -96,7 +96,7 @@ utils.verifyConsumeTransaction = ({ plan, tx, topic, clientId }) => { ) plan.equal(tx.getFullName(), expectedName) - const consume = metrics.findSegment(tx.trace.root, expectedName) + const consume = metrics.findSegment(tx.trace, tx.trace.root, expectedName) plan.equal(consume, tx.baseSegment) const attributes = tx.trace.attributes.get(DESTINATIONS.TRANS_SCOPE) @@ -113,7 +113,7 @@ utils.verifyConsumeTransaction = ({ plan, tx, topic, clientId }) => { */ utils.verifyDistributedTrace = ({ plan, consumeTxs, produceTx }) => { plan.ok(produceTx.isDistributedTrace, 'should mark producer as distributed') - const produceSegment = produceTx.trace.root.children[3] + const [, , , produceSegment] = produceTx.trace.getChildren(produceTx.trace.root.id) consumeTxs.forEach((consumeTx) => { plan.ok(consumeTx.isDistributedTrace, 'should mark consumer as distributed') plan.equal(consumeTx.incomingCatId, null, 'should not set old CAT properties') diff --git a/test/versioned/koa/code-level-metrics.test.js b/test/versioned/koa/code-level-metrics.test.js index 5975dfbb32..3e159bd5db 100644 --- a/test/versioned/koa/code-level-metrics.test.js +++ b/test/versioned/koa/code-level-metrics.test.js @@ -93,18 +93,21 @@ test('vanilla koa, no router', async (t) => { ctx.body = 'done' }) - agent.on('transactionFinished', (tx) => { - const baseSegment = tx.trace.root.children[0] + agent.on('transactionFinished', (transaction) => { + const [baseSegment] = transaction.trace.getChildren(transaction.trace.root.id) + const [one] = transaction.trace.getChildren(baseSegment.id) + const [two] = transaction.trace.getChildren(one.id) + assertClmAttrs( { segments: [ { - segment: baseSegment.children[0], + segment: one, name: 'one', filepath: 'code-level-metrics.test.js' }, { - segment: baseSegment.children[0].children[0], + segment: two, name: 'two', filepath: 'code-level-metrics.test.js' } @@ -155,23 +158,27 @@ test('using koa-router', async (t) => { router.use('/:first', nestedRouter.routes()) app.use(router.routes()) - agent.on('transactionFinished', (tx) => { - const baseSegment = tx.trace.root.children[0] + agent.on('transactionFinished', (transaction) => { + const [baseSegment] = transaction.trace.getChildren(transaction.trace.root.id) + const [dispatch] = transaction.trace.getChildren(baseSegment.id) + const [appLevel] = transaction.trace.getChildren(dispatch.id) + const [secondMw] = transaction.trace.getChildren(appLevel.id) + assertClmAttrs( { segments: [ { - segment: baseSegment.children[0], + segment: dispatch, name: 'dispatch', filepath: 'koa-router/lib/router.js' }, { - segment: baseSegment.children[0].children[0], + segment: appLevel, name: 'appLevelMiddleware', filepath: 'code-level-metrics.test.js' }, { - segment: baseSegment.children[0].children[0].children[0], + segment: secondMw, name: 'secondMiddleware', filepath: 'code-level-metrics.test.js' } @@ -222,23 +229,27 @@ test('using @koa/router', async (t) => { router.use('/:first', nestedRouter.routes()) app.use(router.routes()) - agent.on('transactionFinished', (tx) => { - const baseSegment = tx.trace.root.children[0] + agent.on('transactionFinished', (transaction) => { + const [baseSegment] = transaction.trace.getChildren(transaction.trace.root.id) + const [dispatch] = transaction.trace.getChildren(baseSegment.id) + const [appLevel] = transaction.trace.getChildren(dispatch.id) + const [secondMw] = transaction.trace.getChildren(appLevel.id) + assertClmAttrs( { segments: [ { - segment: baseSegment.children[0], + segment: dispatch, name: 'dispatch', filepath: '@koa/router/lib/router.js' }, { - segment: baseSegment.children[0].children[0], + segment: appLevel, name: 'appLevelMiddleware', filepath: 'code-level-metrics.test.js' }, { - segment: baseSegment.children[0].children[0].children[0], + segment: secondMw, name: 'secondMiddleware', filepath: 'code-level-metrics.test.js' } diff --git a/test/versioned/koa/koa-route.test.js b/test/versioned/koa/koa-route.test.js index 05f9fe5340..d6debfb8f7 100644 --- a/test/versioned/koa/koa-route.test.js +++ b/test/versioned/koa/koa-route.test.js @@ -35,7 +35,7 @@ test('should name and produce segments for koa-route middleware', (t, end) => { }) app.use(first) agent.on('transactionFinished', function (tx) { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//resource', ['Nodejs/Middleware/Koa/firstMiddleware//resource'] ]) @@ -61,7 +61,7 @@ test('should name the transaction after the last responder', (t, end) => { app.use(first) app.use(second) agent.on('transactionFinished', function (tx) { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:second', [ 'Nodejs/Middleware/Koa/firstMiddleware//:first', @@ -91,7 +91,7 @@ test('should name the transaction properly when responding after next', (t, end) app.use(first) app.use(second) agent.on('transactionFinished', function (tx) { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', [ 'Nodejs/Middleware/Koa/firstMiddleware//:first', @@ -120,7 +120,7 @@ test('should work with early responding', (t, end) => { app.use(first) app.use(second) agent.on('transactionFinished', function (tx) { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', ['Nodejs/Middleware/Koa/firstMiddleware//:first'] ]) @@ -146,7 +146,7 @@ test('should name the transaction after the source of the error that occurred', app.use(first) app.use(second) agent.on('transactionFinished', function (tx) { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:second', [ 'Nodejs/Middleware/Koa/firstMiddleware//:first', @@ -179,7 +179,7 @@ test('should work properly when used along with non-route middleware', (t, end) app.use(second) app.use(third) agent.on('transactionFinished', function (tx) { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//resource', [ 'Nodejs/Middleware/Koa/firstMiddleware', diff --git a/test/versioned/koa/koa.test.js b/test/versioned/koa/koa.test.js index b441b85255..9995bd0cde 100644 --- a/test/versioned/koa/koa.test.js +++ b/test/versioned/koa/koa.test.js @@ -54,6 +54,7 @@ function run({ t, expected = 'done', cb, end, plan }) { function checkSegments(plan, tx) { assertSegments( + tx.trace, tx.trace.root, [ // Until koa-router is instrumented and transaction naming is addressed, @@ -275,9 +276,10 @@ test('correctly records actions interspersed among middleware', async (t) => { const { agent, app, testShim } = t.nr app.use(function one(ctx, next) { - testShim.createSegment('testSegment') + const parent = agent.tracer.getSegment() + testShim.createSegment({ name: 'testSegment', parent }) return next().then(function () { - testShim.createSegment('nestedSegment') + testShim.createSegment({ name: 'nestedSegment', parent }) }) }) app.use(function two(ctx, next) { @@ -291,6 +293,7 @@ test('correctly records actions interspersed among middleware', async (t) => { agent.on('transactionFinished', (tx) => { assertSegments( + tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//', @@ -354,6 +357,7 @@ test('maintains transaction state between middleware', async (t) => { agent.on('transactionFinished', function (txn) { assertSegments( + tx.trace, tx.trace.root, [ txn.name, diff --git a/test/versioned/koa/router-common.js b/test/versioned/koa/router-common.js index 1d97300b47..a9c37114f4 100644 --- a/test/versioned/koa/router-common.js +++ b/test/versioned/koa/router-common.js @@ -98,7 +98,7 @@ module.exports = (pkg) => { app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', [ 'Koa/Router: /', @@ -125,7 +125,7 @@ module.exports = (pkg) => { }) app.use(router.middleware()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', ['Koa/Router: /', ['Nodejs/Middleware/Koa/firstMiddleware//:first']] ]) @@ -160,7 +160,7 @@ module.exports = (pkg) => { }) app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//.*rst$', ['Koa/Router: /', ['Nodejs/Middleware/Koa/firstMiddleware//.*rst$/']] ]) @@ -185,7 +185,7 @@ module.exports = (pkg) => { }) app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ `WebTransaction/WebFrameworkUri/Koa/GET//:first/${path}`, ['Koa/Router: /', [`Nodejs/Middleware/Koa/firstMiddleware//:first/${path}`]] ]) @@ -210,7 +210,7 @@ module.exports = (pkg) => { }) app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', [ 'Koa/Router: /', @@ -245,7 +245,7 @@ module.exports = (pkg) => { app.silent = true app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', [ 'Koa/Router: /', @@ -282,7 +282,7 @@ module.exports = (pkg) => { app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:second', [ 'Koa/Router: /', @@ -316,7 +316,7 @@ module.exports = (pkg) => { app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', [ 'Nodejs/Middleware/Koa/errorHandler', @@ -347,7 +347,7 @@ module.exports = (pkg) => { app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', [ 'Nodejs/Middleware/Koa/errorHandler', @@ -392,7 +392,7 @@ module.exports = (pkg) => { ] app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:second', ['Koa/Router: /', segmentTree] ]) @@ -417,7 +417,7 @@ module.exports = (pkg) => { }) app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(not found)', ['Koa/Router: /'] ]) @@ -446,7 +446,7 @@ module.exports = (pkg) => { }) app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(not found)', ['Nodejs/Middleware/Koa/baseMiddleware', ['Koa/Router: /']] ]) @@ -486,7 +486,7 @@ module.exports = (pkg) => { // the dispatch function blocking its returned promise on the // resolution of a recursively returned promise. // https://github.com/koajs/compose/blob/e754ca3c13e9248b3f453d98ea0b618e09578e2d/index.js#L42-L44 - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:second', [ 'Koa/Router: /', @@ -524,7 +524,7 @@ module.exports = (pkg) => { // the dispatch function blocking its returned promise on the // resolution of a recursively returned promise. // https://github.com/koajs/compose/blob/e754ca3c13e9248b3f453d98ea0b618e09578e2d/index.js#L42-L44 - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:second', [ 'Koa/Router: /', @@ -558,7 +558,7 @@ module.exports = (pkg) => { router.use('/:first', router2.routes()) app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first/:second', ['Koa/Router: /', [getNestedSpanName('secondMiddleware')]] ]) @@ -588,7 +588,7 @@ module.exports = (pkg) => { app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first/:second', [ 'Nodejs/Middleware/Koa/appLevelMiddleware', @@ -622,7 +622,7 @@ module.exports = (pkg) => { app.use(router.routes()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first/:second', [ 'Nodejs/Middleware/Koa/appLevelMiddleware', @@ -660,7 +660,7 @@ module.exports = (pkg) => { app.use(router.routes()) app.use(router.allowedMethods({ throw: true })) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(method not allowed)', ['Koa/Router: /', [`Nodejs/Middleware/Koa/${allowedMethodsFnName}`]] ]) @@ -685,7 +685,7 @@ module.exports = (pkg) => { app.use(router.routes()) app.use(router.allowedMethods({ throw: true })) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(not implemented)', ['Koa/Router: /', [`Nodejs/Middleware/Koa/${allowedMethodsFnName}`]] ]) @@ -723,7 +723,7 @@ module.exports = (pkg) => { app.use(router.allowedMethods({ throw: true })) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/NormalizedUri/*', [ 'Nodejs/Middleware/Koa/errorHandler', @@ -763,7 +763,7 @@ module.exports = (pkg) => { app.use(router.allowedMethods({ throw: true })) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(method not allowed)', [ 'Nodejs/Middleware/Koa/baseMiddleware', @@ -797,7 +797,7 @@ module.exports = (pkg) => { app.use(router.routes()) app.use(router.allowedMethods()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(method not allowed)', ['Koa/Router: /', [`Nodejs/Middleware/Koa/${allowedMethodsFnName}`]] ]) @@ -822,7 +822,7 @@ module.exports = (pkg) => { app.use(router.routes()) app.use(router.allowedMethods()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(not implemented)', ['Koa/Router: /', [`Nodejs/Middleware/Koa/${allowedMethodsFnName}`]] ]) @@ -854,7 +854,7 @@ module.exports = (pkg) => { app.use(router.allowedMethods()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(method not allowed)', [ 'Nodejs/Middleware/Koa/appLevelMiddleware', @@ -888,7 +888,7 @@ module.exports = (pkg) => { app.use(router.allowedMethods()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET/(not implemented)', [ 'Nodejs/Middleware/Koa/appLevelMiddleware', @@ -914,7 +914,7 @@ module.exports = (pkg) => { app.use(router.routes()) app.use(router.allowedMethods()) agent.on('transactionFinished', (tx) => { - assertSegments(tx.trace.root, [ + assertSegments(tx.trace, tx.trace.root, [ 'WebTransaction/WebFrameworkUri/Koa/GET//:first', ['Koa/Router: /', ['Nodejs/Middleware/Koa/firstMiddleware//:first']] ]) diff --git a/test/versioned/langchain/common.js b/test/versioned/langchain/common.js index 55f88028c8..5fbea4a87f 100644 --- a/test/versioned/langchain/common.js +++ b/test/versioned/langchain/common.js @@ -25,10 +25,11 @@ function assertLangChainVectorSearch( { tx, vectorSearch, responseDocumentSize }, { assert = require('node:assert') } = {} ) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedSearch = { id: /[a-f0-9]{36}/, appName: 'New Relic for Node.js tests', - span_id: tx.trace.root.children[0].id, + span_id: segment.id, trace_id: tx.traceId, 'request.k': 1, 'request.query': 'This is an embedding test.', @@ -36,7 +37,7 @@ function assertLangChainVectorSearch( vendor: 'langchain', virtual_llm: true, 'response.number_of_documents': responseDocumentSize, - duration: tx.trace.root.children[0].getDurationInMillis() + duration: segment.getDurationInMillis() } assert.equal(vectorSearch[0].type, 'LlmVectorSearch') @@ -47,11 +48,12 @@ function assertLangChainVectorSearchResult( { tx, vectorSearchResult, vectorSearchId }, { assert = require('node:assert') } = {} ) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const baseSearchResult = { id: /[a-f0-9]{36}/, search_id: vectorSearchId, appName: 'New Relic for Node.js tests', - span_id: tx.trace.root.children[0].id, + span_id: segment.id, trace_id: tx.traceId, ingest_source: 'Node', vendor: 'langchain', @@ -78,10 +80,11 @@ function assertLangChainChatCompletionSummary( { tx, chatSummary, withCallback }, { assert = require('node:assert') } = {} ) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedSummary = { id: /[a-f0-9]{36}/, appName: 'New Relic for Node.js tests', - span_id: tx.trace.root.children[0].id, + span_id: segment.id, trace_id: tx.traceId, request_id: undefined, ingest_source: 'Node', @@ -91,7 +94,7 @@ function assertLangChainChatCompletionSummary( tags: 'tag1,tag2', virtual_llm: true, 'response.number_of_messages': 1, - duration: tx.trace.root.children[0].getDurationInMillis() + duration: segment.getDurationInMillis() } if (withCallback) { @@ -114,10 +117,11 @@ function assertLangChainChatCompletionMessages( }, { assert = require('node:assert') } = {} ) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const baseMsg = { id: /[a-f0-9]{36}/, appName: 'New Relic for Node.js tests', - span_id: tx.trace.root.children[0].id, + span_id: segment.id, trace_id: tx.traceId, ingest_source: 'Node', vendor: 'langchain', diff --git a/test/versioned/langchain/runnables-streaming.test.js b/test/versioned/langchain/runnables-streaming.test.js index c3bcf9ff25..b13e34ccad 100644 --- a/test/versioned/langchain/runnables-streaming.test.js +++ b/test/versioned/langchain/runnables-streaming.test.js @@ -407,7 +407,7 @@ test('streaming enabled', async (t) => { // no-op } - assertSegments(tx.trace.root, ['Llm/chain/Langchain/stream'], { exact: false }) + assertSegments(tx.trace, tx.trace.root, ['Llm/chain/Langchain/stream'], { exact: false }) tx.end() end() diff --git a/test/versioned/langchain/runnables.test.js b/test/versioned/langchain/runnables.test.js index 6c0796f9fc..9df12167e3 100644 --- a/test/versioned/langchain/runnables.test.js +++ b/test/versioned/langchain/runnables.test.js @@ -358,7 +358,7 @@ test('should create span on successful runnables create', (t, end) => { const result = await chain.invoke(input, options) assert.ok(result) - assertSegments(tx.trace.root, ['Llm/chain/Langchain/invoke'], { exact: false }) + assertSegments(tx.trace, tx.trace.root, ['Llm/chain/Langchain/invoke'], { exact: false }) tx.end() end() diff --git a/test/versioned/langchain/tools.test.js b/test/versioned/langchain/tools.test.js index ba62670542..9e7f3897ea 100644 --- a/test/versioned/langchain/tools.test.js +++ b/test/versioned/langchain/tools.test.js @@ -44,7 +44,9 @@ test('should create span on successful tools create', (t, end) => { helper.runInTransaction(agent, async (tx) => { const result = await tool.call(input) assert.ok(result) - assertSegments(tx.trace.root, ['Llm/tool/Langchain/node-agent-test-tool'], { exact: false }) + assertSegments(tx.trace, tx.trace.root, ['Llm/tool/Langchain/node-agent-test-tool'], { + exact: false + }) tx.end() end() }) @@ -75,10 +77,11 @@ test('should create LlmTool event for every tool.call', (t, end) => { assert.equal(events.length, 1, 'should create a LlmTool event') const [[{ type }, toolEvent]] = events assert.equal(type, 'LlmTool') + const [segment] = tx.trace.getChildren(tx.trace.root.id) match(toolEvent, { id: /[a-f0-9]{36}/, appName: 'New Relic for Node.js tests', - span_id: tx.trace.root.children[0].id, + span_id: segment.id, trace_id: tx.traceId, ingest_source: 'Node', vendor: 'langchain', @@ -89,7 +92,7 @@ test('should create LlmTool event for every tool.call', (t, end) => { output: tool.fakeData[input], name: tool.name, description: tool.description, - duration: tx.trace.root.children[0].getDurationInMillis(), + duration: segment.getDurationInMillis(), run_id: undefined }) tx.end() diff --git a/test/versioned/langchain/vectorstore.test.js b/test/versioned/langchain/vectorstore.test.js index 57d3c71799..39401fb115 100644 --- a/test/versioned/langchain/vectorstore.test.js +++ b/test/versioned/langchain/vectorstore.test.js @@ -95,7 +95,7 @@ test('should create span on successful vectorstore create', (t, end) => { helper.runInTransaction(agent, async (tx) => { const result = await vs.similaritySearch('This is an embedding test.', 1) assert.ok(result) - assertSegments(tx.trace.root, ['Llm/vectorstore/Langchain/similaritySearch'], { + assertSegments(tx.trace, tx.trace.root, ['Llm/vectorstore/Langchain/similaritySearch'], { exact: false }) tx.end() diff --git a/test/versioned/memcached/memcached.test.js b/test/versioned/memcached/memcached.test.js index f9762b9a1c..9257c779b4 100644 --- a/test/versioned/memcached/memcached.test.js +++ b/test/versioned/memcached/memcached.test.js @@ -56,6 +56,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/touch'], { exact: false }, @@ -91,6 +92,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/get', ['Truncated/Callback: ']], { exact: false }, @@ -126,6 +128,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/gets', ['Truncated/Callback: ']], { exact: false }, @@ -161,6 +164,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/get', ['Truncated/Callback: handle']], { exact: false }, @@ -196,6 +200,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/set', ['Truncated/Callback: ']], { exact: false }, @@ -234,6 +239,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/replace', ['Truncated/Callback: ']], { exact: false }, @@ -270,6 +276,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/add', ['Truncated/Callback: ']], { exact: false }, @@ -311,6 +318,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/cas', ['Truncated/Callback: ']], { exact: false }, @@ -349,6 +357,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(agent.getTransaction(), 'transaction should still be visible') transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/append', ['Truncated/Callback: ']], { exact: false }, @@ -386,6 +395,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(agent.getTransaction(), 'transaction should still be visible') transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/prepend', ['Truncated/Callback: ']], { exact: false }, @@ -423,6 +433,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(agent.getTransaction(), 'transaction should still be visible') transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/delete', ['Truncated/Callback: ']], { exact: false }, @@ -459,6 +470,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/incr', ['Truncated/Callback: ']], { exact: false }, @@ -494,6 +506,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/decr'], { exact: false }, @@ -532,6 +545,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { transaction.end() assertSegments( + transaction.trace, transaction.trace.root, ['Datastore/operation/Memcache/version'], { exact: false }, @@ -587,7 +601,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) plan.equal(segment.getAttributes().key, '"foo"', 'should have the get key as a parameter') }) }) @@ -604,7 +618,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err) transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) plan.ok(!segment.getAttributes().key, 'should not have any attributes') }) }) @@ -620,7 +634,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) plan.equal( segment.getAttributes().key, '["foo","bar"]', @@ -640,7 +654,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) plan.equal(segment.getAttributes().key, '"foo"', 'should have the set key as a parameter') }) }) @@ -677,7 +691,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = segment.getAttributes() plan.equal( attributes.host, @@ -706,7 +720,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = segment.getAttributes() plan.equal( attributes.host, @@ -758,7 +772,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = segment.getAttributes() plan.equal(attributes.host, undefined, 'should not have host instance parameter') plan.equal( @@ -786,7 +800,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { plan.ok(!err, 'should not throw an error') transaction.end() - const segment = transaction.trace.root.children[0] + const [segment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = segment.getAttributes() plan.equal(attributes.host, undefined, 'should not have host instance parameter') plan.equal( @@ -854,13 +868,17 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { helper.runInTransaction(agent, function (transaction) { memcached.get('foo', function (err) { assert.ok(!err) - const firstSegment = agent.tracer.getSegment().parent + const firstSegment = transaction.trace.getParent(agent.tracer.getSegment().parentId) memcached.get('bar', function (err) { assert.ok(!err) transaction.end() checkParams(firstSegment, 'server1', '1111') - checkParams(agent.tracer.getSegment().parent, 'server2', '2222') + checkParams( + transaction.trace.getParent(agent.tracer.getSegment().parentId), + 'server2', + '2222' + ) end() }) }) @@ -872,8 +890,7 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) { helper.runInTransaction(agent, function (transaction) { memcached.getMulti(['foo', 'bar'], function (err) { assert.ok(!err) - const firstGet = transaction.trace.root.children[0] - const secondGet = transaction.trace.root.children[1] + const [firstGet, secondGet] = transaction.trace.getChildren(transaction.trace.root.id) if (firstGet.getAttributes().host === 'server1') { checkParams(firstGet, 'server1', '1111') checkParams(secondGet, 'server2', '2222') diff --git a/test/versioned/mongodb-esm/db.test.mjs b/test/versioned/mongodb-esm/db.test.mjs index 805c059969..0e81485f28 100644 --- a/test/versioned/mongodb-esm/db.test.mjs +++ b/test/versioned/mongodb-esm/db.test.mjs @@ -461,11 +461,12 @@ function verifyMongoSegments({ t, tx, expectedSegments }) { let current = tx.trace.root for (let i = 0, l = expectedSegments.length; i < l; i += 1) { + let children = tx.trace.getChildren(current.id) // Filter out net.createConnection segments as they could occur during // execution, and we don't need to verify them. - current.children = current.children.filter((c) => c.name !== 'net.createConnection') - assert.equal(current.children.length, 1, 'should have one child segment') - current = current.children[0] + children = children.filter((c) => c.name !== 'net.createConnection') + assert.equal(children.length, 1, 'should have one child segment') + current = children[0] assert.equal( current.name, expectedSegments[i], diff --git a/test/versioned/mongodb-esm/test-assertions.mjs b/test/versioned/mongodb-esm/test-assertions.mjs index bd6fc20b82..97044bc10a 100644 --- a/test/versioned/mongodb-esm/test-assertions.mjs +++ b/test/versioned/mongodb-esm/test-assertions.mjs @@ -19,6 +19,7 @@ function getValidatorCallback({ t, tx, segments, metrics, end, childrenLength = const segment = agent.tracer.getSegment() let current = tx.trace.root + let children = tx.trace.getChildren(current.id) if (childrenLength === 2) { // This block is for testing `collection.aggregate`. The `aggregate` @@ -29,27 +30,29 @@ function getValidatorCallback({ t, tx, segments, metrics, end, childrenLength = // on the trace root. We also added a strict flag for `aggregate` because, // depending on the version, there is an extra segment for the callback // of our test which we do not need to assert. - assert.equal(current.children.length, childrenLength, 'should have two children') + assert.equal(children.length, childrenLength, 'should have two children') for (const [i, expectedSegment] of segments.entries()) { - const child = current.children[i] + const child = children[i] + const childChildren = tx.trace.getChildren(child.id) assert.equal(child.name, expectedSegment, `child should be named ${expectedSegment}`) if (common.MONGO_SEGMENT_RE.test(child.name) === true) { checkSegmentParams(child, METRIC_HOST_NAME, METRIC_HOST_PORT) assert.equal(child.ignore, false, 'should not ignore segment') } - assert.equal(child.children.length, 0, 'should have no more children') + assert.equal(childChildren.length, 0, 'should have no more children') } } else { for (let i = 0, l = segments.length; i < l; ++i) { - assert.equal(current.children.length, 1, 'should have one child') - current = current.children[0] + assert.equal(children.length, 1, 'should have one child') + current = children[0] + children = tx.trace.getChildren(current.id) assert.equal(current.name, segments[i], 'child should be named ' + segments[i]) if (common.MONGO_SEGMENT_RE.test(current.name) === true) { checkSegmentParams(current, METRIC_HOST_NAME, METRIC_HOST_PORT) assert.equal(current.ignore, false, 'should not ignore segment') } } - assert.equal(current.children.length, 0, 'should have no more children') + assert.equal(children.length, 0, 'should have no more children') } assert.equal(current === segment, true, 'should test to the current segment') diff --git a/test/versioned/mongodb/collection-common.js b/test/versioned/mongodb/collection-common.js index 0556633f10..3c58b2ff23 100644 --- a/test/versioned/mongodb/collection-common.js +++ b/test/versioned/mongodb/collection-common.js @@ -118,6 +118,7 @@ function collectionTest(name, run) { ) const segment = agent.tracer.getSegment() let current = transaction.trace.root + const children = transaction.trace.getChildren(current.id) // this logic is just for the collection.aggregate. // aggregate no longer returns a callback with cursor @@ -130,10 +131,11 @@ function collectionTest(name, run) { // there is an extra segment for the callback of our test which we do not care // to assert if (childrenLength === 2) { - assert.equal(current.children.length, childrenLength, 'should have one child') + assert.equal(children.length, childrenLength, 'should have one child') segments.forEach((expectedSegment, i) => { - const child = current.children[i] + const child = children[i] + const childChildren = transaction.trace.getChildren(child.id) assert.equal( child.name, @@ -146,13 +148,15 @@ function collectionTest(name, run) { } if (strict) { - assert.equal(child.children.length, 0, 'should have no more children') + assert.equal(childChildren.length, 0, 'should have no more children') } }) } else { + let currentChildren for (let i = 0, l = segments.length; i < l; ++i) { - assert.equal(current.children.length, childrenLength, 'should have one child') - current = current.children[0] + assert.equal(children.length, childrenLength, 'should have one child') + current = children[0] + currentChildren = transaction.trace.getChildren(current.id) assert.equal(current.name, segments[i], 'child should be named ' + segments[i]) if (common.MONGO_SEGMENT_RE.test(current.name)) { checkSegmentParams(current) @@ -161,7 +165,7 @@ function collectionTest(name, run) { } if (strict) { - assert.equal(current.children.length, 0, 'should have no more children') + assert.equal(currentChildren.length, 0, 'should have no more children') } } @@ -202,7 +206,7 @@ function collectionTest(name, run) { assert.ok(attributes.database_name, 'should have database name attribute') assert.ok(attributes.product, 'should have product attribute') } - current = current.children[0] + ;[current] = tx.trace.getChildren(current.id) } end() }) @@ -229,7 +233,7 @@ function collectionTest(name, run) { ) assert.ok(attributes.product, 'should have product attribute') } - current = current.children[0] + ;[current] = tx.trace.getChildren(current.id) } end() }) @@ -278,6 +282,7 @@ function collectionTest(name, run) { ) const segment = agent.tracer.getSegment() let current = transaction.trace.root + const children = transaction.trace.getChildren(current.id) // this logic is just for the collection.aggregate. // aggregate no longer returns a callback with cursor @@ -290,10 +295,11 @@ function collectionTest(name, run) { // there is an extra segment for the callback of our test which we do not care // to assert if (childrenLength === 2) { - assert.equal(current.children.length, childrenLength, 'should have one child') + assert.equal(children.length, childrenLength, 'should have one child') segments.forEach((expectedSegment, i) => { - const child = current.children[i] + const child = children[i] + const childChildren = transaction.trace.getChildren(child.id) assert.equal( child.name, @@ -306,13 +312,15 @@ function collectionTest(name, run) { } if (strict) { - assert.equal(child.children.length, 0, 'should have no more children') + assert.equal(childChildren.length, 0, 'should have no more children') } }) } else { + let currentChildren for (let i = 0, l = segments.length; i < l; ++i) { - assert.equal(current.children.length, childrenLength, 'should have one child') - current = current.children[0] + assert.equal(children.length, childrenLength, 'should have one child') + current = children[0] + currentChildren = transaction.trace.getChildren(current.id) assert.equal(current.name, segments[i], 'child should be named ' + segments[i]) if (common.MONGO_SEGMENT_RE.test(current.name)) { checkSegmentParams(current) @@ -321,7 +329,7 @@ function collectionTest(name, run) { } if (strict) { - assert.equal(current.children.length, 0, 'should have no more children') + assert.equal(currentChildren.length, 0, 'should have no more children') } } diff --git a/test/versioned/mongodb/db-common.js b/test/versioned/mongodb/db-common.js index 4e0edac58b..b102a884e1 100644 --- a/test/versioned/mongodb/db-common.js +++ b/test/versioned/mongodb/db-common.js @@ -79,15 +79,16 @@ function verifyMongoSegments(agent, transaction, names, opts) { let child for (let i = 0, l = names.length; i < l; ++i) { + let children = transaction.trace.getChildren(current.id) if (opts.legacy) { // Filter out net.createConnection segments as they could occur during execution, which is fine // but breaks out assertion function - current.children = current.children.filter((c) => c.name !== 'net.createConnection') - assert.equal(current.children.length, 1, 'should have one child segment') - child = current.children[0] - current = current.children[0] + children = children.filter((c) => c.name !== 'net.createConnection') + assert.equal(children.length, 1, 'should have one child segment') + child = children[0] + current = children[0] } else { - child = current.children[i] + child = children[i] } assert.equal(child.name, names[i], 'segment should be named ' + names[i]) diff --git a/test/versioned/mysql/basic-pool.js b/test/versioned/mysql/basic-pool.js index 050524afea..c494659847 100644 --- a/test/versioned/mysql/basic-pool.js +++ b/test/versioned/mysql/basic-pool.js @@ -120,9 +120,12 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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) { + const [firstChild] = txn.trace.getChildren(txn.trace.root.id) + const children = txn.trace.getChildren(firstChild.id) + + const [seg] = children.filter(function (trace) { return /Datastore\/statement\/MySQL/.test(trace.name) - })[0] + }) const attributes = seg.getAttributes() assert.ok(!err, 'should not error') @@ -146,7 +149,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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()) + const seg = getDatastoreSegment({ trace: txn.trace, segment: agent.tracer.getSegment() }) assert.ok(!err, 'should not error making query') assert.ok(seg, 'should have a segment') @@ -167,7 +170,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 seg = getDatastoreSegment({ trace: txn.trace, segment: agent.tracer.getSegment() }) const attributes = seg.getAttributes() assert.ok(!err, 'no errors') assert.ok(seg, 'there is a segment') @@ -199,7 +202,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { // 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 seg = getDatastoreSegment({ trace: txn.trace, segment: agent.tracer.getSegment() }) const attributes = seg.getAttributes() assert.ok(seg, 'there is a segment') assert.equal(attributes.host, agent.config.getHostnameSafe(), 'set host') @@ -220,7 +223,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 seg = getDatastoreSegment({ trace: txn.trace, segment: agent.tracer.getSegment() }) const attributes = seg.getAttributes() assert.ok(!err, 'should not error making query') @@ -267,7 +270,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) assert.ok(!err, 'no error occurred') assert.ok(transaction, 'transaction should exist') @@ -289,7 +292,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { assert.ok(!err) assert.ok(transaction, 'should not lose transaction') if (transaction) { - const segment = agent.tracer.getSegment().parent + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -314,7 +317,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { connection.query('SELECT 1 + 1 AS solution', function (err) { const transaction = agent.getTransaction() - const segment = agent.tracer.getSegment().parent + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) assert.ok(!err, 'no error occurred') assert.ok(transaction, 'transaction should exist') @@ -344,7 +347,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { assert.ok(!err) assert.ok(transaction, 'should not lose transaction') if (transaction) { - const segment = agent.tracer.getSegment().parent + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -372,7 +375,10 @@ module.exports = function ({ factory, constants, pkgVersion }) { socketPool.query('SELECT 1 + 1 AS solution', function (err) { assert.ok(!err, 'should not error making query') - const seg = getDatastoreSegment(agent.tracer.getSegment()) + const seg = getDatastoreSegment({ + trace: txn.trace, + segment: agent.tracer.getSegment() + }) const attributes = seg.getAttributes() // In the case where you don't have a server running on localhost @@ -455,7 +461,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -494,7 +500,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -533,7 +539,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -571,7 +577,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -611,7 +617,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { 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 + const segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -638,7 +644,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { assert.ok(transaction, 'transaction should exist') assert.equal(transaction, txn, 'transaction must be same') - let segment = agent.tracer.getSegment().parent + let segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -658,7 +664,7 @@ module.exports = function ({ factory, constants, pkgVersion }) { assert.ok(transaction, 'transaction should exist') assert.equal(transaction, txn, 'transaction must be same') - segment = agent.tracer.getSegment().parent + segment = txn.trace.getParent(agent.tracer.getSegment().parentId) 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') @@ -698,8 +704,8 @@ async function getDomainSocketPath() { } } -function getDatastoreSegment(segment) { - return segment.parent.children.filter(function (s) { +function getDatastoreSegment({ segment, trace }) { + return trace.getChildren(trace.getParent(segment.parentId).id).filter(function (s) { return /^Datastore/.test(s && s.name) })[0] } diff --git a/test/versioned/mysql/basic.js b/test/versioned/mysql/basic.js index 8172194d2f..6418fddba2 100644 --- a/test/versioned/mysql/basic.js +++ b/test/versioned/mysql/basic.js @@ -15,6 +15,7 @@ const urltils = require('../../../lib/util/urltils') const params = require('../../lib/params') const setup = require('./setup') const { getClient } = require('./utils') +const { findSegment } = require('../../lib/metrics_helper') module.exports = function ({ lib, factory, poolFactory, constants }) { const { USER, DATABASE, TABLE } = constants @@ -144,7 +145,7 @@ module.exports = function ({ lib, factory, poolFactory, constants }) { 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() { + helper.runInTransaction(agent, function transactionInScope(tx) { assert.ok(agent.getTransaction(), 'we should be in a transaction') getClient(pool, function (err, client) { assert.ok(!err) @@ -155,7 +156,7 @@ module.exports = function ({ lib, factory, poolFactory, constants }) { 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 seg = tx.trace.getParent(agent.tracer.getSegment().parentId) const attributes = seg.getAttributes() assert.ok(!err, 'no errors') @@ -246,7 +247,11 @@ module.exports = function ({ lib, factory, poolFactory, constants }) { 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 segment = findSegment( + transaction.trace, + traceRoot, + 'Datastore/statement/MySQL/unknown/select' + ) const queryNodeDuration = segment.timer.getDurationInMillis() assert.ok( @@ -289,20 +294,17 @@ module.exports = function ({ lib, factory, poolFactory, constants }) { 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 [querySegment] = transaction.trace.getChildren(traceRoot.id) + const queryChildren = transaction.trace.getChildren(querySegment.id) + assert.equal(queryChildren.length, 2, 'the query segment should have two children') - const childSegment = querySegment.children[1] + const childSegment = queryChildren[1] assert.equal( childSegment.name, 'Callback: endCallback', 'children should be callbacks' ) - const grandChildSegment = childSegment.children[0] + const [grandChildSegment] = transaction.trace.getChildren(childSegment.id) assert.equal( grandChildSegment.name, 'timers.setTimeout', @@ -373,7 +375,7 @@ module.exports = function ({ lib, factory, poolFactory, constants }) { 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 seg = txn.trace.getParent(agent.tracer.getSegment().parentId) const attributes = seg.getAttributes() assert.ok(!err) assert.ok(seg, 'should have a segment') @@ -399,12 +401,3 @@ module.exports = function ({ lib, factory, poolFactory, constants }) { }) }) } - -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/pooling.js b/test/versioned/mysql/pooling.js index 06e3ff8cbb..cca9859c9a 100644 --- a/test/versioned/mysql/pooling.js +++ b/test/versioned/mysql/pooling.js @@ -55,9 +55,10 @@ module.exports = function ({ factory, poolFactory, constants }) { 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 children = trace.getChildren(trace.root.id) + plan.equal(children.length, 1, 'There should be only one child.') - const selectSegment = trace.root.children[0] + const selectSegment = children[0] plan.ok(selectSegment, 'trace segment for first SELECT should exist') plan.equal( @@ -66,9 +67,12 @@ module.exports = function ({ factory, poolFactory, constants }) { '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) + const selectChildren = trace.getChildren(selectSegment.id) + plan.equal(selectChildren.length, 1, 'should only have a callback segment') + const cb = selectChildren[0] + plan.equal(cb.name, 'Callback: ') + const cbChildren = trace.getChildren(cb.id) + plan.equal(cbChildren.length, 0) } }) } diff --git a/test/versioned/mysql2/promises.test.js b/test/versioned/mysql2/promises.test.js index 886a505b29..d1975f24f9 100644 --- a/test/versioned/mysql2/promises.test.js +++ b/test/versioned/mysql2/promises.test.js @@ -93,7 +93,7 @@ test('mysql2 promises', { timeout: 30000 }, async (t) => { activeTx = agent.getTransaction() assert.equal(tx.name, activeTx.name) - const segment = agent.getTransaction().trace.root.children[2] + const [, , segment] = tx.trace.getChildren(tx.trace.root.id) const attributes = segment.getAttributes() assert.equal( attributes.host, diff --git a/test/versioned/nextjs/attributes.test.js b/test/versioned/nextjs/attributes.test.js index 909fb443a7..76f27b8166 100644 --- a/test/versioned/nextjs/attributes.test.js +++ b/test/versioned/nextjs/attributes.test.js @@ -10,8 +10,7 @@ const assert = require('node:assert') const helpers = require('./helpers') const nextPkg = require('next/package.json') const { - isMiddlewareInstrumentationSupported, - getServerSidePropsSegment + isMiddlewareInstrumentationSupported } = require('../../../lib/instrumentation/nextjs/utils') const middlewareSupported = isMiddlewareInstrumentationSupported(nextPkg.version) const agentHelper = require('../../lib/agent_helper') @@ -199,16 +198,18 @@ test('Next.js', async (t) => { await helpers.makeRequest('/api/person/2?queryParam=queryValue') const [tx] = await txPromise const rootSegment = tx.trace.root + const [handler] = tx.trace.getChildren(rootSegment.id) const segments = [ { - segment: rootSegment.children[0], + segment: handler, name: 'handler', filepath: 'pages/api/person/[id]' } ] if (middlewareSupported) { + const [middleware] = tx.trace.getChildren(handler.id) segments.push({ - segment: rootSegment.children[0].children[0], + segment: middleware, name: 'middleware', filepath: 'middleware' }) @@ -231,20 +232,22 @@ test('Next.js', async (t) => { const [tx] = await txPromise const rootSegment = tx.trace.root const segments = [] + const [first] = tx.trace.getChildren(rootSegment.id) if (middlewareSupported) { + const [middleware, getServerSideProps] = tx.trace.getChildren(first.id) segments.push({ - segment: rootSegment.children[0].children[0], + segment: middleware, name: 'middleware', filepath: 'middleware' }) segments.push({ - segment: rootSegment.children[0].children[1], + segment: getServerSideProps, name: 'getServerSideProps', filepath: 'pages/ssr/people' }) } else { segments.push({ - segment: getServerSidePropsSegment(rootSegment), + segment: helpers.getServerSidePropsSegment(tx.trace), name: 'getServerSideProps', filepath: 'pages/ssr/people' }) @@ -265,21 +268,23 @@ test('Next.js', async (t) => { await helpers.makeRequest('/static/dynamic/testing?queryParam=queryValue') const [tx] = await txPromise const rootSegment = tx.trace.root + const [root] = tx.trace.getChildren(rootSegment.id) // The segment that names the static page will not contain CLM regardless of the // configuration flag assertCLMAttrs({ - segments: [{ segment: rootSegment.children[0] }], + segments: [{ segment: root }], enabled: false, skipFull: true }) if (middlewareSupported) { + const [middleware] = tx.trace.getChildren(root.id) // this will exist when CLM is enabled assertCLMAttrs({ segments: [ { - segment: rootSegment.children[0].children[0], + segment: middleware, name: 'middleware', filepath: 'middleware' } diff --git a/test/versioned/nextjs/helpers.js b/test/versioned/nextjs/helpers.js index 726f9c35f1..48cff34e42 100644 --- a/test/versioned/nextjs/helpers.js +++ b/test/versioned/nextjs/helpers.js @@ -17,6 +17,7 @@ const noServerClose = semver.gte(nextPkg.version, '13.4.15') // just emit SIGTERM after 14.1.0 const closeEvent = semver.gte(nextPkg.version, '14.1.0') ? 'SIGTERM' : 'exit' const { DESTINATIONS } = require('../../../lib/config/attribute-filter') +const { findSegment } = require('../../lib/metrics_helper') /** * Builds a Next.js app @@ -105,22 +106,6 @@ helpers.registerInstrumentation = function (agent) { hooks.forEach(agent.registerInstrumentation) } -helpers.findSegmentByName = function (root, name) { - if (root.name === name) { - return root - } else if (root.children && root.children.length) { - for (let i = 0; i < root.children.length; i++) { - const child = root.children[i] - const found = helpers.findSegmentByName(child, name) - if (found) { - return found - } - } - } - - return null -} - helpers.getTransactionEventAgentAttributes = function getTransactionEventAgentAttributes( transaction ) { @@ -134,7 +119,7 @@ helpers.getTransactionIntrinsicAttributes = function getTransactionIntrinsicAttr } helpers.getSegmentAgentAttributes = function getSegmentAgentAttributes(transaction, name) { - const segment = helpers.findSegmentByName(transaction.trace.root, name) + const segment = findSegment(transaction.trace, transaction.trace.root, name) if (segment) { return segment.attributes.get(DESTINATIONS.SPAN_EVENT) } @@ -166,3 +151,9 @@ helpers.setupTransactionHandler = function setupTransactionHandler({ }) }) } + +helpers.getServerSidePropsSegment = function getServerSidePropsSegment(trace) { + const [first] = trace.getChildren(trace.root.id) + const children = trace.getChildren(first.id) + return children.find((segment) => segment.name.includes('getServerSideProps')) +} diff --git a/test/versioned/nextjs/segments.test.js b/test/versioned/nextjs/segments.test.js index e3e03d320f..f0cda7dcf8 100644 --- a/test/versioned/nextjs/segments.test.js +++ b/test/versioned/nextjs/segments.test.js @@ -67,7 +67,7 @@ test('Next.js', async (t) => { children: getChildSegments(URI) } ] - assertSegments(tx.trace.root, expectedSegments, { exact: false }) + assertSegments(tx.trace, tx.trace.root, expectedSegments, { exact: false }) }) await t.test('should properly name getServerSideProps segments on dynamic pages', async (t) => { @@ -86,7 +86,7 @@ test('Next.js', async (t) => { children: getChildSegments(EXPECTED_URI) } ] - assertSegments(tx.trace.root, expectedSegments, { exact: false }) + assertSegments(tx.trace, tx.trace.root, expectedSegments, { exact: false }) }) await t.test( @@ -116,7 +116,7 @@ test('Next.js', async (t) => { ] } - assertSegments(tx.trace.root, expectedSegments, { exact: false }) + assertSegments(tx.trace, tx.trace.root, expectedSegments, { exact: false }) } ) }) diff --git a/test/versioned/openai/chat-completions.test.js b/test/versioned/openai/chat-completions.test.js index 19f454a5be..6114302139 100644 --- a/test/versioned/openai/chat-completions.test.js +++ b/test/versioned/openai/chat-completions.test.js @@ -66,6 +66,7 @@ test('should create span on successful chat completion create', (t, end) => { assert.equal(results.choices[0].message.content, '1 plus 2 is 3.') assertSegments( + tx.trace, tx.trace.root, [OPENAI.COMPLETION, [`External/${host}:${port}/chat/completions`]], { exact: false } @@ -148,6 +149,7 @@ if (semver.gte(pkgVersion, '4.12.2')) { assert.equal(chunk.choices[0].message.content, res) assertSegments( + tx.trace, tx.trace.root, [OPENAI.COMPLETION, [`External/${host}:${port}/chat/completions`]], { exact: false } @@ -362,6 +364,7 @@ if (semver.gte(pkgVersion, '4.12.2')) { assert.equal(events.length, 0) // we will still record the external segment but not the chat completion assertSegments( + tx.trace, tx.trace.root, ['timers.setTimeout', `External/${host}:${port}/chat/completions`], { exact: false } diff --git a/test/versioned/openai/common.js b/test/versioned/openai/common.js index 63fe83dc43..468e5e7232 100644 --- a/test/versioned/openai/common.js +++ b/test/versioned/openai/common.js @@ -16,11 +16,12 @@ function assertChatCompletionMessages( { tx, chatMsgs, id, model, reqContent, resContent, tokenUsage }, { assert = require('node:assert') } = {} ) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const baseMsg = { appName: 'New Relic for Node.js tests', request_id: '49dbbffbd3c3f4612aa48def69059aad', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': model, vendor: 'openai', ingest_source: 'Node', @@ -65,17 +66,18 @@ function assertChatCompletionSummary( { tx, model, chatSummary, error = false }, { assert = require('node:assert') } = {} ) { + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedChatSummary = { id: /[a-f0-9]{36}/, appName: 'New Relic for Node.js tests', request_id: '49dbbffbd3c3f4612aa48def69059aad', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': model, vendor: 'openai', ingest_source: 'Node', 'request.model': model, - duration: tx.trace.root.children[0].getDurationInMillis(), + duration: segment.getDurationInMillis(), 'response.organization': 'new-relic-nkmd8b', 'response.headers.llmVersion': '2020-10-01', 'response.headers.ratelimitLimitRequests': '200', diff --git a/test/versioned/openai/embeddings.test.js b/test/versioned/openai/embeddings.test.js index de71aea599..aad0abd293 100644 --- a/test/versioned/openai/embeddings.test.js +++ b/test/versioned/openai/embeddings.test.js @@ -62,9 +62,14 @@ test('should create span on successful embedding create', (t, end) => { assert.equal(results.headers, undefined, 'should remove response headers from user result') assert.equal(results.model, 'text-embedding-ada-002-v2') - assertSegments(tx.trace.root, [OPENAI.EMBEDDING, [`External/${host}:${port}/embeddings`]], { - exact: false - }) + assertSegments( + tx.trace, + tx.trace.root, + [OPENAI.EMBEDDING, [`External/${host}:${port}/embeddings`]], + { + exact: false + } + ) tx.end() end() @@ -97,17 +102,18 @@ test('should create an embedding message', (t, end) => { const events = agent.customEventAggregator.events.toArray() assert.equal(events.length, 1, 'should create a chat completion message and summary event') const [embedding] = events + const [segment] = tx.trace.getChildren(tx.trace.root.id) const expectedEmbedding = { id: /[a-f0-9]{36}/, appName: 'New Relic for Node.js tests', request_id: 'c70828b2293314366a76a2b1dcb20688', trace_id: tx.traceId, - span_id: tx.trace.root.children[0].id, + span_id: segment.id, 'response.model': 'text-embedding-ada-002-v2', vendor: 'openai', ingest_source: 'Node', 'request.model': 'text-embedding-ada-002', - duration: tx.trace.root.children[0].getDurationInMillis(), + duration: segment.getDurationInMillis(), 'response.organization': 'new-relic-nkmd8b', token_count: undefined, 'response.headers.llmVersion': '2020-10-01', diff --git a/test/versioned/opensearch/opensearch.test.js b/test/versioned/opensearch/opensearch.test.js index 1e32278ca9..3bd3fb890b 100644 --- a/test/versioned/opensearch/opensearch.test.js +++ b/test/versioned/opensearch/opensearch.test.js @@ -80,8 +80,7 @@ test('opensearch instrumentation', async (t) => { assert.ok(transaction, 'transaction should be visible') await client.indices.create({ index }) const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, `Datastore/statement/OpenSearch/${index}/index.create`, @@ -96,8 +95,7 @@ test('opensearch instrumentation', async (t) => { await bulkInsert({ client }) assert.ok(transaction, 'transaction should still be visible after bulk create') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/OpenSearch/any/bulk.create', @@ -121,10 +119,9 @@ test('opensearch instrumentation', async (t) => { }) assert.ok(transaction, 'transaction should still be visible after bulk create') const trace = transaction.trace - 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] + const [firstChild, secondChild] = trace.getChildren(trace.root.id) + assert.ok(firstChild, 'trace, trace root, and first child should exist') assert.equal( secondChild.name, 'Datastore/statement/OpenSearch/any/bulk.create', @@ -145,8 +142,7 @@ test('opensearch instrumentation', async (t) => { assert.ok(search, 'search should return a result') assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, `Datastore/statement/OpenSearch/${DB_INDEX_2}/search`, @@ -181,8 +177,7 @@ test('opensearch instrumentation', async (t) => { assert.ok(search, 'search should return a result') assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, `Datastore/statement/OpenSearch/${DB_INDEX}/search`, @@ -220,8 +215,7 @@ test('opensearch instrumentation', async (t) => { assert.ok(search, 'search should return a result') assert.ok(transaction, 'transaction should still be visible after search') const trace = transaction.trace - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/OpenSearch/any/search', @@ -265,8 +259,7 @@ test('opensearch instrumentation', async (t) => { 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 - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/OpenSearch/any/msearch.create', @@ -307,8 +300,7 @@ test('opensearch instrumentation', async (t) => { 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 - assert.ok(trace?.root?.children?.[0], 'trace, trace root, and first child should exist') - const firstChild = trace.root.children[0] + const [firstChild] = trace.getChildren(trace.root.id) assert.equal( firstChild.name, 'timers.setTimeout', @@ -389,7 +381,7 @@ test('opensearch instrumentation', async (t) => { ...documentProp }) - const createSegment = transaction.trace.root.children[0] + const [createSegment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = createSegment.getAttributes() assert.equal(attributes.host, undefined, 'should not have host attribute') assert.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') @@ -412,7 +404,7 @@ test('opensearch instrumentation', async (t) => { } catch (e) { assert.ok(e, 'should not be able to create an index named _search') } - const firstChild = transaction?.trace?.root?.children[0] + const [firstChild] = transaction.trace.getChildren(transaction.trace.root.id) assert.equal( firstChild.name, 'Datastore/statement/OpenSearch/_search/index.create', diff --git a/test/versioned/pg-esm/pg.common.mjs b/test/versioned/pg-esm/pg.common.mjs index aab195c954..cd64de3cd0 100644 --- a/test/versioned/pg-esm/pg.common.mjs +++ b/test/versioned/pg-esm/pg.common.mjs @@ -66,14 +66,13 @@ export default function runTests(name, clientFactory) { return pg } - function verify(expect = assert, segment, selectTable) { - verifyMetrics(expect, segment, selectTable) - verifyTrace(expect, segment, selectTable) - verifyInstanceParameters(expect, segment) + function verify(expect = assert, transaction, selectTable) { + verifyMetrics(expect, transaction, selectTable) + verifyTrace(expect, transaction, selectTable) + verifyInstanceParameters(expect, transaction) } - function verifyMetrics(expect = assert, segment, selectTable) { - const transaction = segment.transaction + function verifyMetrics(expect = assert, transaction, selectTable) { const agent = transaction.agent selectTable = selectTable || TABLE expect.equal( @@ -121,17 +120,21 @@ export default function runTests(name, clientFactory) { ) } - function verifyTrace(expect = assert, segment, selectTable) { - const transaction = segment.transaction + function verifyTrace(expect = assert, transaction, selectTable) { selectTable = selectTable || TABLE const trace = transaction.trace expect.ok(trace, 'trace should exist') expect.ok(trace.root, 'root element should exist') - const setSegment = findSegment(trace.root, 'Datastore/statement/Postgres/' + TABLE + '/insert') + const setSegment = findSegment( + trace, + trace.root, + 'Datastore/statement/Postgres/' + TABLE + '/insert' + ) const getSegment = findSegment( + trace, trace.root, 'Datastore/statement/Postgres/' + selectTable + '/select' ) @@ -152,12 +155,15 @@ export default function runTests(name, clientFactory) { expect.ok(getSegment.timer.hrDuration, 'trace segment should have ended') } - function verifyInstanceParameters(expect = assert, segment) { - const transaction = segment.transaction + function verifyInstanceParameters(expect = assert, transaction) { const agent = transaction.agent const trace = transaction.trace - const setSegment = findSegment(trace.root, 'Datastore/statement/Postgres/' + TABLE + '/insert') + const setSegment = findSegment( + trace, + trace.root, + 'Datastore/statement/Postgres/' + TABLE + '/insert' + ) const attributes = setSegment.getAttributes() const metricHostName = getMetricHostName(agent, params.postgres_host) @@ -264,7 +270,7 @@ export default function runTests(name, clientFactory) { assert.equal(value.rows[0][COL], colVal, 'Postgres client should still work') transaction.end() - verify(assert, agent.tracer.getSegment()) + verify(assert, transaction) end() }) }) @@ -304,7 +310,7 @@ export default function runTests(name, clientFactory) { assert.equal(selectResults.rows[0][COL], colVal, 'Postgres client should still work') transaction.end() - verify(assert, agent.tracer.getSegment()) + verify(assert, transaction) end() } catch (err) { assert.ifError(err) @@ -338,11 +344,11 @@ export default function runTests(name, clientFactory) { }) pgQuery.on('end', () => { - assert.ok(agent.getTransaction(), 'transaction should still be visible') + const finalTx = agent.getTransaction() + assert.ok(finalTx, 'transaction should still be visible') + transaction.end() - const segment = agent.tracer.getSegment() - const finalTx = segment.transaction const metrics = finalTx.metrics.getMetric('Datastore/operation/Postgres/select') assert.ok( @@ -377,12 +383,10 @@ export default function runTests(name, clientFactory) { const selectResults = await client.query(selQuery) - assert.ok(agent.getTransaction(), 'transaction should still still be visible') assert.ok(selectResults, 'Postgres client should still work') + const finalTx = agent.getTransaction() + assert.ok(finalTx, 'transaction should still be visible') transaction.end() - - const segment = agent.tracer.getSegment() - const finalTx = segment.transaction const metrics = finalTx.metrics.getMetric('Datastore/operation/Postgres/select') assert.ok( metrics.total > 2.0, @@ -415,12 +419,11 @@ export default function runTests(name, clientFactory) { client.query(selQuery, function (error, ok) { assert.ifError(error) - assert.ok(agent.getTransaction(), 'transaction should still be visible') assert.ok(ok, 'everything should be peachy after setting') - + const finalTx = agent.getTransaction() + assert.ok(finalTx, 'transaction should still be visible') transaction.end() - const segment = agent.tracer.getSegment() - const finalTx = segment.transaction + const metrics = finalTx.metrics.getMetric('Datastore/operation/Postgres/select') assert.ok( metrics.total > 2.0, @@ -461,7 +464,7 @@ export default function runTests(name, clientFactory) { transaction.end() pool.end() - verify(plan, agent.tracer.getSegment()) + verify(plan, transaction) }) }) }) @@ -506,7 +509,7 @@ export default function runTests(name, clientFactory) { } done(true) - verify(plan, agent.tracer.getSegment()) + verify(plan, transaction) }) }) }) @@ -552,6 +555,7 @@ export default function runTests(name, clientFactory) { plan.ifError(error) const segment = findSegment( + transaction.trace, transaction.trace.root, 'Datastore/statement/Postgres/' + TABLE + '/insert' ) diff --git a/test/versioned/pg/pg.common.js b/test/versioned/pg/pg.common.js index cc23733f0a..2dd54a5002 100644 --- a/test/versioned/pg/pg.common.js +++ b/test/versioned/pg/pg.common.js @@ -67,14 +67,13 @@ module.exports = function runTests(name, clientFactory) { setupClient.end() } - function verify(expect = assert, segment, selectTable) { - verifyMetrics(expect, segment, selectTable) - verifyTrace(expect, segment, selectTable) - verifyInstanceParameters(expect, segment) + function verify(expect = assert, transaction, selectTable) { + verifyMetrics(expect, transaction, selectTable) + verifyTrace(expect, transaction, selectTable) + verifyInstanceParameters(expect, transaction) } - function verifyMetrics(expect = assert, segment, selectTable) { - const transaction = segment.transaction + function verifyMetrics(expect = assert, transaction, selectTable) { const agent = transaction.agent selectTable = selectTable || TABLE expect.equal( @@ -122,17 +121,21 @@ module.exports = function runTests(name, clientFactory) { ) } - function verifyTrace(expect = assert, segment, selectTable) { - const transaction = segment.transaction + function verifyTrace(expect = assert, transaction, selectTable) { selectTable = selectTable || TABLE const trace = transaction.trace expect.ok(trace, 'trace should exist') expect.ok(trace.root, 'root element should exist') - const setSegment = findSegment(trace.root, 'Datastore/statement/Postgres/' + TABLE + '/insert') + const setSegment = findSegment( + trace, + trace.root, + 'Datastore/statement/Postgres/' + TABLE + '/insert' + ) const getSegment = findSegment( + trace, trace.root, 'Datastore/statement/Postgres/' + selectTable + '/select' ) @@ -153,12 +156,15 @@ module.exports = function runTests(name, clientFactory) { expect.ok(getSegment.timer.hrDuration, 'trace segment should have ended') } - function verifyInstanceParameters(expect = assert, segment) { - const transaction = segment.transaction + function verifyInstanceParameters(expect = assert, transaction) { const agent = transaction.agent const trace = transaction.trace - const setSegment = findSegment(trace.root, 'Datastore/statement/Postgres/' + TABLE + '/insert') + const setSegment = findSegment( + trace, + trace.root, + 'Datastore/statement/Postgres/' + TABLE + '/insert' + ) const attributes = setSegment.getAttributes() const metricHostName = getMetricHostName(agent, params.postgres_host) @@ -262,7 +268,7 @@ module.exports = function runTests(name, clientFactory) { assert.equal(value.rows[0][COL], colVal, 'Postgres client should still work') transaction.end() - verify(assert, agent.tracer.getSegment()) + verify(assert, transaction) end() }) }) @@ -304,7 +310,7 @@ module.exports = function runTests(name, clientFactory) { assert.ok(agent.getTransaction(), 'transaction should still still be visible') assert.equal(selectResults.rows[0][COL], colVal, 'Postgres client should still work') transaction.end() - verify(assert, agent.tracer.getSegment()) + verify(assert, transaction) end() } catch (err) { assert.ifError(err) @@ -338,11 +344,11 @@ module.exports = function runTests(name, clientFactory) { }) pgQuery.on('end', () => { - assert.ok(agent.getTransaction(), 'transaction should still be visible') + const finalTx = agent.getTransaction() + assert.ok(finalTx, 'transaction should still be visible') + transaction.end() - const segment = agent.tracer.getSegment() - const finalTx = segment.transaction const metrics = finalTx.metrics.getMetric('Datastore/operation/Postgres/select') assert.ok( metrics.total > 2.0, @@ -376,12 +382,12 @@ module.exports = function runTests(name, clientFactory) { const selQuery = 'SELECT pg_sleep(2), now() as sleep;' const selectResults = await client.query(selQuery) - assert.ok(agent.getTransaction(), 'transaction should still still be visible') + + const finalTx = agent.getTransaction() + assert.ok(finalTx, 'transaction should still be visible') assert.ok(selectResults, 'Postgres client should still work') transaction.end() - const segment = agent.tracer.getSegment() - const finalTx = segment.transaction const metrics = finalTx.metrics.getMetric('Datastore/operation/Postgres/select') assert.ok( metrics.total > 2.0, @@ -414,12 +420,11 @@ module.exports = function runTests(name, clientFactory) { client.query(selQuery, function (error, ok) { assert.ifError(error) - assert.ok(agent.getTransaction(), 'transaction should still be visible') + const finalTx = agent.getTransaction() + assert.ok(finalTx, 'transaction should still be visible') assert.ok(ok, 'everything should be peachy after setting') transaction.end() - const segment = agent.tracer.getSegment() - const finalTx = segment.transaction const metrics = finalTx.metrics.getMetric('Datastore/operation/Postgres/select') assert.ok( metrics.total > 2.0, @@ -461,7 +466,7 @@ module.exports = function runTests(name, clientFactory) { transaction.end() pool.end() - verify(plan, agent.tracer.getSegment()) + verify(plan, transaction) }) }) }) @@ -510,7 +515,7 @@ module.exports = function runTests(name, clientFactory) { } done(true) - verify(plan, agent.tracer.getSegment()) + verify(plan, transaction) }) }) }) @@ -556,6 +561,7 @@ module.exports = function runTests(name, clientFactory) { client.query(config, [pkVal, colVal], function (error) { plan.ifError(error) const segment = findSegment( + transaction.trace, transaction.trace.root, 'Datastore/statement/Postgres/' + TABLE + '/insert' ) diff --git a/test/versioned/prisma/prisma.test.js b/test/versioned/prisma/prisma.test.js index ce9f557799..1e487e2ca3 100644 --- a/test/versioned/prisma/prisma.test.js +++ b/test/versioned/prisma/prisma.test.js @@ -58,7 +58,7 @@ test('Basic run through prisma functionality', { timeout: 30 * 1000 }, async (t) await helper.runInTransaction(agent, async (tx) => { const users = await upsertUsers(prisma) assert.equal(users.length, 2, 'should get two users') - const findManySegment = findSegment(tx.trace.root, findMany) + const findManySegment = findSegment(tx.trace, tx.trace.root, findMany) const attributes = findManySegment.getAttributes() assert.ok(!attributes.host, 'should not have a host set') assert.ok(!attributes.port_path_or_id, 'should not have a port set') @@ -79,7 +79,7 @@ test('Basic run through prisma functionality', { timeout: 30 * 1000 }, async (t) const users = await query assert.equal(users.length, 2, 'should get two users') tx.end() - const rawSegment = findSegment(tx.trace.root, raw) + const rawSegment = findSegment(tx.trace, tx.trace.root, raw) assert.ok(rawSegment, `segment named ${raw} should exist`) }) } @@ -96,7 +96,7 @@ test('Basic run through prisma functionality', { timeout: 30 * 1000 }, async (t) const count = await query assert.equal(count, 2, 'should modify two users') tx.end() - const rawSegment = findSegment(tx.trace.root, rawUpdate) + const rawSegment = findSegment(tx.trace, tx.trace.root, rawUpdate) assert.ok(rawSegment, `segment named ${rawUpdate} should exist`) }) } diff --git a/test/versioned/prisma/utils.js b/test/versioned/prisma/utils.js index 28e89462af..e16735a869 100644 --- a/test/versioned/prisma/utils.js +++ b/test/versioned/prisma/utils.js @@ -59,10 +59,10 @@ function verifyTraces(agent, transaction) { assert.ok(trace, 'trace should exist') assert.ok(trace.root, 'root element should exist') - assertSegments(trace.root, [findMany, update, update, findMany], { exact: true }) - const findManySegment = findSegment(trace.root, findMany) + assertSegments(trace, trace.root, [findMany, update, update, findMany], { exact: true }) + const findManySegment = findSegment(trace, trace.root, findMany) assert.ok(findManySegment.timer.hrDuration, 'findMany segment should have ended') - const updateSegment = findSegment(trace.root, update) + const updateSegment = findSegment(trace, trace.root, update) assert.ok(updateSegment.timer.hrDuration, 'update segment should have ended') for (const segment of [findManySegment, updateSegment]) { const attributes = segment.getAttributes() diff --git a/test/versioned/q/q.test.js b/test/versioned/q/q.test.js index daa7eaa416..b78a190fe3 100644 --- a/test/versioned/q/q.test.js +++ b/test/versioned/q/q.test.js @@ -15,7 +15,8 @@ const helper = require('../../lib/agent_helper') function assertTransaction(agent, tx, expect = assert) { expect.equal(agent.getTransaction(), tx) - expect.equal(agent.getTransaction().trace.root.children.length, 0) + const children = tx.trace.getChildren(tx.trace.root.id) + expect.equal(children.length, 0) } test.beforeEach((ctx) => { diff --git a/test/versioned/redis/redis-v4-legacy-mode.test.js b/test/versioned/redis/redis-v4-legacy-mode.test.js index 110107b7d7..c8cd339762 100644 --- a/test/versioned/redis/redis-v4-legacy-mode.test.js +++ b/test/versioned/redis/redis-v4-legacy-mode.test.js @@ -74,16 +74,17 @@ test('Redis instrumentation', async function (t) { const trace = transaction.trace assert.ok(trace, 'trace should exist') assert.ok(trace.root, 'root element should exist') - assert.equal(trace.root.children.length, 2, 'there should be only two children of the root') + const children = trace.getChildren(trace.root.id) + assert.equal(children.length, 2, 'there should be only two children of the root') - const setSegment = trace.root.children[0] + const [setSegment, getSegment] = children const setAttributes = setSegment.getAttributes() assert.ok(setSegment, 'trace segment for set should exist') assert.equal(setSegment.name, 'Datastore/operation/Redis/set', 'should register the set') assert.equal(setAttributes.key, '"testkey"', 'should have the set key as a attribute') - assert.equal(setSegment.children.length, 0, 'set should have no children') + const setSegmentChildren = trace.getChildren(setSegment.id) + assert.equal(setSegmentChildren.length, 0, 'set should have no children') - const getSegment = trace.root.children[1] const getAttributes = getSegment.getAttributes() assert.ok(getSegment, 'trace segment for get should exist') @@ -123,10 +124,10 @@ test('Redis instrumentation', async function (t) { assert.ok(!agent.getTransaction(), 'no transaction should be in play') agent.config.attributes.enabled = true - helper.runInTransaction(agent, async function () { + helper.runInTransaction(agent, async function (tx) { await client.v4.set('saveme', 'foobar') - const segment = agent.tracer.getSegment().children[0] + const [segment] = tx.trace.getChildren(agent.tracer.getSegment().id) assert.equal(segment.getAttributes().key, '"saveme"', 'should have `key` attribute') end() }) @@ -137,10 +138,10 @@ test('Redis instrumentation', async function (t) { assert.ok(!agent.getTransaction(), 'no transaction should be in play') agent.config.attributes.enabled = false - helper.runInTransaction(agent, async function () { + helper.runInTransaction(agent, async function (tx) { await client.v4.set('saveme', 'foobar') - const segment = agent.tracer.getSegment().children[0] + const [segment] = tx.trace.getChildren(agent.tracer.getSegment().id) assert.ok(!segment.getAttributes().key, 'should not have `key` attribute') end() }) @@ -158,7 +159,7 @@ test('Redis instrumentation', async function (t) { await client.v4.set('testkey', 'arglbargle') const trace = transaction.trace - const setSegment = trace.root.children[0] + const [setSegment] = trace.getChildren(trace.root.id) const attributes = setSegment.getAttributes() assert.equal(attributes.host, METRIC_HOST_NAME, 'should have host as attribute') assert.equal( @@ -187,7 +188,7 @@ test('Redis instrumentation', async function (t) { const transaction = agent.getTransaction() await client.v4.set('testkey', 'arglbargle') - const setSegment = transaction.trace.root.children[0] + const [setSegment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = setSegment.getAttributes() assert.equal(attributes.host, undefined, 'should not have host attribute') assert.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') @@ -225,9 +226,9 @@ test('Redis instrumentation', async function (t) { }) function verify() { - const setSegment1 = transaction.trace.root.children[0] - const selectSegment = transaction.trace.root.children[1] - const setSegment2 = transaction.trace.root.children[2] + const [setSegment1, selectSegment, setSegment2] = transaction.trace.getChildren( + transaction.trace.root.id + ) assert.equal( setSegment1.name, diff --git a/test/versioned/redis/redis-v4.test.js b/test/versioned/redis/redis-v4.test.js index 8b04baaa3e..8e70f23bd9 100644 --- a/test/versioned/redis/redis-v4.test.js +++ b/test/versioned/redis/redis-v4.test.js @@ -73,16 +73,16 @@ test('Redis instrumentation', async function (t) { const trace = transaction.trace assert.ok(trace, 'trace should exist') assert.ok(trace.root, 'root element should exist') - assert.equal(trace.root.children.length, 2, 'there should be only two children of the root') + const children = trace.getChildren(trace.root.id) + assert.equal(children.length, 2, 'there should be only two children of the root') - const setSegment = trace.root.children[0] + const [setSegment, getSegment] = children const setAttributes = setSegment.getAttributes() assert.ok(setSegment, 'trace segment for set should exist') assert.equal(setSegment.name, 'Datastore/operation/Redis/set', 'should register the set') assert.equal(setAttributes.key, '"testkey"', 'should have the set key as a attribute') - assert.equal(setSegment.children.length, 0, 'set should have no children') - - const getSegment = trace.root.children[1] + const setSegmentChildren = trace.getChildren(setSegment.id) + assert.equal(setSegmentChildren.length, 0, 'set should have no children') const getAttributes = getSegment.getAttributes() assert.ok(getSegment, 'trace segment for get should exist') @@ -147,10 +147,10 @@ test('Redis instrumentation', async function (t) { assert.ok(!agent.getTransaction(), 'no transaction should be in play') agent.config.attributes.enabled = true - helper.runInTransaction(agent, async function () { + helper.runInTransaction(agent, async function (tx) { await client.set('saveme', 'foobar') - const segment = agent.tracer.getSegment().children[0] + const [segment] = tx.trace.getChildren(agent.tracer.getSegment().id) assert.equal(segment.getAttributes().key, '"saveme"', 'should have `key` attribute') end() }) @@ -161,10 +161,10 @@ test('Redis instrumentation', async function (t) { assert.ok(!agent.getTransaction(), 'no transaction should be in play') agent.config.attributes.enabled = false - helper.runInTransaction(agent, async function () { + helper.runInTransaction(agent, async function (tx) { await client.set('saveme', 'foobar') - const segment = agent.tracer.getSegment().children[0] + const [segment] = tx.trace.getChildren(agent.tracer.getSegment().id) assert.ok(!segment.getAttributes().key, 'should not have `key` attribute') end() }) @@ -182,7 +182,7 @@ test('Redis instrumentation', async function (t) { await client.set('testkey', 'arglbargle') const trace = transaction.trace - const setSegment = trace.root.children[0] + const [setSegment] = trace.getChildren(trace.root.id) const attributes = setSegment.getAttributes() assert.equal(attributes.host, METRIC_HOST_NAME, 'should have host as attribute') assert.equal( @@ -211,7 +211,7 @@ test('Redis instrumentation', async function (t) { const transaction = agent.getTransaction() await client.set('testkey', 'arglbargle') - const setSegment = transaction.trace.root.children[0] + const [setSegment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = setSegment.getAttributes() assert.equal(attributes.host, undefined, 'should not have host attribute') assert.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') @@ -249,9 +249,9 @@ test('Redis instrumentation', async function (t) { }) function verify() { - const setSegment1 = transaction.trace.root.children[0] - const selectSegment = transaction.trace.root.children[1] - const setSegment2 = transaction.trace.root.children[2] + const [setSegment1, selectSegment, setSegment2] = transaction.trace.getChildren( + transaction.trace.root.id + ) assert.equal( setSegment1.name, diff --git a/test/versioned/redis/redis.test.js b/test/versioned/redis/redis.test.js index 002a596623..e348c6fdd2 100644 --- a/test/versioned/redis/redis.test.js +++ b/test/versioned/redis/redis.test.js @@ -75,16 +75,17 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { 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 of the root') - - const setSegment = trace.root.children[0] + const children = trace.getChildren(trace.root.id) + plan.equal(children.length, 1, 'there should be only one child of the root') + const [setSegment] = children const setAttributes = setSegment.getAttributes() plan.ok(setSegment, 'trace segment for set should exist') plan.equal(setSegment.name, 'Datastore/operation/Redis/set', 'should register the set') plan.equal(setAttributes.key, '"testkey"', 'should have the set key as a attribute') - plan.equal(setSegment.children.length, 1, 'set should have an only child') - - const getSegment = setSegment.children[0].children[0] + const setChildren = trace.getChildren(setSegment.id) + plan.equal(setChildren.length, 1, 'set should have an only child') + const [getSegment] = trace.getChildren(setChildren[0].id) + const getChildren = trace.getChildren(getSegment.id) const getAttributes = getSegment.getAttributes() plan.ok(getSegment, 'trace segment for get should exist') @@ -92,7 +93,7 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { plan.equal(getAttributes.key, '"testkey"', 'should have the get key as a attribute') - plan.ok(getSegment.children.length >= 1, 'get should have a callback segment') + plan.ok(getChildren.length >= 1, 'get should have a callback segment') plan.ok(getSegment.timer.hrDuration, 'trace segment should have ended') }) @@ -142,13 +143,14 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { }) agent.on('transactionFinished', function (tx) { - const redSeg = tx.trace.root.children[0] + const [redSeg] = tx.trace.getChildren(tx.trace.root.id) plan.equal( redSeg.name, 'Datastore/operation/Redis/set', 'should have untruncated redis segment' ) - plan.equal(redSeg.children.length, 0, 'should have no children for redis segment') + const redChildren = tx.trace.getChildren(redSeg.id) + plan.equal(redChildren.length, 0, 'should have no children for redis segment') }) await plan.completed }) @@ -214,11 +216,11 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { const { agent, client } = t.nr agent.config.attributes.enabled = true - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { client.set('saveme', 'foobar', function (error) { // Regardless of error, key should still be captured. assert.ok(!error) - const segment = agent.tracer.getSegment().parent + const segment = tx.trace.getParent(agent.tracer.getSegment().parentId) assert.equal(segment.getAttributes().key, '"saveme"', 'should have `key` attribute') end() }) @@ -229,11 +231,11 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { const { agent, client } = t.nr agent.config.attributes.enabled = false - helper.runInTransaction(agent, function () { + helper.runInTransaction(agent, function (tx) { client.set('saveme', 'foobar', function (error) { // Regardless of error, key should still be captured. assert.ok(!error) - const segment = agent.tracer.getSegment().parent + const segment = tx.trace.getParent(agent.tracer.getSegment().parentId) assert.ok(!segment.getAttributes().key, 'should not have `key` attribute') end() }) @@ -252,7 +254,7 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { client.set('testkey', 'arglbargle', function (error) { plan.ok(!error) const trace = transaction.trace - const setSegment = trace.root.children[0] + const [setSegment] = trace.getChildren(trace.root.id) const attributes = setSegment.getAttributes() plan.equal(attributes.host, METRIC_HOST_NAME, 'should have host as attribute') plan.equal( @@ -282,7 +284,7 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { const transaction = agent.getTransaction() client.set('testkey', 'arglbargle', function (error) { plan.ok(!error) - const setSegment = transaction.trace.root.children[0] + const [setSegment] = transaction.trace.getChildren(transaction.trace.root.id) const attributes = setSegment.getAttributes() plan.equal(attributes.host, undefined, 'should not have host attribute') plan.equal(attributes.port_path_or_id, undefined, 'should not have port attribute') @@ -327,9 +329,13 @@ test('Redis instrumentation', { timeout: 20000 }, async function (t) { await plan.completed function verify() { - const setSegment1 = transaction.trace.root.children[0] - const selectSegment = setSegment1.children[0].children[0] - const setSegment2 = selectSegment.children[0].children[0] + const [setSegment1] = transaction.trace.getChildren(transaction.trace.root.id) + const [selectSegment] = transaction.trace.getChildren( + transaction.trace.getChildren(setSegment1.id)[0].id + ) + const [setSegment2] = transaction.trace.getChildren( + transaction.trace.getChildren(selectSegment.id)[0].id + ) plan.equal(setSegment1.name, 'Datastore/operation/Redis/set', 'should register the first set') plan.equal( diff --git a/test/versioned/restify/router.test.js b/test/versioned/restify/router.test.js index f4daa1703a..4b4c40df6b 100644 --- a/test/versioned/restify/router.test.js +++ b/test/versioned/restify/router.test.js @@ -55,7 +55,7 @@ test('Restify router', async function (t) { plan.equal(transaction.verb, 'GET', 'HTTP method is GET') plan.ok(transaction.trace, 'transaction has trace') - const web = transaction.trace.root.children[0] + const [web] = transaction.trace.getChildren(transaction.trace.root.id) plan.ok(web, 'trace has web segment') plan.equal(web.name, transaction.name, 'segment name and transaction name match') plan.equal(web.partialName, 'Restify/GET//test/:id', 'should have partial name for apdex') diff --git a/test/versioned/superagent/async-await.test.js b/test/versioned/superagent/async-await.test.js index 55f54e7a72..edd706361c 100644 --- a/test/versioned/superagent/async-await.test.js +++ b/test/versioned/superagent/async-await.test.js @@ -41,11 +41,12 @@ test('should maintain transaction context with promises', (t, end) => { const { request } = t.nr await request.get(address) - const mainSegment = tx.trace.root.children[0] + const [mainSegment] = tx.trace.getChildren(tx.trace.root.id) assert.ok(mainSegment) match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') + const mainChildren = tx.trace.getChildren(mainSegment.id) assert.equal( - mainSegment.children.filter((c) => c.name === 'Callback: ').length, + mainChildren.filter((c) => c.name === 'Callback: ').length, 1, 'CB created by superagent is present' ) diff --git a/test/versioned/superagent/superagent.test.js b/test/versioned/superagent/superagent.test.js index 481135cd9a..970bf56544 100644 --- a/test/versioned/superagent/superagent.test.js +++ b/test/versioned/superagent/superagent.test.js @@ -40,11 +40,12 @@ test('should maintain transaction context with callbacks', (t, end) => { request.get(address, function testCallback() { assert.ok(tx) - const mainSegment = tx.trace.root.children[0] + const [mainSegment] = tx.trace.getChildren(tx.trace.root.id) assert.ok(mainSegment) match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') + const mainChildren = tx.trace.getChildren(mainSegment.id) assert.equal( - mainSegment.children.filter((c) => c.name === 'Callback: testCallback').length, + mainChildren.filter((c) => c.name === 'Callback: testCallback').length, 1, 'has segment matching callback' ) @@ -68,11 +69,12 @@ test('should maintain transaction context with promises', (t, end) => { request.get(address).then(function testThen() { assert.ok(tx) - const mainSegment = tx.trace.root.children[0] + const [mainSegment] = tx.trace.getChildren(tx.trace.root.id) assert.ok(mainSegment) match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') + const mainChildren = tx.trace.getChildren(mainSegment.id) assert.equal( - mainSegment.children.filter((c) => c.name === 'Callback: ').length, + mainChildren.filter((c) => c.name === 'Callback: ').length, 1, 'has segment matching callback' ) diff --git a/test/versioned/undici/requests.test.js b/test/versioned/undici/requests.test.js index b5ed2e8f54..23922aa133 100644 --- a/test/versioned/undici/requests.test.js +++ b/test/versioned/undici/requests.test.js @@ -82,7 +82,7 @@ test('Undici request tests', async (t) => { }) assert.equal(statusCode, 200) - assertSegments(tx.trace.root, [`External/${HOST}/post`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/post`], { exact: false }) tx.end() }) }) @@ -115,7 +115,7 @@ test('Undici request tests', async (t) => { await client.request({ path: '/', method: 'GET' }) - assertSegments(transaction.trace.root, [`External/localhost:${port}/`], { + assertSegments(transaction.trace, transaction.trace.root, [`External/localhost:${port}/`], { exact: false }) @@ -130,7 +130,7 @@ test('Undici request tests', async (t) => { method: 'GET' }) assert.equal(statusCode, 200) - const segment = metrics.findSegment(tx.trace.root, `External/${HOST}/get`) + const segment = metrics.findSegment(tx.trace, tx.trace.root, `External/${HOST}/get`) const attrs = segment.getAttributes() assert.equal(attrs.url, `${REQUEST_URL}/get`) assert.equal(attrs.procedure, 'GET') @@ -194,7 +194,7 @@ test('Undici request tests', async (t) => { const [{ statusCode }, { statusCode: statusCode2 }] = await Promise.all([req1, req2]) assert.equal(statusCode, 200) assert.equal(statusCode2, 200) - assertSegments(tx.trace.root, [`External/${HOST}/post`, `External/${HOST}/put`], { + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/post`, `External/${HOST}/put`], { exact: false }) tx.end() @@ -210,7 +210,7 @@ test('Undici request tests', async (t) => { }) } catch (err) { assert.ok(err) - assertSegments(tx.trace.root, ['External/invalidurl/foo'], { exact: false }) + assertSegments(tx.trace, tx.trace.root, ['External/invalidurl/foo'], { exact: false }) assert.equal(tx.exceptions.length, 1) tx.end() } @@ -230,7 +230,7 @@ test('Undici request tests', async (t) => { }, 100) await req } catch { - assertSegments(tx.trace.root, [`External/${HOST}/delay/1000`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/delay/1000`], { exact: false }) assert.equal(tx.exceptions.length, 1) const expectedErrMsg = semver.gte(pkgVersion, '6.3.0') ? 'This operation was aborted' @@ -259,11 +259,11 @@ test('Undici request tests', async (t) => { try { await req } catch { - assertSegments(transaction.trace.root, [`External/localhost:${port}/`], { + assertSegments(transaction.trace, transaction.trace.root, [`External/localhost:${port}/`], { exact: false }) - const segments = transaction.trace.root.children + const segments = transaction.trace.getChildren(transaction.trace.root.id) const segment = segments[segments.length - 1] assert.ok(segment.timer.start, 'should have started') @@ -281,7 +281,7 @@ test('Undici request tests', async (t) => { method: 'GET' }) assert.equal(statusCode, 400) - assertSegments(tx.trace.root, [`External/${HOST}/status/400`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/status/400`], { exact: false }) tx.end() }) }) @@ -290,7 +290,7 @@ test('Undici request tests', async (t) => { await helper.runInTransaction(agent, async (tx) => { const res = await undici.fetch(REQUEST_URL) assert.equal(res.status, 200) - assertSegments(tx.trace.root, [`External/${HOST}/`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/`], { exact: false }) tx.end() }) }) @@ -312,7 +312,7 @@ test('Undici request tests', async (t) => { }) } ) - assertSegments(tx.trace.root, [`External/${HOST}/get`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/get`], { exact: false }) tx.end() }) }) @@ -347,7 +347,7 @@ test('Undici request tests', async (t) => { }), (err) => { assert.ok(!err) - assertSegments(tx.trace.root, [`External/${HOST}/get`], { exact: false }) + assertSegments(tx.trace, tx.trace.root, [`External/${HOST}/get`], { exact: false }) tx.end() end() } diff --git a/test/versioned/when/segments.test.js b/test/versioned/when/segments.test.js index 3eebea1beb..3f9ee55228 100644 --- a/test/versioned/when/segments.test.js +++ b/test/versioned/when/segments.test.js @@ -14,8 +14,14 @@ const helper = require('../../lib/agent_helper') // simulates a function that returns a promise and has a segment created for itself function doSomeWork({ tracer, Promise = global.Promise, segmentName, shouldReject } = {}) { - const segment = tracer.createSegment(segmentName) - return tracer.bindFunction(actualWork, segment)() + const ctx = tracer.getContext() + const segment = tracer.createSegment({ + name: segmentName, + parent: ctx.segment, + transaction: ctx.transaction + }) + const newCtx = ctx.enterSegment({ segment }) + return tracer.bindFunction(actualWork, newCtx)() function actualWork() { segment.touch() @@ -55,9 +61,11 @@ test('segments enabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', (tx) => { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) assertSegments( + tx.trace, tx.trace.root, [ 'doSomeWork', @@ -70,10 +78,16 @@ test('segments enabled', async (t) => { helper.runInTransaction(agent, function transactionWrapper(transaction) { doSomeWork({ tracer, Promise: when.Promise, segmentName: 'doSomeWork' }).then(function () { - const childSegment = tracer.createSegment('someChildSegment') + const ctx = agent.tracer.getContext() + const childSegment = tracer.createSegment({ + name: 'someChildSegment', + parent: ctx.segment, + transaction + }) + const newCtx = ctx.enterSegment({ segment: childSegment }) // touch the segment, so that it is not truncated childSegment.touch() - tracer.bindFunction(function () {}, childSegment) + tracer.bindFunction(function () {}, newCtx) process.nextTick(transaction.end.bind(transaction)) }) }) @@ -86,8 +100,11 @@ test('segments enabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) + assertSegments( + tx.trace, tx.trace.root, [ 'doWork1', @@ -113,7 +130,8 @@ test('segments enabled', async (t) => { return doSomeWork({ tracer, segmentName: 'doWork2', Promise: when.Promise }) }) .then(function secondThen() { - const s = tracer.createSegment('secondThen') + const ctx = agent.tracer.getContext() + const s = tracer.createSegment({ name: 'secondThen', parent: ctx.segment, transaction }) s.start() s.end() process.nextTick(transaction.end.bind(transaction)) @@ -128,9 +146,11 @@ test('segments enabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) assertSegments( + tx.trace, tx.trace.root, [ 'doWork1', @@ -159,9 +179,11 @@ test('segments enabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) assertSegments( + tx.trace, tx.trace.root, ['doWork1', ['Promise startSomeWork', ['Promise#catch catchHandler']]], {}, @@ -187,9 +209,11 @@ test('segments enabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) assertSegments( + tx.trace, tx.trace.root, [ 'doWork1', @@ -217,12 +241,14 @@ test('segments enabled', async (t) => { }) }) .then(function secondThen() { - const s = tracer.createSegment('secondThen') + const ctx = agent.tracer.getContext() + const s = tracer.createSegment({ name: 'secondThen', parent: ctx.segment, transaction }) s.start() s.end() }) .catch(function catchHandler() { - const s = tracer.createSegment('catchHandler') + const ctx = agent.tracer.getContext() + const s = tracer.createSegment({ name: 'catchHandler', parent: ctx.segment, transaction }) s.start() s.end() process.nextTick(transaction.end.bind(transaction)) @@ -238,9 +264,11 @@ test('segments enabled', async (t) => { const { Promise } = when agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 2) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 2) assertSegments( + tx.trace, tx.trace.root, ['Promise startSomeWork', ['Promise#then myThen'], 'doSomeWork'], { exact: true }, @@ -254,8 +282,10 @@ test('segments enabled', async (t) => { resolve = _resolve }) - const segment = tracer.createSegment('doSomeWork') - resolve = tracer.bindFunction(resolve, segment) + const ctx = agent.tracer.getContext() + const segment = tracer.createSegment({ name: 'doSomeWork', parent: ctx.segment, transaction }) + const newCtx = ctx.enterSegment({ segment }) + resolve = tracer.bindFunction(resolve, newCtx) p.then(function myThen() { segment.touch() @@ -291,17 +321,30 @@ test('segments disabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) - assertSegments(tx.trace.root, ['doSomeWork', ['someChildSegment']], {}, { assert: plan }) + assertSegments( + tx.trace, + tx.trace.root, + ['doSomeWork', ['someChildSegment']], + {}, + { assert: plan } + ) }) helper.runInTransaction(agent, function transactionWrapper(transaction) { doSomeWork({ tracer, segmentName: 'doSomeWork', Promise: when.Promise }).then(function () { - const childSegment = tracer.createSegment('someChildSegment') + const ctx = agent.tracer.getContext() + const childSegment = tracer.createSegment({ + name: 'someChildSegment', + parent: ctx.segment, + transaction + }) + const newCtx = ctx.enterSegment({ segment: childSegment }) // touch the segment, so that it is not truncated childSegment.touch() - tracer.bindFunction(function () {}, childSegment) + tracer.bindFunction(function () {}, newCtx) process.nextTick(transaction.end.bind(transaction)) }) }) @@ -314,9 +357,10 @@ test('segments disabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) - assertSegments(tx.trace.root, ['doWork1'], {}, { assert: plan }) + assertSegments(tx.trace, tx.trace.root, ['doWork1'], {}, { assert: plan }) }) helper.runInTransaction(agent, function transactionWrapper(transaction) { @@ -339,9 +383,10 @@ test('segments disabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) - assertSegments(tx.trace.root, ['doWork1'], {}, { assert: plan }) + assertSegments(tx.trace, tx.trace.root, ['doWork1'], {}, { assert: plan }) }) helper.runInTransaction(agent, function transactionWrapper(transaction) { @@ -362,9 +407,10 @@ test('segments disabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) - assertSegments(tx.trace.root, ['doWork1'], {}, { assert: plan }) + assertSegments(tx.trace, tx.trace.root, ['doWork1'], {}, { assert: plan }) }) helper.runInTransaction(agent, function transactionWrapper(transaction) { @@ -385,9 +431,10 @@ test('segments disabled', async (t) => { const { agent, tracer, when } = t.nr agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) - assertSegments(tx.trace.root, ['doWork1', ['doWork2']], {}, { assert: plan }) + assertSegments(tx.trace, tx.trace.root, ['doWork1', ['doWork2']], {}, { assert: plan }) }) helper.runInTransaction(agent, function transactionWrapper(transaction) { @@ -415,9 +462,10 @@ test('segments disabled', async (t) => { const { Promise } = when agent.once('transactionFinished', function (tx) { - plan.equal(tx.trace.root.children.length, 1) + const children = tx.trace.getChildren(tx.trace.root.id) + plan.equal(children.length, 1) - assertSegments(tx.trace.root, ['doSomeWork'], { exact: true }, { assert: plan }) + assertSegments(tx.trace, tx.trace.root, ['doSomeWork'], { exact: true }, { assert: plan }) }) helper.runInTransaction(agent, function transactionWrapper(transaction) { @@ -426,8 +474,10 @@ test('segments disabled', async (t) => { resolve = _resolve }) - const segment = tracer.createSegment('doSomeWork') - resolve = tracer.bindFunction(resolve, segment) + const ctx = agent.tracer.getContext() + const segment = tracer.createSegment({ name: 'doSomeWork', parent: ctx.segment, transaction }) + const newCtx = ctx.enterSegment({ segment }) + resolve = tracer.bindFunction(resolve, newCtx) p.then(function myThen() { segment.touch() diff --git a/third_party_manifest.json b/third_party_manifest.json index 6b8df931fa..56433241da 100644 --- a/third_party_manifest.json +++ b/third_party_manifest.json @@ -80,6 +80,30 @@ "licenseTextSource": "file", "publisher": "newrelic" }, + "@opentelemetry/api@1.9.0": { + "name": "@opentelemetry/api", + "version": "1.9.0", + "range": "^1.9.0", + "licenses": "Apache-2.0", + "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v1.9.0", + "licenseFile": "node_modules/@opentelemetry/api/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v1.9.0/LICENSE", + "licenseTextSource": "file", + "publisher": "OpenTelemetry Authors" + }, + "@opentelemetry/semantic-conventions@1.27.0": { + "name": "@opentelemetry/semantic-conventions", + "version": "1.27.0", + "range": "^1.27.0", + "licenses": "Apache-2.0", + "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v1.27.0", + "licenseFile": "node_modules/@opentelemetry/semantic-conventions/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v1.27.0/LICENSE", + "licenseTextSource": "file", + "publisher": "OpenTelemetry Authors" + }, "@tyriar/fibonacci-heap@2.0.9": { "name": "@tyriar/fibonacci-heap", "version": "2.0.9", @@ -199,15 +223,15 @@ "email": "w@tson.dk", "url": "https://twitter.com/wa7son" }, - "semver@7.6.3": { + "semver@7.6.2": { "name": "semver", - "version": "7.6.3", + "version": "7.6.2", "range": "^7.5.2", "licenses": "ISC", "repoUrl": "https://github.com/npm/node-semver", - "versionedRepoUrl": "https://github.com/npm/node-semver/tree/v7.6.3", + "versionedRepoUrl": "https://github.com/npm/node-semver/tree/v7.6.2", "licenseFile": "node_modules/semver/LICENSE", - "licenseUrl": "https://github.com/npm/node-semver/blob/v7.6.3/LICENSE", + "licenseUrl": "https://github.com/npm/node-semver/blob/v7.6.2/LICENSE", "licenseTextSource": "file", "publisher": "GitHub Inc." }, @@ -327,7 +351,19 @@ "licenseUrl": "https://github.com/octokit/rest.js/blob/v18.12.0/LICENSE", "licenseTextSource": "file" }, - "@slack/bolt@3.22.0": { + "@opentelemetry/sdk-trace-base@1.27.0": { + "name": "@opentelemetry/sdk-trace-base", + "version": "1.27.0", + "range": "^1.27.0", + "licenses": "Apache-2.0", + "repoUrl": "https://github.com/open-telemetry/opentelemetry-js", + "versionedRepoUrl": "https://github.com/open-telemetry/opentelemetry-js/tree/v1.27.0", + "licenseFile": "node_modules/@opentelemetry/sdk-trace-base/LICENSE", + "licenseUrl": "https://github.com/open-telemetry/opentelemetry-js/blob/v1.27.0/LICENSE", + "licenseTextSource": "file", + "publisher": "OpenTelemetry Authors" + }, + "@slack/bolt@3.19.0": { "name": "@slack/bolt", "version": "3.22.0", "range": "^3.7.0",