main 7ceb152c68ca cached
51 files
341.2 KB
85.4k tokens
268 symbols
1 requests
Download .txt
Showing preview only (362K chars total). Download the full file or copy to clipboard to get everything.
Repository: johrstrom/jmeter-prometheus-plugin
Branch: main
Commit: 7ceb152c68ca
Files: 51
Total size: 341.2 KB

Directory structure:
gitextract_e9g8y5hg/

├── .github/
│   └── workflows/
│       ├── release.yml
│       └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── NOTICE
├── README.md
├── docs/
│   └── examples/
│       ├── grafana.json
│       └── simple_prometheus_example.jmx
├── licenses/
│   ├── io.prometheus.LICENSE
│   └── io.prometheus.NOTICE
├── makefile
├── pom.xml
└── src/
    ├── main/
    │   └── java/
    │       └── com/
    │           └── github/
    │               └── johrstrom/
    │                   ├── collector/
    │                   │   ├── BaseCollectorConfig.java
    │                   │   ├── CollectorElement.java
    │                   │   ├── JMeterCollectorRegistry.java
    │                   │   ├── SuccessRatioCollector.java
    │                   │   └── gui/
    │                   │       ├── AbstractCollectorTable.java
    │                   │       └── Flatten.java
    │                   ├── config/
    │                   │   ├── PrometheusMetricsConfig.java
    │                   │   └── gui/
    │                   │       ├── ConfigCollectorTable.java
    │                   │       └── PrometheusMetricsConfigGui.java
    │                   └── listener/
    │                       ├── ListenerCollectorConfig.java
    │                       ├── PrometheusListener.java
    │                       ├── PrometheusServer.java
    │                       ├── gui/
    │                       │   ├── ListenerCollectorTable.java
    │                       │   └── PrometheusListenerGui.java
    │                       └── updater/
    │                           ├── AbstractUpdater.java
    │                           ├── AggregatedTypeUpdater.java
    │                           └── CountTypeUpdater.java
    └── test/
        ├── java/
        │   └── com/
        │       └── github/
        │           └── johrstrom/
        │               ├── collector/
        │               │   ├── BaseCollectorConfigTest.java
        │               │   ├── JMeterCollectorRegistryTest.java
        │               │   └── SuccessRatioCollectorTest.java
        │               ├── config/
        │               │   ├── PrometheusMetricsConfigTest.java
        │               │   └── gui/
        │               │       └── ConfigGuiTest.java
        │               ├── listener/
        │               │   ├── ListenerCollectorConfigTest.java
        │               │   ├── PrometheusListenerTest.java
        │               │   ├── PrometheusServerTest.java
        │               │   ├── gui/
        │               │   │   └── ListenerGuiTest.java
        │               │   └── updater/
        │               │       ├── AbstractUpdaterTest.java
        │               │       ├── AggregatedTypeUpdaterTest.java
        │               │       └── CountTypeUpdaterTest.java
        │               └── test/
        │                   ├── NOOPThreadMonitor.java
        │                   └── TestUtilities.java
        └── resources/
            ├── bin/
            │   ├── jmeter.properties
            │   ├── saveservice.properties
            │   ├── upgrade.properties
            │   └── user.properties
            └── log4j2.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/release.yml
================================================
name: Release Jar

on:
  push:
    tags:
      - '*'

jobs:
  release:
    runs-on: 'ubuntu-latest'

    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: '11'
          cache: maven
      - name: Set version
        id: version
        run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
      - run: mvn clean package
      - name: Get release
        id: get_release
        uses: bruceadams/get-release@v1.3.2
        env:
          GITHUB_TOKEN: ${{ github.token }}
      - name: Upload Jar
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.get_release.outputs.upload_url }}
          asset_path: target/jmeter-prometheus-plugin-${{ steps.version.outputs.version }}.jar
          asset_name: jmeter-prometheus-plugin-${{ steps.version.outputs.version }}.jar
          asset_content_type: application/java-archive
      



================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests

on:
  push:
    branches: 
      - master
      - main
  pull_request:

jobs:
  tests:
    strategy:
      matrix:
        java_version:
          - '8'
          - '11'
          # TODO: Fix for java version 17
          # - '17'
    runs-on: 'ubuntu-latest'

    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: '${{ matrix.java_version }}'
          cache: maven
      - run: mvn clean test



