├── .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 | 
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 |
--------------------------------------------------------------------------------