├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ └── pr.yml ├── .gitignore ├── .gitlab-ci.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── LICENSE-3rdparty.csv ├── NOTICE ├── README.md ├── images └── screenshot.png ├── pom.xml ├── settings.xml └── src ├── main └── java │ └── org │ └── datadog │ └── jmeter │ └── plugins │ ├── DatadogBackendClient.java │ ├── DatadogConfiguration.java │ ├── DatadogHttpClient.java │ ├── aggregation │ ├── ConcurrentAggregator.java │ └── DatadogSketch.java │ ├── exceptions │ ├── DatadogApiException.java │ └── DatadogConfigurationException.java │ ├── metrics │ ├── DatadogMetric.java │ └── DatadogMetricContext.java │ └── util │ └── CommonUtils.java └── test └── java └── org └── datadog └── jmeter └── plugins ├── DatadogBackendClientTest.java ├── DatadogConfigurationTest.java ├── aggregation ├── ConcurrentAggregatorTest.java └── DatadogSketchTest.java └── metrics └── DatadogMetricContextTest.java /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ for syntax 2 | # Rules are matched bottom-to-top, so one team can own subdirectories 3 | # and another the rest of the directory. 4 | 5 | # All your base 6 | * @DataDog/agent-integrations 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Ubuntu] 28 | - Version [e.g. 18.04] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: kind/feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Note:** 11 | If you have a feature request, you should [contact support](https://docs.datadoghq.com/help/) so the request can be properly tracked. 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Requirements for Contributing to this repository 2 | 3 | * Fill out the template below. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * The pull request must only fix one issue at the time. 5 | * The pull request must update the test suite to demonstrate the changed functionality. 6 | * After you create the pull request, all status checks must be pass before a maintainer reviews your contribution. For more details, please see [CONTRIBUTING](/README.md#contributing). 7 | 8 | ### What does this PR do? 9 | 10 | 19 | 20 | ### Description of the Change 21 | 22 | 31 | 32 | ### Alternate Designs 33 | 34 | 35 | 36 | ### Possible Drawbacks 37 | 38 | 39 | 40 | ### Verification Process 41 | 42 | 53 | 54 | ### Additional Notes 55 | 56 | 57 | 58 | ### Review checklist (to be filled by reviewers) 59 | 60 | - [ ] Feature or bug fix MUST have appropriate tests (unit, integration, etc...) 61 | - [ ] Files changes must correspond to the primary purpose of the PR as described in the title (small unrelated changes should have their own PR) 62 | - [ ] PR must have one `changelog/` label attached. If applicable it should have the `backward-incompatible` label attached. 63 | - [ ] PR should not have `do-not-merge/` label attached. 64 | - [ ] If Applicable, issue must have `kind/` and `severity/` labels attached at least. 65 | 66 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ main ] 9 | 10 | jobs: 11 | analyze: 12 | name: Analyze 13 | runs-on: ubuntu-latest 14 | permissions: 15 | actions: read 16 | contents: read 17 | security-events: write 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: [ 'java' ] 23 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 24 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v3 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v2 33 | with: 34 | languages: ${{ matrix.language }} 35 | # If you wish to specify custom queries, you can do so here or in a config file. 36 | # By default, queries listed here will override any specified in a config file. 37 | # Prefix the list here with "+" to use these queries and those in the config file. 38 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 39 | 40 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 41 | # If this step fails, then you should remove it and run the build manually. 42 | - name: Autobuild 43 | uses: github/codeql-action/autobuild@v2 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@v2 47 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Maven tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | linux: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up JDK 1.8 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.8 21 | - name: Cache Maven packages 22 | uses: actions/cache@v2 23 | with: 24 | path: ~/.m2 25 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 26 | restore-keys: ${{ runner.os }}-m2 27 | - name: Build with Maven 28 | run: mvn --batch-mode --update-snapshots package 29 | - run: mkdir artifacts && cp target/*.jar artifacts 30 | - uses: actions/upload-artifact@v2 31 | with: 32 | name: Package 33 | path: artifacts 34 | windows: 35 | runs-on: windows-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Set up JDK 1.8 40 | uses: actions/setup-java@v1 41 | with: 42 | java-version: 1.8 43 | - name: Cache Maven packages 44 | uses: actions/cache@v2 45 | with: 46 | path: ~/.m2 47 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 48 | restore-keys: ${{ runner.os }}-m2 49 | - name: Build with Maven 50 | run: mvn --batch-mode --update-snapshots package 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Target classes 26 | target/ 27 | 28 | # Editors 29 | .idea/ 30 | 31 | # Eclipse 32 | .settings/ 33 | 34 | .classpath 35 | .factorypath 36 | .project 37 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - deploy 4 | - rotate_gpg_keys 5 | 6 | variables: 7 | REGISTRY: 486234852809.dkr.ecr.us-east-1.amazonaws.com 8 | BUILDER_IMAGE: $REGISTRY/ci/jmeter-datadog-backend-listener:latest 9 | 10 | deploy_to_sonatype: 11 | stage: deploy 12 | tags: [ "runner:docker" ] 13 | image: $BUILDER_IMAGE 14 | 15 | only: 16 | - tags 17 | when: manual 18 | 19 | script: 20 | # Ensure we don't print commands being run to the logs during credential 21 | # operations 22 | - set +eux 23 | 24 | - echo "Fetching Sonatype user..." 25 | - export SONATYPE_USER=$(aws ssm get-parameter --region us-east-1 --name ci.jmeter-datadog-backend-listener.publishing.sonatype_username --with-decryption --query "Parameter.Value" --out text) 26 | 27 | - echo "Fetching Sonatype password..." 28 | - export SONATYPE_PASS=$(aws ssm get-parameter --region us-east-1 --name ci.jmeter-datadog-backend-listener.publishing.sonatype_password --with-decryption --query "Parameter.Value" --out text) 29 | 30 | - echo "Fetching signing key password..." 31 | - export GPG_PASSPHRASE=$(aws ssm get-parameter --region us-east-1 --name ci.jmeter-datadog-backend-listener.signing.gpg_passphrase --with-decryption --query "Parameter.Value" --out text) 32 | 33 | - echo "Fetching signing key..." 34 | - GPG_KEY=$(aws ssm get-parameter --region us-east-1 --name ci.jmeter-datadog-backend-listener.signing.gpg_private_key --with-decryption --query "Parameter.Value" --out text) 35 | 36 | - printf -- "$GPG_KEY" | gpg --import --batch 37 | 38 | - echo "Building release..." 39 | - mvn -DperformRelease=true -DskipTests -Darguments=-DskipTests --settings ./settings.xml clean deploy 40 | 41 | - echo "Cleaning up..." 42 | 43 | # unset the env variables 44 | - unset SONATYPE_USER 45 | - unset SONATYPE_PASS 46 | - unset GPG_PASSPHRASE 47 | - unset GPG_KEY 48 | 49 | - set -x 50 | 51 | 52 | # This job generates the GPG key 53 | # NOTE: This is included for once gpg keys expire, this has not been run before 54 | create_key: 55 | stage: rotate_gpg_keys 56 | when: manual 57 | 58 | tags: 59 | - "runner:docker" 60 | 61 | image: $REGISTRY/ci/agent-key-management-tools/gpg:1 62 | variables: 63 | PROJECT_NAME: "jmeter-datadog-backend-listener" 64 | EXPORT_TO_KEYSERVER: "true" 65 | script: 66 | - /create.sh 67 | artifacts: 68 | expire_in: 13 mos 69 | paths: 70 | - ./pubkeys/ 71 | 72 | # This job makes a new Docker image of maven used for `deploy_to_sonatype` and sends it to $BUILDER_IMAGE 73 | rebuild_maven_image: 74 | stage: build 75 | image: $REGISTRY/docker:18.03.1 76 | when: manual 77 | script: 78 | - docker build --tag $BUILDER_IMAGE . # Build the Dockerfile from this directory 79 | - docker push $BUILDER_IMAGE 80 | tags: [ "runner:docker" ] -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 0.5.0 4 | * [Added] Add ability to exclude sample results to be sent as logs based on response code regex 5 | See [#47](https://github.com/DataDog/jmeter-datadog-backend-listener/issues/47) 6 | 7 | ## 0.4.0 8 | * [Changed] Set configured tags on plugin generated logs. (See [#45](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/45)). 9 | 10 | ## 0.3.1 11 | * [Fixed] Setting `includeSubresults` to `true` will now also include the parent results as well as subresults recursively (See [#35](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/35)). 12 | 13 | ## 0.3.0 14 | * [Added] Add ability to release to Maven Central. See [#26](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/26) 15 | * [Added] Add custom tags to global metrics. See [#23](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/23) 16 | 17 | ## 0.2.0 18 | * [Added] Add `customTags` config option. See [#15](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/15) 19 | * [Added] Tag metrics by `thread_group`. See [#17](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/17) 20 | * [Added] Add `thread_group` to log payload. See [#18](https://github.com/DataDog/jmeter-datadog-backend-listener/pull/18) 21 | 22 | ## 0.1.0 23 | Initial release 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.2-jdk-8-slim 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt update 4 | RUN apt install -y python3 python3-pip 5 | RUN python3 -m pip install awscli -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-3rdparty.csv: -------------------------------------------------------------------------------- 1 | Component,Origin,License,Copyright, 2 | import,com.datadoghq.sketches-java,Apache-2.0,Copyright 2020 Datadog," Inc." 3 | import,org.apache.jmeter,Apache-2.0,Copyright 1998-2021 The Apache Software Foundation, 4 | import (test),org.junit,EPL-1.0,, 5 | import (test),org.mockito,MIT,Copyright (c) 2007 Mockito contributors, 6 | import (test),org.powermock,Apache-2.0,Copyright 2007-2017 PowerMock Contributors 7 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Datadog JMeter Backend Listener Plugin 2 | Copyright 2021-present Datadog, Inc. 3 | 4 | This product includes software developed at Datadog (https://www.datadoghq.com/). 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datadog Backend Listener for Apache JMeter 2 | 3 | ![screenshot](images/screenshot.png) 4 | 5 | ## Overview 6 | Datadog Backend Listener for Apache JMeter is a JMeter plugin used to send test results to the Datadog platform. It includes the following features: 7 | - Real time reporting of test metrics (latency, bytes sent and more. See the `metrics` section. 8 | - Real time reporting of test results as Datadog log events. 9 | - Ability to include sub results. 10 | 11 | ## Installation 12 | 13 | You can install the plugin either manually or with JMeter Plugins Manager. 14 | 15 | ### Manual installation 16 | 1. Download the Datadog plugin JAR file from the [release page](https://github.com/DataDog/jmeter-datadog-backend-listener/releases) 17 | 2. Place the JAR in the `lib/ext` directory within your JMeter installation. 18 | 3. Launch JMeter (or quit and re-open the application). 19 | 20 | ### JMeter plugins Manager 21 | 1. If not already configured, download the [JMeter Plugins Manager JAR](https://jmeter-plugins.org/wiki/PluginsManager/). 22 | 2. Once you've completed the download, place the `.jar` in the `lib/ext` directory within your JMeter installation. 23 | 3. Launch JMeter (or quit and re-open the application). 24 | 4. Go to `Options > Plugins Manager > Available Plugins`. 25 | 5. Search for "Datadog Backend Listener". 26 | 6. Click the checbox next to the Datadog Backend Listener plugin. 27 | 7. Click "Apply Changes and Restart JMeter". 28 | 29 | ## Configuration 30 | To start reporting metrics to Datadog: 31 | 32 | 1. Right click on the thread group or the test plan for which you want to send metrics to Datadog. 33 | 2. Go to `Add > Listener > Backend Listener`. 34 | 3. Modify the `Backend Listener Implementation` and select `org.datadog.jmeter.plugins.DatadogBackendClient` from the drop-down. 35 | 4. Set the `apiKey` variable to [your Datadog API key](https://app.datadoghq.com/account/settings#api). 36 | 5. Run your test and validate that metrics have appeared in Datadog. 37 | 38 | The plugin has the following configuration options: 39 | 40 | | Name | Required | Default value | description| 41 | |------------|:--------:|---------------|------------| 42 | |apiKey | true | NA | Your Datadog API key.| 43 | |datadogUrl | false | https://api.datadoghq.com/api/ | You can configure a different endpoint, for instance https://api.datadoghq.eu/api/ if your datadog instance is in the EU| 44 | |logIntakeUrl | false | https://http-intake.logs.datadoghq.com/v1/input/ | You can configure a different endpoint, for instance https://http-intake.logs.datadoghq.eu/v1/input/ if your datadog instance is in the EU| 45 | |metricsMaxBatchSize|false|200|Metrics are submitted every 10 seconds in batches of size `metricsMaxBatchSize`| 46 | |logsBatchSize|false|500|Logs are submitted in batches of size `logsBatchSize` as soon as this size is reached.| 47 | |sendResultsAsLogs|false|false|By default only metrics are reported to Datadog. To report individual test results as log events, set this field to `true`.| 48 | |includeSubresults|false|false|A subresult is for instance when an individual HTTP request has to follow redirects. By default subresults are ignored.| 49 | |excludeLogsResponseCodeRegex|false|`""`| Setting `sendResultsAsLogs` will submit all results as logs to Datadog by default. This option lets you exclude results whose response code matches a given regex. For example, you may set this option to `[123][0-5][0-9]` to only submit errors.| 50 | |customTags|false|`""`|Comma-separated list of tags to add to every metric 51 | 52 | ## Troubleshooting 53 | 54 | If for whatever reason you are not seeing JMeter metrics in Datadog, check your `jmeter.log` file, which should be in the `/bin` folder of your JMeter installation. 55 | 56 | ## Contributing 57 | 58 | ### Reporting a bug and feature requests 59 | - **Ensure the bug was not already reported** 60 | - If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/DataDog/jmeter-datadog-backend-listene/issues/new). 61 | - If you have a feature request, it is encouraged to contact the [Datadog support](https://docs.datadoghq.com/help) so the request can be prioritized and properly tracked. 62 | - **Do not open an issue if you have a question**, instead contact the [Datadog support](https://docs.datadoghq.com/help). 63 | 64 | ### Pull requests 65 | Have you fixed an issue or adding a new feature? Many thanks for your work and for letting other to benefit from it. 66 | 67 | Here are some generic guidelines: 68 | - Avoid changing too many things at once. 69 | - **Write tests** for the code you wrote. 70 | - Make sure **all tests pass locally**. 71 | - Summarize your PR with a **meaningful title** and **write a meaningful description for it**. 72 | 73 | Your pull request must pass the CI before we can merge it. If you're seeing an error and don't think it's your fault, it may not be. Let us know in the PR and we'll get it sorted out. 74 | 75 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DataDog/jmeter-datadog-backend-listener/e17d84e708c23969115eef2f52a0e10a6aa272ef/images/screenshot.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | jar 7 | com.datadoghq 8 | jmeter-datadog-backend-listener 9 | 10 | 0.5.0 11 | jmeter-datadog-backend-listener 12 | https://github.com/DataDog/jmeter-datadog-backend-listener 13 | Datadog JMeter plugin 14 | 15 | scm:git:git://github.com/DataDog/jmeter-datadog-backend-listener.git 16 | scm:git:https://github.com/DataDog/jmeter-datadog-backend-listener.git 17 | https://github.com/DataDog/jmeter-datadog-backend-listener 18 | HEAD 19 | 20 | 21 | 22 | 23 | Apache 2.0 24 | https://github.com/DataDog/jmeter-datadog-backend-listener/blob/main/LICENSE 25 | repo 26 | 27 | 28 | 29 | 30 | 31 | 32 | datadog 33 | Datadog developers 34 | dev@datadoghq.com 35 | 36 | 37 | 38 | 39 | 40 | 41 | nexus 42 | 43 | 44 | https://oss.sonatype.org/content/repositories/snapshots 45 | 46 | 47 | nexus 48 | 49 | 50 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 51 | 52 | 53 | 54 | 55 | UTF-8 56 | 1.8 57 | 1.8 58 | 1.8 59 | 5.3 60 | github 61 | 62 | 63 | 64 | 65 | com.datadoghq 66 | sketches-java 67 | 0.6.1 68 | compile 69 | 70 | 71 | org.apache.jmeter 72 | ApacheJMeter_config 73 | ${org.apache.jmeter.version} 74 | provided 75 | 76 | 77 | org.apache.jmeter 78 | ApacheJMeter_core 79 | ${org.apache.jmeter.version} 80 | provided 81 | 82 | 83 | org.apache.jmeter 84 | ApacheJMeter_components 85 | ${org.apache.jmeter.version} 86 | provided 87 | 88 | 89 | org.apache.jmeter 90 | ApacheJMeter_http 91 | ${org.apache.jmeter.version} 92 | provided 93 | 94 | 95 | junit 96 | junit 97 | 4.13.1 98 | test 99 | 100 | 101 | org.mockito 102 | mockito-core 103 | 3.5.11 104 | test 105 | 106 | 107 | org.powermock 108 | powermock-module-junit4 109 | 2.0.7 110 | test 111 | 112 | 113 | org.powermock 114 | powermock-api-mockito2 115 | 2.0.7 116 | test 117 | 118 | 119 | 120 | 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-source-plugin 125 | 2.2.1 126 | 127 | 128 | attach-sources 129 | 130 | jar-no-fork 131 | 132 | 133 | 134 | 135 | 136 | org.apache.maven.plugins 137 | maven-javadoc-plugin 138 | 2.10.4 139 | 140 | 141 | attach-javadocs 142 | 143 | jar 144 | 145 | 146 | 147 | 148 | 149 | 150 | http.response.details 151 | a 152 | Http Response Details: 153 | 154 | 155 | 156 | 157 | 158 | maven-assembly-plugin 159 | 2.5.3 160 | 161 | 162 | jar-with-dependencies 163 | 164 | 165 | 166 | 167 | make-assembly 168 | package 169 | 170 | single 171 | 172 | 173 | 174 | 175 | 176 | org.sonatype.plugins 177 | nexus-staging-maven-plugin 178 | 1.6.6 179 | true 180 | 181 | 182 | default-deploy 183 | deploy 184 | 185 | deploy 186 | 187 | 188 | 189 | 190 | nexus 191 | 192 | 193 | https://oss.sonatype.org/ 194 | 378eecbbe2cf9 195 | false 196 | 197 | 198 | 199 | org.apache.maven.plugins 200 | maven-gpg-plugin 201 | 1.6 202 | 203 | 204 | sign-artifacts 205 | verify 206 | 207 | sign 208 | 209 | 210 | 211 | --pinentry-mode 212 | loopback 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | maven-clean-plugin 224 | 3.1.0 225 | 226 | 227 | 228 | maven-resources-plugin 229 | 3.0.2 230 | 231 | 232 | maven-compiler-plugin 233 | 3.8.0 234 | 235 | 236 | maven-surefire-plugin 237 | 2.22.1 238 | 239 | false 240 | 241 | 242 | 243 | maven-jar-plugin 244 | 3.0.2 245 | 246 | 247 | maven-install-plugin 248 | 2.5.2 249 | 250 | 251 | maven-deploy-plugin 252 | 2.8.2 253 | 254 | true 255 | 256 | 257 | 258 | 259 | maven-site-plugin 260 | 3.7.1 261 | 262 | 263 | maven-project-info-reports-plugin 264 | 3.0.0 265 | 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | nexus 7 | ${env.SONATYPE_USER} 8 | ${env.SONATYPE_PASS} 9 | 10 | 11 | gpg.passphrase 12 | ${env.GPG_PASSPHRASE} 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/DatadogBackendClient.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.ScheduledExecutorService; 13 | import java.util.concurrent.ScheduledFuture; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | import java.util.regex.Matcher; 17 | import java.util.stream.Collectors; 18 | import net.minidev.json.JSONObject; 19 | import org.apache.jmeter.config.Arguments; 20 | import org.apache.jmeter.protocol.http.sampler.HTTPSampleResult; 21 | import org.apache.jmeter.samplers.SampleResult; 22 | import org.apache.jmeter.util.JMeterUtils; 23 | import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient; 24 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 25 | import org.apache.jmeter.visualizers.backend.UserMetric; 26 | import org.datadog.jmeter.plugins.aggregation.ConcurrentAggregator; 27 | import org.datadog.jmeter.plugins.exceptions.DatadogApiException; 28 | import org.datadog.jmeter.plugins.exceptions.DatadogConfigurationException; 29 | import org.datadog.jmeter.plugins.metrics.DatadogMetric; 30 | import org.datadog.jmeter.plugins.util.CommonUtils; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | /** 35 | * An implementation of AbstractBackendListenerClient that aggregates and forwards metrics and log events 36 | * to Datadog. 37 | */ 38 | @SuppressWarnings("unused") 39 | public class DatadogBackendClient extends AbstractBackendListenerClient implements Runnable { 40 | 41 | /** 42 | * The logger. Anything written with it appears on JMeter console tab. 43 | */ 44 | private static final Logger log = LoggerFactory.getLogger(DatadogBackendClient.class); 45 | 46 | /** 47 | * An instance of {@link DatadogHttpClient}. 48 | * Instantiated during the test set up phase, and used to send metrics and logs. 49 | */ 50 | private DatadogHttpClient datadogClient; 51 | 52 | /** 53 | * An instance of {@link DatadogConfiguration}. 54 | * Instantiated during the test set up phase and contains the plugin configuration. 55 | */ 56 | private DatadogConfiguration configuration; 57 | 58 | /** 59 | * An instance of {@link ConcurrentAggregator}. 60 | * Instantiated upon creation of the DatadogBackendClient class. Aggregates metrics until instructed to flush. 61 | * Flushing occurs at a fixed schedule rate, @see {@link #timerHandle} and {@link #METRICS_SEND_INTERVAL}. 62 | */ 63 | private ConcurrentAggregator aggregator = new ConcurrentAggregator(); 64 | 65 | /** 66 | * An list of JSON log payloads to buffer calls to the Datadog API. Unlike metrics, logs are not aggregated before being sent. Thus 67 | * flushing of logs doesn't occur at a fixed time interval but rather once the buffer is bigger than {@link DatadogConfiguration#getLogsBatchSize()}. 68 | */ 69 | private List logsBuffer = new ArrayList<>(); 70 | 71 | 72 | /** 73 | * How often to send metrics. During this interval metrics are aggregated (i.e multiple counts values are added together, gauge 74 | * replaces the previous value etc.). At the end of that interval the result of aggregation is sent to Datadog. 75 | */ 76 | private static final long METRICS_SEND_INTERVAL = JMeterUtils.getPropDefault("datadog.send_interval", 10); 77 | 78 | /** 79 | * Used to schedule flushing of metrics every {@link #METRICS_SEND_INTERVAL} seconds. 80 | */ 81 | private ScheduledExecutorService scheduler; 82 | 83 | /** 84 | * The resulting future object after scheduling. Keeping the reference as an instance variable to be able to cancel it. 85 | */ 86 | private ScheduledFuture timerHandle; 87 | 88 | /** 89 | * Calls at a fixed schedule and sends metrics to Datadog. 90 | */ 91 | @Override 92 | public void run() { 93 | sendMetrics(); 94 | } 95 | 96 | /** 97 | * Used by JMeter to know the list of parameters to show in the UI. 98 | * @return the parameters as an Arguments object. 99 | */ 100 | @Override 101 | public Arguments getDefaultParameters() { 102 | return DatadogConfiguration.getPluginArguments(); 103 | } 104 | 105 | /** 106 | * Called before starting the test. 107 | * @param context An object used to fetch user configuration. 108 | * @throws DatadogConfigurationException If the configuration is invalid. 109 | * @throws DatadogApiException If the plugin can't connect to Datadog. 110 | */ 111 | @Override 112 | public void setupTest(BackendListenerContext context) throws Exception { 113 | this.configuration = DatadogConfiguration.parseConfiguration(context); 114 | 115 | datadogClient = new DatadogHttpClient(configuration.getApiKey(), configuration.getApiUrl(), configuration.getLogIntakeUrl()); 116 | boolean valid = datadogClient.validateConnection(); 117 | if(!valid) { 118 | throw new DatadogApiException("Invalid apiKey"); 119 | } 120 | 121 | scheduler = Executors.newScheduledThreadPool(1); 122 | this.timerHandle = scheduler.scheduleAtFixedRate(this, METRICS_SEND_INTERVAL, METRICS_SEND_INTERVAL, TimeUnit.SECONDS); 123 | super.setupTest(context); 124 | } 125 | 126 | /** 127 | * Called after completion of the test. 128 | * @param context unused - An object used to fetch user configuration. 129 | * @throws Exception If something goes wrong while stopping 130 | */ 131 | @Override 132 | public void teardownTest(BackendListenerContext context) throws Exception { 133 | this.timerHandle.cancel(false); 134 | this.scheduler.shutdown(); 135 | try { 136 | scheduler.awaitTermination(30, TimeUnit.SECONDS); 137 | } catch (InterruptedException e) { 138 | log.error("Error waiting for end of scheduler"); 139 | Thread.currentThread().interrupt(); 140 | } 141 | 142 | this.sendMetrics(); 143 | 144 | if (this.logsBuffer.size() > 0) { 145 | this.datadogClient.submitLogs(this.logsBuffer, this.configuration.getCustomTags()); 146 | this.logsBuffer.clear(); 147 | } 148 | this.datadogClient = null; 149 | super.teardownTest(context); 150 | } 151 | 152 | /** 153 | * Main entry point, this method is called when new results are computed. 154 | * @param list The results to parse. 155 | * @param backendListenerContext unused - An object used to fetch user configuration. 156 | */ 157 | @Override 158 | public void handleSampleResults(List list, BackendListenerContext backendListenerContext) { 159 | for (SampleResult sampleResult : list) { 160 | Matcher matcher = configuration.getSamplersRegex().matcher(sampleResult.getSampleLabel()); 161 | if(!matcher.find()) { 162 | continue; 163 | } 164 | this.extractData(sampleResult); 165 | } 166 | } 167 | 168 | /** 169 | * Called for each individual result. It calls {@link #extractMetrics(SampleResult)} and {@link #extractLogs(SampleResult)}. 170 | * @param sampleResult the result 171 | */ 172 | private void extractData(SampleResult sampleResult) { 173 | UserMetric userMetrics = this.getUserMetrics(); 174 | userMetrics.add(sampleResult); 175 | this.extractMetrics(sampleResult); 176 | if(configuration.shouldSendResultsAsLogs()) { 177 | if(!shouldExcludeSampleResultAsLogs(sampleResult)) { 178 | this.extractLogs(sampleResult); 179 | if (logsBuffer.size() >= configuration.getLogsBatchSize()) { 180 | datadogClient.submitLogs(logsBuffer, this.configuration.getCustomTags()); 181 | logsBuffer.clear(); 182 | } 183 | } 184 | } 185 | if(configuration.shouldIncludeSubResults()) { 186 | for (SampleResult subResult : sampleResult.getSubResults()) { 187 | this.extractData(subResult); 188 | } 189 | } 190 | } 191 | 192 | /** 193 | * Called for each individual result. It checks if logs for the sample result should be excluded 194 | * @param sampleResult the result 195 | */ 196 | private boolean shouldExcludeSampleResultAsLogs(SampleResult sampleResult) { 197 | return configuration.getExcludeLogsResponseCodeRegex().matcher(sampleResult.getResponseCode()).matches(); 198 | } 199 | 200 | /** 201 | * Called for each individual result. It extracts metrics and give them to the {@link ConcurrentAggregator} instance for aggregation. 202 | * @param sampleResult the result 203 | */ 204 | private void extractMetrics(SampleResult sampleResult) { 205 | String resultStatus = sampleResult.isSuccessful() ? "ok" : "ko"; 206 | 207 | String threadGroup = CommonUtils.parseThreadGroup(sampleResult.getThreadName()); 208 | 209 | List allTags = new ArrayList<>(Arrays.asList("response_code:" + sampleResult.getResponseCode(), "sample_label:" + sampleResult.getSampleLabel(), "thread_group:" + threadGroup, "result:" + resultStatus)); 210 | allTags.addAll(this.configuration.getCustomTags()); 211 | String[] tags = allTags.toArray(new String[allTags.size()]); 212 | 213 | if(sampleResult.isSuccessful()) { 214 | aggregator.incrementCounter("jmeter.responses_count", tags, sampleResult.getSampleCount() - sampleResult.getErrorCount()); 215 | } else { 216 | aggregator.incrementCounter("jmeter.responses_count", tags, sampleResult.getErrorCount()); 217 | } 218 | 219 | aggregator.histogram("jmeter.response_time", tags, sampleResult.getTime() / 1000f); 220 | aggregator.histogram("jmeter.bytes_sent", tags, sampleResult.getSentBytes()); 221 | aggregator.histogram("jmeter.bytes_received", tags, sampleResult.getBytesAsLong()); 222 | aggregator.histogram("jmeter.latency", tags, sampleResult.getLatency() / 1000f); 223 | } 224 | 225 | /** 226 | * Called for each individual result. It extracts logs and append them to the {@link #logsBuffer} buffer. 227 | * @param sampleResult the result 228 | */ 229 | private void extractLogs(SampleResult sampleResult) { 230 | JSONObject payload = new JSONObject(); 231 | 232 | String threadName = sampleResult.getThreadName(); 233 | String threadGroup = CommonUtils.parseThreadGroup(threadName); 234 | 235 | if(sampleResult instanceof HTTPSampleResult) { 236 | payload.put("http_method", ((HTTPSampleResult) sampleResult).getHTTPMethod()); 237 | } 238 | payload.put("thread_name", threadName); 239 | payload.put("thread_group", threadGroup); 240 | payload.put("sample_start_time", (double) sampleResult.getStartTime()); 241 | payload.put("sample_end_time", (double) sampleResult.getEndTime()); 242 | payload.put("load_time", (double) sampleResult.getTime()); 243 | payload.put("connect_time", (double) sampleResult.getConnectTime()); 244 | payload.put("latency", (double) sampleResult.getLatency()); 245 | payload.put("bytes", (double) sampleResult.getBytesAsLong()); 246 | payload.put("sent_bytes", (double) sampleResult.getSentBytes()); 247 | payload.put("headers_size", (double) sampleResult.getHeadersSize()); 248 | payload.put("body_size", (double) sampleResult.getBodySizeAsLong()); 249 | payload.put("sample_count", (double) sampleResult.getSampleCount()); 250 | payload.put("error_count", (double) sampleResult.getErrorCount()); 251 | payload.put("data_type", sampleResult.getDataType()); 252 | payload.put("response_code", sampleResult.getResponseCode()); 253 | payload.put("url", sampleResult.getUrlAsString()); 254 | payload.put("sample_label", sampleResult.getSampleLabel()); 255 | payload.put("idle_time", (double) sampleResult.getIdleTime()); 256 | payload.put("group_threads", (double) sampleResult.getGroupThreads()); 257 | payload.put("all_threads", (double) sampleResult.getAllThreads()); 258 | 259 | payload.put("ddsource", "jmeter"); 260 | payload.put("message", sampleResult.getResponseMessage()); 261 | payload.put("content_type", sampleResult.getContentType()); 262 | payload.put("data_encoding", sampleResult.getDataEncodingNoDefault()); 263 | 264 | // NOTE: Headers are not extracted as they might contain secrets. 265 | 266 | this.logsBuffer.add(payload); 267 | } 268 | 269 | /** 270 | * Computes thread related metrics and collects the aggregator. 271 | */ 272 | public void addGlobalMetrics() { 273 | UserMetric userMetrics = getUserMetrics(); 274 | List allTags = this.configuration.getCustomTags(); 275 | String[] tags = allTags.toArray(new String[allTags.size()]); 276 | 277 | aggregator.addGauge("jmeter.active_threads.min", tags, userMetrics.getMinActiveThreads()); 278 | aggregator.addGauge("jmeter.active_threads.max", tags, userMetrics.getMaxActiveThreads()); 279 | aggregator.addGauge("jmeter.active_threads.avg", tags, userMetrics.getMeanActiveThreads()); 280 | aggregator.addGauge("jmeter.threads.finished", tags, userMetrics.getFinishedThreads()); 281 | aggregator.addGauge("jmeter.threads.started", tags, userMetrics.getStartedThreads()); 282 | } 283 | 284 | /** 285 | * Called on a fixed schedule. Resets the aggregator, and sends all metrics to Datadog in batches. 286 | */ 287 | private void sendMetrics() { 288 | this.addGlobalMetrics(); 289 | 290 | List metrics = aggregator.flushMetrics(); 291 | 292 | AtomicInteger counter = new AtomicInteger(); 293 | metrics.stream().collect(Collectors.groupingBy(it -> counter.getAndIncrement() / configuration.getMetricsMaxBatchSize())).values().forEach( 294 | x -> datadogClient.submitMetrics(x) 295 | ); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/DatadogConfiguration.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.regex.Pattern; 11 | import org.apache.jmeter.config.Arguments; 12 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 13 | import org.datadog.jmeter.plugins.exceptions.DatadogConfigurationException; 14 | 15 | 16 | public class DatadogConfiguration { 17 | 18 | /** 19 | * The Datadog api key to use for submitting metrics and logs. 20 | */ 21 | private String apiKey; 22 | 23 | /** 24 | * The Datadog api url to use for submitting metrics. 25 | */ 26 | private String apiUrl; 27 | 28 | /** 29 | * The Datadog api url to use for submitting logs. 30 | */ 31 | private String logIntakeUrl; 32 | 33 | /** 34 | * This option configures how many metrics are sent in the same http request to Datadog. 35 | * NOTE: Metrics are always sent at a fixed interval. This field configures how many API calls will be perfomed at the end of 36 | * said interval. 37 | */ 38 | private int metricsMaxBatchSize; 39 | 40 | /** 41 | * This option configures the size of the logs buffer. The bigger the value, the more logs will be kept in memory 42 | * before being sent to Datadog and the bigger the resulting api call will be. Once that buffer size is reached, all pending log events 43 | * are sent. 44 | */ 45 | private int logsBatchSize; 46 | 47 | /** 48 | * This options configures whether or not the plugin should submit individual results as Datadog logs. 49 | */ 50 | private boolean sendResultsAsLogs; 51 | 52 | /** 53 | * User configurable. This options configures which Datadog logs to exclude from submission using a regex 54 | * that matches on the response code. 55 | */ 56 | private Pattern excludeLogsResponseCodeRegex = null; 57 | 58 | /** 59 | * This options configures whether or not to collect metrics and logs for jmeter subresults. 60 | */ 61 | private boolean includeSubResults; 62 | 63 | /** 64 | * User configurable. This options configures which samplers to include in monitoring results using a regex 65 | * that matches on the sampler name. 66 | */ 67 | private Pattern samplersRegex = null; 68 | 69 | /** 70 | * User configurable. This options configures which tags that are needed during a performance test 71 | */ 72 | private List customTags; 73 | 74 | 75 | /* The names of configuration options that are shown in JMeter UI */ 76 | private static final String API_URL_PARAM = "datadogUrl"; 77 | private static final String LOG_INTAKE_URL_PARAM = "logIntakeUrl"; 78 | private static final String API_KEY_PARAM = "apiKey"; 79 | private static final String METRICS_MAX_BATCH_SIZE = "metricsMaxBatchSize"; 80 | private static final String LOGS_BATCH_SIZE = "logsBatchSize"; 81 | private static final String SEND_RESULTS_AS_LOGS = "sendResultsAsLogs"; 82 | private static final String INCLUDE_SUB_RESULTS = "includeSubresults"; 83 | private static final String EXCLUDE_LOGS_RESPONSE_CODE_REGEX = "excludeLogsResponseCodeRegex"; 84 | private static final String SAMPLERS_REGEX = "samplersRegex"; 85 | private static final String CUSTOM_TAGS ="customTags"; 86 | 87 | /* The default values for all configuration options */ 88 | private static final String DEFAULT_API_URL = "https://api.datadoghq.com/api/"; 89 | private static final String DEFAULT_LOG_INTAKE_URL = "https://http-intake.logs.datadoghq.com/v1/input/"; 90 | private static final int DEFAULT_METRICS_MAX_BATCH_SIZE = 200; 91 | private static final int DEFAULT_LOGS_BATCH_SIZE = 500; 92 | private static final boolean DEFAULT_SEND_RESULTS_AS_LOGS = true; 93 | private static final boolean DEFAULT_INCLUDE_SUB_RESULTS = false; 94 | private static final String DEFAULT_EXCLUDE_LOGS_RESPONSE_CODE_REGEX = ""; 95 | private static final String DEFAULT_SAMPLERS_REGEX = ""; 96 | private static final String DEFAULT_CUSTOM_TAGS = ""; 97 | 98 | private DatadogConfiguration(){} 99 | 100 | public static Arguments getPluginArguments() { 101 | Arguments arguments = new Arguments(); 102 | arguments.addArgument(API_KEY_PARAM, null); 103 | arguments.addArgument(API_URL_PARAM, DEFAULT_API_URL); 104 | arguments.addArgument(LOG_INTAKE_URL_PARAM, DEFAULT_LOG_INTAKE_URL); 105 | arguments.addArgument(METRICS_MAX_BATCH_SIZE, String.valueOf(DEFAULT_METRICS_MAX_BATCH_SIZE)); 106 | arguments.addArgument(LOGS_BATCH_SIZE, String.valueOf(DEFAULT_LOGS_BATCH_SIZE)); 107 | arguments.addArgument(SEND_RESULTS_AS_LOGS, String.valueOf(DEFAULT_SEND_RESULTS_AS_LOGS)); 108 | arguments.addArgument(INCLUDE_SUB_RESULTS, String.valueOf(DEFAULT_INCLUDE_SUB_RESULTS)); 109 | arguments.addArgument(EXCLUDE_LOGS_RESPONSE_CODE_REGEX, DEFAULT_EXCLUDE_LOGS_RESPONSE_CODE_REGEX); 110 | arguments.addArgument(SAMPLERS_REGEX, DEFAULT_SAMPLERS_REGEX); 111 | arguments.addArgument(CUSTOM_TAGS, DEFAULT_CUSTOM_TAGS); 112 | return arguments; 113 | } 114 | 115 | public static DatadogConfiguration parseConfiguration(BackendListenerContext context) throws DatadogConfigurationException { 116 | DatadogConfiguration configuration = new DatadogConfiguration(); 117 | 118 | String apiKey = context.getParameter(API_KEY_PARAM); 119 | if (apiKey == null) { 120 | throw new DatadogConfigurationException("apiKey needs to be configured."); 121 | } 122 | configuration.apiKey = apiKey; 123 | 124 | configuration.apiUrl = context.getParameter(API_URL_PARAM, DEFAULT_API_URL); 125 | configuration.logIntakeUrl = context.getParameter(LOG_INTAKE_URL_PARAM, DEFAULT_LOG_INTAKE_URL); 126 | 127 | 128 | String metricsMaxBatchSize = context.getParameter(METRICS_MAX_BATCH_SIZE, String.valueOf(DEFAULT_METRICS_MAX_BATCH_SIZE)); 129 | try { 130 | configuration.metricsMaxBatchSize = Integer.parseUnsignedInt(metricsMaxBatchSize); 131 | } catch (NumberFormatException e) { 132 | throw new DatadogConfigurationException("Invalid 'metricsMaxBatchSize'. Value '" + metricsMaxBatchSize + "' is not an integer."); 133 | } 134 | 135 | String logsBatchSize = context.getParameter(LOGS_BATCH_SIZE, String.valueOf(DEFAULT_LOGS_BATCH_SIZE)); 136 | try { 137 | configuration.logsBatchSize = Integer.parseUnsignedInt(logsBatchSize); 138 | } catch (NumberFormatException e) { 139 | throw new DatadogConfigurationException("Invalid 'logsBatchSize'. Value '" + logsBatchSize + "' is not an integer."); 140 | } 141 | 142 | String sendResultsAsLogs = context.getParameter(SEND_RESULTS_AS_LOGS, String.valueOf(DEFAULT_SEND_RESULTS_AS_LOGS)); 143 | if(!sendResultsAsLogs.toLowerCase().equals("false") && !sendResultsAsLogs.toLowerCase().equals("true")) { 144 | throw new DatadogConfigurationException("Invalid 'sendResultsAsLogs'. Value '" + sendResultsAsLogs + "' is not a boolean."); 145 | } 146 | configuration.sendResultsAsLogs = Boolean.parseBoolean(sendResultsAsLogs); 147 | 148 | String includeSubResults = context.getParameter(INCLUDE_SUB_RESULTS, String.valueOf(DEFAULT_INCLUDE_SUB_RESULTS)); 149 | if(!includeSubResults.toLowerCase().equals("false") && !includeSubResults.toLowerCase().equals("true")) { 150 | throw new DatadogConfigurationException("Invalid 'includeSubResults'. Value '" + includeSubResults + "' is not a boolean."); 151 | } 152 | configuration.includeSubResults = Boolean.parseBoolean(includeSubResults); 153 | 154 | configuration.samplersRegex = Pattern.compile(context.getParameter(SAMPLERS_REGEX, DEFAULT_SAMPLERS_REGEX)); 155 | 156 | configuration.excludeLogsResponseCodeRegex = Pattern.compile(context.getParameter(EXCLUDE_LOGS_RESPONSE_CODE_REGEX, DEFAULT_EXCLUDE_LOGS_RESPONSE_CODE_REGEX)); 157 | 158 | String customTagsString = context.getParameter(CUSTOM_TAGS, String.valueOf(DEFAULT_CUSTOM_TAGS)); 159 | List customTags = new ArrayList<>(); 160 | if(customTagsString.contains(",")){ 161 | for (String item:customTagsString.split(",")) { 162 | customTags.add(item); 163 | } 164 | }else if(!customTagsString.equals("")){ 165 | customTags.add(customTagsString); 166 | } 167 | 168 | configuration.customTags = customTags; 169 | 170 | return configuration; 171 | } 172 | 173 | public String getApiKey() { 174 | return apiKey; 175 | } 176 | 177 | public String getApiUrl() { 178 | return apiUrl; 179 | } 180 | 181 | public String getLogIntakeUrl() { 182 | return logIntakeUrl; 183 | } 184 | 185 | public int getMetricsMaxBatchSize() { 186 | return metricsMaxBatchSize; 187 | } 188 | 189 | public int getLogsBatchSize() { 190 | return logsBatchSize; 191 | } 192 | 193 | public boolean shouldSendResultsAsLogs() { 194 | return sendResultsAsLogs; 195 | } 196 | 197 | public boolean shouldIncludeSubResults() { 198 | return includeSubResults; 199 | } 200 | 201 | public Pattern getExcludeLogsResponseCodeRegex() { 202 | return excludeLogsResponseCodeRegex; 203 | } 204 | 205 | public Pattern getSamplersRegex() { 206 | return samplersRegex; 207 | } 208 | 209 | public List getCustomTags(){ 210 | return customTags; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/DatadogHttpClient.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.InputStreamReader; 10 | import java.io.OutputStreamWriter; 11 | import java.net.HttpURLConnection; 12 | import java.net.URISyntaxException; 13 | import java.net.URL; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.Arrays; 16 | import java.util.List; 17 | import net.minidev.json.JSONArray; 18 | import net.minidev.json.JSONObject; 19 | import net.minidev.json.parser.JSONParser; 20 | import org.apache.http.client.utils.URIBuilder; 21 | import org.datadog.jmeter.plugins.metrics.DatadogMetric; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | /** 26 | * The type Datadog http client. 27 | */ 28 | public class DatadogHttpClient { 29 | private String apiKey; 30 | private static final Logger logger = LoggerFactory.getLogger(DatadogHttpClient.class); 31 | private static final String METRIC = "v1/series"; 32 | private static final String VALIDATE = "v1/validate"; 33 | private String apiUrl = null; 34 | private String logIntakeUrl = null; 35 | private static final int timeoutMS = 60 * 1000; 36 | 37 | /** 38 | * Instantiates a new Datadog http client. 39 | * 40 | * @param apiKey the api key 41 | * @param apiUrl the api url 42 | * @param logIntakeUrl the log intake url 43 | */ 44 | public DatadogHttpClient(String apiKey, String apiUrl, String logIntakeUrl) { 45 | this.apiKey = apiKey; 46 | this.apiUrl = apiUrl; 47 | this.logIntakeUrl = logIntakeUrl; 48 | } 49 | 50 | /** 51 | * Validate connection boolean. 52 | * 53 | * @return the boolean 54 | */ 55 | public boolean validateConnection() { 56 | String urlParameters = "?api_key=" + this.apiKey; 57 | HttpURLConnection conn = null; 58 | 59 | try { 60 | URL url = new URL(this.apiUrl + VALIDATE + urlParameters); 61 | conn = (HttpURLConnection) url.openConnection(); 62 | logger.debug("Connecting to " + this.apiUrl + VALIDATE); 63 | conn.setConnectTimeout(timeoutMS); 64 | conn.setReadTimeout(timeoutMS); 65 | conn.setRequestMethod("GET"); 66 | 67 | BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); 68 | StringBuilder result = new StringBuilder(); 69 | String line; 70 | while ((line = rd.readLine()) != null) { 71 | result.append(line); 72 | } 73 | rd.close(); 74 | if (conn.getResponseCode() != 200) { 75 | logger.error("Invalid api key"); 76 | logger.debug("The api endpoint returned: " + result.toString()); 77 | return false; 78 | } 79 | return true; 80 | } catch (Exception e){ 81 | logger.error(e.getLocalizedMessage()); 82 | return false; 83 | } 84 | } 85 | 86 | /** 87 | * Submit metrics boolean. 88 | * 89 | * @param datadogMetrics the datadog metrics 90 | */ 91 | public void submitMetrics(List datadogMetrics) { 92 | 93 | // Place metric as item of series list 94 | JSONArray series = new JSONArray(); 95 | 96 | for (DatadogMetric datadogMetric : datadogMetrics) { 97 | JSONArray points = new JSONArray(); 98 | JSONArray point = new JSONArray(); 99 | point.add(System.currentTimeMillis() / 1000); 100 | point.add(datadogMetric.getValue()); 101 | points.add(point); 102 | 103 | JSONArray tags = new JSONArray(); 104 | tags.addAll(Arrays.asList(datadogMetric.getTags())); 105 | 106 | JSONObject metric = new JSONObject(); 107 | metric.put("metric", datadogMetric.getName()); 108 | metric.put("points", points); 109 | metric.put("type", datadogMetric.getType()); 110 | metric.put("tags", tags); 111 | 112 | series.add(metric); 113 | } 114 | 115 | // Add series to payload 116 | JSONObject payload = new JSONObject(); 117 | payload.put("series", series); 118 | 119 | String urlParameters = "?api_key=" + this.apiKey; 120 | HttpURLConnection conn = null; 121 | try { 122 | URL url = new URL(this.apiUrl + METRIC + urlParameters); 123 | conn = (HttpURLConnection) url.openConnection(); 124 | conn.setRequestProperty("Content-Type", "application/json"); 125 | conn.setUseCaches(false); 126 | conn.setDoInput(true); 127 | conn.setDoOutput(true); 128 | 129 | OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8); 130 | logger.debug("Writing to OutputStreamWriter..."); 131 | wr.write(payload.toString()); 132 | wr.close(); 133 | 134 | // Get response 135 | BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); 136 | StringBuilder result = new StringBuilder(); 137 | String line; 138 | while ((line = rd.readLine()) != null) { 139 | result.append(line); 140 | } 141 | rd.close(); 142 | 143 | JSONObject json = (JSONObject) new JSONParser(JSONParser.MODE_PERMISSIVE).parse(result.toString()); 144 | if ("ok".equals(json.getAsString("status"))) { 145 | logger.info(String.format("'%s' metrics were sent to Datadog", datadogMetrics.size())); 146 | logger.debug(String.format("Payload: %s", payload)); 147 | } else { 148 | logger.error(String.format("Unable to send '%s' metrics to Datadog!", datadogMetrics.size())); 149 | logger.debug(String.format("Payload: %s", payload)); 150 | } 151 | } catch (Exception e) { 152 | e.printStackTrace(); 153 | } 154 | } 155 | 156 | /** 157 | * Submit logs. 158 | * 159 | * @param payload the payload 160 | */ 161 | public void submitLogs(List payload, List tags) { 162 | JSONArray logsArray = new JSONArray(); 163 | logsArray.addAll(payload); 164 | 165 | HttpURLConnection conn; 166 | try { 167 | URL url = new URL(buildLogsUrl(this.logIntakeUrl, tags)); 168 | conn = (HttpURLConnection) url.openConnection(); 169 | conn.setRequestMethod("POST"); 170 | conn.setRequestProperty("Content-Type", "application/json"); 171 | conn.setRequestProperty("DD-API-KEY", this.apiKey); 172 | conn.setRequestProperty("User-Agent", "Datadog/jmeter-plugin"); 173 | conn.setUseCaches(false); 174 | conn.setDoInput(true); 175 | conn.setDoOutput(true); 176 | 177 | OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8); 178 | wr.write(logsArray.toString()); 179 | wr.close(); 180 | 181 | // Get response 182 | BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); 183 | StringBuilder result = new StringBuilder(); 184 | String line; 185 | while ((line = rd.readLine()) != null) { 186 | result.append(line); 187 | } 188 | rd.close(); 189 | 190 | if ("{}".equals(result.toString())) { 191 | logger.info(String.format("Sent '%s' logs to Datadog", payload.size())); 192 | } else { 193 | logger.error(String.format("Unable to send '%s' logs to Datadog", payload.size())); 194 | } 195 | } catch (Exception e) { 196 | e.printStackTrace(); 197 | } 198 | } 199 | 200 | private String buildLogsUrl(String logsUrl, List tags) throws URISyntaxException { 201 | if (tags.isEmpty()) { 202 | return logsUrl; 203 | } 204 | return new URIBuilder(logsUrl) 205 | .addParameter("ddtags", String.join(",", tags)) 206 | .toString(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/aggregation/ConcurrentAggregator.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.aggregation; 7 | 8 | import com.datadoghq.sketch.ddsketch.mapping.CubicallyInterpolatedMapping; 9 | import com.datadoghq.sketch.ddsketch.store.UnboundedSizeDenseStore; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.concurrent.Semaphore; 15 | import java.util.concurrent.locks.Lock; 16 | import java.util.concurrent.locks.ReentrantLock; 17 | import org.datadog.jmeter.plugins.metrics.DatadogMetric; 18 | import org.datadog.jmeter.plugins.metrics.DatadogMetricContext; 19 | 20 | public class ConcurrentAggregator { 21 | private static final double RELATIVE_ACCURACY = 0.01; 22 | private Map counters = new HashMap<>(); 23 | private Map gauges = new HashMap<>(); 24 | private Map histograms = new HashMap<>(); 25 | private Lock lock = new ReentrantLock(); 26 | Semaphore testOnlyBlocker; 27 | 28 | public void incrementCounter(String name, String[] tags, int incrementValue) { 29 | incrementCounter(new DatadogMetricContext(name, tags), incrementValue); 30 | } 31 | public void incrementCounter(DatadogMetricContext context, int incrementValue) { 32 | lock.lock(); 33 | Long previousValue = counters.getOrDefault(context, (long) 0); 34 | if(testOnlyBlocker != null) { 35 | testOnlyBlocker.acquireUninterruptibly(); 36 | } 37 | counters.put(context, previousValue + incrementValue); 38 | 39 | lock.unlock(); 40 | } 41 | 42 | public void addGauge(String name, String[] tags, double value) { 43 | addGauge(new DatadogMetricContext(name, tags), value); 44 | } 45 | public void addGauge(DatadogMetricContext context, double value) { 46 | lock.lock(); 47 | 48 | gauges.put(context, value); 49 | 50 | lock.unlock(); 51 | } 52 | 53 | public void histogram(String name, String[] tags, double value) { 54 | histogram(new DatadogMetricContext(name, tags), value); 55 | } 56 | public void histogram(DatadogMetricContext context, double value) { 57 | lock.lock(); 58 | DatadogSketch sketch = histograms.get(context); 59 | if (sketch == null) { 60 | sketch = new DatadogSketch(new CubicallyInterpolatedMapping(RELATIVE_ACCURACY), UnboundedSizeDenseStore::new); 61 | histograms.put(context, sketch); 62 | } 63 | if(testOnlyBlocker != null) { 64 | testOnlyBlocker.acquireUninterruptibly(); 65 | } 66 | 67 | sketch.accept(value); 68 | lock.unlock(); 69 | } 70 | 71 | public List flushMetrics() { 72 | lock.lock(); 73 | Map countersPtr = counters; 74 | Map gaugesPtr = gauges; 75 | Map histrogramsPtr = histograms; 76 | 77 | counters = new HashMap<>(); 78 | gauges = new HashMap<>(); 79 | histograms = new HashMap<>(); 80 | lock.unlock(); 81 | 82 | List metrics = new ArrayList<>(); 83 | for(DatadogMetricContext context : countersPtr.keySet()) { 84 | Long counterValue = countersPtr.get(context); 85 | metrics.add(new DatadogMetric(context.getName(), "count", counterValue, context.getTags())); 86 | } 87 | for(DatadogMetricContext context : gaugesPtr.keySet()) { 88 | Double counterValue = gaugesPtr.get(context); 89 | metrics.add(new DatadogMetric(context.getName(), "gauge", counterValue, context.getTags())); 90 | } 91 | for(DatadogMetricContext context : histrogramsPtr.keySet()) { 92 | DatadogSketch sketch = histrogramsPtr.get(context); 93 | metrics.add(new DatadogMetric(context.getName() + ".max", "gauge", sketch.getMaxValue(), context.getTags())); 94 | metrics.add(new DatadogMetric(context.getName() + ".min", "gauge", sketch.getMinValue(), context.getTags())); 95 | metrics.add(new DatadogMetric(context.getName() + ".p99", "gauge", sketch.getValueAtQuantile(0.99), context.getTags())); 96 | metrics.add(new DatadogMetric(context.getName() + ".p95", "gauge", sketch.getValueAtQuantile(0.95), context.getTags())); 97 | metrics.add(new DatadogMetric(context.getName() + ".p90", "gauge", sketch.getValueAtQuantile(0.90), context.getTags())); 98 | metrics.add(new DatadogMetric(context.getName() + ".avg", "gauge", sketch.getAverageValue(), context.getTags())); 99 | metrics.add(new DatadogMetric(context.getName() + ".count", "count", sketch.getCountValue(), context.getTags())); 100 | } 101 | 102 | return metrics; 103 | } 104 | 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/aggregation/DatadogSketch.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.aggregation; 7 | 8 | import com.datadoghq.sketch.ddsketch.DDSketch; 9 | import com.datadoghq.sketch.ddsketch.mapping.IndexMapping; 10 | import com.datadoghq.sketch.ddsketch.store.Store; 11 | import java.util.function.Supplier; 12 | 13 | public class DatadogSketch extends DDSketch { 14 | 15 | private long count = 0; 16 | private double sum = 0; 17 | 18 | public DatadogSketch(IndexMapping indexMapping, Supplier storeSupplier) { 19 | super(indexMapping, storeSupplier); 20 | } 21 | 22 | @Override 23 | public void accept(double value) { 24 | this.count += 1; 25 | this.sum += value; 26 | super.accept(value); 27 | } 28 | 29 | public long getCountValue() { 30 | return this.count; 31 | } 32 | 33 | public double getAverageValue() { 34 | return this.sum / this.count; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/exceptions/DatadogApiException.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.exceptions; 7 | 8 | public class DatadogApiException extends Exception { 9 | public DatadogApiException(String message){ 10 | super(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/exceptions/DatadogConfigurationException.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.exceptions; 7 | 8 | public class DatadogConfigurationException extends Exception { 9 | public DatadogConfigurationException(String message){ 10 | super(message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/metrics/DatadogMetric.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.metrics; 7 | 8 | public class DatadogMetric { 9 | private DatadogMetricContext context; 10 | private String type; 11 | private double value; 12 | 13 | public DatadogMetric(String name, String type, double value, String[] tags) { 14 | this.context = new DatadogMetricContext(name, tags); 15 | this.type = type; 16 | this.value = value; 17 | } 18 | 19 | public String getName() { 20 | return this.context.getName(); 21 | } 22 | 23 | public String[] getTags() { 24 | return this.context.getTags(); 25 | } 26 | 27 | public double getValue() { 28 | return value; 29 | } 30 | 31 | public String getType() { 32 | return type; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/metrics/DatadogMetricContext.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.metrics; 7 | 8 | import java.util.Arrays; 9 | 10 | public class DatadogMetricContext { 11 | private String name; 12 | private String[] tags; 13 | 14 | public DatadogMetricContext(String name, String[] tags){ 15 | this.name = name; 16 | this.tags = tags; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public String[] getTags() { 24 | return tags; 25 | } 26 | 27 | 28 | @Override 29 | public boolean equals(Object obj) { 30 | if (this == obj) return true; 31 | if (obj == null || getClass() != obj.getClass()) return false; 32 | DatadogMetricContext context = (DatadogMetricContext) obj; 33 | if (!context.name.equals(this.name)) return false; 34 | 35 | return Arrays.equals(context.tags, this.tags); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | int prime = 31; 41 | int result = 1; 42 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 43 | result = prime * result + ((tags == null) ? 0 : Arrays.hashCode(tags)); 44 | return result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/datadog/jmeter/plugins/util/CommonUtils.java: -------------------------------------------------------------------------------- 1 | package org.datadog.jmeter.plugins.util; 2 | 3 | public class CommonUtils { 4 | 5 | public static String parseThreadGroup(String threadName) { 6 | // https://github.com/apache/jmeter/pull/622 7 | return threadName.substring(0, Math.max(0, threadName.lastIndexOf(" "))); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/org/datadog/jmeter/plugins/DatadogBackendClientTest.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins; 7 | 8 | import static org.mockito.ArgumentMatchers.any; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import net.minidev.json.JSONObject; 17 | import net.minidev.json.parser.JSONParser; 18 | import net.minidev.json.parser.ParseException; 19 | import org.apache.jmeter.samplers.SampleResult; 20 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 21 | import org.datadog.jmeter.plugins.aggregation.ConcurrentAggregator; 22 | import org.datadog.jmeter.plugins.metrics.DatadogMetric; 23 | import org.junit.After; 24 | import org.junit.Assert; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | import org.junit.runner.RunWith; 28 | import org.powermock.api.mockito.PowerMockito; 29 | import org.powermock.core.classloader.annotations.PowerMockIgnore; 30 | import org.powermock.core.classloader.annotations.PrepareForTest; 31 | import org.powermock.modules.junit4.PowerMockRunner; 32 | 33 | /** 34 | * Unit test for simple App. 35 | */ 36 | @RunWith(PowerMockRunner.class) 37 | @PrepareForTest(DatadogBackendClient.class) 38 | @PowerMockIgnore({ 39 | "javax.management.*", 40 | "javax.script.*" 41 | }) 42 | public class DatadogBackendClientTest 43 | { 44 | private DatadogBackendClient client; 45 | private Map DEFAULT_VALID_TEST_CONFIG = new HashMap() { 46 | { 47 | put("apiKey", "123456"); 48 | put("datadogUrl", "datadogUrl"); 49 | put("logIntakeUrl", "logIntakeUrl"); 50 | put("metricsMaxBatchSize", "10"); 51 | put("logsBatchSize", "0"); 52 | put("sendResultsAsLogs", "true"); 53 | put("includeSubresults", "false"); 54 | put("excludeLogsResponseCodeRegex", ""); 55 | put("samplersRegex", "^foo\\d*$"); 56 | put("customTags", "key:value"); 57 | } 58 | }; 59 | private ConcurrentAggregator aggregator = new ConcurrentAggregator(); 60 | private BackendListenerContext context = new BackendListenerContext(DEFAULT_VALID_TEST_CONFIG); 61 | private List logsBuffer; 62 | private List logsTags; 63 | 64 | @Before 65 | public void setUpMocks() throws Exception { 66 | logsBuffer = new ArrayList<>(); 67 | DatadogHttpClient httpClientMock = PowerMockito.mock(DatadogHttpClient.class); 68 | PowerMockito.whenNew(ConcurrentAggregator.class).withAnyArguments().thenReturn(aggregator); 69 | PowerMockito.whenNew(DatadogHttpClient.class).withAnyArguments().thenReturn(httpClientMock); 70 | PowerMockito.when(httpClientMock.validateConnection()).thenReturn(true); 71 | PowerMockito.doAnswer((e) -> { 72 | logsBuffer.addAll(e.getArgument(0)); 73 | logsTags = (List) e.getArgument(1, List.class); 74 | return null; 75 | }).when(httpClientMock).submitLogs(any(), any()); 76 | client = new DatadogBackendClient(); 77 | client.setupTest(context); 78 | } 79 | 80 | @After 81 | public void teardownMocks() throws Exception { 82 | client.teardownTest(context); 83 | logsBuffer.clear(); 84 | } 85 | 86 | 87 | 88 | private SampleResult createDummySampleResult(String sampleLabel) { 89 | return createDummySampleResult(sampleLabel, "123"); 90 | } 91 | 92 | private SampleResult createDummySampleResult(String sampleLabel, String responseCode) { 93 | SampleResult result = SampleResult.createTestSample(1, 126); 94 | result.setSuccessful(true); 95 | result.setResponseCode(responseCode); 96 | result.setSampleLabel(sampleLabel); 97 | result.setSampleCount(10); 98 | result.setErrorCount(1); 99 | result.setSentBytes(124); 100 | result.setBytes((long)12345); 101 | result.setLatency(12); 102 | result.setThreadName("bar baz"); 103 | return result; 104 | } 105 | 106 | @Test 107 | public void testExtractMetrics() { 108 | SampleResult result = createDummySampleResult("foo"); 109 | this.client.handleSampleResults(Collections.singletonList(result), context); 110 | List metrics = this.aggregator.flushMetrics(); 111 | String[] expectedTags = new String[] {"response_code:123", "sample_label:foo", "thread_group:bar", "result:ok", "key:value"}; 112 | Map expectedMetrics = new HashMap() { 113 | { 114 | put("jmeter.responses_count", 10.0); 115 | put("jmeter.latency.max", 0.01195256210245945); 116 | put("jmeter.latency.min", 0.01195256210245945); 117 | put("jmeter.latency.p99", 0.01195256210245945); 118 | put("jmeter.latency.p95", 0.01195256210245945); 119 | put("jmeter.latency.p90", 0.01195256210245945); 120 | put("jmeter.latency.avg", 0.012000000104308128); 121 | put("jmeter.latency.count", 1.0); 122 | put("jmeter.response_time.max", 0.12624150202599055); 123 | put("jmeter.response_time.min", 0.12624150202599055); 124 | put("jmeter.response_time.p99", 0.12624150202599055); 125 | put("jmeter.response_time.p95", 0.12624150202599055); 126 | put("jmeter.response_time.p90", 0.12624150202599055); 127 | put("jmeter.response_time.avg", 0.125); 128 | put("jmeter.response_time.count", 1.0); 129 | put("jmeter.bytes_received.max", 12291.916561360777); 130 | put("jmeter.bytes_received.min", 12291.916561360777); 131 | put("jmeter.bytes_received.p99", 12291.916561360777); 132 | put("jmeter.bytes_received.p95", 12291.916561360777); 133 | put("jmeter.bytes_received.p90", 12291.916561360777); 134 | put("jmeter.bytes_received.avg", 12345.0); 135 | put("jmeter.bytes_received.count", 1.0); 136 | put("jmeter.bytes_sent.max", 124.37724692430666); 137 | put("jmeter.bytes_sent.min", 124.37724692430666); 138 | put("jmeter.bytes_sent.p99", 124.37724692430666); 139 | put("jmeter.bytes_sent.p95", 124.37724692430666); 140 | put("jmeter.bytes_sent.p90", 124.37724692430666); 141 | put("jmeter.bytes_sent.avg", 124.0); 142 | put("jmeter.bytes_sent.count", 1.0); 143 | } 144 | }; 145 | 146 | for(DatadogMetric metric : metrics) { 147 | Assert.assertTrue(expectedMetrics.containsKey(metric.getName())); 148 | Double expectedMetricValue = expectedMetrics.get(metric.getName()); 149 | Assert.assertArrayEquals(expectedTags, metric.getTags()); 150 | if(metric.getName().endsWith("count")) { 151 | Assert.assertEquals("count", metric.getType()); 152 | } else { 153 | Assert.assertEquals("gauge", metric.getType()); 154 | } 155 | Assert.assertEquals(expectedMetricValue, metric.getValue(), 1e-12); 156 | } 157 | 158 | this.client.addGlobalMetrics(); 159 | List globalMetrics = this.aggregator.flushMetrics(); 160 | String[] expectedGlobalTags = new String[] {"key:value"}; 161 | Map expectedGlobalMetrics = new HashMap() { 162 | { 163 | put("jmeter.active_threads.max", 0.0); 164 | put("jmeter.active_threads.min", 0.0); 165 | put("jmeter.active_threads.avg", 0.0); 166 | put("jmeter.threads.finished", 0.0); 167 | put("jmeter.threads.started", 0.0); 168 | } 169 | }; 170 | 171 | for(DatadogMetric metric : globalMetrics) { 172 | Assert.assertTrue(expectedGlobalMetrics.containsKey(metric.getName())); 173 | Double expectedMetricValue = expectedGlobalMetrics.get(metric.getName()); 174 | Assert.assertArrayEquals(expectedGlobalTags, metric.getTags()); 175 | if(metric.getName().endsWith("count")) { 176 | Assert.assertEquals("count", metric.getType()); 177 | } else { 178 | Assert.assertEquals("gauge", metric.getType()); 179 | } 180 | Assert.assertEquals(expectedMetricValue, metric.getValue(), 1e-12); 181 | } 182 | } 183 | 184 | @Test 185 | public void testExtractLogs() throws ParseException { 186 | SampleResult result = createDummySampleResult("foo"); 187 | this.client.handleSampleResults(Collections.singletonList(result), context); 188 | Assert.assertEquals(1, this.logsBuffer.size()); 189 | String expectedPayload = "{\"sample_start_time\":1.0,\"response_code\":\"123\",\"headers_size\":0.0,\"sample_label\":\"foo\",\"latency\":12.0,\"group_threads\":0.0,\"idle_time\":0.0,\"error_count\":0.0,\"message\":\"\",\"url\":\"\",\"ddsource\":\"jmeter\",\"sent_bytes\":124.0,\"thread_group\":\"bar\",\"body_size\":0.0,\"content_type\":\"\",\"load_time\":125.0,\"thread_name\":\"bar baz\",\"sample_end_time\":126.0,\"bytes\":12345.0,\"connect_time\":0.0,\"sample_count\":10.0,\"data_type\":\"\",\"all_threads\":0.0,\"data_encoding\":null}"; 190 | JSONParser parser = new JSONParser(JSONParser.MODE_PERMISSIVE); 191 | Assert.assertEquals(this.logsBuffer.get(0), parser.parse(expectedPayload)); 192 | Assert.assertEquals(this.logsTags, Collections.singletonList("key:value")); 193 | } 194 | 195 | @Test 196 | public void testExtractMetricsWithSubResults() throws Exception { 197 | // Set up a client with the `includeSubresults` option set to `true` 198 | HashMap config = new HashMap(DEFAULT_VALID_TEST_CONFIG); 199 | config.put("includeSubresults", "true"); 200 | DatadogBackendClient client = new DatadogBackendClient(); 201 | BackendListenerContext context = new BackendListenerContext(config); 202 | client.setupTest(context); 203 | 204 | SampleResult result = createDummySampleResult("foo"); 205 | // Add subresults (2 deep), as we want to ensure they're also included. 206 | // Note that subresults get re-labeled here to -. 207 | SampleResult subresult = createDummySampleResult("subresult"); 208 | result.addRawSubResult(subresult); 209 | subresult.addRawSubResult(createDummySampleResult("subresult")); 210 | 211 | client.handleSampleResults(Collections.singletonList(result), context); 212 | List metrics = this.aggregator.flushMetrics(); 213 | Map expectedMetrics = new HashMap() { 214 | { 215 | put("jmeter.responses_count", 10.0); 216 | put("jmeter.latency.max", 0.01195256210245945); 217 | put("jmeter.latency.min", 0.01195256210245945); 218 | put("jmeter.latency.p99", 0.01195256210245945); 219 | put("jmeter.latency.p95", 0.01195256210245945); 220 | put("jmeter.latency.p90", 0.01195256210245945); 221 | put("jmeter.latency.avg", 0.012000000104308128); 222 | put("jmeter.latency.count", 1.0); 223 | put("jmeter.response_time.max", 0.12624150202599055); 224 | put("jmeter.response_time.min", 0.12624150202599055); 225 | put("jmeter.response_time.p99", 0.12624150202599055); 226 | put("jmeter.response_time.p95", 0.12624150202599055); 227 | put("jmeter.response_time.p90", 0.12624150202599055); 228 | put("jmeter.response_time.avg", 0.125); 229 | put("jmeter.response_time.count", 1.0); 230 | put("jmeter.bytes_received.max", 12291.916561360777); 231 | put("jmeter.bytes_received.min", 12291.916561360777); 232 | put("jmeter.bytes_received.p99", 12291.916561360777); 233 | put("jmeter.bytes_received.p95", 12291.916561360777); 234 | put("jmeter.bytes_received.p90", 12291.916561360777); 235 | put("jmeter.bytes_received.avg", 12345.0); 236 | put("jmeter.bytes_received.count", 1.0); 237 | put("jmeter.bytes_sent.max", 124.37724692430666); 238 | put("jmeter.bytes_sent.min", 124.37724692430666); 239 | put("jmeter.bytes_sent.p99", 124.37724692430666); 240 | put("jmeter.bytes_sent.p95", 124.37724692430666); 241 | put("jmeter.bytes_sent.p90", 124.37724692430666); 242 | put("jmeter.bytes_sent.avg", 124.0); 243 | put("jmeter.bytes_sent.count", 1.0); 244 | } 245 | }; 246 | 247 | // We need to assert that the metrics of both the parent results as well as 248 | // those in the subresults are present. 249 | assertMetricsWithTag(metrics, expectedMetrics, "sample_label:foo"); 250 | assertMetricsWithTag(metrics, expectedMetrics, "sample_label:foo-0"); 251 | assertMetricsWithTag(metrics, expectedMetrics, "sample_label:foo-0-0"); 252 | } 253 | 254 | private void assertMetricsWithTag(List metrics, Map expectedMetrics, String tag) { 255 | Map metricsMap = new HashMap(); 256 | for(DatadogMetric metric : metrics) { 257 | if (Arrays.asList(metric.getTags()).contains(tag)) { 258 | metricsMap.put(metric.getName(), metric); 259 | } 260 | } 261 | 262 | for(Map.Entry expectedMetric : expectedMetrics.entrySet()) { 263 | Assert.assertTrue(metricsMap.containsKey(expectedMetric.getKey())); 264 | DatadogMetric metric = metricsMap.get(expectedMetric.getKey()); 265 | 266 | if(metric.getName().endsWith("count")) { 267 | Assert.assertEquals("count", metric.getType()); 268 | } else { 269 | Assert.assertEquals("gauge", metric.getType()); 270 | } 271 | Assert.assertEquals(expectedMetric.getValue(), metric.getValue(), 1e-12); 272 | } 273 | } 274 | 275 | @Test 276 | public void testSamplersRegexNotMatching() { 277 | SampleResult result1 = createDummySampleResult("foo1"); 278 | SampleResult resultA = createDummySampleResult("fooA"); 279 | 280 | this.client.handleSampleResults(Arrays.asList(result1, resultA), context); 281 | String[] expectedTagsResult1 = new String[] {"response_code:123", "sample_label:foo1", "thread_group:bar", "result:ok", "key:value"}; 282 | for(DatadogMetric metric : this.aggregator.flushMetrics()){ 283 | Assert.assertArrayEquals(expectedTagsResult1, metric.getTags()); 284 | } 285 | Assert.assertEquals(1, this.logsBuffer.size()); 286 | Assert.assertEquals("foo1", this.logsBuffer.get(0).getAsString("sample_label")); 287 | } 288 | 289 | @Test 290 | public void testExcludeLogsResponseCodeRegexDefaultEmpty() { 291 | SampleResult result1 = createDummySampleResult("foo1", "200"); 292 | SampleResult result2 = createDummySampleResult("foo2", "301"); 293 | SampleResult result3 = createDummySampleResult("foo3", "404"); 294 | SampleResult result4 = createDummySampleResult("foo4", "Non HTTP response code: java.net.NoRouteToHostException"); 295 | 296 | this.client.handleSampleResults(Arrays.asList(result1, result2, result3, result4), context); 297 | Assert.assertEquals(4, this.logsBuffer.size()); 298 | Assert.assertEquals("foo1", this.logsBuffer.get(0).getAsString("sample_label")); 299 | Assert.assertEquals("foo2", this.logsBuffer.get(1).getAsString("sample_label")); 300 | Assert.assertEquals("foo3", this.logsBuffer.get(2).getAsString("sample_label")); 301 | Assert.assertEquals("foo4", this.logsBuffer.get(3).getAsString("sample_label")); 302 | } 303 | 304 | @Test 305 | public void testExcludeLogsResponseCodeRegexMatching() throws Exception { 306 | HashMap config = new HashMap<>(DEFAULT_VALID_TEST_CONFIG); 307 | config.put("excludeLogsResponseCodeRegex", "^[23][0-5][0-9]$"); 308 | DatadogBackendClient client = new DatadogBackendClient(); 309 | BackendListenerContext context = new BackendListenerContext(config); 310 | client.setupTest(context); 311 | 312 | SampleResult result1 = createDummySampleResult("foo1", "200"); 313 | SampleResult result2 = createDummySampleResult("foo2", "301"); 314 | SampleResult result3 = createDummySampleResult("foo3", "404"); 315 | SampleResult result4 = createDummySampleResult("foo4", "Non HTTP response code: java.net.NoRouteToHostException"); 316 | 317 | client.handleSampleResults(Arrays.asList(result1, result2, result3, result4), context); 318 | Assert.assertEquals(2, this.logsBuffer.size()); 319 | Assert.assertEquals("foo3", this.logsBuffer.get(0).getAsString("sample_label")); 320 | Assert.assertEquals("foo4", this.logsBuffer.get(1).getAsString("sample_label")); 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /src/test/java/org/datadog/jmeter/plugins/DatadogConfigurationTest.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.regex.PatternSyntaxException; 13 | import org.apache.jmeter.config.Arguments; 14 | import org.apache.jmeter.visualizers.backend.BackendListenerContext; 15 | import org.datadog.jmeter.plugins.exceptions.DatadogConfigurationException; 16 | import org.junit.Assert; 17 | import org.junit.Test; 18 | 19 | public class DatadogConfigurationTest { 20 | private static final String API_URL_PARAM = "datadogUrl"; 21 | private static final String LOG_INTAKE_URL_PARAM = "logIntakeUrl"; 22 | private static final String API_KEY_PARAM = "apiKey"; 23 | private static final String METRICS_MAX_BATCH_SIZE = "metricsMaxBatchSize"; 24 | private static final String LOGS_BATCH_SIZE = "logsBatchSize"; 25 | private static final String SEND_RESULTS_AS_LOGS = "sendResultsAsLogs"; 26 | private static final String INCLUDE_SUB_RESULTS = "includeSubresults"; 27 | private static final String EXCLUDE_LOGS_RESPONSE_CODE_REGEX = "excludeLogsResponseCodeRegex"; 28 | private static final String SAMPLERS_REGEX = "samplersRegex"; 29 | private static final String CUSTOM_TAGS = "customTags"; 30 | 31 | @Test 32 | public void testArguments(){ 33 | Arguments args = DatadogConfiguration.getPluginArguments(); 34 | Assert.assertEquals(10, args.getArgumentCount()); 35 | 36 | Map argumentsMap = args.getArgumentsAsMap(); 37 | Assert.assertTrue(argumentsMap.containsKey(API_URL_PARAM)); 38 | Assert.assertTrue(argumentsMap.containsKey(LOG_INTAKE_URL_PARAM)); 39 | Assert.assertTrue(argumentsMap.containsKey(API_KEY_PARAM)); 40 | Assert.assertTrue(argumentsMap.containsKey(METRICS_MAX_BATCH_SIZE)); 41 | Assert.assertTrue(argumentsMap.containsKey(LOGS_BATCH_SIZE)); 42 | Assert.assertTrue(argumentsMap.containsKey(SEND_RESULTS_AS_LOGS)); 43 | Assert.assertTrue(argumentsMap.containsKey(INCLUDE_SUB_RESULTS)); 44 | Assert.assertTrue(argumentsMap.containsKey(EXCLUDE_LOGS_RESPONSE_CODE_REGEX)); 45 | Assert.assertTrue(argumentsMap.containsKey(SAMPLERS_REGEX)); 46 | Assert.assertTrue(argumentsMap.containsKey(CUSTOM_TAGS)); 47 | } 48 | 49 | @Test 50 | public void testValidConfiguration() throws DatadogConfigurationException { 51 | Map config = new HashMap() { 52 | { 53 | put(API_KEY_PARAM, "123456"); 54 | put(API_URL_PARAM, "datadogUrl"); 55 | put(LOG_INTAKE_URL_PARAM, "logIntakeUrl"); 56 | put(METRICS_MAX_BATCH_SIZE, "10"); 57 | put(LOGS_BATCH_SIZE, "11"); 58 | put(SEND_RESULTS_AS_LOGS, "true"); 59 | put(INCLUDE_SUB_RESULTS, "false"); 60 | put(EXCLUDE_LOGS_RESPONSE_CODE_REGEX, ""); 61 | put(SAMPLERS_REGEX, "false"); 62 | put(CUSTOM_TAGS, "key:value"); 63 | } 64 | }; 65 | 66 | BackendListenerContext context = new BackendListenerContext(config); 67 | DatadogConfiguration datadogConfiguration = DatadogConfiguration.parseConfiguration(context); 68 | 69 | Assert.assertEquals("123456", datadogConfiguration.getApiKey()); 70 | Assert.assertEquals("datadogUrl", datadogConfiguration.getApiUrl()); 71 | Assert.assertEquals("logIntakeUrl", datadogConfiguration.getLogIntakeUrl()); 72 | Assert.assertEquals(10, datadogConfiguration.getMetricsMaxBatchSize()); 73 | Assert.assertEquals(11, datadogConfiguration.getLogsBatchSize()); 74 | Assert.assertEquals(new ArrayList<>(Arrays.asList("key:value")), datadogConfiguration.getCustomTags()); 75 | Assert.assertTrue(datadogConfiguration.shouldSendResultsAsLogs()); 76 | Assert.assertFalse(datadogConfiguration.shouldIncludeSubResults()); 77 | } 78 | 79 | @Test(expected = DatadogConfigurationException.class) 80 | public void testMissingApiKey() throws DatadogConfigurationException { 81 | Map config = new HashMap<>(); 82 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 83 | } 84 | 85 | @Test 86 | public void testApiKeyIsTheOnlyRequiredParam() throws DatadogConfigurationException { 87 | Map config = new HashMap() { 88 | { 89 | put("apiKey", "123456"); 90 | } 91 | }; 92 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 93 | } 94 | 95 | @Test(expected = DatadogConfigurationException.class) 96 | public void testMetricsBatchSizeNotInt() throws DatadogConfigurationException { 97 | Map config = new HashMap() { 98 | { 99 | put("apiKey", "123456"); 100 | put("metricsMaxBatchSize", "foo"); 101 | } 102 | }; 103 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 104 | } 105 | 106 | @Test(expected = DatadogConfigurationException.class) 107 | public void testLogsBatchSizeNotInt() throws DatadogConfigurationException { 108 | Map config = new HashMap() { 109 | { 110 | put("apiKey", "123456"); 111 | put("logsBatchSize", "foo"); 112 | } 113 | }; 114 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 115 | } 116 | 117 | @Test(expected = DatadogConfigurationException.class) 118 | public void testSendResultsAsLogsNotBoolean() throws DatadogConfigurationException { 119 | Map config = new HashMap() { 120 | { 121 | put("apiKey", "123456"); 122 | put("sendResultsAsLogs", "foo"); 123 | } 124 | }; 125 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 126 | } 127 | 128 | @Test(expected = DatadogConfigurationException.class) 129 | public void testIncludeSubresultsNotBoolean() throws DatadogConfigurationException { 130 | Map config = new HashMap() { 131 | { 132 | put("apiKey", "123456"); 133 | put("includeSubresults", "foo"); 134 | } 135 | }; 136 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 137 | } 138 | 139 | @Test(expected = PatternSyntaxException.class) 140 | public void testInvalidExcludeLogsResponseCodeRegex() throws DatadogConfigurationException { 141 | Map config = new HashMap() { 142 | { 143 | put("apiKey", "123456"); 144 | put(EXCLUDE_LOGS_RESPONSE_CODE_REGEX, "["); 145 | } 146 | }; 147 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 148 | } 149 | 150 | @Test 151 | public void testValidExcludeLogsResponseCodeRegex() throws DatadogConfigurationException { 152 | Map config = new HashMap() { 153 | { 154 | put("apiKey", "123456"); 155 | put(EXCLUDE_LOGS_RESPONSE_CODE_REGEX, "[123][0-5][0-9]"); 156 | } 157 | }; 158 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 159 | } 160 | 161 | @Test(expected = PatternSyntaxException.class) 162 | public void testInvalidSamplersRegex() throws DatadogConfigurationException { 163 | Map config = new HashMap() { 164 | { 165 | put("apiKey", "123456"); 166 | put(SAMPLERS_REGEX, "["); 167 | } 168 | }; 169 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 170 | } 171 | 172 | @Test 173 | public void testValidSamplersRegex() throws DatadogConfigurationException { 174 | Map config = new HashMap() { 175 | { 176 | put("apiKey", "123456"); 177 | put(SAMPLERS_REGEX, "[asd]\\d+"); 178 | } 179 | }; 180 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 181 | } 182 | 183 | @Test 184 | public void testNoValueProvidedForCustomTags() throws DatadogConfigurationException { 185 | Map config = new HashMap() { 186 | { 187 | put("apiKey", "123456"); 188 | put(CUSTOM_TAGS, ""); 189 | } 190 | }; 191 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 192 | } 193 | 194 | @Test 195 | public void testOnlyKeyForCustomTags() throws DatadogConfigurationException { 196 | Map config = new HashMap() { 197 | { 198 | put("apiKey", "123456"); 199 | put(CUSTOM_TAGS, "key"); 200 | } 201 | }; 202 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 203 | } 204 | 205 | @Test 206 | public void testSpecialCharacterForCustomTags() throws DatadogConfigurationException { 207 | Map config = new HashMap() { 208 | { 209 | put("apiKey", "123456"); 210 | put(CUSTOM_TAGS, "key*value"); 211 | } 212 | }; 213 | DatadogConfiguration.parseConfiguration(new BackendListenerContext(config)); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/org/datadog/jmeter/plugins/aggregation/ConcurrentAggregatorTest.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.aggregation; 7 | 8 | import java.util.List; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.Semaphore; 12 | import java.util.concurrent.TimeUnit; 13 | import org.datadog.jmeter.plugins.metrics.DatadogMetric; 14 | import org.datadog.jmeter.plugins.metrics.DatadogMetricContext; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | 20 | 21 | public class ConcurrentAggregatorTest { 22 | private ConcurrentAggregator aggregator; 23 | private static final int N_THREADS = 50; 24 | 25 | @Before 26 | public void setUp() { 27 | aggregator = new ConcurrentAggregator(); 28 | 29 | aggregator.testOnlyBlocker = new Semaphore(0); 30 | 31 | // All tests should fail if you uncomment this 32 | //aggregator.lock = mock(ReentrantLock.class); 33 | //doNothing().when(aggregator.lock).lock(); 34 | //doNothing().when(aggregator.lock).unlock(); 35 | } 36 | 37 | @Test 38 | public void testCounterIncrement() throws InterruptedException { 39 | String metricName = "foo"; 40 | String[] tags = new String[]{}; 41 | DatadogMetricContext ctx = new DatadogMetricContext(metricName, tags); 42 | 43 | ExecutorService service = Executors.newFixedThreadPool(N_THREADS); 44 | for(int i = 0; i < N_THREADS; i++){ 45 | service.execute(() -> aggregator.incrementCounter(ctx, 1)); 46 | } 47 | aggregator.testOnlyBlocker.release(N_THREADS); 48 | service.awaitTermination(2, TimeUnit.SECONDS); 49 | 50 | List metrics = aggregator.flushMetrics(); 51 | assertEquals(1, metrics.size()); 52 | assertEquals(metricName, metrics.get(0).getName()); 53 | assertEquals("count", metrics.get(0).getType()); 54 | assertEquals(N_THREADS, (int)metrics.get(0).getValue()); 55 | } 56 | 57 | @Test 58 | public void testGauge() throws InterruptedException { 59 | String metricName = "foo"; 60 | String[] tags = new String[]{}; 61 | DatadogMetricContext ctx = new DatadogMetricContext(metricName, tags); 62 | 63 | ExecutorService service = Executors.newFixedThreadPool(N_THREADS); 64 | for(int i = 0; i < N_THREADS; i++){ 65 | service.execute(() -> aggregator.addGauge(ctx, 55)); 66 | } 67 | aggregator.testOnlyBlocker.release(N_THREADS); 68 | service.awaitTermination(2, TimeUnit.SECONDS); 69 | 70 | List metrics = aggregator.flushMetrics(); 71 | assertEquals(1, metrics.size()); 72 | assertEquals(metricName, metrics.get(0).getName()); 73 | assertEquals("gauge", metrics.get(0).getType()); 74 | assertEquals(55, (int)metrics.get(0).getValue()); 75 | } 76 | 77 | @Test 78 | public void testSketch() throws InterruptedException { 79 | String metricName = "foo"; 80 | String[] tags = new String[]{}; 81 | DatadogMetricContext ctx = new DatadogMetricContext(metricName, tags); 82 | 83 | ExecutorService service = Executors.newFixedThreadPool(N_THREADS); 84 | for(int i = 0; i < N_THREADS; i++){ 85 | final int x = i + 1; 86 | service.execute(() -> aggregator.histogram(ctx, x)); 87 | } 88 | aggregator.testOnlyBlocker.release(N_THREADS); 89 | service.awaitTermination(2, TimeUnit.SECONDS); 90 | 91 | List metrics = aggregator.flushMetrics(); 92 | assertEquals(7, metrics.size()); 93 | 94 | String[] suffixes = new String[] {".max", ".min", ".p99", ".p95", ".p90", ".avg", ".count"}; 95 | double[] values = new double[] {49, 1, 48, 47, 45, 25, N_THREADS}; 96 | for(int i = 0; i < suffixes.length; i++) { 97 | assertEquals(metricName + suffixes[i], metrics.get(i).getName()); 98 | if(suffixes[i].equals(".count")){ 99 | assertEquals("count", metrics.get(i).getType()); 100 | } else { 101 | assertEquals("gauge", metrics.get(i).getType()); 102 | } 103 | assertEquals(suffixes[i], values[i], (int)metrics.get(i).getValue(), 1e-10); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/org/datadog/jmeter/plugins/aggregation/DatadogSketchTest.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.aggregation; 7 | 8 | import com.datadoghq.sketch.ddsketch.mapping.CubicallyInterpolatedMapping; 9 | import com.datadoghq.sketch.ddsketch.store.UnboundedSizeDenseStore; 10 | import org.junit.Test; 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class DatadogSketchTest { 14 | private static final double RELATIVE_ACCURACY = 0.01; 15 | 16 | @Test 17 | public void testSketch(){ 18 | DatadogSketch sketch = new DatadogSketch(new CubicallyInterpolatedMapping(RELATIVE_ACCURACY), UnboundedSizeDenseStore::new); 19 | 20 | for(int i = -10; i < 120; i++) { 21 | sketch.accept(i); 22 | } 23 | 24 | assertEquals(130, sketch.getCountValue()); 25 | assertEquals(54.5, sketch.getAverageValue(), RELATIVE_ACCURACY); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/org/datadog/jmeter/plugins/metrics/DatadogMetricContextTest.java: -------------------------------------------------------------------------------- 1 | /* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License 2.0. 2 | * This product includes software developed at Datadog (https://www.datadoghq.com/). 3 | * Copyright 2021-present Datadog, Inc. 4 | */ 5 | 6 | package org.datadog.jmeter.plugins.metrics; 7 | 8 | import static org.junit.Assert.assertFalse; 9 | import static org.junit.Assert.assertNotSame; 10 | import static org.junit.Assert.assertSame; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | import org.junit.Test; 16 | 17 | public class DatadogMetricContextTest { 18 | 19 | 20 | private boolean contextAreEquals(DatadogMetricContext ctx1, DatadogMetricContext ctx2) { 21 | if(ctx1 == ctx2) { 22 | return true; 23 | } 24 | if(ctx1.hashCode() != ctx2.hashCode()) { 25 | return false; 26 | } 27 | if(!ctx1.equals(ctx2) || !ctx2.equals(ctx1)) { 28 | return false; 29 | } 30 | 31 | Set ctxSet = new HashSet<>(); 32 | ctxSet.add(ctx1); 33 | ctxSet.add(ctx2); 34 | return ctxSet.size() == 1; 35 | } 36 | 37 | @Test 38 | public void allRefsAreUnique() 39 | { 40 | // Create 6 references: 41 | // - 2 strings of value 'foo' 42 | // - 2 strings of value 'bar', of ref X1 and X2 43 | // - 2 array of 1 string, the strings refs being X1 and X2 44 | String foo1 = new String("foo"); 45 | String foo2 = new String("foo"); 46 | String bar1 = new String("bar"); 47 | String bar2 = new String("bar"); 48 | String[] tags1 = new String[]{bar1}; 49 | String[] tags2 = new String[]{bar2}; 50 | 51 | assertNotSame(foo1, foo2); 52 | assertNotSame(tags1, tags2); 53 | assertSame(tags1[0], bar1); 54 | assertSame(tags2[0], bar2); 55 | assertNotSame(tags1[0], tags2[0]); 56 | DatadogMetricContext ctx1 = new DatadogMetricContext(foo1, tags1); 57 | DatadogMetricContext ctx2 = new DatadogMetricContext(foo2, tags2); 58 | 59 | assertTrue(contextAreEquals(ctx1, ctx2)); 60 | } 61 | 62 | @Test 63 | public void singleTagStringRef() 64 | { 65 | String foo1 = new String("foo"); 66 | String foo2 = new String("foo"); 67 | String bar = new String("bar"); 68 | String[] tags1 = new String[]{bar}; 69 | String[] tags2 = new String[]{bar}; 70 | 71 | assertNotSame(foo1, foo2); 72 | assertNotSame(tags1, tags2); 73 | assertSame(tags1[0], tags2[0]); 74 | 75 | DatadogMetricContext ctx1 = new DatadogMetricContext(foo1, tags1); 76 | DatadogMetricContext ctx2 = new DatadogMetricContext(foo2, tags2); 77 | 78 | assertTrue(contextAreEquals(ctx1, ctx2)); 79 | } 80 | 81 | @Test 82 | public void singleTagSingleNameRefs() 83 | { 84 | String foo = new String("foo"); 85 | String bar = new String("bar"); 86 | String[] tags1 = new String[]{bar}; 87 | String[] tags2 = new String[]{bar}; 88 | 89 | assertNotSame(tags1, tags2); 90 | assertSame(tags1[0], bar); 91 | assertSame(tags2[0], bar); 92 | assertSame(tags1[0], tags2[0]); 93 | 94 | DatadogMetricContext ctx1 = new DatadogMetricContext(foo, tags1); 95 | DatadogMetricContext ctx2 = new DatadogMetricContext(foo, tags2); 96 | 97 | assertTrue(contextAreEquals(ctx1, ctx2)); 98 | } 99 | 100 | @Test 101 | public void singleTagArrayRef() 102 | { 103 | String foo1 = new String("foo"); 104 | String foo2 = new String("foo"); 105 | 106 | String bar = new String("bar"); 107 | String[] tags = new String[]{bar}; 108 | 109 | assertNotSame(foo1, foo2); 110 | assertSame(tags[0], bar); 111 | 112 | DatadogMetricContext ctx1 = new DatadogMetricContext(foo1, tags); 113 | DatadogMetricContext ctx2 = new DatadogMetricContext(foo2, tags); 114 | 115 | assertTrue(contextAreEquals(ctx1, ctx2)); 116 | } 117 | 118 | @Test 119 | public void allRefsAreTheSame() 120 | { 121 | String foo = new String("foo"); 122 | 123 | String bar = new String("bar"); 124 | String[] tags = new String[]{bar}; 125 | 126 | assertSame(tags[0], bar); 127 | 128 | DatadogMetricContext ctx1 = new DatadogMetricContext(foo, tags); 129 | DatadogMetricContext ctx2 = new DatadogMetricContext(foo, tags); 130 | 131 | assertTrue(contextAreEquals(ctx1, ctx2)); 132 | } 133 | 134 | @Test 135 | public void differentNamesMakesDifferentObjects() 136 | { 137 | String[] tags = new String[]{}; 138 | DatadogMetricContext ctx1 = new DatadogMetricContext("foo1", tags); 139 | DatadogMetricContext ctx2 = new DatadogMetricContext("foo2", tags); 140 | 141 | assertFalse(contextAreEquals(ctx1, ctx2)); 142 | } 143 | 144 | @Test 145 | public void differentTagsMakesDifferentObjects() 146 | { 147 | String[] tags1 = new String[]{"bar1"}; 148 | String[] tags2 = new String[]{"bar2"}; 149 | DatadogMetricContext ctx1 = new DatadogMetricContext("foo", tags1); 150 | DatadogMetricContext ctx2 = new DatadogMetricContext("foo", tags2); 151 | 152 | assertFalse(contextAreEquals(ctx1, ctx2)); 153 | } 154 | 155 | } 156 | --------------------------------------------------------------------------------