================================================
FILE: .gitignore
================================================
target/
.project
.settings/*
.classpath
./bin/
package_and_mv.sh
dependency-reduced-pom.xml
jmeter.log
pom.xml.releaseBackup
release.properties
/.idea/
/jmeter-prometheus-plugin.iml


================================================
FILE: CHANGELOG.md
================================================
# 0.6.2

* #117 is a bugfix in PrometheusListener.java to clear the collectors after the server is closed.

# 0.6.1

* #116 is a bugfix in PrometheusServer.java to close the server after the delay.
  `prometheus.delay` will now work as expected.

# 0.4.0

* #47 - bugfix so to correctly rename gui elements.\
* #51 - enable listening to assertions in counter type metrics.

# 0.3.0

* #50 - listening to latency
* #49 - listening to idle time
* #48 - listening to connection time

# 0.2.0

* #24 - success ratios to send zeros.
* #46 - code is a label keyword for response code.
* #11 - save jvm stats and allow for configuration.


# 0.2.0-rc3
* #42 - implement counters (success, failure and total)
* listener can now measure response sizes of samples in histogram and summary.
* #41 - enhancement for metric re-use.

# 0.2.0-rc2
Bugfixes for previous version.

* #40
* #34
* undocumented problem copy/paste (serializable issues) of the config


# 0.2.0-rc1
* first real release with only response time functionality in the listener.


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct

## 1. Purpose

A primary goal of Jmeter Prometheus Plugin is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).

This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.

We invite all those who participate in Jmeter Prometheus Plugin to help us create safe and positive experiences for everyone.

## 2. Open Source Citizenship

A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.

Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.

If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.

## 3. Expected Behavior

The following behaviors are expected and requested of all community members:

*   Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
*   Exercise consideration and respect in your speech and actions.
*   Attempt collaboration before conflict.
*   Refrain from demeaning, discriminatory, or harassing behavior and speech.
*   Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
*   Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.

## 4. Unacceptable Behavior

The following behaviors are considered harassment and are unacceptable within our community:

*   Violence, threats of violence or violent language directed against another person.
*   Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
*   Posting or displaying sexually explicit or violent material.
*   Posting or threatening to post other people’s personally identifying information ("doxing").
*   Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
*   Inappropriate photography or recording.
*   Inappropriate physical contact. You should have someone’s consent before touching them.
*   Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
*   Deliberate intimidation, stalking or following (online or in person).
*   Advocating for, or encouraging, any of the above behavior.
*   Sustained disruption of community events, including talks and presentations.

## 5. Consequences of Unacceptable Behavior

Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.

Anyone asked to stop unacceptable behavior is expected to comply immediately.

If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).

## 6. Reporting Guidelines

If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. johrstrom@hotmail.com.



Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.

## 7. Addressing Grievances

If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Johrstrom with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.



## 8. Scope

We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business.

This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.

## 9. Contact info

johrstrom@hotmail.com

## 10. License and attribution

This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).

Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).

Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/)


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing


* If you have a trivial fix or improvement, go ahead and create a pull request, addressing me (Jeff Ohrstrom) in the description of the pull request.
* If you plan to do something more involved, you should probably open an issue so we can talk about it. However you can also just create the pull request and the issue and we can discuss the concept in the issue and the implementation in the pull request.
* In terms of coding styles, I'm a native english speaker who just read [Clean Code](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)(tm) so I tend to value readable code.  This library specifically tends towards basic Java standards and JMeter conventions. 


# side/related 
* This is an open source project, so commits/pull request are welcome. Forks are awesome. Issues are abundant and highly desired. 


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2017 Jeff Ohrstrom

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: NOTICE
================================================
Prometheus instrumentation library Apache JMeter
Copyright 2016-2019 Jeff Ohrstrom

This product includes software developed at
AT&T Inc. (https://www.att.com)


================================================
FILE: README.md
================================================
[![Build Status](https://github.com/johrstrom/jmeter-prometheus-plugin/workflows/Tests/badge.svg)](https://github.com/johrstrom/jmeter-prometheus-plugin/actions?query=workflow%3ATests)
[![Current Version](https://img.shields.io/maven-central/v/com.github.johrstrom/jmeter-prometheus-plugin.svg)](maven-central)

# Prometheus Listener for Jmeter

# Overview
This JMeter plugin is highly configurable listener (and config element) to allow users define they're own metrics (names, types etc.) and expose them through a Prometheus /metrics API to be scraped by a Prometheus server.

# Documentation
More documentation can be found on [this project's wiki](https://github.com/johrstrom/jmeter-prometheus-plugin/wiki).

# Listener QuickDoc
Here's a simple example to get us started.  This example [can be found here](/docs/examples/simple_prometheus_example.jmx).  All the documentation on this README is from this jmx file.

![JMeter testplan](/docs/imgs/simple_testplan.png?raw=true)

If we look closer at the very first Prometheus listener, it looks like the image below.

![JMeter testplan](/docs/imgs/listener_full.png?raw=true)

Let's go through all the columns one by one.

- **Name**: the name of the metric.
- **Help**: The help message of the metric.
- **Labels**: A comma seperated list of labels you want to apply to the metric.
  - `label` is a keyword. In JMeter it means the *name* of the sampler.
  - `code` is a keyword. It's the response code of the result.
  - JMeter variables can be used here. See the section [below](#Using-JMeter-variables-as-labels).
- **Type**: The type of metric you're creating.
  - See [Prometheus documentation](https://prometheus.io/docs/concepts/metric_types/) about metric types.
  - [Success Ratio](#Success-Ratio) is something specific to this plugin and you can see the documentation below.
- **Buckets of Quantiles**:
  - Buckets are comma seperated list of numbers. Can be integers or decimals.
  - Quantiles are comma `,` separated pair of decimals separated by a vertical bar `|`. The first decimal being the quantile and the second being the error rating. Optionally, after a `;` separator the lenght of the window used to calculate the quantile can be specified. Sample: `0.8,0.01|0.9,0.01|0.95,0.005|0.99,0.001;60`
- **Listen To**: Dropdown to listen to samples or assertions. This only applies to Counters and SuccessRatio type metrics.
- **Measuring**: Dropdown menu of all the things you can measure
  - See the [Type and Measuring compatibility matrix](#Type-and-Measuring-compatibility-matrix) section below.

### Using JMeter variables as labels

Notice in the image above `jsr223_rt_as_summary` (the 2nd down) has `category,label` in it's labels column.

This plugin allows you to use variables in the test plan as label values for a given metric.  You can see here in the above image, I simply generated a random string and assigned it to the `category` jmeter variable, and this plugin exposed it as a label.

As you can see below, it produced that metric with the `category="[A,B,C]"`. Again, this example [can be found here](/docs/examples/simple_prometheus_example.jmx).

![JMeter testplan](/docs/imgs/category_variable.png?raw=true)


### Listener output

![JMeter testplan](/docs/imgs/rt_as_sum.png?raw=true)

### Success Ratio

Success ratio is a concept specific to this plugin library.  Often we want measure success rates of samplers and it's difficult to do so when the failure for a given metric or labelset has never occurred.  It's difficult because it involves computations with NaNs.

So, we want to measure success ratios and be sure to emit zeros for both failures and success when the other is created for the first time.  That way you can always safely run computations like rate() and so on.

[Here's prometheus](https://www.robustperception.io/existential-issues-with-metrics) documentation on why it's important.  [Here](https://github.com/johrstrom/jmeter-prometheus-plugin/issues/24) is this repositories issue for this feature.

Here you can see from the example `jsr223_can_fail` turned into 3 metrics.  The names have appeneded `_success`, `_failure` and `_total`.  They're all counter type metrics that increment on success failure and total, respectively.  

From this example you can see that `4 success + 2 failures = 6 total`.  Again, this metric guarantees a zero, for success or failure, for a metric that has a total of one or more.

![JMeter testplan](/docs/imgs/success_ratio_output.png?raw=true)

### Type and Measuring compatibility matrix

Does it make sense to have a Counter measuring Response Time? No. Does it make sense to have a Histogram of total successes? No.  

This is a matrix of what metric types can measure what metrics.  If you configure, say a histogram to measure count total, the plugin will likely do nothing to update that metric.

**Bold** types can listen to samples or assertions (not both at the same time).  Note that if you don't use `label` when listening to assertions you may get strange results.  This is because *one* sample can generate many *assertion results* which are then counted. When there's no label to distinguish those counts, they'll be summed together which may or may not be expected.

| | Histogram | Summary | Counter | Guage | Success Ratio |
|:-----:|:------:|:------:|:------:|:------:|:------:|
| Response time  | x | x |   |   |   |
| Response size  | x | x |   |   |   |
| Latency        | x | x |   |   |   |
| Idle time      | x | x |   |   |   |
| Connect time   | x | x |   |   |   |
| Count total    |   |   | **x** |   |   |
| Failure total  |   |   | **x** |   |   |
| Success total  |   |   | **x** |   |   |
| Success Ratio  |   |   |   |   | **x** |

#### What about gauges
I'm not quite sure how Guages make sense in the plugin.  If you have a use case, I'd love to hear it. I wrote them in without actually having one, so you can technically create one, I just don't know how the listener may update it.

# Config QuickDoc

This library provides not only a listener, but a configuration element as well.  This is useful when users have to make some computation for a specific use case and then want to expose that metric in Prometheus.

These use cases for this are mostly functional, where say you're validating something about a response that's specific the thing under test.  

It works like this.  Define a metric with this configuration element, and at test run-time this library will place that object in the jmeter variables from which you can access it.

Let's consider this use case.  You, have an API that tells you the current number animals categorized by `color` `size`and `mammal` - perhaps running down your street.  You may create a counter like this as you watch them go by.
![JMeter testplan](/docs/imgs/prometheus_cfg.png?raw=true)


So I then access the object I've created above like so.  This is an absolutely trivial case of randomly generating the variables but the example is only trying to convey how one may interact with the objects created by the configuration element.  However you extract the data, you can access and interact with the Prometheus metric you've created.  

See the [Prometheus Javadocs](https://prometheus.github.io/client_java/) for more information on their API.

![JMeter testplan](/docs/imgs/jsr223_use_prometheus_cfg.png?raw=true)

Which will expose a counter with all the appropriate labels.

![JMeter testplan](/docs/imgs/prom_cfg_output.png?raw=true)

# Usage Tips

### Skipping samplers or other elements

You can re-use metrics in multiple listeners so long as they're defined in the **exact** same way. There will be undefined behavior if two or more listeners have the same metric (the same metric name) with different configurations.


Here you see `first_random_sampler` and `second_random_sampler` in the labels of this metric, but you do not see `want_to_skip`, the thing that we're trying to skip.  Note the composition of the test plan as shown [at the top of this page](#Listener-QuickDoc)

![JMeter testplan](/docs/imgs/rt_as_hist.png?raw=true)

### Visualization

This plugin has limited "out of the box" functionality because it gives you, the user, total control over what the metric names may be.  That said, here's a sample dashboard given [here in examples](/docs/examples/grafana_jsr223_test.json) such that if you have a local prometheus/grafana stack you can a dashboard that looks something like this.

![JMeter testplan](/docs/imgs/grafana_jsr223_test.png?raw=true)


# Properties you can override
To overrider properties, add the Properties in the jmeter.properties file (JMETER_HOME/bin folder) and restart Jmeter to take effect

|Property | default | description|
|:----------:|:-----------:|:-------------------------------:|
|prometheus.port|9270|The port the http server will bind to |
|prometheus.ip|127.0.0.1|The ip the http server will bind to. Containers may need `0.0.0.0`|
|prometheus.delay|0|The delay (in seconds) the http server will wait before being destroyed|
|prometheus.save.threads|true|True or false value to save and collect jmeter thread metrics|
|prometheus.save.threads.name|jmeter\_threads|The name of the metric describing jmeter threads|
|prometheus.save.jvm|true|Collect metrics from the JVM|

# Download

## Maven Dependency

We're now hosted on maven central! If you want to download this jar in a maven style project, simply add this dependency:

```xml
    <!-- you'll have to specify jmeter-prometheus-plugin.version here -->
    <dependency>
      <groupId>com.github.johrstrom</groupId>
      <artifactId>jmeter-prometheus-plugin</artifactId>
      <version>${jmeter-prometheus-plugin.version}</version>
    </dependency>
```

## Programatically

This URL below seems to be the only way to download jars from maven through `curl` or `wget`.  Again, replace `0.6.0` here with the
current version, which can be viewed at the top of this README.

`https://search.maven.org/remotecontent?filepath=com/github/johrstrom/jmeter-prometheus-plugin/0.6.0/jmeter-prometheus-plugin-0.6.0.jar`

## Web Browser

Search [maven central](https://search.maven.org/search?q=a:jmeter-prometheus-plugin) to get the latest version.

This project is hosted [here](https://oss.sonatype.org/content/groups/public/com/github/johrstrom/jmeter-prometheus-plugin/) on 
[OSS sonatype org](https://oss.sonatype.org).

## Verifying

I sign these release jars so you can verify with this method (of course the version is going to change):

```bash
gpg --verify jmeter-prometheus-plugin-0.5.0.jar.asc
```

You should see output similar to this.

```bash
gpg: assuming signed data in 'jmeter-prometheus-plugin-0.5.0.jar'
gpg: Signature made Thu 22 Aug 2019 09:35:15 PM EDT
gpg:                using RSA key 6F5EAC674B279301932EC1FEAC2AEC6C76D4AF12
gpg: Good signature from "Jeff Ohrstrom (Jeff Ohrstrom's personal key) <johrstrom@hotmail.com>" [ultimate]
```

# Building

To build, simply maven package:
```
mvn clean package
```
This creates 2 jars, a shaded jar that has all the dependencies within it (this is the one you want) and the original jar. Both are in the target directory.  Simply move the jar to your $JMETER\_HOME/lib/ext directory as with any JMeter plugin and you're ready to go!

## Feedback

Feel free to open issues against this project, even to ask questions.


================================================
FILE: docs/examples/grafana.json
================================================
{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "description": "A grafana dashboard to inspect jmeter metrics via prometheus exporter",
  "editable": true,
  "gnetId": 2492,
  "graphTooltip": 1,
  "id": 2,
  "iteration": 1582126578426,
  "links": [],
  "panels": [
    {
      "cacheTimeout": null,
      "colorBackground": true,
      "colorValue": false,
      "colors": [
        "#3274D9",
        "#3274D9",
        "#3274D9"
      ],
      "datasource": "Prometheus",
      "format": "none",
      "gauge": {
        "maxValue": 100,
        "minValue": 0,
        "show": false,
        "thresholdLabels": false,
        "thresholdMarkers": true
      },
      "gridPos": {
        "h": 2,
        "w": 6,
        "x": 0,
        "y": 0
      },
      "id": 12,
      "interval": null,
      "links": [],
      "mappingType": 1,
      "mappingTypes": [
        {
          "name": "value to text",
          "value": 1
        },
        {
          "name": "range to text",
          "value": 2
        }
      ],
      "maxDataPoints": 100,
      "nullPointMode": "connected",
      "nullText": null,
      "options": {},
      "postfix": " vus",
      "postfixFontSize": "100%",
      "prefix": "",
      "prefixFontSize": "50%",
      "rangeMaps": [
        {
          "from": "null",
          "text": "N/A",
          "to": "null"
        }
      ],
      "sparkline": {
        "fillColor": "#447ebc",
        "full": true,
        "lineColor": "#447ebc",
        "show": false
      },
      "tableColumn": "",
      "targets": [
        {
          "expr": "sum(jmeter_threads{state=\"active\"})",
          "format": "time_series",
          "hide": false,
          "instant": true,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "",
          "refId": "A",
          "step": 4
        }
      ],
      "thresholds": "",
      "title": "",
      "type": "singlestat",
      "valueFontSize": "100%",
      "valueMaps": [
        {
          "op": "=",
          "text": "N/A",
          "value": "null"
        }
      ],
      "valueName": "current"
    },
    {
      "cacheTimeout": null,
      "colorBackground": true,
      "colorValue": false,
      "colors": [
        "#A352CC",
        "#A352CC",
        "#A352CC"
      ],
      "datasource": "Prometheus",
      "decimals": 1,
      "format": "ops",
      "gauge": {
        "maxValue": 100,
        "minValue": 0,
        "show": false,
        "thresholdLabels": false,
        "thresholdMarkers": true
      },
      "gridPos": {
        "h": 2,
        "w": 6,
        "x": 6,
        "y": 0
      },
      "id": 7,
      "interval": null,
      "links": [],
      "mappingType": 1,
      "mappingTypes": [
        {
          "name": "value to text",
          "value": 1
        },
        {
          "name": "range to text",
          "value": 2
        }
      ],
      "maxDataPoints": 100,
      "nullPointMode": "connected",
      "nullText": null,
      "options": {},
      "postfix": "",
      "postfixFontSize": "50%",
      "prefix": "",
      "prefixFontSize": "50%",
      "rangeMaps": [
        {
          "from": "null",
          "text": "N/A",
          "to": "null"
        }
      ],
      "sparkline": {
        "fillColor": "#508642",
        "full": true,
        "lineColor": "#508642",
        "show": false
      },
      "tableColumn": "",
      "targets": [
        {
          "expr": "sum(irate(Ratio_success[$interval]))",
          "format": "time_series",
          "instant": true,
          "intervalFactor": 1,
          "legendFormat": "",
          "refId": "E"
        }
      ],
      "thresholds": "",
      "title": "",
      "type": "singlestat",
      "valueFontSize": "100%",
      "valueMaps": [
        {
          "op": "=",
          "text": "N/A",
          "value": "null"
        }
      ],
      "valueName": "current"
    },
    {
      "cacheTimeout": null,
      "colorBackground": true,
      "colorValue": false,
      "colors": [
        "#E0B400",
        "#E0B400",
        "#E0B400"
      ],
      "datasource": "Prometheus",
      "decimals": 1,
      "format": "ms",
      "gauge": {
        "maxValue": 100,
        "minValue": 0,
        "show": false,
        "thresholdLabels": false,
        "thresholdMarkers": true
      },
      "gridPos": {
        "h": 2,
        "w": 6,
        "x": 12,
        "y": 0
      },
      "id": 14,
      "interval": null,
      "links": [],
      "mappingType": 1,
      "mappingTypes": [
        {
          "name": "value to text",
          "value": 1
        },
        {
          "name": "range to text",
          "value": 2
        }
      ],
      "maxDataPoints": 100,
      "nullPointMode": "connected",
      "nullText": null,
      "options": {},
      "postfix": "",
      "postfixFontSize": "50%",
      "prefix": "Avg. ",
      "prefixFontSize": "100%",
      "rangeMaps": [
        {
          "from": "null",
          "text": "N/A",
          "to": "null"
        }
      ],
      "sparkline": {
        "fillColor": "#e5ac0e",
        "full": true,
        "lineColor": "#e5ac0e",
        "show": false
      },
      "tableColumn": "",
      "targets": [
        {
          "expr": "avg((rate(ResponseTime_sum{code=\"200\"}[$interval])/(rate(ResponseTime_count{code=\"200\"}[$interval])>0)))",
          "format": "time_series",
          "hide": false,
          "instant": true,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "Response time",
          "metric": "",
          "refId": "A",
          "step": 4
        }
      ],
      "thresholds": "",
      "title": "",
      "type": "singlestat",
      "valueFontSize": "100%",
      "valueMaps": [
        {
          "op": "=",
          "text": "N/A",
          "value": "null"
        }
      ],
      "valueName": "current"
    },
    {
      "cacheTimeout": null,
      "colorBackground": true,
      "colorValue": false,
      "colors": [
        "#37872D",
        "#FF9830",
        "#E02F44"
      ],
      "datasource": "Prometheus",
      "decimals": 1,
      "format": "percentunit",
      "gauge": {
        "maxValue": 100,
        "minValue": 0,
        "show": false,
        "thresholdLabels": false,
        "thresholdMarkers": true
      },
      "gridPos": {
        "h": 2,
        "w": 6,
        "x": 18,
        "y": 0
      },
      "id": 8,
      "interval": null,
      "links": [],
      "mappingType": 1,
      "mappingTypes": [
        {
          "name": "value to text",
          "value": 1
        },
        {
          "name": "range to text",
          "value": 2
        }
      ],
      "maxDataPoints": 100,
      "nullPointMode": "connected",
      "nullText": null,
      "options": {},
      "postfix": " error rate",
      "postfixFontSize": "100%",
      "prefix": "",
      "prefixFontSize": "50%",
      "rangeMaps": [
        {
          "from": "null",
          "text": "N/A",
          "to": "null"
        }
      ],
      "sparkline": {
        "fillColor": "#bf1b00",
        "full": true,
        "lineColor": "#bf1b00",
        "show": false
      },
      "tableColumn": "",
      "targets": [
        {
          "expr": "(avg(rate(Ratio_failure[$interval]))/avg(rate(Ratio_total[$interval])))",
          "format": "time_series",
          "instant": true,
          "intervalFactor": 1,
          "legendFormat": "",
          "refId": "A",
          "step": 4
        }
      ],
      "thresholds": "0.1,10",
      "title": "",
      "type": "singlestat",
      "valueFontSize": "100%",
      "valueMaps": [
        {
          "op": "=",
          "text": "N/A",
          "value": "null"
        }
      ],
      "valueName": "current"
    },
    {
      "collapsed": false,
      "datasource": null,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 2
      },
      "id": 17,
      "panels": [],
      "repeat": null,
      "title": "Summary",
      "type": "row"
    },
    {
      "aliasColors": {
        "Failure ": "#bf1b00",
        "OK": "dark-green"
      },
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "description": "",
      "fill": 0,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 12,
        "x": 0,
        "y": 3
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 36,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "hideEmpty": true,
        "hideZero": true,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 3,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [
        {
          "alias": "Virtual Users",
          "color": "#3274D9",
          "fill": 4,
          "lines": true,
          "linewidth": 3,
          "yaxis": 2
        },
        {
          "alias": "Request OK per second",
          "color": "#19730E"
        },
        {
          "alias": "Request KO per second",
          "color": "#AD0317"
        }
      ],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum(jmeter_threads{state=\"active\"})",
          "format": "time_series",
          "hide": false,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "Virtual Users",
          "refId": "B",
          "step": 1
        },
        {
          "expr": "sum(rate(Ratio_success[$interval]))",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "Request OK per second",
          "refId": "A"
        },
        {
          "expr": "sum(rate(Ratio_failure[$interval]))   ",
          "legendFormat": "Request KO per second",
          "refId": "C"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Virtual Users vs OK/KO",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "none",
          "label": "ops",
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "decimals": null,
          "format": "short",
          "label": "VU",
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {
        "KO": "#890F02",
        "Requests KO": "#bf1b00",
        "Requests OK": "#508642",
        "Requests per second": "#447ebc",
        "failure": "#890F02",
        "success": "#7EB26D"
      },
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 10,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 12,
        "x": 12,
        "y": 3
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 3,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "max": true,
        "min": true,
        "show": true,
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [
        {
          "alias": "Total Requests per second",
          "color": "#A352CC",
          "fill": 0,
          "linewidth": 3,
          "steppedLine": false
        },
        {
          "alias": "OK",
          "yaxis": 2
        },
        {
          "alias": "KO",
          "yaxis": 2
        }
      ],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": true,
      "targets": [
        {
          "expr": "(sum(rate(Ratio_success[$interval]))/sum(rate(Ratio_total[$interval])))",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "OK",
          "refId": "A",
          "step": 2
        },
        {
          "expr": "(sum(rate(Ratio_failure[$interval]))/sum(rate(Ratio_total[$interval])))",
          "format": "time_series",
          "intervalFactor": 1,
          "legendFormat": "KO",
          "refId": "B"
        },
        {
          "expr": "sum(rate(Ratio_total[$interval]))",
          "format": "time_series",
          "intervalFactor": 1,
          "legendFormat": "Total Requests per second",
          "refId": "C"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Total Requests vs OK/KO",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "none",
          "label": "ops",
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "decimals": 0,
          "format": "percentunit",
          "label": "",
          "logBase": 1,
          "max": "1",
          "min": "0",
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {
        "Failure ": "#bf1b00",
        "Virtual Users": "semi-dark-blue"
      },
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "description": "",
      "fill": 0,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 12,
        "x": 0,
        "y": 13
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 30,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "hideEmpty": true,
        "hideZero": true,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 3,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [
        {
          "alias": "Avg Response Time",
          "color": "#F2CC0C"
        },
        {
          "alias": "Virtual Users",
          "color": "#3274D9",
          "fill": 4,
          "yaxis": 2
        }
      ],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "avg((rate(ResponseTime_sum{code=\"200\"}[$interval])/(rate(ResponseTime_count{code=\"200\"}[$interval])>0)))",
          "format": "time_series",
          "hide": false,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "Avg Response Time",
          "refId": "B",
          "step": 1
        },
        {
          "expr": "sum(jmeter_threads{state=\"active\"})",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "Virtual Users",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Avg. Response Time vs Virtual Users",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "ms",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": "0",
          "show": true
        },
        {
          "decimals": null,
          "format": "short",
          "label": "VU",
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {
        "Failure ": "#bf1b00"
      },
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "description": "",
      "fill": 0,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 12,
        "x": 12,
        "y": 13
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 29,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "hideEmpty": true,
        "hideZero": true,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 3,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [
        {
          "alias": "OK",
          "color": "#19730E",
          "yaxis": 2
        },
        {
          "alias": "Avg Response Time",
          "color": "#F2CC0C"
        },
        {
          "alias": "KO",
          "color": "#AD0317",
          "yaxis": 2
        }
      ],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "avg((rate(ResponseTime_sum{code=\"200\"}[$interval])/(rate(ResponseTime_count{code=\"200\"}[$interval])>0)))",
          "format": "time_series",
          "hide": false,
          "instant": false,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "Avg Response Time",
          "refId": "B",
          "step": 1
        },
        {
          "expr": "sum(rate(Ratio_success[$interval]))",
          "format": "time_series",
          "hide": true,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "OK",
          "refId": "A"
        },
        {
          "expr": "sum(rate(Ratio_failure[$interval]))   ",
          "format": "time_series",
          "hide": true,
          "intervalFactor": 1,
          "legendFormat": "KO",
          "refId": "C"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Avg. Response Time vs OK/KO",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "ms",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": "0",
          "show": true
        },
        {
          "decimals": null,
          "format": "short",
          "label": "ops",
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "columns": [],
      "datasource": "Prometheus",
      "fontSize": "100%",
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 23
      },
      "id": 33,
      "links": [],
      "options": {},
      "pageSize": 20,
      "scroll": true,
      "showHeader": true,
      "sort": {
        "col": 1,
        "desc": false
      },
      "styles": [
        {
          "alias": "Time",
          "align": "auto",
          "dateFormat": "YYYY-MM-DD HH:mm:ss",
          "link": false,
          "pattern": "Time",
          "type": "hidden"
        },
        {
          "alias": "Label",
          "align": "auto",
          "colorMode": null,
          "colors": [
            "rgba(245, 54, 54, 0.9)",
            "rgba(237, 129, 40, 0.89)",
            "rgba(50, 172, 45, 0.97)"
          ],
          "dateFormat": "YYYY-MM-DD HH:mm:ss",
          "decimals": 2,
          "mappingType": 1,
          "pattern": "label",
          "thresholds": [],
          "type": "number",
          "unit": "short"
        },
        {
          "alias": "Count",
          "align": "auto",
          "colorMode": null,
          "colors": [
            "rgba(245, 54, 54, 0.9)",
            "rgba(237, 129, 40, 0.89)",
            "rgba(50, 172, 45, 0.97)"
          ],
          "dateFormat": "YYYY-MM-DD HH:mm:ss",
          "decimals": 0,
          "mappingType": 1,
          "pattern": "Value",
          "thresholds": [],
          "type": "number",
          "unit": "locale"
        },
        {
          "alias": "",
          "align": "auto",
          "colorMode": null,
          "colors": [
            "rgba(245, 54, 54, 0.9)",
            "rgba(237, 129, 40, 0.89)",
            "rgba(50, 172, 45, 0.97)"
          ],
          "decimals": 2,
          "pattern": "/.*/",
          "thresholds": [],
          "type": "number",
          "unit": "short"
        }
      ],
      "targets": [
        {
          "expr": "sum(Ratio_success) by (label) ",
          "format": "table",
          "instant": true,
          "intervalFactor": 1,
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "OK",
      "transform": "table",
      "type": "table"
    },
    {
      "columns": [],
      "datasource": "Prometheus",
      "fontSize": "100%",
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 23
      },
      "id": 34,
      "links": [],
      "options": {},
      "pageSize": 25,
      "scroll": true,
      "showHeader": true,
      "sort": {
        "col": 1,
        "desc": false
      },
      "styles": [
        {
          "alias": "Time",
          "align": "auto",
          "dateFormat": "YYYY-MM-DD HH:mm:ss",
          "link": false,
          "pattern": "Time",
          "type": "hidden"
        },
        {
          "alias": "Label",
          "align": "auto",
          "colorMode": null,
          "colors": [
            "rgba(245, 54, 54, 0.9)",
            "rgba(237, 129, 40, 0.89)",
            "rgba(50, 172, 45, 0.97)"
          ],
          "dateFormat": "YYYY-MM-DD HH:mm:ss",
          "decimals": 2,
          "mappingType": 1,
          "pattern": "label",
          "thresholds": [],
          "type": "number",
          "unit": "short"
        },
        {
          "alias": "Count",
          "align": "auto",
          "colorMode": null,
          "colors": [
            "rgba(245, 54, 54, 0.9)",
            "rgba(237, 129, 40, 0.89)",
            "rgba(50, 172, 45, 0)"
          ],
          "dateFormat": "YYYY-MM-DD HH:mm:ss",
          "decimals": 0,
          "mappingType": 1,
          "pattern": "Value",
          "thresholds": [
            ""
          ],
          "type": "number",
          "unit": "none"
        },
        {
          "alias": "",
          "align": "auto",
          "colorMode": null,
          "colors": [
            "rgba(245, 54, 54, 0.9)",
            "rgba(237, 129, 40, 0.89)",
            "rgba(50, 172, 45, 0.97)"
          ],
          "decimals": 2,
          "pattern": "/.*/",
          "thresholds": [],
          "type": "number",
          "unit": "short"
        }
      ],
      "targets": [
        {
          "expr": "sum(Ratio_failure) by (label) ",
          "format": "table",
          "instant": true,
          "intervalFactor": 1,
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "KO",
      "transform": "table",
      "type": "table"
    },
    {
      "collapsed": false,
      "datasource": null,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 31
      },
      "id": 18,
      "panels": [],
      "repeat": null,
      "title": "Response times",
      "type": "row"
    },
    {
      "aliasColors": {
        "avg jmeter": "light-green"
      },
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "description": "",
      "fill": 0,
      "fillGradient": 0,
      "gridPos": {
        "h": 11,
        "w": 24,
        "x": 0,
        "y": 32
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 35,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": true,
        "hideEmpty": true,
        "hideZero": true,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "sort": "avg",
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 3,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": " avg (rate(ResponseTime_sum{code=~\"1.*|2.*|3.*|4.*|5.*\"}[$interval]) / (rate(ResponseTime_count{code=~\"1.*|2.*|3.*|4.*|5.*\"}[$interval])>0)) by (label)",
          "format": "time_series",
          "hide": false,
          "instant": false,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "{{label}} ",
          "refId": "A",
          "step": 1
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Avg. response time",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "ms",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "description": "",
      "fill": 1,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 24,
        "x": 0,
        "y": 43
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 13,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": true,
        "hideEmpty": true,
        "hideZero": true,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "sort": "avg",
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 3,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "avg(ResponseTime{code=\"200\"}) by (quantile)",
          "format": "time_series",
          "hide": false,
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "{{quantile}}",
          "refId": "B",
          "step": 1
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Pct response times",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "ms",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "decimals": null,
          "format": "short",
          "label": "",
          "logBase": 1,
          "max": null,
          "min": null,
          "show": false
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "collapsed": false,
      "datasource": null,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 53
      },
      "id": 19,
      "panels": [],
      "repeat": null,
      "title": "Requests per second",
      "type": "row"
    },
    {
      "aliasColors": {
        "KO": "semi-dark-red",
        "OK": "semi-dark-green",
        "failure": "#890F02"
      },
      "bars": true,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 0,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 24,
        "x": 0,
        "y": 54
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 4,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "hideEmpty": false,
        "hideZero": false,
        "max": true,
        "min": true,
        "show": true,
        "total": false,
        "values": true
      },
      "lines": false,
      "linewidth": 3,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "repeat": null,
      "repeatDirection": "h",
      "seriesOverrides": [
        {
          "alias": "KO",
          "color": "#E02F44"
        },
        {
          "alias": "OK",
          "color": "#56A64B"
        }
      ],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum(rate(Ratio_success[$interval]))",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 1,
          "legendFormat": "OK",
          "metric": "",
          "refId": "A",
          "step": 2
        },
        {
          "expr": "sum(rate(Ratio_failure[$interval]))",
          "format": "time_series",
          "hide": false,
          "instant": false,
          "intervalFactor": 1,
          "legendFormat": "KO",
          "refId": "B",
          "step": 2
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "Total",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {
        "requests": "#64B0C8"
      },
      "bars": true,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 5,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 24,
        "x": 0,
        "y": 64
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 5,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "sort": "max",
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": false,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum(rate(Ratio_total[$interval])) by (label)",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "{{label}}",
          "refId": "A",
          "step": 2
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "by label",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {
        "HTTP 404": "#BF1B00"
      },
      "bars": true,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 5,
      "fillGradient": 0,
      "gridPos": {
        "h": 10,
        "w": 24,
        "x": 0,
        "y": 74
      },
      "height": "400",
      "hiddenSeries": false,
      "id": 6,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": false,
        "max": true,
        "min": true,
        "rightSide": false,
        "show": true,
        "total": false,
        "values": true
      },
      "lines": false,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 2,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": false,
      "targets": [
        {
          "expr": "sum by (code) (rate(Ratio_total[$interval]))",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "HTTP {{code}}",
          "refId": "A",
          "step": 2
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "by HTTP code",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "collapsed": true,
      "datasource": null,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 84
      },
      "id": 20,
      "panels": [
        {
          "aliasColors": {
            "moviri.com/404 - HTTP 404": "#58140C"
          },
          "bars": true,
          "dashLength": 10,
          "dashes": false,
          "datasource": "Prometheus",
          "fill": 5,
          "fillGradient": 0,
          "gridPos": {
            "h": 10,
            "w": 24,
            "x": 0,
            "y": 86
          },
          "height": "400",
          "hiddenSeries": false,
          "id": 15,
          "legend": {
            "alignAsTable": true,
            "avg": true,
            "current": false,
            "max": true,
            "min": true,
            "show": true,
            "total": false,
            "values": true
          },
          "lines": false,
          "linewidth": 1,
          "links": [],
          "nullPointMode": "null",
          "options": {
            "dataLinks": []
          },
          "percentage": false,
          "pointradius": 5,
          "points": false,
          "renderer": "flot",
          "seriesOverrides": [],
          "spaceLength": 10,
          "stack": true,
          "steppedLine": false,
          "targets": [
            {
              "expr": "sum by(code) (rate(Ratio_failure[$interval]))",
              "format": "time_series",
              "hide": false,
              "interval": "",
              "intervalFactor": 1,
              "legendFormat": "HTTP {{code}}",
              "refId": "A",
              "step": 2
            }
          ],
          "thresholds": [],
          "timeFrom": null,
          "timeRegions": [],
          "timeShift": null,
          "title": "by HTTP code",
          "tooltip": {
            "shared": true,
            "sort": 0,
            "value_type": "individual"
          },
          "type": "graph",
          "xaxis": {
            "buckets": null,
            "mode": "time",
            "name": null,
            "show": true,
            "values": []
          },
          "yaxes": [
            {
              "format": "short",
              "label": null,
              "logBase": 1,
              "max": null,
              "min": null,
              "show": true
            },
            {
              "format": "short",
              "label": null,
              "logBase": 1,
              "max": null,
              "min": null,
              "show": true
            }
          ],
          "yaxis": {
            "align": false,
            "alignLevel": null
          }
        },
        {
          "aliasColors": {
            "moviri.com/servicesKO - servicesKO": "#58140C"
          },
          "bars": true,
          "dashLength": 10,
          "dashes": false,
          "datasource": "Prometheus",
          "fill": 5,
          "fillGradient": 0,
          "gridPos": {
            "h": 10,
            "w": 24,
            "x": 0,
            "y": 96
          },
          "height": "400",
          "hiddenSeries": false,
          "id": 16,
          "legend": {
            "alignAsTable": true,
            "avg": true,
            "current": false,
            "max": true,
            "min": true,
            "show": true,
            "sort": "max",
            "sortDesc": true,
            "total": false,
            "values": true
          },
          "lines": false,
          "linewidth": 1,
          "links": [],
          "nullPointMode": "null",
          "options": {
            "dataLinks": []
          },
          "percentage": false,
          "pointradius": 5,
          "points": false,
          "renderer": "flot",
          "seriesOverrides": [],
          "spaceLength": 10,
          "stack": true,
          "steppedLine": false,
          "targets": [
            {
              "expr": "sum by (label) (rate(Ratio_failure[$interval]))",
              "format": "time_series",
              "interval": "",
              "intervalFactor": 1,
              "legendFormat": "{{label}}",
              "refId": "B",
              "step": 2
            }
          ],
          "thresholds": [],
          "timeFrom": null,
          "timeRegions": [],
          "timeShift": null,
          "title": "by label",
          "tooltip": {
            "shared": true,
            "sort": 0,
            "value_type": "individual"
          },
          "type": "graph",
          "xaxis": {
            "buckets": null,
            "mode": "time",
            "name": null,
            "show": true,
            "values": []
          },
          "yaxes": [
            {
              "format": "short",
              "label": null,
              "logBase": 1,
              "max": null,
              "min": null,
              "show": true
            },
            {
              "format": "short",
              "label": null,
              "logBase": 1,
              "max": null,
              "min": null,
              "show": true
            }
          ],
          "yaxis": {
            "align": false,
            "alignLevel": null
          }
        }
      ],
      "repeat": null,
      "title": "Errors",
      "type": "row"
    },
    {
      "collapsed": false,
      "datasource": null,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 85
      },
      "id": 22,
      "panels": [],
      "title": "Test farm",
      "type": "row"
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 1,
      "fillGradient": 0,
      "gridPos": {
        "h": 8,
        "w": 24,
        "x": 0,
        "y": 86
      },
      "hiddenSeries": false,
      "id": 24,
      "legend": {
        "avg": false,
        "current": false,
        "max": false,
        "min": false,
        "show": true,
        "total": false,
        "values": false
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [
        {
          "alias": "jmeter - PS Scavenge v1",
          "yaxis": 1
        }
      ],
      "spaceLength": 10,
      "stack": false,
      "steppedLine": false,
      "targets": [
        {
          "expr": "rate(jvm_gc_collection_seconds_sum{job=~\"jmeter\"}[1m])",
          "format": "time_series",
          "hide": false,
          "intervalFactor": 1,
          "legendFormat": "{{instance}} - {{gc}} ",
          "refId": "A"
        },
        {
          "expr": "rate(jvm_gc_collection_seconds_sum{job=~\"jmeter\"}[$interval]) / ignoring(gc) group_left rate(process_cpu_seconds_total{job=~\"jmeter\"}[$interval])",
          "format": "time_series",
          "hide": true,
          "intervalFactor": 1,
          "legendFormat": "{{instance}} - {{gc}} v2",
          "refId": "B"
        },
        {
          "expr": "rate(jvm_gc_collection_seconds_sum{job=~\"jmeter\"}[1m]) /  rate(jvm_gc_collection_seconds_count{job=~\"jmeter\"}[1m])",
          "hide": true,
          "legendFormat": "{{instance}} - {{gc}} v3",
          "refId": "C"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "GC % time",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "decimals": null,
          "format": "percentunit",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": "0",
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 1,
      "fillGradient": 0,
      "gridPos": {
        "h": 8,
        "w": 24,
        "x": 0,
        "y": 94
      },
      "hiddenSeries": false,
      "id": 27,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": true,
        "max": false,
        "min": false,
        "rightSide": true,
        "show": true,
        "sort": "avg",
        "sortDesc": false,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": false,
      "targets": [
        {
          "expr": "(jvm_memory_pool_bytes_committed {job=~\"jmeter\"}) - (jvm_memory_pool_bytes_used{job=~\"jmeter\"}) ",
          "format": "time_series",
          "hide": false,
          "intervalFactor": 1,
          "legendFormat": "{{instance}} - {{pool}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "JVM heap  - free memory by pool",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "bytes",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": "0",
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 1,
      "fillGradient": 0,
      "gridPos": {
        "h": 8,
        "w": 24,
        "x": 0,
        "y": 102
      },
      "hiddenSeries": false,
      "id": 26,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": true,
        "max": false,
        "min": false,
        "rightSide": true,
        "show": true,
        "sort": "avg",
        "sortDesc": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": false,
      "targets": [
        {
          "expr": "(jvm_memory_pool_bytes_used{job=\"jmeter\"}) ",
          "format": "time_series",
          "hide": false,
          "intervalFactor": 1,
          "legendFormat": "{{instance}} - {{pool}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "JVM heap -  used memory by pool",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "bytes",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": "0",
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    },
    {
      "aliasColors": {},
      "bars": false,
      "dashLength": 10,
      "dashes": false,
      "datasource": "Prometheus",
      "fill": 1,
      "fillGradient": 0,
      "gridPos": {
        "h": 8,
        "w": 24,
        "x": 0,
        "y": 110
      },
      "hiddenSeries": false,
      "id": 25,
      "legend": {
        "alignAsTable": true,
        "avg": true,
        "current": true,
        "max": false,
        "min": false,
        "rightSide": true,
        "show": true,
        "total": false,
        "values": true
      },
      "lines": true,
      "linewidth": 1,
      "links": [],
      "nullPointMode": "null",
      "options": {
        "dataLinks": []
      },
      "percentage": false,
      "pointradius": 5,
      "points": false,
      "renderer": "flot",
      "seriesOverrides": [],
      "spaceLength": 10,
      "stack": true,
      "steppedLine": false,
      "targets": [
        {
          "expr": "(jvm_memory_bytes_max {job=\"jmeter\"}) - (jvm_memory_bytes_used{job=\"jmeter\"})",
          "format": "time_series",
          "hide": false,
          "intervalFactor": 1,
          "legendFormat": "{{instance}} - {{area}}",
          "refId": "A"
        }
      ],
      "thresholds": [],
      "timeFrom": null,
      "timeRegions": [],
      "timeShift": null,
      "title": "JVM free memory",
      "tooltip": {
        "shared": true,
        "sort": 0,
        "value_type": "individual"
      },
      "type": "graph",
      "xaxis": {
        "buckets": null,
        "mode": "time",
        "name": null,
        "show": true,
        "values": []
      },
      "yaxes": [
        {
          "format": "bytes",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": "0",
          "show": true
        },
        {
          "format": "short",
          "label": null,
          "logBase": 1,
          "max": null,
          "min": null,
          "show": true
        }
      ],
      "yaxis": {
        "align": false,
        "alignLevel": null
      }
    }
  ],
  "refresh": "30s",
  "schemaVersion": 22,
  "style": "dark",
  "tags": [
    "jmeter"
  ],
  "templating": {
    "list": [
      {
        "allValue": null,
        "current": {
          "selected": false,
          "text": "30s",
          "value": "30s"
        },
        "hide": 0,
        "includeAll": false,
        "label": "interval",
        "multi": false,
        "name": "interval",
        "options": [
          {
            "selected": true,
            "text": "30s",
            "value": "30s"
          },
          {
            "selected": false,
            "text": "1m",
            "value": "1m"
          },
          {
            "selected": false,
            "text": "5m",
            "value": "5m"
          },
          {
            "selected": false,
            "text": "10m",
            "value": "10m"
          }
        ],
        "query": "30s,1m,5m,10m",
        "skipUrlSync": false,
        "type": "custom"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {
    "refresh_intervals": [
      "5s",
      "10s",
      "30s",
      "1m",
      "5m",
      "15m",
      "30m",
      "1h",
      "2h",
      "1d"
    ],
    "time_options": [
      "5m",
      "15m",
      "1h",
      "6h",
      "12h",
      "24h",
      "2d",
      "7d",
      "30d"
    ]
  },
  "timezone": "browser",
  "title": "JMeter",
  "uid": "jbtLA0-Wk5",
  "version": 2
}

================================================
FILE: docs/examples/simple_prometheus_example.jmx
================================================
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="4.0" jmeter="4.0 r1823414">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Test Plan" enabled="true">
      <stringProp name="TestPlan.comments"></stringProp>
      <boolProp name="TestPlan.functional_mode">false</boolProp>
      <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
      <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
      <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
        <collectionProp name="Arguments.arguments"/>
      </elementProp>
      <stringProp name="TestPlan.user_define_classpath"></stringProp>
    </TestPlan>
    <hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="listener tg" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="can_fail_sampler" enabled="true">
          <stringProp name="TestPlan.comments">This sampler just does something to generate data for the prometheus listener.

Note that just by calling the function in the parameters section assigns the variable &apos;category&apos; which the PrometheusListener plugin will pick up on.</stringProp>
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters">${__RandomString(1,ABC,category)}</stringProp>
          <stringProp name="script">import java.util.Random;
import org.apache.commons.lang3.RandomStringUtils;

Random rand = new Random();
int maxWait = 3500;

// emulate different &apos;categories&apos; being slower than others
if(args[0].equals(&quot;A&quot;)) { 
	maxWait = 1000;	
} else if (args[0].equals(&quot;B&quot;)) {
	maxWait = 1750;	
} else if (args[0].equals(&quot;C&quot;)) {
	maxWait = 3000;	
}

int wait = rand.nextInt(maxWait);
int fail = rand.nextInt(10);

log.info(&quot;sleeping for {} ms&quot;, wait);

Thread.currentThread().sleep(wait);

if(fail &gt;= 8 ){
	SampleResult.setSuccessful(false);
	SampleResult.setResponseCode(&quot;404&quot;);
}else{
	SampleResult.setResponseCode(&quot;204&quot;);
}

SampleResult.setResponseData(new byte[wait]);
</stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
        </JSR223Sampler>
        <hashTree>
          <SizeAssertion guiclass="SizeAssertionGui" testclass="SizeAssertion" testname="less than 2kB" enabled="true">
            <stringProp name="Assertion.test_field">SizeAssertion.response_network_size</stringProp>
            <stringProp name="SizeAssertion.size">2000</stringProp>
            <intProp name="SizeAssertion.operator">4</intProp>
          </SizeAssertion>
          <hashTree/>
        </hashTree>
        <com.github.johrstrom.listener.PrometheusListener guiclass="com.github.johrstrom.listener.gui.PrometheusListenerGui" testclass="com.github.johrstrom.listener.PrometheusListener" testname="Main prometheus listener" enabled="true">
          <collectionProp name="prometheus.collector_definitions">
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the response time for a jsr223 sampler</stringProp>
              <stringProp name="collector.metric_name">jsr223_rt_as_hist</stringProp>
              <stringProp name="collector.type">HISTOGRAM</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="102727412">label</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets">100,500,1000,3000</stringProp>
              <stringProp name="listener.collector.listen_to">samples</stringProp>
              <stringProp name="listener.collector.measuring">ResponseTime</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the response time for a jsr223 sampler</stringProp>
              <stringProp name="collector.metric_name">jsr223_rt_as_summary</stringProp>
              <stringProp name="collector.type">SUMMARY</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="50511102">category</stringProp>
                <stringProp name="102727412">label</stringProp>
                <stringProp name="3059181">code</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets">0.75,0.5|0.95,0.1|0.99,0.01</stringProp>
              <stringProp name="listener.collector.measuring">ResponseTime</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the total number of samplers</stringProp>
              <stringProp name="collector.metric_name">jsr223_count_total</stringProp>
              <stringProp name="collector.type">COUNTER</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="102727412">label</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets"></stringProp>
              <stringProp name="listener.collector.measuring">CountTotal</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the total number of successful samplers</stringProp>
              <stringProp name="collector.metric_name">jsr223_success_total</stringProp>
              <stringProp name="collector.type">COUNTER</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="102727412">label</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets"></stringProp>
              <stringProp name="listener.collector.measuring">SuccessTotal</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the response size for a jsr223 sampler</stringProp>
              <stringProp name="collector.metric_name">jsr223_rsize_as_hist</stringProp>
              <stringProp name="collector.type">HISTOGRAM</stringProp>
              <collectionProp name="collector.labels"/>
              <stringProp name="collector.quantiles_or_buckets">100,500,1000,3000</stringProp>
              <stringProp name="listener.collector.measuring">ResponseSize</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">success ratio of the can_fail_sampler</stringProp>
              <stringProp name="collector.metric_name">jsr223_can_fail</stringProp>
              <stringProp name="collector.type">SUCCESS_RATIO</stringProp>
              <collectionProp name="collector.labels"/>
              <stringProp name="collector.quantiles_or_buckets"></stringProp>
              <stringProp name="listener.collector.measuring">SuccessRatio</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the latency (ttfb) for a jsr223 sampler</stringProp>
              <stringProp name="collector.metric_name">jsr223_latency_as_hist</stringProp>
              <stringProp name="collector.type">HISTOGRAM</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="102727412">label</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets">100,500,1000,3000</stringProp>
              <stringProp name="listener.collector.measuring">Latency</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">the idle time for a jsr223 sampler</stringProp>
              <stringProp name="collector.metric_name">jsr223_idle_time</stringProp>
              <stringProp name="collector.type">SUMMARY</stringProp>
              <collectionProp name="collector.labels"/>
              <stringProp name="collector.quantiles_or_buckets">0.75,0.5|0.95,0.1|0.99,0.01</stringProp>
              <stringProp name="listener.collector.measuring">IdleTime</stringProp>
            </elementProp>
            <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
              <stringProp name="collector.help">default help string</stringProp>
              <stringProp name="collector.metric_name">jsr223_assertions</stringProp>
              <stringProp name="collector.type">SUCCESS_RATIO</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="102727412">label</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets"></stringProp>
              <stringProp name="listener.collector.measuring">SuccessRatio</stringProp>
              <stringProp name="listener.collector.listen_to">assertions</stringProp>
            </elementProp>
          </collectionProp>
          <stringProp name="TestPlan.comments">This listener &quot;measures&quot; everything, sometimes in summaries, sometimes in histograms.</stringProp>
        </com.github.johrstrom.listener.PrometheusListener>
        <hashTree/>
      </hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="config tg" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <com.github.johrstrom.config.PrometheusMetricsConfig guiclass="com.github.johrstrom.config.gui.PrometheusMetricsConfigGui" testclass="com.github.johrstrom.config.PrometheusMetricsConfig" testname="PrometheusMetricsConfig" enabled="true">
          <collectionProp name="prometheus.collector_definitions">
            <elementProp name="" elementType="com.github.johrstrom.collector.BaseCollectorConfig">
              <stringProp name="collector.help">default help string</stringProp>
              <stringProp name="collector.metric_name">jsr223_animals_total</stringProp>
              <stringProp name="collector.type">COUNTER</stringProp>
              <collectionProp name="collector.labels">
                <stringProp name="94842723">color</stringProp>
                <stringProp name="3530753">size</stringProp>
                <stringProp name="-1081453217">mammal</stringProp>
              </collectionProp>
              <stringProp name="collector.quantiles_or_buckets"></stringProp>
            </elementProp>
          </collectionProp>
        </com.github.johrstrom.config.PrometheusMetricsConfig>
        <hashTree/>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="JSR223 Sampler" enabled="true">
          <stringProp name="scriptLanguage">groovy</stringProp>
          <stringProp name="parameters">${__RandomString(1,RGB,color)} ${__RandomString(1,SML,size)} ${__RandomString(1,YN,mammal)}</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="script">import io.prometheus.client.*;


String color = vars.get(&quot;color&quot;);
String size = vars.get(&quot;size&quot;);
String mammal = vars.get(&quot;mammal&quot;);

Counter counter = (Counter) vars.getObject(&quot;jsr223_animals_total&quot;);

counter.labels(color,size,mammal).inc();
</stringProp>
        </JSR223Sampler>
        <hashTree>
          <ConstantTimer guiclass="ConstantTimerGui" testclass="ConstantTimer" testname="Constant Timer" enabled="true">
            <stringProp name="ConstantTimer.delay">3000</stringProp>
          </ConstantTimer>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="skip tg" enabled="true">
        <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
        <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
          <boolProp name="LoopController.continue_forever">false</boolProp>
          <intProp name="LoopController.loops">-1</intProp>
        </elementProp>
        <stringProp name="ThreadGroup.num_threads">1</stringProp>
        <stringProp name="ThreadGroup.ramp_time">1</stringProp>
        <boolProp name="ThreadGroup.scheduler">false</boolProp>
        <stringProp name="ThreadGroup.duration"></stringProp>
        <stringProp name="ThreadGroup.delay"></stringProp>
      </ThreadGroup>
      <hashTree>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="first_random_sampler" enabled="true">
          <stringProp name="TestPlan.comments">This sampler just does something to generate data for the prometheus listener.

Note that just by calling the function in the parameters section assigns the variable &apos;category&apos; which the PrometheusListener plugin will pick up on.</stringProp>
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters">${__RandomString(1,ABC,category)}</stringProp>
          <stringProp name="script">import java.util.Random;

Random rand = new Random();
int wait = rand.nextInt(3500);

log.info(&quot;sleeping for {} ms&quot;, wait);

Thread.currentThread().sleep(wait);</stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
        </JSR223Sampler>
        <hashTree>
          <com.github.johrstrom.listener.PrometheusListener guiclass="com.github.johrstrom.listener.gui.PrometheusListenerGui" testclass="com.github.johrstrom.listener.PrometheusListener" testname="GetFirstSample" enabled="true">
            <collectionProp name="prometheus.collector_definitions">
              <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
                <stringProp name="collector.help">the response time for a jsr223 sampler</stringProp>
                <stringProp name="collector.metric_name">jsr223_rt_as_hist</stringProp>
                <stringProp name="collector.type">HISTOGRAM</stringProp>
                <collectionProp name="collector.labels">
                  <stringProp name="102727412">label</stringProp>
                </collectionProp>
                <stringProp name="collector.quantiles_or_buckets">100,500,1000,3000</stringProp>
                <stringProp name="listener.collector.listen_to">samples</stringProp>
                <stringProp name="listener.collector.measuring">ResponseTime</stringProp>
              </elementProp>
            </collectionProp>
          </com.github.johrstrom.listener.PrometheusListener>
          <hashTree/>
        </hashTree>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="want_to_skp" enabled="true">
          <stringProp name="TestPlan.comments">This sampler just does something to generate data for the prometheus listener.

Note that just by calling the function in the parameters section assigns the variable &apos;category&apos; which the PrometheusListener plugin will pick up on.</stringProp>
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters">${__RandomString(1,ABC,category)}</stringProp>
          <stringProp name="script">import java.util.Random;

Random rand = new Random();
int wait = rand.nextInt(3500);

log.info(&quot;sleeping for {} ms&quot;, wait);

Thread.currentThread().sleep(wait);</stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
        </JSR223Sampler>
        <hashTree/>
        <JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="second_random_sampler" enabled="true">
          <stringProp name="TestPlan.comments">This sampler just does something to generate data for the prometheus listener.

Note that just by calling the function in the parameters section assigns the variable &apos;category&apos; which the PrometheusListener plugin will pick up on.</stringProp>
          <stringProp name="cacheKey">true</stringProp>
          <stringProp name="filename"></stringProp>
          <stringProp name="parameters">${__RandomString(1,ABC,category)}</stringProp>
          <stringProp name="script">import java.util.Random;

Random rand = new Random();
int wait = rand.nextInt(3500);

log.info(&quot;sleeping for {} ms&quot;, wait);

Thread.currentThread().sleep(wait);</stringProp>
          <stringProp name="scriptLanguage">groovy</stringProp>
        </JSR223Sampler>
        <hashTree>
          <com.github.johrstrom.listener.PrometheusListener guiclass="com.github.johrstrom.listener.gui.PrometheusListenerGui" testclass="com.github.johrstrom.listener.PrometheusListener" testname="GetSampleSample" enabled="true">
            <collectionProp name="prometheus.collector_definitions">
              <elementProp name="" elementType="com.github.johrstrom.listener.ListenerCollectorConfig">
                <stringProp name="collector.help">the response time for a jsr223 sampler</stringProp>
                <stringProp name="collector.metric_name">jsr223_rt_as_hist</stringProp>
                <stringProp name="collector.type">HISTOGRAM</stringProp>
                <collectionProp name="collector.labels">
                  <stringProp name="102727412">label</stringProp>
                </collectionProp>
                <stringProp name="collector.quantiles_or_buckets">100,500,1000,3000</stringProp>
                <stringProp name="listener.collector.listen_to">samples</stringProp>
                <stringProp name="listener.collector.measuring">ResponseTime</stringProp>
              </elementProp>
            </collectionProp>
          </com.github.johrstrom.listener.PrometheusListener>
          <hashTree/>
        </hashTree>
      </hashTree>
      <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
        <boolProp name="ResultCollector.error_logging">false</boolProp>
        <objProp>
          <name>saveConfig</name>
          <value class="SampleSaveConfiguration">
            <time>true</time>
            <latency>true</latency>
            <timestamp>true</timestamp>
            <success>true</success>
            <label>true</label>
            <code>true</code>
            <message>true</message>
            <threadName>true</threadName>
            <dataType>true</dataType>
            <encoding>false</encoding>
            <assertions>true</assertions>
            <subresults>true</subresults>
            <responseData>false</responseData>
            <samplerData>false</samplerData>
            <xml>false</xml>
            <fieldNames>true</fieldNames>
            <responseHeaders>false</responseHeaders>
            <requestHeaders>false</requestHeaders>
            <responseDataOnError>false</responseDataOnError>
            <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
            <assertionsResultsToSave>0</assertionsResultsToSave>
            <bytes>true</bytes>
            <sentBytes>true</sentBytes>
            <threadCounts>true</threadCounts>
            <idleTime>true</idleTime>
            <connectTime>true</connectTime>
          </value>
        </objProp>
        <stringProp name="filename"></stringProp>
      </ResultCollector>
      <hashTree/>
    </hashTree>
  </hashTree>
</jmeterTestPlan>


================================================
FILE: licenses/io.prometheus.LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.



================================================
FILE: licenses/io.prometheus.NOTICE
================================================
Prometheus instrumentation library for JVM applications
Copyright 2012-2015 The Prometheus Authors

This product includes software developed at
Boxever Ltd. (http://www.boxever.com/).

This product includes software developed at
SoundCloud Ltd. (http://soundcloud.com/).

This product includes software developed as part of the
Ocelli project by Netflix Inc. (https://github.com/Netflix/ocelli/).


================================================
FILE: makefile
================================================
.DEFAULT_GOAL := help
SHELL := /bin/bash
THIS_FILE := $(lastword $(MAKEFILE_LIST))
THIS_FOLDER := $(shell basename $(CURDIR))

#Service version, tag and image name
master_version := $$(git show master:pom.xml | xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" -)
this_version := $$(git show master:pom.xml | xmllint --xpath "//*[local-name()='project']/*[local-name()='version']/text()" -)
this_branch := $(shell git rev-parse --abbrev-ref HEAD)
repo_location := $(strip $(shell  git rev-parse --show-toplevel))
clean ?= false

ifeq ($(strip $(clean)),true)
CLEAN := clean
endif

.PHONY: help
help:					## Show this help
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
	
.PHONY: clean
clean:              ## Cleans the repo
	@mvn clean

.PHONY: build
build:              ## Builds jar
	@mvn install -Dgpg.skip

.PHONY: test
test:              ## run tests
	@mvn verify -Dgpg.skip


.PHONY: info
info:    					## Print some info on the repo
	@echo "this_version: $(this_version)" && \
	echo "this_branch: $(this_branch)" && \
	echo "repo_location: $(repo_location)" &&\
	echo "master_version: $(master_version)"
	

================================================
FILE: pom.xml
================================================
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.github.johrstrom</groupId>
	<artifactId>jmeter-prometheus-plugin</artifactId>
	<version>0.7.2-SNAPSHOT</version>
	<name>Jmeter-Prometheus Listener Plugin</name>
	<description>A Jmeter plugin that creates a Prometheus endpoint of results.</description>
	<url>https://github.com/johrstrom/jmeter-prometheus-plugin</url>

	<properties>
		<prometheus.version>0.16.0</prometheus.version>
		<jmeter.version>5.5</jmeter.version>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
	</properties>

	<issueManagement>
		<url>https://github.com/johrstrom/jmeter-prometheus-plugin/issues</url>
		<system>GitHub Issues</system>
	</issueManagement>

	<licenses>
		<license>
			<name>Apache License, Version 2.0</name>
			<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
			<distribution>repo</distribution>
			<comments>A business-friendly OSS license</comments>
		</license>
	</licenses>

	<scm>
		<url>https://github.com/johrstrom/jmeter-prometheus-plugin</url>
		<connection>scm:git:git://github.com/johrstrom/jmeter-prometheus-plugin.git</connection>
		<developerConnection>scm:git:git@github.com:johrstrom/jmeter-prometheus-plugin.git</developerConnection>
	  <tag>HEAD</tag>
  	</scm>

	<distributionManagement>
		<snapshotRepository>
			<id>ossrh</id>
			<url>https://oss.sonatype.org/content/repositories/snapshots</url>
		</snapshotRepository>

		<repository>
			<id>ossrh</id>
			<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
		</repository>
	</distributionManagement>

	<developers>
		<developer>
			<email>johrstrom@hotmail.com</email>
			<name>Jeff Ohrstrom</name>
			<url>https://github.com/johrstrom</url>
			<id>kevinsawicki</id>
		</developer>
		<developer>
			<email>giovanni.gibilisco@akamas.io</email>
			<name>Giovanni Paolo Gibilisco</name>
			<url>https://github.com/GiovanniPaoloGibilisco</url>
			<id>GiovanniPaoloGibilisco</id>
		</developer>
	</developers>

	<dependencies>
		<dependency>
			<groupId>org.apache.jmeter</groupId>
			<artifactId>ApacheJMeter_core</artifactId>
			<version>${jmeter.version}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.apache.jmeter</groupId>
			<artifactId>ApacheJMeter_java</artifactId>
			<version>${jmeter.version}</version>
			<scope>provided</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.apache.jmeter/ApacheJMeter_components -->
		<dependency>
			<groupId>org.apache.jmeter</groupId>
			<artifactId>ApacheJMeter_components</artifactId>
			<version>${jmeter.version}</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>io.prometheus</groupId>
			<artifactId>simpleclient</artifactId>
			<version>${prometheus.version}</version>
		</dependency>
		<dependency>
			<groupId>io.prometheus</groupId>
			<artifactId>simpleclient_servlet</artifactId>
			<version>${prometheus.version}</version>
		</dependency>
		<dependency>
			<groupId>io.prometheus</groupId>
			<artifactId>simpleclient_httpserver</artifactId>
			<version>${prometheus.version}</version>
		</dependency>
		<dependency>
			<groupId>io.prometheus</groupId>
			<artifactId>simpleclient_hotspot</artifactId>
			<version>${prometheus.version}</version>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.13.1</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<resources>
			<resource>
				<directory>licenses</directory>
				<includes>
					<include>*</include>
				</includes>
			</resource>
			<resource>
				<directory>.</directory>
				<includes>
					<include>LICENSE</include>
					<include>NOTICE</include>
				</includes>
			</resource>
		</resources>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>3.1.0</version>
				<configuration>
					<parallel>methods</parallel>
					<threadCount>4</threadCount>
					<trimStackTrace>false</trimStackTrace>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-shade-plugin</artifactId>
				<version>3.0.0</version>
				<configuration>
					<artifactSet>
						<excludes>
							<exclude>org.apache.jmeter:*</exclude>
						</excludes>
					</artifactSet>
					<!-- put your configurations here -->
					<filters>
						<filter>
							<artifact>*:*</artifact>
							<excludes>
								<exclude>META-INF/*.SF</exclude>
								<exclude>META-INF/*.DSA</exclude>
								<exclude>META-INF/*.RSA</exclude>
							</excludes>
						</filter>
					</filters>
				</configuration>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>shade</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.0.1</version>
				<executions>
					<execution>
						<id>copy-resources</id>
						<!-- here the phase you need -->
						<phase>process-test-resources</phase>
						<goals>
							<goal>copy-resources</goal>
						</goals>
						<configuration>
							<outputDirectory>${basedir}/target/test-classes</outputDirectory>
							<resources>
								<resource>
									<directory>docs/examples</directory>
									<filtering>true</filtering>
								</resource>
							</resources>
						</configuration>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-release-plugin</artifactId>
				<version>2.5.3</version>
				<configuration>
					<localCheckout>true</localCheckout>
					<pushChanges>false</pushChanges>
					<mavenExecutorId>forked-path</mavenExecutorId>
					<!-- <arguments>-Dgpg.passphrase=${gpg.passphrase}</arguments> -->
				</configuration>
				<dependencies>
					<dependency>
						<groupId>org.apache.maven.scm</groupId>
						<artifactId>maven-scm-provider-gitexe</artifactId>
						<version>1.9.5</version>
					</dependency>
				</dependencies>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-gpg-plugin</artifactId>
				<version>${maven-gpg-plugin.version}</version>
				<executions>
					<execution>
						<id>sign-artifacts</id>
						<phase>verify</phase>
						<goals>
							<goal>sign</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<artifactId>maven-deploy-plugin</artifactId>
				<version>2.8.2</version>
				<executions>
					<execution>
						<id>default-deploy</id>
						<phase>deploy</phase>
						<goals>
							<goal>deploy</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.sonatype.plugins</groupId>
				<artifactId>nexus-staging-maven-plugin</artifactId>
				<version>1.6.7</version>
				<extensions>true</extensions>
				<configuration>
					<serverId>ossrh</serverId>
					<nexusUrl>https://oss.sonatype.org/</nexusUrl>
					<autoReleaseAfterClose>true</autoReleaseAfterClose>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-source-plugin</artifactId>
				<version>3.0.1</version>
				<executions>
					<execution>
						<id>attach-sources</id>
						<goals>
							<goal>jar</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			
		</plugins>
	</build>
</project>


================================================
FILE: src/main/java/com/github/johrstrom/collector/BaseCollectorConfig.java
================================================
package com.github.johrstrom.collector;

import io.prometheus.client.*;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.testelement.property.NullProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * The base class for turning text/strings (from the JMeter GUI, or a .jmx file, etc.) into Prometheus Collector
 * objects.  Along with being a TestElement so that it can be serialized (for the .jmx file) it also handles all
 * the logic of parsing strings into double arrays.
 *
 * There are also static utility functions to actually instantiate a Collector of a given type from a configuration.
 *
 *
 * buckets are comma seperated list of decimals. An example:
 * 		100,200,300,400.3
 * quantiles are comma seperated pair of decimals seperated by |. The first decimal being the quantile and the second being the
 * error rating. An example:
 * 		0.999,0.1|0.99,0.2|0.75,0.3
 *
 * @author Jeff Ohrstrom
 *
 */
public class BaseCollectorConfig extends AbstractTestElement  {

	private static final long serialVersionUID = 1520731432941268549L;

	public static String HELP = "collector.help";
	public static String NAME = "collector.metric_name";
	public static String TYPE = "collector.type";
	public static String LABELS = "collector.labels";
	public static String QUANTILES_OR_BUCKETS = "collector.quantiles_or_buckets";

	public static double[] DEFAULT_BUCKET_SIZES = {100,500,1000,3000};
	public static String DEFAULT_BUCKET_SIZES_STRING = "100,500,1000,3000";
	public static long DEFAULT_QUANTILE_WINDOW_LENGHT = 60;

	public static QuantileDefinition[] DEFAULT_QUANTILES = QuantileDefinition.defaultQuantiles();
	public static String DEFAULT_QUANTILES_STRING = QuantileDefinition.arrayToString(DEFAULT_QUANTILES);


	public static String DEFAULT_HELP_STRING = "default help string";
	public static String METRIC_NAME_BASE = "jmeter_autogenerated_metric_";

	private static Logger log = LoggerFactory.getLogger(BaseCollectorConfig.class);

	public enum JMeterCollectorType {
		COUNTER,
		GAUGE,
		HISTOGRAM,
		SUMMARY,
		SUCCESS_RATIO
	}

	public BaseCollectorConfig(){
		this.setHelp(DEFAULT_HELP_STRING);
		this.setMetricName(this.getRandomMetricName());
		this.setType(JMeterCollectorType.COUNTER.toString());
		this.setLabels(new String[0]);
		this.setQuantileOrBucket("");
	}

	public String getHelp() {
		return this.getPropertyAsString(HELP, DEFAULT_HELP_STRING);
	}

	public void setHelp(String help) {
		if(help == null || help.isEmpty())
			this.setProperty(HELP, DEFAULT_HELP_STRING);
		else
			this.setProperty(HELP, help);
	}

	public String getType() {
		return this.getPropertyAsString(TYPE, JMeterCollectorType.COUNTER.name());
	}

	public JMeterCollectorType getCollectorType() {
		return JMeterCollectorType.valueOf(this.getType());
	}

	public void setType(String type) {
		this.setProperty(TYPE, type);

		if(type != null && type.equals(JMeterCollectorType.HISTOGRAM.name()) && this.getQuantileOrBucket().isEmpty())
			this.setQuantileOrBucket(DEFAULT_BUCKET_SIZES_STRING);
		if(type != null && type.equals(JMeterCollectorType.SUMMARY.name()) && this.getQuantileOrBucket().isEmpty())
			this.setQuantileOrBucket(DEFAULT_QUANTILES_STRING);
	}

	public String getQuantileOrBucket() {
		return this.getPropertyAsString(QUANTILES_OR_BUCKETS,"");
	}

	public void setQuantileOrBucket(String quantileOrBucket) {
		this.setProperty(QUANTILES_OR_BUCKETS, quantileOrBucket);
	}

	public double[] getBuckets() {
		String buckets = getQuantileOrBucket();

		if(buckets == null || buckets.isEmpty()) {
			return DEFAULT_BUCKET_SIZES;
		}else {
			return this.parseBucketsFromString(buckets);
		}
	}

	public QuantileDefinition[] getQuantiles() {
		String quantiles = getQuantileOrBucket();

		if(quantiles == null || quantiles.isEmpty()) {
			return DEFAULT_QUANTILES;
		}else {
			return QuantileDefinition.parseQuantilesFromString(quantiles);
		}
	}

	public long getQuantileWindowLength() {
		String quantiles = getQuantileOrBucket();

		if (quantiles == null || quantiles.isEmpty()) {
			return DEFAULT_QUANTILE_WINDOW_LENGHT;
		} else {
			return QuantileDefinition.parseQuantilesWindowLengthFromString(quantiles);
		}
	}

	public String getMetricName() {
		return this.getPropertyAsString(NAME, getRandomMetricName());
	}

	public void setMetricName(String name) {
		if(name == null || name.isEmpty())
			this.setProperty(NAME, getRandomMetricName());
		else
			this.setProperty(NAME, name);
	}

	public String getRandomMetricName() {
		return METRIC_NAME_BASE + RandomStringUtils.randomAlphanumeric(8);
	}

	public void setLabels(String labels) {
		this.setLabels(labels.split(","));
	}

	public void setLabels(String[] labels) {
		List<String> list = new ArrayList<String>(Arrays.asList(labels));

		Iterator<String> it = list.iterator();
		while(it.hasNext()) { // can't have empty strings from Gui
			String item = it.next();
			if(item == null || item.isEmpty()) {
				it.remove();
			}
		}

		this.setProperty(new CollectionProperty(LABELS, list));
	}

	public String[] getLabels() {
		JMeterProperty prop = this.getProperty(LABELS);
		if(prop == null || prop instanceof NullProperty) {
			return new String[0];
		}

		CollectionProperty colletion = (CollectionProperty) prop;
		String[] retArray = new String[colletion.size()];
		PropertyIterator it = colletion.iterator();

		int i=0;
		while(it.hasNext()) {
			String next = it.next().getStringValue();
			retArray[i] = next;
			i++;
		}

		return retArray;
	}

	public String getLabelsAsString() {
		StringBuilder sb = new StringBuilder();

		String[] labels = this.getLabels();
		for(int i = 0; i < labels.length; i++) {
			sb.append(labels[i]);
			if(i+1 < labels.length)
				sb.append(",");
		}

		return sb.toString();
	}

	public static Counter newCounter(BaseCollectorConfig cfg) throws Exception {
		io.prometheus.client.Counter.Builder builder = new Counter.Builder()
			.help(cfg.getHelp())
			.name(cfg.getMetricName());

		String[] labels = cfg.getLabels();
		if(labels.length != 0) {
			builder.labelNames(labels);
		}

		return builder.create();
	}

	public static Summary newSummary(BaseCollectorConfig cfg) throws Exception {
		io.prometheus.client.Summary.Builder builder = new Summary.Builder()
				.name(cfg.getMetricName())
				.help(cfg.getHelp())
				.maxAgeSeconds(cfg.getQuantileWindowLength());

		String[] labels = cfg.getLabels();
		if(labels.length != 0) {
			builder.labelNames(labels);
		}

		for(QuantileDefinition def : cfg.getQuantiles()) {
			builder.quantile(def.quantile, def.error);
		}

		return builder.create();
	}

	public static Histogram newHistogram(BaseCollectorConfig cfg) throws Exception {
		io.prometheus.client.Histogram.Builder builder = new Histogram.Builder()
				.name(cfg.getMetricName())
				.help(cfg.getHelp())
				.buckets(cfg.getBuckets());

		String[] labels = cfg.getLabels();
		if(labels.length != 0) {
			builder.labelNames(labels);
		}

		return builder.create();
	}

	public static Gauge newGauge(BaseCollectorConfig cfg) throws Exception {
		io.prometheus.client.Gauge.Builder builder =  new Gauge.Builder()
				.name(cfg.getMetricName())
				.help(cfg.getHelp());

		String[] labels = cfg.getLabels();
		if(labels.length != 0) {
			builder.labelNames(labels);
		}

		return builder.create();
	}

	public static Collector fromConfig(BaseCollectorConfig cfg) {
		JMeterCollectorType t = cfg.getCollectorType();
		Collector c = null;

		try {
			if(t.equals(JMeterCollectorType.COUNTER)) {
				c = BaseCollectorConfig.newCounter(cfg);

			}else if(t.equals(JMeterCollectorType.SUMMARY)) {
				c = BaseCollectorConfig.newSummary(cfg);

			}else if(t.equals(JMeterCollectorType.HISTOGRAM)) {
				c = BaseCollectorConfig.newHistogram(cfg);
			}else if(t.equals(JMeterCollectorType.GAUGE)) {
				c = BaseCollectorConfig.newGauge(cfg);
			}else if(t.equals(JMeterCollectorType.SUCCESS_RATIO)) {
				c = new SuccessRatioCollector(cfg);
			}

		} catch(Exception e) {
			log.error(String.format("Didn't create collector from definition %s because of an error", cfg), e);
		}

		return c;
	}





	@Override
	public boolean equals(Object o) {
		if(o instanceof BaseCollectorConfig) {
			BaseCollectorConfig other = (BaseCollectorConfig) o;
			boolean sameName = this.getName().equals(other.getName());
			boolean sameHelp = this.getHelp().equals(other.getHelp());
			boolean sameType = this.getType().equals(other.getType());
			boolean sameQB = this.getQuantileOrBucket().equals(other.getQuantileOrBucket());
			boolean sameLabels = this.getLabelsAsString().equalsIgnoreCase(other.getLabelsAsString());

			return sameName && sameHelp && sameType && sameQB & sameLabels;
		}

		return false;
	}

	@Override
	public int hashCode() {

	    final int prime = 31;
	    int result = 1;
	    result = prime * result + this.getMetricName().hashCode();
	    result = prime * result + this.getHelp().hashCode();
	    result = prime * result + this.getType().hashCode();
	    result = prime * result + this.getQuantileOrBucket().hashCode();
	    result = prime * result + this.getLabelsAsString().hashCode();

	    return result;
	}

	@Override
	public String toString() {
		PropertyIterator it = this.propertyIterator();
		StringBuilder sb = new StringBuilder();
		sb.append("[");
		while(it.hasNext()) {
			JMeterProperty prop = it.next();
			sb.append(prop.getName()).append(": ").append(prop.getStringValue()).append(", ");
		}

		sb.append("]");
		return sb.toString();
	}

	protected double[] parseBucketsFromString(String fullBucketString) {
		String[] bucketStrings = fullBucketString.split(",");
		List<Double> buckets = new ArrayList<Double>();

		for(String bucket : bucketStrings) {
			try {
				double d = Double.parseDouble(bucket);
				buckets.add(d);
			}catch(Exception e) {
				log.warn("couldn't parse {} because of error {}:{}. It wont be included in buckets for the metric {}",
						bucket, e.getClass().toString(), e.getMessage(), this.getMetricName());
			}
		}

		if(buckets.isEmpty()) {
			log.warn("Did not parse any buckets for metric {}. Returning defaults", this.getMetricName());
			return DEFAULT_BUCKET_SIZES;
		}else {
			return buckets.stream().mapToDouble(Double::doubleValue).toArray();
		}
	}


	/**
	 * A very simple POJO for holding Quantiles and the error rating for them.
	 *
	 * @author Jeff ohrstrom
	 *
	 */
	public static class QuantileDefinition {
		public double quantile;
		public double error;
		public static final String QUANTILE_ERROR_SEPERATOR = ",";
		public static final String QUANTILE_DEFINITION_SEPERATOR = "|";
		public static final String QUANTILE_DEFINITION_SEPERATOR_REGEX = "\\|";
		public static final String QUANTILE_LENGTH_SEPERATOR_REGEX = "\\;";

		QuantileDefinition(double quantile, double error) {
			this.quantile = quantile;
			this.error = error;
		}

		QuantileDefinition(String quantile, String error) throws NumberFormatException {
			this(new String[]{quantile, error});
		}

		QuantileDefinition(String[] definition) throws NumberFormatException {
			if(definition.length != 2) {
				throw new IllegalArgumentException(String.format("Quantiles need exactly 2 parameters. %d given.", definition.length));
			}
			this.quantile = Double.parseDouble(definition[0]);
			this.error = Double.parseDouble(definition[1]);
		}

		@Override
		public String toString() {
			return new StringBuilder()
					.append(this.quantile)
					.append(QUANTILE_ERROR_SEPERATOR)
					.append(this.error)
					.toString();
		}

		public static QuantileDefinition[] defaultQuantiles() {
			QuantileDefinition[] def = new QuantileDefinition[3];

			def[0] = new QuantileDefinition(0.75,0.5);
			def[1] = new QuantileDefinition(0.95,0.1);
			def[2] = new QuantileDefinition(0.99,0.01);

			return def;
		}

		public static String arrayToString(QuantileDefinition[] definitions) {
			StringBuilder sb = new StringBuilder();

			for(int i = 0; i < definitions.length; i++) {
				sb.append(definitions[i]);
				if(i+1 < definitions.length)
					sb.append(QUANTILE_DEFINITION_SEPERATOR);
			}

			return sb.toString();
		}

		public static QuantileDefinition[] parseQuantilesFromString(String fullQuantileString) {
			String quantileOnlyString = fullQuantileString.split(QUANTILE_LENGTH_SEPERATOR_REGEX)[0];
			String[] quantileDefStrings = quantileOnlyString.split(QUANTILE_DEFINITION_SEPERATOR_REGEX);
			List<QuantileDefinition> quantiles = new ArrayList<QuantileDefinition>();

			for(String quantile : quantileDefStrings) {
				try {
					QuantileDefinition q = new QuantileDefinition(quantile.split(QUANTILE_ERROR_SEPERATOR));
					quantiles.add(q);
				}catch(Exception e) {
					log.warn("couldn't parse {} because of error {}:{}. It wont be included in quantiles for the metric",
							quantile, e.getClass().toString(), e.getMessage());
				}
			}

			if(quantiles.isEmpty()) {
				log.warn("Did not parse any quantiles, returning defaults.");
				return DEFAULT_QUANTILES;
			}else {
				return quantiles.toArray(new QuantileDefinition[quantiles.size()]);
			}
		}


		public static long parseQuantilesWindowLengthFromString(String fullQuantileString) {
			String[] quantileDefStrings = fullQuantileString.split(QUANTILE_LENGTH_SEPERATOR_REGEX);
			if (quantileDefStrings.length < 2) {
				log.debug("Using default quantile window lenght of " + DEFAULT_QUANTILE_WINDOW_LENGHT + " seconds");
				return DEFAULT_QUANTILE_WINDOW_LENGHT;
			}
			return Long.parseLong(quantileDefStrings[1]);
		}


	}


}




================================================
FILE: src/main/java/com/github/johrstrom/collector/CollectorElement.java
================================================
package com.github.johrstrom.collector;

import java.util.*;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.jmeter.testelement.AbstractTestElement;
import org.apache.jmeter.testelement.property.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.prometheus.client.Collector;

public abstract class CollectorElement<C extends BaseCollectorConfig> extends AbstractTestElement {

	public static final String COLLECTOR_DEF = "prometheus.collector_definitions";

	protected Map<C, Collector> collectors = new HashMap<C, Collector>();
	protected transient JMeterCollectorRegistry registry = JMeterCollectorRegistry.getInstance();

	private static Logger log = LoggerFactory.getLogger(CollectorElement.class);
	private static final long serialVersionUID = 963612021269632269L;

	public CollectorElement() {
		log.debug("making a new config element: " + this.toString());
		this.setCollectorConfigs(new ArrayList<C>());
	}

	public CollectionProperty getCollectorConfigs() {
		JMeterProperty collectorDefinitions = this.getProperty(COLLECTOR_DEF);

		if (collectorDefinitions == null || collectorDefinitions instanceof NullProperty) {
			collectorDefinitions = new CollectionProperty(COLLECTOR_DEF, new ArrayList<C>());
			collectorDefinitions.setName(COLLECTOR_DEF);
		}

		return (CollectionProperty) collectorDefinitions;

	}

	public void setCollectorConfigs(List<C> collectors) {
		log.debug("setting new collectors. size is: " + collectors.size());
		this.setCollectorConfigs(new CollectionProperty(COLLECTOR_DEF, collectors));
	}

	public void setCollectorConfigs(CollectionProperty collectors) {
		this.setProperty(collectors);
	}

	protected void clearCollectors() {
		Iterator<Entry<C, Collector>> iter = this.collectors.entrySet().iterator();
		while (iter.hasNext()) {
			Entry<C, Collector> entry = iter.next();
			this.registry.unregister(entry.getKey());
			iter.remove();
		}
	}

	protected void makeNewCollectors() {
		this.clearCollectors();

		CollectionProperty collectorDefs = this.getCollectorConfigs();
		PropertyIterator iter = collectorDefs.iterator();

		while (iter.hasNext()) {

			try {
				@SuppressWarnings("unchecked")
				C config = (C) iter.next().getObjectValue();
				Collector collector = registry.getOrCreateAndRegister(config);

				this.collectors.put(config, collector);
				log.debug("added " + config.getMetricName() + " to list of collectors");
			} catch (Exception e) {
				log.error("Didn't create new collector because of error, ", e);
			}

		}

	}

}


================================================
FILE: src/main/java/com/github/johrstrom/collector/JMeterCollectorRegistry.java
================================================
package com.github.johrstrom.collector;

import io.prometheus.client.Collector;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Gauge;
import io.prometheus.client.hotspot.*;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterContextService.ThreadCounts;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

public class JMeterCollectorRegistry extends CollectorRegistry {

	private static JMeterCollectorRegistry instance = null;
	private static Logger log = LoggerFactory.getLogger(JMeterCollectorRegistry.class);
	private ConcurrentHashMap<BaseCollectorConfig,Collector> registered = new ConcurrentHashMap<>();
	
	private static final boolean saveThreads = 
			JMeterUtils.getPropDefault(ThreadCollector.COLLECT_THREADS, ThreadCollector.COLLECT_THREADS_DEFAULT);
	
	public static final String COLLECT_JVM  = "prometheus.save.jvm";
	public static final boolean COLLECT_JVM_DEFAULT  = true;
	private static final boolean saveJVM = JMeterUtils.getPropDefault(COLLECT_JVM, COLLECT_JVM_DEFAULT);
	
			
	public synchronized static JMeterCollectorRegistry getInstance() {
		if (instance == null) {
			log.debug("Creating prometheus collector registry");
			instance = new JMeterCollectorRegistry();
		}
		return instance;
	}
	
	private JMeterCollectorRegistry() {
		super(true);
		this.initDefaultExports();
		this.createJMeterExports();
	}
	
	private void initDefaultExports() {
		if(saveJVM) {
		    new StandardExports().register(this);
		    new MemoryPoolsExports().register(this);
		    new MemoryAllocationExports().register(this);
		    new BufferPoolsExports().register(this);
		    new GarbageCollectorExports().register(this);
		    new ThreadExports().register(this);
		    new ClassLoadingExports().register(this);
		    new VersionInfoExports().register(this);	
		}
	}
	
	private void createJMeterExports() {
		if(saveThreads) {
			ThreadCollector tc = new ThreadCollector();
			this.register(tc);
			this.registered.put(ThreadCollector.getConfig(), tc);
		}
	}

	
	public synchronized void unregister(BaseCollectorConfig cfg) {
		log.debug("unregistering {}", cfg.getMetricName());
		if(registered.containsKey(cfg)) {
			Collector collector = registered.get(cfg);
			
			try {
				this.unregister(collector);
				this.registered.remove(cfg);
			} catch(Exception e) {
				log.error("can't unregister collector because error: ", e);
			}
		}
	}
	
	
	public synchronized Collector getOrCreateAndRegister(BaseCollectorConfig cfg) { 
		if(registered.containsKey(cfg)) {
			log.trace("{} found already registered.", cfg.getMetricName());
			return registered.get(cfg);
		}else {
			Collector c = BaseCollectorConfig.fromConfig(cfg);
			this.register(c);	//throws exception here if it fails to register
			
			this.registered.put(cfg, c);
			log.debug("created and registered {}", cfg);
			return c;
		}
	}
	
	@Override
	public synchronized void clear() {
		super.clear();
		this.registered.clear();
	}
	
	private static class ThreadCollector extends Collector {
		
		public static final String COLLECT_THREADS_NAME = "prometheus.save.threads.name";
		public static final String COLLECT_THREADS_NAME_DEFAULT = "jmeter_threads";
		
		public static final String COLLECT_THREADS = "prometheus.save.threads";
		public static final boolean COLLECT_THREADS_DEFAULT = true;
		
		
		private final Gauge innerCollector;
	 
		protected ThreadCollector() {
			BaseCollectorConfig cfg = getConfig();
			
			innerCollector = Gauge.build()
					.name(cfg.getMetricName())
					.labelNames(cfg.getLabels())
					.help(cfg.getHelp())
					.create();
		}
		
		protected static BaseCollectorConfig getConfig() {
			BaseCollectorConfig cfg = new BaseCollectorConfig();

			cfg.setHelp("Gauge for jmeter threads");
			cfg.setMetricName(threadMetricName());
			cfg.setLabels(new String[] {"state"});
			cfg.setType(Type.GAUGE.name());
			
			return cfg;
		}
		
		
		public static String threadMetricName() {
			return JMeterUtils.getPropDefault(COLLECT_THREADS_NAME, COLLECT_THREADS_NAME_DEFAULT);
		}

		@Override
		public List<MetricFamilySamples> collect() {	
			ThreadCounts tc = JMeterContextService.getThreadCounts();
			
			innerCollector.labels("active").set(tc.activeThreads);
			innerCollector.labels("finished").set(tc.finishedThreads);
			innerCollector.labels("started").set(tc.startedThreads);
			
			return innerCollector.collect();
		}

	}
	
}


================================================
FILE: src/main/java/com/github/johrstrom/collector/SuccessRatioCollector.java
================================================
package com.github.johrstrom.collector;

import io.prometheus.client.Collector;
import io.prometheus.client.Counter;

import java.util.ArrayList;
import java.util.List;

public class SuccessRatioCollector extends Collector {

	private final Counter success, failure, total;
	
	public SuccessRatioCollector(BaseCollectorConfig config) {
		this.success = new Counter.Builder()
				.help(config.getHelp())
				.name(extendedName(config.getMetricName(), "success"))
				.labelNames(config.getLabels())
				.create();
		
		this.failure = new Counter.Builder()
				.help(config.getHelp())
				.name(extendedName(config.getMetricName(), "failure"))
				.labelNames(config.getLabels())
				.create();
		
		this.total = new Counter.Builder()
				.help(config.getHelp())
				.name(extendedName(config.getMetricName(), "total"))
				.labelNames(config.getLabels())
				.create();
		
	}
	
	public void incrementSuccess(String[] labels) {
		this.success.labels(labels).inc();
		this.total.labels(labels).inc();
		
		// this ensures that we emit 0 when this set of labels has 
		// never failed. i.e, total = success when failure = 0. 
		if(this.getFailure(labels) < 1 ) {
			this.failure.labels(labels).inc(0);
		}
	}
	
	public void incrementFailure(String[] labels) {
		this.failure.labels(labels).inc();
		this.total.labels(labels).inc();
		
		// this ensures that we emit 0 when this set of labels has 
		// never succeeded. i.e, total = failures when success = 0.
		if(this.getSuccess(labels) < 1) {
			this.success.labels(labels).inc(0);
		}
	}
	
	public double getSuccess(String[] labels) {
		return this.success.labels(labels).get();
	}
	
	public double getFailure(String[] labels) {
		return this.failure.labels(labels).get();
	}
	
	public double getTotal(String[] labels) {
		return this.total.labels(labels).get();
	}

	@Override
	public List<MetricFamilySamples> collect() {
		ArrayList<MetricFamilySamples> metrics = new ArrayList<MetricFamilySamples>();

		metrics.addAll(this.success.collect());
		metrics.addAll(this.failure.collect());
		metrics.addAll(this.total.collect());
		
		return metrics;
	}
	
	private static String extendedName(String orig, String append) {
		StringBuilder sb = new StringBuilder(32);
		
		sb.append(orig);
		if(!orig.endsWith("_")) {
			sb.append("_");
		}
		sb.append(append);
		
		return sb.toString();
	}

}


================================================
FILE: src/main/java/com/github/johrstrom/collector/gui/AbstractCollectorTable.java
================================================
package com.github.johrstrom.collector.gui;

import com.github.johrstrom.collector.BaseCollectorConfig;
import com.github.johrstrom.collector.CollectorElement;
import org.apache.jmeter.gui.util.HorizontalPanel;
import org.apache.jmeter.gui.util.VerticalPanel;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;
import org.apache.jorphan.gui.ObjectTableModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public abstract class AbstractCollectorTable<C extends BaseCollectorConfig> 
	extends JPanel implements ActionListener {

	public static final String ADD = "Add";
	public static final String DELETE = "Delete";
	
	protected transient JTable table;
	protected transient ObjectTableModel model;
	protected JButton add,delete;
	
	
	private final Class<C> clazzType;
	private static final long serialVersionUID = 2027712606129940455L;
	private Logger log = LoggerFactory.getLogger(AbstractCollectorTable.class);
	
	
	
	/**
	 * @return
	 */
	public abstract Flatten getGuiHelper();
	
	
	/**
	 * 
	 */
	public abstract void modifyColumns();
	
	
	public AbstractCollectorTable(Class<C> collectorType) {
		clazzType = collectorType;
		this.init();
		this.modifyColumns();
	}
	
	public List<C> getRowsAsCollectors(){
		ArrayList<C> collectors = new ArrayList<>();
		
		@SuppressWarnings("unchecked")
		Iterator<C> iter = (Iterator<C>) this.model.iterator();
		
		while(iter.hasNext()) {
			C cfg = this.clazzType.cast(iter.next());
			collectors.add(cfg);
			log.debug("populated config: " + cfg.toString() + " from table.");
		}
		
		return collectors;
	}
	
	public void clearModelData() {
		this.model.clearData();
	}
	
//	public void modifyTestElement(CollectorElement<C> ele) {
//		
//		if(!(ele instanceof CollectorElement)) {
//			return;
//		}
//		
//		int rows = this.model.getRowCount();
//		ArrayList<C> collectors = new ArrayList<>();
//		
//		@SuppressWarnings("unchecked")
//		CollectorElement<C> config = (CollectorElement<C>) ele;
//
//		log.debug("modifying test element " + ele.toString() + ". row count in model is " + rows);
//		
//		@SuppressWarnings("unchecked")
//		Iterator<C> iter = (Iterator<C>) model.iterator();
//		
//		while(iter.hasNext()) {
//			C cfg = this.clazzType.cast(iter.next());
//			collectors.add(cfg);
//			log.debug("populated config: " + cfg.toString() + " from table.");
//		}
//		
//		config.setCollectorConfigs(collectors);
//		this.setCollector(config);
//	}
	
	
	public void populateTable(CollectorElement<C> config) {
		
		CollectionProperty collectors = config.getCollectorConfigs();
		log.debug("Configuring table with " + collectors.size() + " collectors.");
		
		this.model.clearData();
		
		PropertyIterator it = collectors.iterator();
		while(it.hasNext()) {
			BaseCollectorConfig cfg = (BaseCollectorConfig) it.next().getObjectValue();			 
			this.model.addRow(cfg);
			log.debug("added row into table: " + cfg.toString());
		}
		
	}
	
	
	/**
	 * Private helper function to initialize all the Swing components.
	 */
	protected void init() {
		this.setLayout(new BorderLayout(0, 5));
		
		VerticalPanel panel = new VerticalPanel();
		panel.add(makeTablePanel());
		panel.add(makeButtonPanel());
		
		this.add(panel, BorderLayout.CENTER);
	}
	
	
	protected Component makeTablePanel() {
		Flatten helper = this.getGuiHelper();
		
		this.model = new ObjectTableModel(
				helper.getHeaders(),
				this.clazzType,
				helper.getReadFunctors(),
				helper.getWriteFunctors(),
				helper.getEditorClasses()
		);
		
		this.table = new JTable(this.model);
		
		JScrollPane scrollPane = new JScrollPane(this.table);
		table.setFillsViewportHeight(true);
		
		return scrollPane;
	}

	protected JPanel makeButtonPanel() {
		
		add = new JButton(ADD); 
		add.setActionCommand(ADD);
		add.setEnabled(true);
		add.addActionListener(this);
		
		delete = new JButton(DELETE); 
		delete.setActionCommand(DELETE);
		delete.setEnabled(true);
		delete.addActionListener(this);
		
		HorizontalPanel panel = new HorizontalPanel();
		panel.add(add);
		panel.add(delete);
		
		return panel;
	}
	
	@Override
	public void actionPerformed(ActionEvent event) {
		switch (event.getActionCommand()) {
		case ADD:
			try {
				this.model.addRow(this.clazzType.getDeclaredConstructor().newInstance());
			} catch (Exception e) {
				log.error("Couldn't add to model. ", e);
			}
			break;
		case DELETE:
			deleteSelectedRows();
			break;
		default:
			break;
		}
	}
	
	protected void deleteSelectedRows() {
		int[] rows = table.getSelectedRows();
		
		for(int i = 0; i < rows.length; i++) {
			this.model.removeRow(rows[i]);
		}
	}
	
}


================================================
FILE: src/main/java/com/github/johrstrom/collector/gui/Flatten.java
================================================
package com.github.johrstrom.collector.gui;

import org.apache.jorphan.reflect.Functor;

public interface Flatten {
	
	public Functor[] getReadFunctors();
	public Functor[] getWriteFunctors();
	public String[] getHeaders();
	public Class<?>[] getEditorClasses();
}


================================================
FILE: src/main/java/com/github/johrstrom/config/PrometheusMetricsConfig.java
================================================
package com.github.johrstrom.config;

import com.github.johrstrom.collector.BaseCollectorConfig;
import com.github.johrstrom.collector.CollectorElement;
import io.prometheus.client.Collector;
import org.apache.jmeter.engine.util.NoThreadClone;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.apache.jmeter.threads.JMeterVariables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map.Entry;

public class PrometheusMetricsConfig extends CollectorElement<BaseCollectorConfig>
	implements NoThreadClone, TestStateListener {

	private static final long serialVersionUID = 7602510312126862226L;

	private Logger log = LoggerFactory.getLogger(PrometheusMetricsConfig.class);
	
	@Override
	public void testEnded() {
		this.setRunningVersion(false);
		JMeterVariables variables = getThreadContext().getVariables();
		
		for (Entry<BaseCollectorConfig, Collector> entry : this.collectors.entrySet()) {
			BaseCollectorConfig cfg = entry.getKey();
			variables.remove(cfg.getMetricName());
		}
	
		this.clearCollectors();	
	}

	@Override
	public void testEnded(String arg0) {
		this.testEnded();
	}

	@Override
	public void testStarted() {
		this.setRunningVersion(true);
		this.makeNewCollectors();
		JMeterVariables variables = getThreadContext().getVariables();

		
		log.debug("Test started, adding {} collectors to variables", this.collectors.size());
		
		for (Entry<BaseCollectorConfig, Collector> entry : this.collectors.entrySet()) {
			BaseCollectorConfig cfg = entry.getKey(); 
			variables.putObject(cfg.getMetricName(), entry.getValue());
			log.debug("Added ({},{}) to variables.", entry.getKey(), entry.getValue().toString());
		}
 	}

	@Override
	public void testStarted(String arg0) {
		this.testStarted();		
	}
	
	@Override
	public PrometheusMetricsConfig clone() {
		PrometheusMetricsConfig clone = new PrometheusMetricsConfig();
		clone.setCollectorConfigs(this.getCollectorConfigs());
				
		return clone;
	}
	
	@Override
	public boolean equals(Object o) {
		if (o instanceof PrometheusMetricsConfig) {
			PrometheusMetricsConfig other = (PrometheusMetricsConfig) o;
			
			CollectionProperty thisConfig = this.getCollectorConfigs();
			CollectionProperty otherConfig = other.getCollectorConfigs();
			boolean sameSize = thisConfig.size() == otherConfig.size();
			
			for (int i = 0; i < thisConfig.size(); i++) {
				JMeterProperty left = thisConfig.get(i);
				JMeterProperty right = otherConfig.get(i);
				
				if(!left.equals(right)) {
					return false;
				}
			}
			
			return true && sameSize;
		}
		
		return false;
	}

}


================================================
FILE: src/main/java/com/github/johrstrom/config/gui/ConfigCollectorTable.java
================================================
package com.github.johrstrom.config.gui;

import com.github.johrstrom.collector.BaseCollectorConfig;
import com.github.johrstrom.collector.BaseCollectorConfig.JMeterCollectorType;
import com.github.johrstrom.collector.gui.AbstractCollectorTable;
import com.github.johrstrom.collector.gui.Flatten;
import org.apache.jorphan.reflect.Functor;

import javax.swing.*;
import javax.swing.table.TableColumn;


public class ConfigCollectorTable extends AbstractCollectorTable<BaseCollectorConfig>  
	implements Flatten {

	public static JComboBox<String> typeComboBox; 
	
	public static int METRIC_NAME_INDEX = 0;
	public static int HELP_INDEX = 1;
	public static int LABEL_NAME_INDEX = 2;
	public static int TYPE_INDEX = 3;
	public static int QUANTILE_OR_BUCKET_INDEX = 4;
	public static int BASE_COLUMN_SIZE = 5;
	
	private static final long serialVersionUID = 8675797078488652676L;
	
	static {
		typeComboBox = new JComboBox<>();
		typeComboBox.addItem(JMeterCollectorType.COUNTER.toString());
		typeComboBox.addItem(JMeterCollectorType.SUMMARY.toString());
		typeComboBox.addItem(JMeterCollectorType.HISTOGRAM.toString());
		typeComboBox.addItem(JMeterCollectorType.GAUGE.toString());
		typeComboBox.addItem(JMeterCollectorType.SUCCESS_RATIO.toString());
	}
	
	public ConfigCollectorTable() {
		super(BaseCollectorConfig.class);
	}

	@Override
	public Flatten getGuiHelper() {
		return this;
	}

	@Override
	public void modifyColumns() {
		TableColumn column = this.table.getColumnModel().getColumn(TYPE_INDEX);
		column.setCellEditor(new DefaultCellEditor(typeComboBox));
	}
		
	@Override
	public Functor[] getReadFunctors() {
		Functor[] functors = new Functor[BASE_COLUMN_SIZE];
		
		functors[METRIC_NAME_INDEX] = new Functor("getMetricName");
		functors[HELP_INDEX] = new Functor("getHelp");
		functors[LABEL_NAME_INDEX] = new Functor("getLabelsAsString");
		functors[TYPE_INDEX] = new Functor("getType");
		functors[QUANTILE_OR_BUCKET_INDEX] = new Functor("getQuantileOrBucket");
			
		return functors;
	}

	@Override
	public Functor[] getWriteFunctors() {
		Functor[] functors = new Functor[BASE_COLUMN_SIZE];
		
		functors[METRIC_NAME_INDEX] = new Functor("setMetricName");
		functors[HELP_INDEX] = new Functor("setHelp");
		functors[LABEL_NAME_INDEX] = new Functor("setLabels");
		functors[TYPE_INDEX] = new Functor("setType");
		functors[QUANTILE_OR_BUCKET_INDEX] = new Functor("setQuantileOrBucket");
		
		return functors;
	}

	@Override
	public String[] getHeaders() {
		String[] headers = new String[BASE_COLUMN_SIZE];
		
		headers[METRIC_NAME_INDEX] = "Name";
		headers[HELP_INDEX] = "Help";
		headers[LABEL_NAME_INDEX] = "Labels";
		headers[TYPE_INDEX] = "Type";
		headers[QUANTILE_OR_BUCKET_INDEX] = "Buckets or Quantiles";
		
		return headers;
	}

	@Override
	public Class<?>[] getEditorClasses() {
		Class<?>[] clazzes = new Class<?>[BASE_COLUMN_SIZE];
		
		clazzes[METRIC_NAME_INDEX] = String.class;
		clazzes[HELP_INDEX] = String.class;
		clazzes[LABEL_NAME_INDEX] = String.class;
		clazzes[TYPE_INDEX] = ComboBoxEditor.class;
		clazzes[QUANTILE_OR_BUCKET_INDEX] = String.class;
		
		return clazzes;
	}

}


================================================
FILE: src/main/java/com/github/johrstrom/config/gui/PrometheusMetricsConfigGui.java
================================================
package com.github.johrstrom.config.gui;

import com.github.johrstrom.collector.BaseCollectorConfig;
import com.github.johrstrom.collector.CollectorElement;
import com.github.johrstrom.config.PrometheusMetricsConfig;
import org.apache.jmeter.config.gui.AbstractConfigGui;
import org.apache.jmeter.testelement.TestElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.util.List;


public class PrometheusMetricsConfigGui<C> extends AbstractConfigGui {
	
	private static final long serialVersionUID = 6741986237897976082L;
//	private PrometheusMetricsConfig config;
	private ConfigCollectorTable table = new ConfigCollectorTable();

	private Logger log = LoggerFactory.getLogger(PrometheusMetricsConfigGui.class);
	
	public PrometheusMetricsConfigGui(){
		super();
		log.debug("making a new config gui: {}", this.toString());
		init();
	}
	
	@Override
	public TestElement createTestElement() {
		PrometheusMetricsConfig cfg = new PrometheusMetricsConfig();
		
		cfg.setProperty(TestElement.GUI_CLASS, PrometheusMetricsConfigGui.class.getName());
		cfg.setProperty(TestElement.TEST_CLASS, PrometheusMetricsConfig.class.getName());
		this.modifyTestElement(cfg);
		
		return cfg;
	}

	@Override
	public String getLabelResource() {
		return getClass().getCanonicalName();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.AbstractJMeterGuiComponent#getStaticLabel()
	 */
	@Override
	public String getStaticLabel() {
		return "Prometheus Metrics";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.apache.jmeter.gui.AbstractJMeterGuiComponent#getName()
	 */
	@Override
	public String getName() {
		return super.getName() == null ? this.getStaticLabel() : super.getName();
	}

	@SuppressWarnings("unchecked")
	@Override
	public void modifyTestElement(TestElement ele) {
		
		if(!(ele instanceof CollectorElement)) {
			return;
		}
		
		CollectorElement<BaseCollectorConfig> config = (CollectorElement<BaseCollectorConfig>) ele;
		
		List<BaseCollectorConfig> collectors = this.table.getRowsAsCollectors();
		config.setCollectorConfigs(collectors);	
		

		config.setName(this.getName());
		config.setComment(this.getComment());
	}

	private void init() {
		this.setLayout(new BorderLayout(0, 5));
		
		this.add(makeTitlePanel(), BorderLayout.NORTH);
		this.add(this.table, BorderLayout.CENTER);
	}
	
	

	@SuppressWarnings("unchecked")
	@Override
	public void configure(TestElement ele) {
		super.configure(ele);
		
		if(ele instanceof CollectorElement<?>) {
			try {
				this.table.populateTable((CollectorElement<BaseCollectorConfig>) ele);
			} catch(Exception e) {
				log.error("didn't modify test element because {}:{}", e.getClass(), e.getMessage());
			}
		}
		
		this.setName(ele.getName());
		this.setComment(ele.getComment());
	}

	@Override
	public void clearGui() {
		super.clearGui();
		this.table.clearModelData();
	}
	
	@Override
	protected PrometheusMetricsConfigGui<C> clone() {
		return new PrometheusMetricsConfigGui<C>();
	}
	
	
	
}









================================================
FILE: src/main/java/com/github/johrstrom/listener/ListenerCollectorConfig.java
================================================
package com.github.johrstrom.listener;

import com.github.johrstrom.collector.BaseCollectorConfig;

public class ListenerCollectorConfig extends BaseCollectorConfig {

	private static final long serialVersionUID = -8968099072667146399L;

	public static final String LISTEN_TO = "listener.collector.listen_to";
	public static final String MEASURING = "listener.collector.measuring";
	public static final String SAMPLES = "samples";
	public static final String ASSERTIONS = "assertions";
	
	public ListenerCollectorConfig() {
		this(new BaseCollectorConfig());
	}
	
	public ListenerCollectorConfig(BaseCollectorConfig base) {
		this.setMetricName(base.getMetricName());
		this.setType(base.getType());
		this.setHelp(base.getHelp());
		this.setLabels(base.getLabels());
	}
	
	public enum Measurable {
		// for aggregated type updater
		ResponseTime,
		ResponseSize,
		Latency,
		IdleTime,
		ConnectTime,
		
		// for count type updater
		SuccessTotal,
		FailureTotal,
		CountTotal,
		SuccessRatio;
	}
	
	public void setListenTo(String listenTo) {
		if(listenTo.equalsIgnoreCase(SAMPLES)) {
			this.setProperty(LISTEN_TO, SAMPLES);
		}else if(listenTo.equalsIgnoreCase(ASSERTIONS)) {
			this.setProperty(LISTEN_TO, ASSERTIONS);
		} else {
			this.setProperty(LISTEN_TO, SAMPLES);
		}
	}
	
	public String getListenTo() {
		return this.getPropertyAsString(LISTEN_TO, SAMPLES);
	}
	
	public void setMeasuring(String measuring) {
		this.setProperty(MEASURING, measuring);
	}
	
	public String getMeasuring() {
		return this.getPropertyAsString(MEASURING, Measurable.ResponseTime.toString());
	}
	
	public Measurable getMeasuringAsEnum() {		
		return Measurable.valueOf(this.getMeasuring());
	}
	
	public boolean listenToSamples() {
		return this.getListenTo().equalsIgnoreCase(SAMPLES);
	}
	
	public boolean listenToAssertions() {
		return this.getListenTo().equalsIgnoreCase(ASSERTIONS);
	}
	
//	@Override
//	public boolean equals(Object o) {
//		if(o instanceof ListenerCollectorConfig) {
//			boolean base = super.equals(o);
//			ListenerCollectorConfig other = (ListenerCollectorConfig) o;
//			
//			boolean measuring = this.getMeasuring().equals(other.getMeasuring());
//			boolean listenTo = this.getListenTo().equals(other.getListenTo());
//			
//			return base && measuring && listenTo;
//		} 
//		
//		return false;
//	}
//
//	@Override
//	public int hashCode() {
//		
//	    final int prime = 31;
//	    int result = 1;
//	    result = prime * result + this.getName().hashCode();
//	    result = prime * result + this.getHelp().hashCode();
//	    result = prime * result + this.getType().hashCode();
//	    result = prime * result + this.getQuantileOrBucket().hashCode();
//	    result = prime * result + this.getLabelsAsString().hashCode();
//	    result = prime * result + this.getListenTo().hashCode();
//	    result = prime * result + this.getMeasuring().hashCode();
//	   
//	    return result;
//	}
	
	
	
}


================================================
FILE: src/main/java/com/github/johrstrom/listener/PrometheusListener.java
================================================
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed  under the  License is distributed on an "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.johrstrom.listener;

import com.github.johrstrom.collector.CollectorElement;
import com.github.johrstrom.collector.JMeterCollectorRegistry;
import com.github.johrstrom.listener.updater.AbstractUpdater;
import com.github.johrstrom.listener.updater.AggregatedTypeUpdater;
import com.github.johrstrom.listener.updater.CountTypeUpdater;
import io.prometheus.client.Collector;
import org.apache.jmeter.engine.util.NoThreadClone;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleListener;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.JMeterProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * The main test element listener class of this library. Jmeter updates this
 * class through the SampleListener interface and it in turn updates the
 * CollectorRegistry. This class is also a TestStateListener to control when it
 * starts up or shuts down the server that ultimately serves Prometheus the
 * results through an http api.
 *
 * @author Jeff Ohrstrom
 */
public class PrometheusListener extends CollectorElement<ListenerCollectorConfig>
		implements SampleListener, Serializable, TestStateListener, NoThreadClone {

	private static final long serialVersionUID = -4833646252357876746L;

	private static final Logger log = LoggerFactory.getLogger(PrometheusListener.class);

	private transient PrometheusServer server = PrometheusServer.getInstance();

	private List<AbstractUpdater> updaters;

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.samplers.SampleListener#sampleOccurred(org.apache.
	 * jmeter.samplers.SampleEvent)
	 */
	@Override
	public void sampleOccurred(SampleEvent event) {

		for (AbstractUpdater updater : this.updaters) {
			updater.update(event);
		}

	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.apache.jmeter.samplers.SampleListener#sampleStarted(org.apache.jmeter
	 * .samplers.SampleEvent)
	 */
	@Override
	public void sampleStarted(SampleEvent arg0) {
		// do nothing
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.apache.jmeter.samplers.SampleListener#sampleStopped(org.apache.jmeter
	 * .samplers.SampleEvent)
	 */
	@Override
	public void sampleStopped(SampleEvent arg0) {
		// do nothing
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.testelement.TestStateListener#testEnded()
	 */
	@Override
	public void testEnded() {
		try {
			this.server.stop();
		} catch (Exception e) {
			log.error("Couldn't stop http server", e);
		}

		this.clearCollectors();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.testelement.TestStateListener#testEnded(java.lang.
	 * String)
	 */
	@Override
	public void testEnded(String arg0) {
		this.testEnded();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.testelement.TestStateListener#testStarted()
	 */
	@Override
	public void testStarted() {
		// update the configuration
		this.makeNewCollectors();
		// this.registerAllCollectors();

		try {
			if (server == null) {
				log.warn("Prometheus server has not yet been initialized, doing it now");
				server = PrometheusServer.getInstance();
			}
			server.start();
		} catch (Exception e) {
			log.error("Couldn't start http server", e);
		}

	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.testelement.TestStateListener#testStarted(java.lang.
	 * String)
	 */
	@Override
	public void testStarted(String arg0) {
		this.testStarted();
	}

	@Override
	protected void makeNewCollectors() {
		// this.clearCollectors();
		if (this.registry == null) {
			log.warn("Collector registry has not yet been initialized, doing it now");
			registry = JMeterCollectorRegistry.getInstance();
		}
		this.updaters = new ArrayList<AbstractUpdater>();

		CollectionProperty collectorDefs = this.getCollectorConfigs();

		for (JMeterProperty collectorDef : collectorDefs) {

			try {
				ListenerCollectorConfig config = (ListenerCollectorConfig) collectorDef.getObjectValue();
				log.debug("Creating collector from configuration: " + config);
				Collector collector = this.registry.getOrCreateAndRegister(config);
				AbstractUpdater updater = null;

				switch (config.getMeasuringAsEnum()) {
				case CountTotal:
				case FailureTotal:
				case SuccessTotal:
				case SuccessRatio:
					updater = new CountTypeUpdater(config);
					break;
				case ResponseSize:
				case ResponseTime:
				case Latency:
				case IdleTime:
				case ConnectTime:
					updater = new AggregatedTypeUpdater(config);
					break;
				default:
					// hope our IDEs are telling us to use all possible enums!
					log.error(config.getMeasuringAsEnum() + " triggered default case, which means there's "
							+ "no functionality for this and is likely a bug");
					break;
				}

				this.collectors.put(config, collector);
				this.updaters.add(updater);
				log.debug("added " + config.getMetricName() + " to list of collectors");

			} catch (Exception e) {
				log.error("Didn't create new collector because of error, ", e);
			}

		}

	}

}


================================================
FILE: src/main/java/com/github/johrstrom/listener/PrometheusServer.java
================================================
package com.github.johrstrom.listener;

import com.github.johrstrom.collector.JMeterCollectorRegistry;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.exporter.common.TextFormat;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.GZIPOutputStream;
import java.util.concurrent.TimeUnit;

/**
 * Expose Prometheus metrics using a plain Java HttpServer.
 * <p>
 * Example Usage:
 * <pre>
 * {@code
 * HTTPServer server = new HTTPServer(1234);
 * }
 * </pre>
 * */
public class PrometheusServer {
	
	public static final String PROMETHEUS_PORT = "prometheus.port";
	public static final int PROMETHEUS_PORT_DEFAULT = 9270;
	
	public static final String PROMETHEUS_DELAY = "prometheus.delay";
	public static final int PROMETHEUS_DELAY_DEFAULT = 0;

	public static final String PROMETHEUS_IP = "prometheus.ip";
	public static final String PROMETHEUS_IP_DEFAULT = "127.0.0.1";

	private static final Logger log = LoggerFactory.getLogger(PrometheusServer.class);

	private static class LocalByteArray extends ThreadLocal<ByteArrayOutputStream> {
	    protected ByteArrayOutputStream initialValue() {
	        return new ByteArrayOutputStream(1 << 20);
	    }
	}

    static class HTTPMetricHandler implements HttpHandler {
        private CollectorRegistry registry;
        private final LocalByteArray response = new LocalByteArray();

        HTTPMetricHandler(CollectorRegistry registry) {
          this.registry = registry;
        }


        public void handle(HttpExchange t) throws IOException {
            String query = t.getRequestURI().getRawQuery();

            ByteArrayOutputStream response = this.response.get();
            response.reset();
            OutputStreamWriter osw = new OutputStreamWriter(response);
            TextFormat.write004(osw,
                    registry.filteredMetricFamilySamples(parseQuery(query)));
            osw.flush();
            osw.close();
            response.flush();
            response.close();

            t.getResponseHeaders().set("Content-Type",
                    TextFormat.CONTENT_TYPE_004);
            if (shouldUseCompression(t)) {
                t.getResponseHeaders().set("Content-Encoding", "gzip");
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
                final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
                response.writeTo(os);
                os.close();
            } else {
                t.getResponseHeaders().set("Content-Length",
                        String.valueOf(response.size()));
                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.size());
                response.writeTo(t.getResponseBody());
            }
            t.close();
        }

    }

    protected static boolean shouldUseCompression(HttpExchange exchange) {
        List<String> encodingHeaders = exchange.getRequestHeaders().get("Accept-Encoding");
        if (encodingHeaders == null) return false;

        for (String encodingHeader : encodingHeaders) {
            String[] encodings = encodingHeader.split(",");
            for (String encoding : encodings) {
                if (encoding.trim().toLowerCase().equals("gzip")) {
                    return true;
                }
            }
        }
        return false;
    }

    protected static Set<String> parseQuery(String query) throws IOException {
        Set<String> names = new HashSet<String>();
        if (query != null) {
            String[] pairs = query.split("&");
            for (String pair : pairs) {
                int idx = pair.indexOf("=");
                if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals("name[]")) {
                    names.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
                }
            }
        }
        return names;
    }

    private HttpServer server;
    private static PrometheusServer instance = null;
    private int port = JMeterUtils.getPropDefault(PROMETHEUS_PORT, PROMETHEUS_PORT_DEFAULT);
    private int delay = JMeterUtils.getPropDefault(PROMETHEUS_DELAY, PROMETHEUS_DELAY_DEFAULT);
    private String ip = JMeterUtils.getPropDefault(PROMETHEUS_IP, PROMETHEUS_IP_DEFAULT);

    protected static final HTTPMetricHandler metricHandler = new HTTPMetricHandler(JMeterCollectorRegistry.getInstance());

    public synchronized static PrometheusServer getInstance() {
    	if(instance == null) {
    		log.debug("Creating Prometheus Server");
    		instance = new PrometheusServer();
    	}
    	
    	return instance;
    }

    private PrometheusServer() { }
    
    public synchronized void start() throws IOException {
    	if(server != null){
    		server.stop(0);
            ((ExecutorService) this.server.getExecutor()).shutdown();
    	}

        server = HttpServer.create();
        InetSocketAddress addr = new InetSocketAddress(InetAddress.getByName(ip), port);
        
        server.bind(addr, 3);

        server.createContext("/", metricHandler);
        server.createContext("/metrics", metricHandler);
        

        server.setExecutor(Executors.newSingleThreadExecutor());
        server.start();      
    }
    
    public synchronized void stop() throws InterruptedException {
    	TimeUnit.SECONDS.sleep(delay);
    	server.stop(0);
    	((ExecutorService) this.server.getExecutor()).shutdown();
    }

}



================================================
FILE: src/main/java/com/github/johrstrom/listener/gui/ListenerCollectorTable.java
================================================
package com.github.johrstrom.listener.gui;

import com.github.johrstrom.collector.gui.AbstractCollectorTable;
import com.github.johrstrom.collector.gui.Flatten;
import com.github.johrstrom.config.gui.ConfigCollectorTable;
import com.github.johrstrom.listener.ListenerCollectorConfig;
import com.github.johrstrom.listener.ListenerCollectorConfig.Measurable;
import org.apache.jorphan.reflect.Functor;

import javax.swing.*;
import javax.swing.table.TableColumn;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ListenerCollectorTable extends AbstractCollectorTable<ListenerCollectorConfig> 
	implements Flatten {



	private static final long serialVersionUID = 4429063284832140575L;
	
	public static JComboBox<String> listenToComboBox, measuringComboBox; 
	private static ConfigCollectorTable stealFrom = new ConfigCollectorTable();
	
	public static int LISTEN_TO_INDEX = ConfigCollectorTable.BASE_COLUMN_SIZE;
	public static int MEASURING_INDEX = LISTEN_TO_INDEX + 1;
	
	static {
		listenToComboBox = new JComboBox<>();
		listenToComboBox.addItem(ListenerCollectorConfig.SAMPLES);
		listenToComboBox.addItem(ListenerCollectorConfig.ASSERTIONS);
		
		measuringComboBox = measuringBox();
	}
	
	public ListenerCollectorTable() {
		super(ListenerCollectorConfig.class);
	}
	
	@Override
	public Flatten getGuiHelper() {
		return this;
	}

	@Override
	public void modifyColumns() {
		TableColumn column = table.getColumnModel().getColumn(ConfigCollectorTable.TYPE_INDEX);
		column.setCellEditor(new DefaultCellEditor(ConfigCollectorTable.typeComboBox));
		
		column = table.getColumnModel().getColumn(ListenerCollectorTable.LISTEN_TO_INDEX);
		column.setCellEditor(new DefaultCellEditor(listenToComboBox));
		
		column = table.getColumnModel().getColumn(ListenerCollectorTable.MEASURING_INDEX);
		column.setCellEditor(new DefaultCellEditor(measuringComboBox));
	}
	
	@Override
	public Functor[] getReadFunctors() {
	
		List<Functor> functors = new ArrayList<>(Arrays.asList(stealFrom.getReadFunctors()));
		
		functors.add(new Functor("getListenTo"));
		functors.add(new Functor("getMeasuring"));
		
		return functors.toArray(new Functor[functors.size()]);
	}

	@Override
	public Functor[] getWriteFunctors() {
		List<Functor> functors = new ArrayList<>(Arrays.asList(stealFrom.getWriteFunctors()));
		
		functors.add(new Functor("setListenTo"));
		functors.add(new Functor("setMeasuring"));
		
		return functors.toArray(new Functor[functors.size()]);
	}

	@Override
	public String[] getHeaders() {
		List<String> headers = new ArrayList<>(Arrays.asList(stealFrom.getHeaders()));
		
		headers.add("Listen to");
		headers.add("Measuring");
		
		return headers.toArray(new String[headers.size()]);
	}

	@Override
	public Class<?>[] getEditorClasses() {
		List<Class<?>> editors = new ArrayList<>(Arrays.asList(stealFrom.getEditorClasses()));
		
		editors.add(ComboBoxEditor.class);
		editors.add(ComboBoxEditor.class);
		
		return editors.toArray(new Class<?>[editors.size()]);
	}
	
	public static JComboBox<String> measuringBox() {
		JComboBox<String> box = new JComboBox<String>();
		for (Measurable value : Measurable.values()) {
			box.addItem(value.toString());
		}
		return box;
	}

}


================================================
FILE: src/main/java/com/github/johrstrom/listener/gui/PrometheusListenerGui.java
================================================
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed  under the  License is distributed on an "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.johrstrom.listener.gui;


import com.github.johrstrom.collector.BaseCollectorConfig;
import com.github.johrstrom.collector.CollectorElement;
import com.github.johrstrom.listener.ListenerCollectorConfig;
import com.github.johrstrom.listener.PrometheusListener;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.visualizers.gui.AbstractListenerGui;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.util.ArrayList;
import java.util.List;


/**
 * The GUI class for the Prometheus Listener.
 * <p>
 * Currently, all configurations are done through properties files so this class
 * shows nothing visually other than comments.
 *
 * @author Jeff Ohrstrom
 */
public class PrometheusListenerGui extends AbstractListenerGui {


	private static final long serialVersionUID = 4984653136457108054L;

	private ListenerCollectorTable table = new ListenerCollectorTable();
	private Logger log = LoggerFactory.getLogger(PrometheusListenerGui.class);

	public PrometheusListenerGui() {
		super();
		log.debug("making a new listener gui: {}", this.toString());
		init();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.gui.JMeterGUIComponent#getLabelResource()
	 */
	@Override
	public String getLabelResource() {
		return getClass().getCanonicalName();
	}


	@Override
	protected PrometheusListenerGui clone() throws CloneNotSupportedException {
		return new PrometheusListenerGui();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.gui.AbstractJMeterGuiComponent#getStaticLabel()
	 */
	@Override
	public String getStaticLabel() {
		return "Prometheus Listener";
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see org.apache.jmeter.gui.AbstractJMeterGuiComponent#getName()
	 */
	@Override
	public String getName() {
		return super.getName() == null ? this.getStaticLabel() : super.getName();
	}

	@SuppressWarnings("unchecked")
	@Override
	public void configure(TestElement ele) {
		super.configure(ele);
		if (ele instanceof CollectorElement<?>) {
			try {
				this.table.populateTable((CollectorElement<ListenerCollectorConfig>) ele);
			} catch (Exception e) {
				log.error("didn't modify test element because {}. {}", e.getClass(), e.getMessage());
			}
		}

		//ele.getName() == null ? this.setName(ele.getName()) : this.setName(getStaticLabel());;
		this.setName(ele.getName() == null ? getStaticLabel() : ele.getName());
		this.setComment(ele.getComment() == null ? "" : ele.getComment());
	}

	@Override
	public TestElement createTestElement() {
		PrometheusListener listener = new PrometheusListener();

		listener.setProperty(TestElement.GUI_CLASS, PrometheusListenerGui.class.getName());
		listener.setProperty(TestElement.TEST_CLASS, PrometheusListener.class.getName());
		this.modifyTestElement(listener);
		listener.setCollectorConfigs(defaultCollectors());
		return listener;
	}

	private void init() {
		this.setLayout(new BorderLayout(0, 5));
		this.add(makeTitlePanel(), BorderLayout.NORTH);
		this.add(this.table, BorderLayout.CENTER);
	}

	@Override
	public void modifyTestElement(TestElement ele) {
		if (!(ele instanceof CollectorElement)) {
			return;
		}

		@SuppressWarnings("unchecked")
		CollectorElement<ListenerCollectorConfig> config = (CollectorElement<ListenerCollectorConfig>) ele;
		List<ListenerCollectorConfig> collectors = this.table.getRowsAsCollectors();
		config.setCollectorConfigs(collectors);

		config.setName(this.getName());
		config.setComment(this.getComment());
	}

	@Override
	public void clearGui() {
		super.clearGui();
		this.table.clearModelData();
	}


	private List<ListenerCollectorConfig> defaultCollectors() {
		List<ListenerCollectorConfig> collectors = new ArrayList<>();
		collectors.add(buildResponseTimeCollector());
		collectors.add(buildSuccessRatioCollector());
		return collectors;
	}

	private ListenerCollectorConfig buildSuccessRatioCollector() {
		BaseCollectorConfig cfg = new BaseCollectorConfig();
		cfg.setMetricName("Ratio");
		cfg.setHelp("Success and failure ratio");
		cfg.setLabels("label,code");
		cfg.setType(BaseCollectorConfig.JMeterCollectorType.SUCCESS_RATIO.toString());
		ListenerCollectorConfig listenerCfg = new ListenerCollectorConfig(cfg);
		listenerCfg.setListenTo(ListenerCollectorConfig.SAMPLES.toString());
		listenerCfg.setMeasuring(ListenerCollectorConfig.Measurable.SuccessRatio.toString());
		return listenerCfg;
	}

	private ListenerCollectorConfig buildResponseTimeCollector() {
		BaseCollectorConfig cfg = new BaseCollectorConfig();
		cfg.setMetricName("ResponseTime");
		cfg.setHelp("Sampler Response Time");
		cfg.setLabels("label,code");
		cfg.setType(BaseCollectorConfig.JMeterCollectorType.SUMMARY.toString());
		cfg.setQuantileOrBucket("0.75,0.5|0.95,0.1|0.99,0.01;60");
		ListenerCollectorConfig listenerCfg = new ListenerCollectorConfig(cfg);
		listenerCfg.setListenTo(ListenerCollectorConfig.SAMPLES.toString());
		listenerCfg.setMeasuring(ListenerCollectorConfig.Measurable.ResponseTime.toString());
		return listenerCfg;
	}

}


================================================
FILE: src/main/java/com/github/johrstrom/listener/updater/AbstractUpdater.java
================================================
package com.github.johrstrom.listener.updater;

import com.github.johrstrom.collector.JMeterCollectorRegistry;
import com.github.johrstrom.listener.ListenerCollectorConfig;
import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.threads.JMeterContextService;
import org.apache.jmeter.threads.JMeterVariables;

import java.util.HashMap;
import java.util.Map;

/**
 * The Updater family of classes are meant to update the actual Collectors given the configuration. The main problem
 * it tries to solve is tying a Prometheus Collector (with a type like 'Historgram', labels, etc.) to the JMeter data
 * that collector is measuring.
 * 
 * Note: This class assumes that the Collector object passed into the constructor is valid. I.e., it is not null and 
 * registered. Of course, being null has much more serious consequences. 
 * 
 * @author Jeff Ohrstrom
 *
 */
public abstract class AbstractUpdater {
	
	public static String NULL = "null";

	protected ListenerCollectorConfig config;
	protected static final JMeterCollectorRegistry registry = JMeterCollectorRegistry.getInstance();
	
	// helper lookup table for sample variables, so we don't loop over arrays every update.
	private Map<String,Integer> varIndexLookup;

	/**
	 * All subclasses should have this and only this constructor signature. 
	 *
	 * @param cfg the configuration of the collector
	 */
	public AbstractUpdater(ListenerCollectorConfig cfg) {
		//this.collector = c;
		this.config = cfg;
		this.buildVarLookup();
	}

	
	/**
	 * Updates the collector it was instantiated with with the given event e. 
	 * 
	 * @param e
	 */
	public abstract void update(SampleEvent e);
	
	public static class AssertionContext {
		public AssertionResult assertion;
		public SampleEvent event;
		
		public AssertionContext(AssertionResult a, SampleEvent e) {
			this.assertion = a;
			this.event = e;
		}
		
	}

	/**
	 * Helper function to extract the label values from the Sample Event. Values
	 * depend on how the Updater was configured. 
	 * 
	 * @param event
	 * @return the label values.
	 */
	protected String[] labelValues(SampleEvent event) {
		String[] labels = config.getLabels();
		String[] values =  new String[labels.length];
		JMeterVariables vars = JMeterContextService.getContext().getVariables();
		
		for (int i = 0; i < labels.length; i++) {
			String name = labels[i];
			String value = null;
			
			// reserved keywords for the sampler's label name.
			if (name.equalsIgnoreCase("label")) {
				value = event.getResult().getSampleLabel();
			} else if (name.equalsIgnoreCase("code")) {
				// code also reserved
				value = event.getResult().getResponseCode();
			} else if (name.equalsIgnoreCase("thread_group")) {
				value = event.getThreadGroup();
				
			// try to find it as a plain'ol variable.
			} else if (this.varIndexLookup.get(name) != null) {
				int idx = this.varIndexLookup.get(name);
				value = event.getVarValue(idx);
			
			// lastly look in sample_variables
			} else if (vars != null) {
				value = vars.get(name);
			}
			
			values[i] = (value == null || value.isEmpty()) ? NULL : value;
		}
		
		return values;
	}
	
	protected String[] labelValues(AssertionContext ctx) {
		String[] labels = config.getLabels();
		String[] values =  new String[labels.length];
		JMeterVariables vars = JMeterContextService.getContext().getVariables();
		
		for(int i = 0; i < labels.length; i++) {
			String name = labels[i];
			String value = null;
			
			if(name.equalsIgnoreCase("label")) {
				value = ctx.assertion.getName();
				
			// try to find it as a plain'ol variable.
			} else if (this.varIndexLookup.get(name) != null){
				int idx = this.varIndexLookup.get(name);
				value = ctx.event.getVarValue(idx);
				
			
			// lastly look in sample_variables
			}else if (vars != null){
				value = vars.get(name);
			}
						
			values[i] = (value == null || value.isEmpty()) ? NULL : value;
		}
		
		return values;
	}
	
	
	private void buildVarLookup() {
		this.varIndexLookup = new HashMap<String,Integer>();
				
		for(int i = 0; i < SampleEvent.getVarCount(); i++) {
			String name = SampleEvent.getVarName(i);
			if(inLabels(name)) {
				this.varIndexLookup.put(name, i);
			}
		}
		
	}
	
	private boolean inLabels(String searchFor) {
		String[] labels = config.getLabels();
		for(int i = 0; i < labels.length; i++) {
			if(labels[i].equalsIgnoreCase(searchFor)) {
				return true;
			}
		}
		
		return false;
	}
	
}


================================================
FILE: src/main/java/com/github/johrstrom/listener/updater/AggregatedTypeUpdater.java
================================================
package com.github.johrstrom.listener.updater;

import com.github.johrstrom.listener.ListenerCollectorConfig;
import io.prometheus.client.Collector;
import io.prometheus.client.Histogram;
import io.prometheus.client.Summary;
import org.apache.jmeter.samplers.SampleEvent;
import org.apache.jmeter.samplers.SampleResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AggregatedTypeUpdater extends AbstractUpdater {
	
	private static final Logger log = LoggerFactory.getLogger(AggregatedTypeUpdater.class);

	public AggregatedTypeUpdater(ListenerCollectorConfig cfg) {
		super(cfg);
	}

	@Override
	public void update(SampleEvent event) {
		try {
			Collector collector = registry.getOrCreateAndRegister(this.config);
			
			String[] labels = this.labelValues(event);
			long measurement = this.measure(event);
			
			if(collector instanceof Histogram) {
				Histogram hist = (Histogram) collector;
				hist.labels(labels).observe(measurement);
				
			}else if(collector instanceof Summary) {
				Summary sum = (Summary) collector;
				sum.labels(labels).observe(measurement);
			}
			
		} catch(Exception e) {
			log.error("Did not update {} because of error: {}", this.config.getMetricName(), e.getMessage());
			log.debug(e.getMessage(), e);
		}

	}
	
	
	protected long measure(SampleEvent event) {
		SampleResult result = event.getResult();
		switch(this.config.getMeasuringAsEnum()) {
		case ResponseSize:
			return result.getBodySizeAsLong();
		case ResponseTime:
			return result.getTime();
		case Latency:
			return result.getLatency();
		case IdleTime:
			return result.getIdleTime();
		case ConnectTime:
			return result.getConnectTime();
		default:
			return 0;
		}
	}

}


================================================
FILE: src/main/java/com/github/johrstrom/listener/updater/CountTypeUpdater.java
================================================
package com.github.johrstrom.listener.updater;

import com.github.johrstrom.collector.SuccessRatioCollector;
import com.github.johrstrom.listener.ListenerCollectorConfig;
import io.prometheus.client.Collector;
import io.prometheus.client.Counter;
import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.samplers.SampleEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is the AbstractUpdater sub-type that can handle updating any kind of Counter metrics 
Download .txt
gitextract_e9g8y5hg/

├── .github/
│   └── workflows/
│       ├── release.yml
│       └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── NOTICE
├── README.md
├── docs/
│   └── examples/
│       ├── grafana.json
│       └── simple_prometheus_example.jmx
├── licenses/
│   ├── io.prometheus.LICENSE
│   └── io.prometheus.NOTICE
├── makefile
├── pom.xml
└── src/
    ├── main/
    │   └── java/
    │       └── com/
    │           └── github/
    │               └── johrstrom/
    │                   ├── collector/
    │                   │   ├── BaseCollectorConfig.java
    │                   │   ├── CollectorElement.java
    │                   │   ├── JMeterCollectorRegistry.java
    │                   │   ├── SuccessRatioCollector.java
    │                   │   └── gui/
    │                   │       ├── AbstractCollectorTable.java
    │                   │       └── Flatten.java
    │                   ├── config/
    │                   │   ├── PrometheusMetricsConfig.java
    │                   │   └── gui/
    │                   │       ├── ConfigCollectorTable.java
    │                   │       └── PrometheusMetricsConfigGui.java
    │                   └── listener/
    │                       ├── ListenerCollectorConfig.java
    │                       ├── PrometheusListener.java
    │                       ├── PrometheusServer.java
    │                       ├── gui/
    │                       │   ├── ListenerCollectorTable.java
    │                       │   └── PrometheusListenerGui.java
    │                       └── updater/
    │                           ├── AbstractUpdater.java
    │                           ├── AggregatedTypeUpdater.java
    │                           └── CountTypeUpdater.java
    └── test/
        ├── java/
        │   └── com/
        │       └── github/
        │           └── johrstrom/
        │               ├── collector/
        │               │   ├── BaseCollectorConfigTest.java
        │               │   ├── JMeterCollectorRegistryTest.java
        │               │   └── SuccessRatioCollectorTest.java
        │               ├── config/
        │               │   ├── PrometheusMetricsConfigTest.java
        │               │   └── gui/
        │               │       └── ConfigGuiTest.java
        │               ├── listener/
        │               │   ├── ListenerCollectorConfigTest.java
        │               │   ├── PrometheusListenerTest.java
        │               │   ├── PrometheusServerTest.java
        │               │   ├── gui/
        │               │   │   └── ListenerGuiTest.java
        │               │   └── updater/
        │               │       ├── AbstractUpdaterTest.java
        │               │       ├── AggregatedTypeUpdaterTest.java
        │               │       └── CountTypeUpdaterTest.java
        │               └── test/
        │                   ├── NOOPThreadMonitor.java
        │                   └── TestUtilities.java
        └── resources/
            ├── bin/
            │   ├── jmeter.properties
            │   ├── saveservice.properties
            │   ├── upgrade.properties
            │   └── user.properties
            └── log4j2.xml
Download .txt
SYMBOL INDEX (268 symbols across 31 files)

FILE: src/main/java/com/github/johrstrom/collector/BaseCollectorConfig.java
  class BaseCollectorConfig (line 35) | public class BaseCollectorConfig extends AbstractTestElement  {
    type JMeterCollectorType (line 58) | public enum JMeterCollectorType {
    method BaseCollectorConfig (line 66) | public BaseCollectorConfig(){
    method getHelp (line 74) | public String getHelp() {
    method setHelp (line 78) | public void setHelp(String help) {
    method getType (line 85) | public String getType() {
    method getCollectorType (line 89) | public JMeterCollectorType getCollectorType() {
    method setType (line 93) | public void setType(String type) {
    method getQuantileOrBucket (line 102) | public String getQuantileOrBucket() {
    method setQuantileOrBucket (line 106) | public void setQuantileOrBucket(String quantileOrBucket) {
    method getBuckets (line 110) | public double[] getBuckets() {
    method getQuantiles (line 120) | public QuantileDefinition[] getQuantiles() {
    method getQuantileWindowLength (line 130) | public long getQuantileWindowLength() {
    method getMetricName (line 140) | public String getMetricName() {
    method setMetricName (line 144) | public void setMetricName(String name) {
    method getRandomMetricName (line 151) | public String getRandomMetricName() {
    method setLabels (line 155) | public void setLabels(String labels) {
    method setLabels (line 159) | public void setLabels(String[] labels) {
    method getLabels (line 173) | public String[] getLabels() {
    method getLabelsAsString (line 193) | public String getLabelsAsString() {
    method newCounter (line 206) | public static Counter newCounter(BaseCollectorConfig cfg) throws Excep...
    method newSummary (line 219) | public static Summary newSummary(BaseCollectorConfig cfg) throws Excep...
    method newHistogram (line 237) | public static Histogram newHistogram(BaseCollectorConfig cfg) throws E...
    method newGauge (line 251) | public static Gauge newGauge(BaseCollectorConfig cfg) throws Exception {
    method fromConfig (line 264) | public static Collector fromConfig(BaseCollectorConfig cfg) {
    method equals (line 294) | @Override
    method hashCode (line 310) | @Override
    method toString (line 324) | @Override
    method parseBucketsFromString (line 338) | protected double[] parseBucketsFromString(String fullBucketString) {
    class QuantileDefinition (line 367) | public static class QuantileDefinition {
      method QuantileDefinition (line 375) | QuantileDefinition(double quantile, double error) {
      method QuantileDefinition (line 380) | QuantileDefinition(String quantile, String error) throws NumberForma...
      method QuantileDefinition (line 384) | QuantileDefinition(String[] definition) throws NumberFormatException {
      method toString (line 392) | @Override
      method defaultQuantiles (line 401) | public static QuantileDefinition[] defaultQuantiles() {
      method arrayToString (line 411) | public static String arrayToString(QuantileDefinition[] definitions) {
      method parseQuantilesFromString (line 423) | public static QuantileDefinition[] parseQuantilesFromString(String f...
      method parseQuantilesWindowLengthFromString (line 447) | public static long parseQuantilesWindowLengthFromString(String fullQ...

FILE: src/main/java/com/github/johrstrom/collector/CollectorElement.java
  class CollectorElement (line 14) | public abstract class CollectorElement<C extends BaseCollectorConfig> ex...
    method CollectorElement (line 24) | public CollectorElement() {
    method getCollectorConfigs (line 29) | public CollectionProperty getCollectorConfigs() {
    method setCollectorConfigs (line 41) | public void setCollectorConfigs(List<C> collectors) {
    method setCollectorConfigs (line 46) | public void setCollectorConfigs(CollectionProperty collectors) {
    method clearCollectors (line 50) | protected void clearCollectors() {
    method makeNewCollectors (line 59) | protected void makeNewCollectors() {

FILE: src/main/java/com/github/johrstrom/collector/JMeterCollectorRegistry.java
  class JMeterCollectorRegistry (line 16) | public class JMeterCollectorRegistry extends CollectorRegistry {
    method getInstance (line 30) | public synchronized static JMeterCollectorRegistry getInstance() {
    method JMeterCollectorRegistry (line 38) | private JMeterCollectorRegistry() {
    method initDefaultExports (line 44) | private void initDefaultExports() {
    method createJMeterExports (line 57) | private void createJMeterExports() {
    method unregister (line 66) | public synchronized void unregister(BaseCollectorConfig cfg) {
    method getOrCreateAndRegister (line 81) | public synchronized Collector getOrCreateAndRegister(BaseCollectorConf...
    method clear (line 95) | @Override
    class ThreadCollector (line 101) | private static class ThreadCollector extends Collector {
      method ThreadCollector (line 112) | protected ThreadCollector() {
      method getConfig (line 122) | protected static BaseCollectorConfig getConfig() {
      method threadMetricName (line 134) | public static String threadMetricName() {
      method collect (line 138) | @Override

FILE: src/main/java/com/github/johrstrom/collector/SuccessRatioCollector.java
  class SuccessRatioCollector (line 9) | public class SuccessRatioCollector extends Collector {
    method SuccessRatioCollector (line 13) | public SuccessRatioCollector(BaseCollectorConfig config) {
    method incrementSuccess (line 34) | public void incrementSuccess(String[] labels) {
    method incrementFailure (line 45) | public void incrementFailure(String[] labels) {
    method getSuccess (line 56) | public double getSuccess(String[] labels) {
    method getFailure (line 60) | public double getFailure(String[] labels) {
    method getTotal (line 64) | public double getTotal(String[] labels) {
    method collect (line 68) | @Override
    method extendedName (line 79) | private static String extendedName(String orig, String append) {

FILE: src/main/java/com/github/johrstrom/collector/gui/AbstractCollectorTable.java
  class AbstractCollectorTable (line 21) | public abstract class AbstractCollectorTable<C extends BaseCollectorConfig>
    method getGuiHelper (line 41) | public abstract Flatten getGuiHelper();
    method modifyColumns (line 47) | public abstract void modifyColumns();
    method AbstractCollectorTable (line 50) | public AbstractCollectorTable(Class<C> collectorType) {
    method getRowsAsCollectors (line 56) | public List<C> getRowsAsCollectors(){
    method clearModelData (line 71) | public void clearModelData() {
    method populateTable (line 103) | public void populateTable(CollectorElement<C> config) {
    method init (line 123) | protected void init() {
    method makeTablePanel (line 134) | protected Component makeTablePanel() {
    method makeButtonPanel (line 153) | protected JPanel makeButtonPanel() {
    method actionPerformed (line 172) | @Override
    method deleteSelectedRows (line 190) | protected void deleteSelectedRows() {

FILE: src/main/java/com/github/johrstrom/collector/gui/Flatten.java
  type Flatten (line 5) | public interface Flatten {
    method getReadFunctors (line 7) | public Functor[] getReadFunctors();
    method getWriteFunctors (line 8) | public Functor[] getWriteFunctors();
    method getHeaders (line 9) | public String[] getHeaders();
    method getEditorClasses (line 10) | public Class<?>[] getEditorClasses();

FILE: src/main/java/com/github/johrstrom/config/PrometheusMetricsConfig.java
  class PrometheusMetricsConfig (line 16) | public class PrometheusMetricsConfig extends CollectorElement<BaseCollec...
    method testEnded (line 23) | @Override
    method testEnded (line 36) | @Override
    method testStarted (line 41) | @Override
    method testStarted (line 57) | @Override
    method clone (line 62) | @Override
    method equals (line 70) | @Override

FILE: src/main/java/com/github/johrstrom/config/gui/ConfigCollectorTable.java
  class ConfigCollectorTable (line 13) | public class ConfigCollectorTable extends AbstractCollectorTable<BaseCol...
    method ConfigCollectorTable (line 36) | public ConfigCollectorTable() {
    method getGuiHelper (line 40) | @Override
    method modifyColumns (line 45) | @Override
    method getReadFunctors (line 51) | @Override
    method getWriteFunctors (line 64) | @Override
    method getHeaders (line 77) | @Override
    method getEditorClasses (line 90) | @Override

FILE: src/main/java/com/github/johrstrom/config/gui/PrometheusMetricsConfigGui.java
  class PrometheusMetricsConfigGui (line 15) | public class PrometheusMetricsConfigGui<C> extends AbstractConfigGui {
    method PrometheusMetricsConfigGui (line 23) | public PrometheusMetricsConfigGui(){
    method createTestElement (line 29) | @Override
    method getLabelResource (line 40) | @Override
    method getStaticLabel (line 50) | @Override
    method getName (line 60) | @Override
    method modifyTestElement (line 65) | @SuppressWarnings("unchecked")
    method init (line 83) | private void init() {
    method configure (line 92) | @SuppressWarnings("unchecked")
    method clearGui (line 109) | @Override
    method clone (line 115) | @Override

FILE: src/main/java/com/github/johrstrom/listener/ListenerCollectorConfig.java
  class ListenerCollectorConfig (line 5) | public class ListenerCollectorConfig extends BaseCollectorConfig {
    method ListenerCollectorConfig (line 14) | public ListenerCollectorConfig() {
    method ListenerCollectorConfig (line 18) | public ListenerCollectorConfig(BaseCollectorConfig base) {
    type Measurable (line 25) | public enum Measurable {
    method setListenTo (line 40) | public void setListenTo(String listenTo) {
    method getListenTo (line 50) | public String getListenTo() {
    method setMeasuring (line 54) | public void setMeasuring(String measuring) {
    method getMeasuring (line 58) | public String getMeasuring() {
    method getMeasuringAsEnum (line 62) | public Measurable getMeasuringAsEnum() {
    method listenToSamples (line 66) | public boolean listenToSamples() {
    method listenToAssertions (line 70) | public boolean listenToAssertions() {

FILE: src/main/java/com/github/johrstrom/listener/PrometheusListener.java
  class PrometheusListener (line 49) | public class PrometheusListener extends CollectorElement<ListenerCollect...
    method sampleOccurred (line 66) | @Override
    method sampleStarted (line 82) | @Override
    method sampleStopped (line 94) | @Override
    method testEnded (line 104) | @Override
    method testEnded (line 121) | @Override
    method testStarted (line 131) | @Override
    method testStarted (line 155) | @Override
    method makeNewCollectors (line 160) | @Override

FILE: src/main/java/com/github/johrstrom/listener/PrometheusServer.java
  class PrometheusServer (line 38) | public class PrometheusServer {
    class LocalByteArray (line 51) | private static class LocalByteArray extends ThreadLocal<ByteArrayOutpu...
      method initialValue (line 52) | protected ByteArrayOutputStream initialValue() {
    class HTTPMetricHandler (line 57) | static class HTTPMetricHandler implements HttpHandler {
      method HTTPMetricHandler (line 61) | HTTPMetricHandler(CollectorRegistry registry) {
      method handle (line 66) | public void handle(HttpExchange t) throws IOException {
    method shouldUseCompression (line 98) | protected static boolean shouldUseCompression(HttpExchange exchange) {
    method parseQuery (line 113) | protected static Set<String> parseQuery(String query) throws IOExcepti...
    method getInstance (line 135) | public synchronized static PrometheusServer getInstance() {
    method PrometheusServer (line 144) | private PrometheusServer() { }
    method start (line 146) | public synchronized void start() throws IOException {
    method stop (line 165) | public synchronized void stop() throws InterruptedException {

FILE: src/main/java/com/github/johrstrom/listener/gui/ListenerCollectorTable.java
  class ListenerCollectorTable (line 16) | public class ListenerCollectorTable extends AbstractCollectorTable<Liste...
    method ListenerCollectorTable (line 37) | public ListenerCollectorTable() {
    method getGuiHelper (line 41) | @Override
    method modifyColumns (line 46) | @Override
    method getReadFunctors (line 58) | @Override
    method getWriteFunctors (line 69) | @Override
    method getHeaders (line 79) | @Override
    method getEditorClasses (line 89) | @Override
    method measuringBox (line 99) | public static JComboBox<String> measuringBox() {

FILE: src/main/java/com/github/johrstrom/listener/gui/PrometheusListenerGui.java
  class PrometheusListenerGui (line 44) | public class PrometheusListenerGui extends AbstractListenerGui {
    method PrometheusListenerGui (line 52) | public PrometheusListenerGui() {
    method getLabelResource (line 63) | @Override
    method clone (line 69) | @Override
    method getStaticLabel (line 79) | @Override
    method getName (line 89) | @Override
    method configure (line 94) | @SuppressWarnings("unchecked")
    method createTestElement (line 111) | @Override
    method init (line 122) | private void init() {
    method modifyTestElement (line 128) | @Override
    method clearGui (line 143) | @Override
    method defaultCollectors (line 150) | private List<ListenerCollectorConfig> defaultCollectors() {
    method buildSuccessRatioCollector (line 157) | private ListenerCollectorConfig buildSuccessRatioCollector() {
    method buildResponseTimeCollector (line 169) | private ListenerCollectorConfig buildResponseTimeCollector() {

FILE: src/main/java/com/github/johrstrom/listener/updater/AbstractUpdater.java
  class AbstractUpdater (line 24) | public abstract class AbstractUpdater {
    method AbstractUpdater (line 39) | public AbstractUpdater(ListenerCollectorConfig cfg) {
    method update (line 51) | public abstract void update(SampleEvent e);
    class AssertionContext (line 53) | public static class AssertionContext {
      method AssertionContext (line 57) | public AssertionContext(AssertionResult a, SampleEvent e) {
    method labelValues (line 71) | protected String[] labelValues(SampleEvent event) {
    method labelValues (line 105) | protected String[] labelValues(AssertionContext ctx) {
    method buildVarLookup (line 135) | private void buildVarLookup() {
    method inLabels (line 147) | private boolean inLabels(String searchFor) {

FILE: src/main/java/com/github/johrstrom/listener/updater/AggregatedTypeUpdater.java
  class AggregatedTypeUpdater (line 12) | public class AggregatedTypeUpdater extends AbstractUpdater {
    method AggregatedTypeUpdater (line 16) | public AggregatedTypeUpdater(ListenerCollectorConfig cfg) {
    method update (line 20) | @Override
    method measure (line 45) | protected long measure(SampleEvent event) {

FILE: src/main/java/com/github/johrstrom/listener/updater/CountTypeUpdater.java
  class CountTypeUpdater (line 19) | public class CountTypeUpdater extends AbstractUpdater {
    method CountTypeUpdater (line 23) | public CountTypeUpdater(ListenerCollectorConfig cfg) {
    method update (line 27) | @Override
    method inc (line 42) | protected void inc(String[] labels, boolean successful) {
    method updateAssertions (line 82) | protected void updateAssertions(AssertionContext ctx) {

FILE: src/test/java/com/github/johrstrom/collector/BaseCollectorConfigTest.java
  class BaseCollectorConfigTest (line 15) | public class BaseCollectorConfigTest {
    method emptyLabelsOK (line 17) | @Test
    method parseSingleQuantilesCorrectly (line 39) | @Test
    method parseMultipleQuantilesCorrectly (line 51) | @Test
    method parseMultipleQuantilesWithWindowCorrectly (line 67) | @Test
    method parseQauntileFailsAndGivesDEFAULTs (line 85) | @Test
    method parseReturnsPartialForQuantiles (line 100) | @Test
    method parseBucketsCorrectly (line 119) | @Test
    method parseBucketFailureReturnsDefaults (line 130) | @Test
    method parseBucketsWithPartialSuccess (line 146) | @Test
    method initCorrectly (line 158) | @Test
    method createCorrectType (line 175) | @Test
    method setOfElementsTest (line 199) | @Test

FILE: src/test/java/com/github/johrstrom/collector/JMeterCollectorRegistryTest.java
  class JMeterCollectorRegistryTest (line 8) | public class JMeterCollectorRegistryTest {
    method safelyGetOrCreate (line 12) | @Test

FILE: src/test/java/com/github/johrstrom/collector/SuccessRatioCollectorTest.java
  class SuccessRatioCollectorTest (line 12) | public class SuccessRatioCollectorTest {
    method testSuccess (line 19) | @Test
    method testFailure (line 64) | @Test
    method assertOnSingleFamily (line 109) | private void assertOnSingleFamily(MetricFamilySamples family, double e...

FILE: src/test/java/com/github/johrstrom/config/PrometheusMetricsConfigTest.java
  class PrometheusMetricsConfigTest (line 12) | public class PrometheusMetricsConfigTest {
    method listenerIsSerializable (line 14) | @Test

FILE: src/test/java/com/github/johrstrom/config/gui/ConfigGuiTest.java
  class ConfigGuiTest (line 12) | public class ConfigGuiTest {
    method simpleTest (line 14) | @Test

FILE: src/test/java/com/github/johrstrom/listener/ListenerCollectorConfigTest.java
  class ListenerCollectorConfigTest (line 7) | public class ListenerCollectorConfigTest {
    method setOfElementsTest (line 9) | @Test

FILE: src/test/java/com/github/johrstrom/listener/PrometheusListenerTest.java
  class PrometheusListenerTest (line 24) | public class PrometheusListenerTest {
    method listenerIsSerializable (line 32) | @Test
    method canHaveDuplicateMetrics (line 45) | @Test
    method lazyInizializationPrometheusServer (line 64) | @Test
    method lazyInizializationRegistry (line 77) | @Test
    method canReadJMX (line 90) | @Test
    method updateAllTypes (line 98) | @Test
    method assertOnRatio (line 207) | private void assertOnRatio(ListenerCollectorConfig cfg) {
    method assertOnHistogram (line 219) | protected static void assertOnHistogram(Collector collector, double ex...
    method assertOnSummary (line 273) | protected void assertOnSummary(Collector collector, double expectedSum...

FILE: src/test/java/com/github/johrstrom/listener/PrometheusServerTest.java
  class PrometheusServerTest (line 13) | public class PrometheusServerTest {
    method ensureCleanStartStop (line 15) | @Test
    method pingAPI (line 35) | private String pingAPI() throws IOException {

FILE: src/test/java/com/github/johrstrom/listener/gui/ListenerGuiTest.java
  class ListenerGuiTest (line 10) | public class ListenerGuiTest {
    method simpleTest (line 12) | @Test

FILE: src/test/java/com/github/johrstrom/listener/updater/AbstractUpdaterTest.java
  class AbstractUpdaterTest (line 13) | public class AbstractUpdaterTest {
    class TestUpdater (line 15) | public static class TestUpdater extends  AbstractUpdater {
      method TestUpdater (line 18) | public TestUpdater(ListenerCollectorConfig cfg) {
      method update (line 22) | @Override
    method testKeywords (line 29) | @Test
    method testVariables (line 49) | @Test
    method testCombo (line 73) | @Test
    method testNulls (line 97) | @Test

FILE: src/test/java/com/github/johrstrom/listener/updater/AggregatedTypeUpdaterTest.java
  class AggregatedTypeUpdaterTest (line 24) | public class AggregatedTypeUpdaterTest {
    method testHistogramResponseTime (line 34) | @Test
    method testSummaryResponseTime (line 116) | @Test
    method testHistogramResponseSize (line 186) | @Test
    method testSummaryResponseSize (line 261) | @Test
    method correctLabels (line 320) | private void correctLabels(List<String> names, List<String> values) {

FILE: src/test/java/com/github/johrstrom/listener/updater/CountTypeUpdaterTest.java
  class CountTypeUpdaterTest (line 19) | public class CountTypeUpdaterTest {
    method successCountOnSamplesTest (line 26) | @Test
    method failureCountOnSamplesTest (line 69) | @Test
    method countSamplesTotalTest (line 113) | @Test
    method testSROnSamples (line 155) | @Test
    method testRatioOnAssertions (line 208) | @Test
    method testSuccessAssertions (line 258) | @Test
    method testFailureAssertions (line 288) | @Test
    method testTotalAssertions (line 319) | @Test
    method newSampleResult (line 350) | public static SampleResult newSampleResult(boolean success) {
    method newSampleResultWithAssertion (line 358) | public static SampleResult newSampleResultWithAssertion(boolean succes...
    method altAssertion (line 368) | public static AssertionResult altAssertion(boolean success) {
    method vars (line 374) | public static JMeterVariables vars() {

FILE: src/test/java/com/github/johrstrom/test/NOOPThreadMonitor.java
  class NOOPThreadMonitor (line 6) | public class NOOPThreadMonitor implements JMeterThreadMonitor {
    method threadFinished (line 8) | @Override

FILE: src/test/java/com/github/johrstrom/test/TestUtilities.java
  class TestUtilities (line 23) | public class TestUtilities {
    method simpleCounterCfg (line 39) | public static BaseCollectorConfig simpleCounterCfg() {
    method listenerCounterCfg (line 48) | public static ListenerCollectorConfig listenerCounterCfg(String name, ...
    method listenerSuccessRatioCfg (line 59) | public static ListenerCollectorConfig listenerSuccessRatioCfg(String n...
    method simpleSummaryCfg (line 70) | public static BaseCollectorConfig simpleSummaryCfg() {
    method simpleHistogramCfg (line 79) | public static BaseCollectorConfig simpleHistogramCfg() {
    method simpleGaugeCfg (line 88) | public static BaseCollectorConfig simpleGaugeCfg() {
    method simpleSuccessRatioCfg (line 97) | public static BaseCollectorConfig simpleSuccessRatioCfg() {
    method createJmeterEnv (line 106) | public static void createJmeterEnv() {
    method simpleListConfig (line 144) | public static List<BaseCollectorConfig> simpleListConfig() {
    method simpleListListener (line 156) | public static List<ListenerCollectorConfig> simpleListListener() {
    method fullListListener (line 168) | public static List<ListenerCollectorConfig> fullListListener(){
    method redoNameAndMeasuring (line 223) | public static ListenerCollectorConfig redoNameAndMeasuring(ListenerCol...
    method resultWithLabels (line 232) | public static ResultAndVariables resultWithLabels() {
    class ResultAndVariables (line 245) | public static class ResultAndVariables {
      method ResultAndVariables (line 249) | public ResultAndVariables(SampleResult result, JMeterVariables vars) {
Condensed preview — 51 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (378K chars).
[
  {
    "path": ".github/workflows/release.yml",
    "chars": 1063,
    "preview": "name: Release Jar\n\non:\n  push:\n    tags:\n      - '*'\n\njobs:\n  release:\n    runs-on: 'ubuntu-latest'\n\n    steps:\n      - "
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 528,
    "preview": "name: Tests\n\non:\n  push:\n    branches: \n      - master\n      - main\n  pull_request:\n\njobs:\n  tests:\n    strategy:\n      "
  },
  {
    "path": ".gitignore",
    "chars": 182,
    "preview": "target/\n.project\n.settings/*\n.classpath\n./bin/\npackage_and_mv.sh\ndependency-reduced-pom.xml\njmeter.log\npom.xml.releaseBa"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1036,
    "preview": "# 0.6.2\n\n* #117 is a bugfix in PrometheusListener.java to clear the collectors after the server is closed.\n\n# 0.6.1\n\n* #"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5497,
    "preview": "# Code of Conduct\n\n## 1. Purpose\n\nA primary goal of Jmeter Prometheus Plugin is to be inclusive to the largest number of"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 863,
    "preview": "# Contributing\n\n\n* If you have a trivial fix or improvement, go ahead and create a pull request, addressing me (Jeff Ohr"
  },
  {
    "path": "LICENSE",
    "chars": 11344,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "NOTICE",
    "chars": 160,
    "preview": "Prometheus instrumentation library Apache JMeter\nCopyright 2016-2019 Jeff Ohrstrom\n\nThis product includes software devel"
  },
  {
    "path": "README.md",
    "chars": 11331,
    "preview": "[![Build Status](https://github.com/johrstrom/jmeter-prometheus-plugin/workflows/Tests/badge.svg)](https://github.com/jo"
  },
  {
    "path": "docs/examples/grafana.json",
    "chars": 52997,
    "preview": "{\n  \"annotations\": {\n    \"list\": [\n      {\n        \"builtIn\": 1,\n        \"datasource\": \"-- Grafana --\",\n        \"enable\""
  },
  {
    "path": "docs/examples/simple_prometheus_example.jmx",
    "chars": 21594,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<jmeterTestPlan version=\"1.2\" properties=\"4.0\" jmeter=\"4.0 r1823414\">\n  <hashTree"
  },
  {
    "path": "licenses/io.prometheus.LICENSE",
    "chars": 11358,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "licenses/io.prometheus.NOTICE",
    "chars": 397,
    "preview": "Prometheus instrumentation library for JVM applications\nCopyright 2012-2015 The Prometheus Authors\n\nThis product include"
  },
  {
    "path": "makefile",
    "chars": 1233,
    "preview": ".DEFAULT_GOAL := help\nSHELL := /bin/bash\nTHIS_FILE := $(lastword $(MAKEFILE_LIST))\nTHIS_FOLDER := $(shell basename $(CUR"
  },
  {
    "path": "pom.xml",
    "chars": 7787,
    "preview": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocat"
  },
  {
    "path": "src/main/java/com/github/johrstrom/collector/BaseCollectorConfig.java",
    "chars": 13792,
    "preview": "package com.github.johrstrom.collector;\n\nimport io.prometheus.client.*;\nimport org.apache.commons.lang3.RandomStringUtil"
  },
  {
    "path": "src/main/java/com/github/johrstrom/collector/CollectorElement.java",
    "chars": 2534,
    "preview": "package com.github.johrstrom.collector;\n\nimport java.util.*;\nimport java.util.Map;\nimport java.util.Map.Entry;\n\nimport o"
  },
  {
    "path": "src/main/java/com/github/johrstrom/collector/JMeterCollectorRegistry.java",
    "chars": 4532,
    "preview": "package com.github.johrstrom.collector;\n\nimport io.prometheus.client.Collector;\nimport io.prometheus.client.CollectorReg"
  },
  {
    "path": "src/main/java/com/github/johrstrom/collector/SuccessRatioCollector.java",
    "chars": 2342,
    "preview": "package com.github.johrstrom.collector;\n\nimport io.prometheus.client.Collector;\nimport io.prometheus.client.Counter;\n\nim"
  },
  {
    "path": "src/main/java/com/github/johrstrom/collector/gui/AbstractCollectorTable.java",
    "chars": 4842,
    "preview": "package com.github.johrstrom.collector.gui;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.githu"
  },
  {
    "path": "src/main/java/com/github/johrstrom/collector/gui/Flatten.java",
    "chars": 265,
    "preview": "package com.github.johrstrom.collector.gui;\n\nimport org.apache.jorphan.reflect.Functor;\n\npublic interface Flatten {\n\t\n\tp"
  },
  {
    "path": "src/main/java/com/github/johrstrom/config/PrometheusMetricsConfig.java",
    "chars": 2718,
    "preview": "package com.github.johrstrom.config;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.github.johrs"
  },
  {
    "path": "src/main/java/com/github/johrstrom/config/gui/ConfigCollectorTable.java",
    "chars": 3121,
    "preview": "package com.github.johrstrom.config.gui;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.github.j"
  },
  {
    "path": "src/main/java/com/github/johrstrom/config/gui/PrometheusMetricsConfigGui.java",
    "chars": 3002,
    "preview": "package com.github.johrstrom.config.gui;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.github.j"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/ListenerCollectorConfig.java",
    "chars": 2915,
    "preview": "package com.github.johrstrom.listener;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\n\npublic class Listene"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/PrometheusListener.java",
    "chars": 5957,
    "preview": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOT"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/PrometheusServer.java",
    "chars": 5959,
    "preview": "package com.github.johrstrom.listener;\n\nimport com.github.johrstrom.collector.JMeterCollectorRegistry;\nimport com.sun.ne"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/gui/ListenerCollectorTable.java",
    "chars": 3225,
    "preview": "package com.github.johrstrom.listener.gui;\n\nimport com.github.johrstrom.collector.gui.AbstractCollectorTable;\nimport com"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/gui/PrometheusListenerGui.java",
    "chars": 5828,
    "preview": "/*\n * Licensed to the Apache Software Foundation (ASF) under one or more\n * contributor license agreements.  See the NOT"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/updater/AbstractUpdater.java",
    "chars": 4481,
    "preview": "package com.github.johrstrom.listener.updater;\n\nimport com.github.johrstrom.collector.JMeterCollectorRegistry;\nimport co"
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/updater/AggregatedTypeUpdater.java",
    "chars": 1707,
    "preview": "package com.github.johrstrom.listener.updater;\n\nimport com.github.johrstrom.listener.ListenerCollectorConfig;\nimport io."
  },
  {
    "path": "src/main/java/com/github/johrstrom/listener/updater/CountTypeUpdater.java",
    "chars": 2373,
    "preview": "package com.github.johrstrom.listener.updater;\n\nimport com.github.johrstrom.collector.SuccessRatioCollector;\nimport com."
  },
  {
    "path": "src/test/java/com/github/johrstrom/collector/BaseCollectorConfigTest.java",
    "chars": 7210,
    "preview": "package com.github.johrstrom.collector;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig.JMeterCollectorType;\n"
  },
  {
    "path": "src/test/java/com/github/johrstrom/collector/JMeterCollectorRegistryTest.java",
    "chars": 936,
    "preview": "package com.github.johrstrom.collector;\n\nimport com.github.johrstrom.test.TestUtilities;\nimport io.prometheus.client.Col"
  },
  {
    "path": "src/test/java/com/github/johrstrom/collector/SuccessRatioCollectorTest.java",
    "chars": 3352,
    "preview": "package com.github.johrstrom.collector;\n\nimport com.github.johrstrom.listener.ListenerCollectorConfig;\nimport com.github"
  },
  {
    "path": "src/test/java/com/github/johrstrom/config/PrometheusMetricsConfigTest.java",
    "chars": 721,
    "preview": "package com.github.johrstrom.config;\n\nimport com.github.johrstrom.test.TestUtilities;\nimport org.junit.Assert;\nimport or"
  },
  {
    "path": "src/test/java/com/github/johrstrom/config/gui/ConfigGuiTest.java",
    "chars": 663,
    "preview": "package com.github.johrstrom.config.gui;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport org.apache.j"
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/ListenerCollectorConfigTest.java",
    "chars": 615,
    "preview": "package com.github.johrstrom.listener;\n\nimport com.github.johrstrom.test.TestUtilities;\nimport org.junit.Assert;\nimport "
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/PrometheusListenerTest.java",
    "chars": 11188,
    "preview": "package com.github.johrstrom.listener;\n\nimport com.github.johrstrom.collector.JMeterCollectorRegistry;\nimport com.github"
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/PrometheusServerTest.java",
    "chars": 1135,
    "preview": "package com.github.johrstrom.listener;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputS"
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/gui/ListenerGuiTest.java",
    "chars": 579,
    "preview": "package com.github.johrstrom.listener.gui;\n\nimport org.apache.jmeter.util.JMeterUtils;\nimport org.junit.Assert;\nimport o"
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/updater/AbstractUpdaterTest.java",
    "chars": 3416,
    "preview": "package com.github.johrstrom.listener.updater;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.gi"
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/updater/AggregatedTypeUpdaterTest.java",
    "chars": 11189,
    "preview": "package com.github.johrstrom.listener.updater;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.gi"
  },
  {
    "path": "src/test/java/com/github/johrstrom/listener/updater/CountTypeUpdaterTest.java",
    "chars": 13923,
    "preview": "package com.github.johrstrom.listener.updater;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.gi"
  },
  {
    "path": "src/test/java/com/github/johrstrom/test/NOOPThreadMonitor.java",
    "chars": 286,
    "preview": "package com.github.johrstrom.test;\n\nimport org.apache.jmeter.threads.JMeterThread;\nimport org.apache.jmeter.threads.JMet"
  },
  {
    "path": "src/test/java/com/github/johrstrom/test/TestUtilities.java",
    "chars": 9311,
    "preview": "package com.github.johrstrom.test;\n\nimport com.github.johrstrom.collector.BaseCollectorConfig;\nimport com.github.johrstr"
  },
  {
    "path": "src/test/resources/bin/jmeter.properties",
    "chars": 52376,
    "preview": "################################################################################\n# Apache JMeter Property file\n#########"
  },
  {
    "path": "src/test/resources/bin/saveservice.properties",
    "chars": 24184,
    "preview": "#---------------------------------------------------------\n#         SAVESERVICE PROPERTIES - JMETER INTERNAL USE ONLY\n#"
  },
  {
    "path": "src/test/resources/bin/upgrade.properties",
    "chars": 7379,
    "preview": "# Class, property and value upgrade equivalences.\n\n##   Licensed to the Apache Software Foundation (ASF) under one or mo"
  },
  {
    "path": "src/test/resources/bin/user.properties",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/test/resources/log4j2.xml",
    "chars": 3962,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n   Licensed to the Apache Software Foundation (ASF) under one or more\n   con"
  }
]

About this extraction

This page contains the full source code of the johrstrom/jmeter-prometheus-plugin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 51 files (341.2 KB), approximately 85.4k tokens, and a symbol index with 268 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!