diff --git a/contributions/metric/prometheus/.gitignore b/contributions/metric/prometheus/.gitignore
new file mode 100644
index 00000000000..bcc1958da5f
--- /dev/null
+++ b/contributions/metric/prometheus/.gitignore
@@ -0,0 +1,27 @@
+# Compiled class file
+target/*
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# hidden files
+.*
diff --git a/contributions/metric/prometheus/README.md b/contributions/metric/prometheus/README.md
new file mode 100644
index 00000000000..c8837cfdf0d
--- /dev/null
+++ b/contributions/metric/prometheus/README.md
@@ -0,0 +1,86 @@
+
+# Athenz metric for Prometheus
+Athenz Yahoo Server metrics interface implementation for Prometheus
+
+
+
+- [Athenz metric for Prometheus](#athenz-metric-for-prometheus)
+ - [Usage](#usage)
+ - [Build](#build)
+ - [Integrate with Athenz](#integrate-with-athenz)
+ - [For developer](#for-developer)
+ - [Test coverage](#test-coverage)
+ - [Performance test result](#performance-test-result)
+ - [Design concerns](#design-concerns)
+ - [example main for integration test](#example-main-for-integration-test)
+
+
+
+
+## Usage
+
+
+### Build
+```bash
+mvn clean package
+ls ./target/athenz_metrics_prometheus-*.jar
+```
+
+
+### Integrate with Athenz
+1. add `athenz_metrics_prometheus-*.jar` in Athenz server's classpath
+1. overwrite existing system property
+ ```properties
+ # ZMS server
+ athenz.zms.metric_factory_class=com.yahoo.athenz.common.metrics.impl.prometheus.PrometheusMetricFactory
+
+ # ZTS server
+ athenz.zts.metric_factory_class=com.yahoo.athenz.common.metrics.impl.prometheus.PrometheusMetricFactory
+ ```
+1. add system property for `PrometheusMetric`
+ ```properties
+ # enable PrometheusMetric class
+ athenz.metrics.prometheus.enable=true
+ # export JVM metrics
+ athenz.metrics.prometheus.jvm.enable=true
+ # the Prometheus /metrics endpoint
+ athenz.metrics.prometheus.http_server.enable=true
+ athenz.metrics.prometheus.http_server.port=8181
+ # Prometheus metric prefix
+ athenz.metrics.prometheus.namespace=athenz_zms
+ # for dev. env. ONLY, record Athenz domain data as label
+ athenz.metrics.prometheus.label.request_domain_name.enable=false
+ athenz.metrics.prometheus.label.principal_domain_name.enable=false
+ ```
+1. verify setup: `curl localhost:8181/metrics`
+1. add job in your Prometheus server
+ ```yaml
+ scrape_configs:
+ - job_name: 'athenz-server'
+ scrape_interval: 10s
+ honor_labels: true
+ static_configs:
+ - targets: ['athenz.server.domain:8181']
+ ```
+
+
+## For developer
+
+
+### Test coverage
+```bash
+mvn clover:instrument clover:aggregate clover:clover clover:check
+open ./target/site/clover/index.html
+```
+
+
+### Performance test result
+- [performance.md](./doc/performance.md)
+
+
+### Design concerns
+- [design-concerns.md](./doc/design-concerns.md)
+
+
+### example main for integration test
+- [example-main.md](./doc/example-main.md)
diff --git a/contributions/metric/prometheus/doc/assets/jmeter/metric-2000-domain/summary.csv b/contributions/metric/prometheus/doc/assets/jmeter/metric-2000-domain/summary.csv
new file mode 100644
index 00000000000..8a803c01e62
--- /dev/null
+++ b/contributions/metric/prometheus/doc/assets/jmeter/metric-2000-domain/summary.csv
@@ -0,0 +1,3 @@
+Label,# Samples,Average,Min,Max,Std. Dev.,Error %,Throughput,Received KB/sec,Sent KB/sec,Avg. Bytes
+get metrics,537,223,122,501,46.75,0.000%,4.47139,2819.47,0.62,645691.9
+TOTAL,537,223,122,501,46.75,0.000%,4.47139,2819.47,0.62,645691.9
diff --git a/contributions/metric/prometheus/doc/assets/jmeter/metric-no-label/summary.csv b/contributions/metric/prometheus/doc/assets/jmeter/metric-no-label/summary.csv
new file mode 100644
index 00000000000..b2c16e13902
--- /dev/null
+++ b/contributions/metric/prometheus/doc/assets/jmeter/metric-no-label/summary.csv
@@ -0,0 +1,3 @@
+Label,# Samples,Average,Min,Max,Std. Dev.,Error %,Throughput,Received KB/sec,Sent KB/sec,Avg. Bytes
+get metrics,5372,22,18,646,10.21,0.000%,44.76443,421.94,6.25,9651.9
+TOTAL,5372,22,18,646,10.21,0.000%,44.76443,421.94,6.25,9651.9
diff --git a/contributions/metric/prometheus/doc/assets/jmeter/no_ops/summary.csv b/contributions/metric/prometheus/doc/assets/jmeter/no_ops/summary.csv
new file mode 100644
index 00000000000..e98deb24341
--- /dev/null
+++ b/contributions/metric/prometheus/doc/assets/jmeter/no_ops/summary.csv
@@ -0,0 +1,5 @@
+Label,# Samples,Average,Min,Max,Std. Dev.,Error %,Throughput,Received KB/sec,Sent KB/sec,Avg. Bytes
+get user token,48,429,162,707,153.93,0.000%,30.88803,22.50,6.94,745.9
+get domain list,61130,26,10,333,13.96,0.026%,510.33953,75.11,392.08,150.7
+get role list,61113,66,33,576,22.58,0.034%,510.39361,57.79,401.56,115.9
+TOTAL,122291,46,10,707,28.67,0.030%,1018.69268,132.88,791.83,133.6
diff --git a/contributions/metric/prometheus/doc/assets/jmeter/prometheus/summary.csv b/contributions/metric/prometheus/doc/assets/jmeter/prometheus/summary.csv
new file mode 100644
index 00000000000..d541a642fd0
--- /dev/null
+++ b/contributions/metric/prometheus/doc/assets/jmeter/prometheus/summary.csv
@@ -0,0 +1,5 @@
+Label,# Samples,Average,Min,Max,Std. Dev.,Error %,Throughput,Received KB/sec,Sent KB/sec,Avg. Bytes
+get user token,48,399,125,988,199.86,0.000%,27.11864,19.75,6.09,745.9
+get domain list,89780,27,10,642,16.25,0.012%,499.12717,73.27,383.53,150.3
+get role list,89769,68,32,536,23.14,0.035%,499.19923,56.53,392.76,116.0
+TOTAL,179597,47,10,988,29.35,0.023%,997.47295,129.85,775.48,133.3
diff --git a/contributions/metric/prometheus/doc/design-concerns.md b/contributions/metric/prometheus/doc/design-concerns.md
new file mode 100644
index 00000000000..78123cfe0d8
--- /dev/null
+++ b/contributions/metric/prometheus/doc/design-concerns.md
@@ -0,0 +1,20 @@
+# Design concerns
+
+1. metric name format
+ 1. `{namespace}_{metric}_{unit}`
+ 1. namespace = set by system properties
+ 1. metric hard coded inside Athenz
+ 1. unit = `total` or `seconds`
+ 1. reference: [Metric and label naming | Prometheus](https://prometheus.io/docs/practices/naming/#metric-names)
+1. labels for `requestDomainName` and `principalDomainName`
+ 1. disable by default
+ 1. reasons
+ 1. not a suggested way in Prometheus
+ - [Instrumentation#Use labels | Prometheus](https://prometheus.io/docs/practices/instrumentation/#use-labels)
+ - [Instrumentation#Do not overuse labels | Prometheus](https://prometheus.io/docs/practices/instrumentation/#do-not-overuse-labels)
+ 1. the response's size of the `/metrics` request will become very large, causing bandwidth/latency problem at the prometheus side
+ - [performance test result](./performance.md#without-domain-vs-with-2000-domain-prometheus-endpoint)
+1. Prometheus pull as default
+ 1. require same network (Prometheus server, Athenz server)
+ 1. the suggested deployment for Prometheus
+ 1. open firewall port for Grafana for query from prometheus server
diff --git a/contributions/metric/prometheus/doc/example-main.md b/contributions/metric/prometheus/doc/example-main.md
new file mode 100644
index 00000000000..fba7a8eabb4
--- /dev/null
+++ b/contributions/metric/prometheus/doc/example-main.md
@@ -0,0 +1,70 @@
+# Example main
+
+`Main.java`
+```java
+package com.yahoo.athenz.common.metrics;
+
+import com.yahoo.athenz.common.metrics.Metric;
+import com.yahoo.athenz.common.metrics.impl.prometheus.PrometheusMetricFactory;
+
+public class Main {
+ public static void main(String[] args) throws InterruptedException {
+ System.out.println("PrometheusMetric start");
+
+ PrometheusMetricFactory pmf = new PrometheusMetricFactory();
+ Metric pm = pmf.create();
+
+ // counter
+ pm.increment("request_no_label");
+ pm.increment("request01", null, 5);
+ pm.increment("request01", "domain01", 10);
+ pm.increment("request01", "domain02", 20);
+
+ // timer
+ Object timer = pm.startTiming("timer_test", null);
+ Thread.sleep(99L);
+ pm.stopTiming(timer);
+
+ Object timerD = pm.startTiming("timer_test_domain", "domain01");
+ Thread.sleep(111L);
+ pm.stopTiming(timerD);
+
+ // flush
+ System.out.println("before flush...");
+ pm.flush();
+ System.out.println("If you are using pull exporter, run 'curl localhost:8181/metrics' to verify");
+
+ // quit
+ System.out.println("wait 1 min, before quit...");
+ Thread.sleep(1L * 1000 * 60);
+ pm.quit();
+ }
+}
+```
+
+## Run
+```bash
+cat > "$(git rev-parse --show-toplevel)/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/Main.java"
+# copy and paste the Main.java's content
+cd "$(git rev-parse --show-toplevel)/contributions/metric/prometheus"
+mvn package exec:java -Dexec.mainClass="com.yahoo.athenz.common.metrics.Main"
+```
+
+## sample output (with default values)
+```bash
+$ curl localhost:8181/metrics
+# HELP athenz_server_request_no_label_total request_no_label_total
+# TYPE athenz_server_request_no_label_total counter
+athenz_server_request_no_label_total{domain="",principal="",} 1.0
+# HELP athenz_server_request01_total request01_total
+# TYPE athenz_server_request01_total counter
+athenz_server_request01_total{domain="",principal="",} 35.0
+# HELP athenz_server_timer_test_domain_seconds timer_test_domain_seconds
+# TYPE athenz_server_timer_test_domain_seconds summary
+athenz_server_timer_test_domain_seconds_count{domain="",principal="",} 1.0
+athenz_server_timer_test_domain_seconds_sum{domain="",principal="",} 0.113545231
+# HELP athenz_server_timer_test_seconds timer_test_seconds
+# TYPE athenz_server_timer_test_seconds summary
+athenz_server_timer_test_seconds_count{domain="",principal="",} 1.0
+athenz_server_timer_test_seconds_sum{domain="",principal="",} 0.101996235
+```
diff --git a/contributions/metric/prometheus/doc/performance.md b/contributions/metric/prometheus/doc/performance.md
new file mode 100644
index 00000000000..63af886be18
--- /dev/null
+++ b/contributions/metric/prometheus/doc/performance.md
@@ -0,0 +1,17 @@
+# Test Summary
+
+## NoOps V.S. Prometheus (Athenz endpoint)
+- [Using NoOpMetric](./assets/jmeter/no_ops/summary.csv)
+- [Using PrometheusMetric](./assets/jmeter/prometheus/summary.csv)
+
+### Conclusion
+- Throughput: (499-510)/510 * 100% = `-2.16%`
+- **not much performance impact on existing API**
+
+## without domain V.S. with 2000 domain (prometheus endpoint)
+- [label disabled](./assets/jmeter/metric-no-label/summary.csv)
+- [label enabled, with 2000 domain as label](./assets/jmeter/metric-2000-domain/summary.csv)
+
+### Conclusion
+- Throughput: (4-44)/44 * 100% = `-90.9%`
+- **should not enable metric label for Athenz domain**
diff --git a/contributions/metric/prometheus/pom.xml b/contributions/metric/prometheus/pom.xml
new file mode 100644
index 00000000000..a8085cbc7cd
--- /dev/null
+++ b/contributions/metric/prometheus/pom.xml
@@ -0,0 +1,235 @@
+
+
+
+4.0.0
+
+ com.yahoo.athenz
+ athenz_metrics_prometheus
+ jar
+ 1.0.1
+ athenz_metrics_prometheus
+ Athenz Yahoo Server Metrics Interface implementation for Prometheus
+
+
+ 1.8.24
+ UTF-8
+ UTF-8
+ 0.6.0
+ 1.7.28
+ 1.2.3
+ 6.14.3
+ 1.9.5
+ 4.3.1
+ 1.8
+ 1.8
+ ${env.DO_NOT_PUBLISH}
+
+
+
+
+
+
+ com.yahoo.athenz
+ athenz-server-common
+ ${athenz.version}
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
+
+ io.prometheus
+ simpleclient
+ ${prometheus.version}
+
+
+
+ io.prometheus
+ simpleclient_hotspot
+ ${prometheus.version}
+
+
+
+ io.prometheus
+ simpleclient_httpserver
+ ${prometheus.version}
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ test
+
+
+
+ org.testng
+ testng
+ ${testng.version}
+ test
+
+
+ junit
+ junit
+
+
+
+
+ org.mockito
+ mockito-all
+ ${mockito.version}
+ test
+
+
+ org.slf4j
+ slf4j-log4j12
+
+
+ junit
+ junit
+
+
+
+
+
+
+
+
+ coverage
+
+ true
+
+
+
+ coverage
+
+
+
+
+
+ org.openclover
+ clover-maven-plugin
+
+ 80%
+
+
+
+ verify
+
+ check
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.openclover
+ clover-maven-plugin
+ ${clover.version}
+
+
+
+
+
+ org.openclover
+ clover-maven-plugin
+ ${clover.version}
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.1.1
+
+
+ copy
+ package
+
+ copy-dependencies
+
+
+ runtime
+ false
+ false
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.22.2
+
+
+ default-test
+
+ test
+
+
+
+
+ surefire.testng.verbose
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+ org.openclover
+ clover-maven-plugin
+ ${clover.version}
+
+
+
+
+
+
+
+ false
+
+ bintray-yahoo-maven
+ bintray
+ http://yahoo.bintray.com/maven
+
+
+
+
diff --git a/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusExporter.java b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusExporter.java
new file mode 100644
index 00000000000..88842ce59e6
--- /dev/null
+++ b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusExporter.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+public interface PrometheusExporter {
+
+ /**
+ * Flush any buffered metrics to destination.
+ */
+ public void flush();
+
+ /**
+ * Flush buffers and shutdown any tasks.
+ */
+ public void quit();
+
+}
diff --git a/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetric.java b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetric.java
new file mode 100644
index 00000000000..12c03cec15b
--- /dev/null
+++ b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetric.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+import java.util.Objects;
+import java.util.concurrent.ConcurrentMap;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.Counter;
+import io.prometheus.client.SimpleCollector;
+import io.prometheus.client.Summary;
+
+import com.yahoo.athenz.common.metrics.Metric;
+
+public class PrometheusMetric implements Metric {
+
+ public static final String REQUEST_DOMAIN_LABEL_NAME = "domain";
+ public static final String PRINCIPAL_DOMAIN_LABEL_NAME = "principal";
+
+ public static final String METRIC_NAME_DELIMITER = "_";
+ public static final String COUNTER_SUFFIX = "total";
+ public static final String TIMER_UNIT = "seconds";
+
+ private final CollectorRegistry registry;
+ private final ConcurrentMap namesToCollectors;
+ private final PrometheusExporter exporter;
+ private String namespace;
+ private boolean isLabelRequestDomainNameEnable;
+ private boolean isLabelPrincipalDomainNameEnable;
+
+ /**
+ * @param registry CollectorRegistry of all metrics
+ * @param exporter Prometheus metrics exporter
+ * @param namespace prefix of all metrics
+ */
+ public PrometheusMetric(CollectorRegistry registry, ConcurrentMap namesToCollectors, PrometheusExporter exporter, String namespace) {
+ this(registry, namesToCollectors, exporter, namespace, false, false);
+ }
+
+ /**
+ * @param registry CollectorRegistry of all metrics
+ * @param exporter Prometheus metrics exporter
+ * @param namespace prefix of all metrics
+ * @param isLabelRequestDomainNameEnable enable requestDomainName label
+ * @param isLabelPrincipalDomainNameEnable enable principalDomainName label
+ */
+ public PrometheusMetric(CollectorRegistry registry, ConcurrentMap namesToCollectors, PrometheusExporter exporter, String namespace, boolean isLabelRequestDomainNameEnable, boolean isLabelPrincipalDomainNameEnable) {
+ this.registry = registry;
+ this.namesToCollectors = namesToCollectors;
+ this.exporter = exporter;
+ this.namespace = namespace;
+
+ this.isLabelRequestDomainNameEnable = isLabelRequestDomainNameEnable;
+ this.isLabelPrincipalDomainNameEnable = isLabelPrincipalDomainNameEnable;
+ }
+
+ @Override
+ public void increment(String metricName) {
+ increment(metricName, null, 1);
+ }
+
+ @Override
+ public void increment(String metricName, String requestDomainName) {
+ increment(metricName, requestDomainName, 1);
+ }
+
+ @Override
+ public void increment(String metricName, String requestDomainName, String principalDomainName) {
+ increment(metricName, requestDomainName, principalDomainName, 1);
+ }
+
+ @Override
+ public void increment(String metricName, String requestDomainName, int count) {
+ increment(metricName, requestDomainName, null, count);
+ }
+
+ @Override
+ public void increment(String metricName, String requestDomainName, String principalDomainName, int count) {
+ // prometheus does not allow null labels
+ requestDomainName = (this.isLabelRequestDomainNameEnable) ? Objects.toString(requestDomainName, "") : "";
+ principalDomainName = (this.isLabelPrincipalDomainNameEnable) ? Objects.toString(principalDomainName, "") : "";
+
+ metricName = this.normalizeCounterMetricName(metricName);
+ Counter counter = (Counter) createOrGetCollector(metricName, Counter.build());
+ counter.labels(requestDomainName, principalDomainName).inc(count);
+ }
+
+ @Override
+ public Object startTiming(String metricName, String requestDomainName) {
+ return startTiming(metricName, requestDomainName, null);
+ }
+
+ @Override
+ public Object startTiming(String metricName, String requestDomainName, String principalDomainName) {
+ // prometheus does not allow null labels
+ requestDomainName = (this.isLabelRequestDomainNameEnable) ? Objects.toString(requestDomainName, "") : "";
+ principalDomainName = (this.isLabelPrincipalDomainNameEnable) ? Objects.toString(principalDomainName, "") : "";
+
+ metricName = this.normalizeTimerMetricName(metricName);
+ Summary summary = (Summary) createOrGetCollector(metricName, Summary.build()
+ // .quantile(0.5, 0.05)
+ // .quantile(0.9, 0.01)
+ );
+ return summary.labels(requestDomainName, principalDomainName).startTimer();
+ }
+
+ @Override
+ public void stopTiming(Object timerObj) {
+ if (timerObj == null) {
+ return;
+ }
+ Summary.Timer timer = (Summary.Timer) timerObj;
+ timer.observeDuration();
+ }
+
+ @Override
+ public void stopTiming(Object timerObj, String requestDomainName, String principalDomainName) {
+ stopTiming(timerObj);
+ }
+
+ @Override
+ public void flush() {
+ if (this.exporter != null) {
+ this.exporter.flush();
+ }
+ }
+
+ @Override
+ public void quit() {
+ if (this.exporter != null) {
+ this.exporter.flush();
+ this.exporter.quit();
+ }
+ }
+
+ /**
+ * Create collector and register it to the registry.
+ * This is needed since Athenz metric names are defined on runtime and we need the same collector object to record the data.
+ * @param metricName Name of the metric
+ * @param builder Prometheus Collector Builder
+ */
+ private Collector createOrGetCollector(String metricName, SimpleCollector.Builder, ?> builder) {
+ String key = metricName;
+ ConcurrentMap map = this.namesToCollectors;
+ Collector collector = map.get(key);
+
+ // double checked locking
+ if (collector == null) {
+ synchronized (map) {
+ if (!map.containsKey(key)) {
+ // create
+ builder = builder
+ .namespace(this.namespace)
+ .name(metricName)
+ .help(metricName)
+ .labelNames(REQUEST_DOMAIN_LABEL_NAME, PRINCIPAL_DOMAIN_LABEL_NAME);
+ collector = builder.register(this.registry);
+ // put
+ map.put(key, collector);
+ } else {
+ // get
+ collector = map.get(key);
+ }
+ }
+ };
+
+ return collector;
+ }
+
+ /**
+ * Create counter metric name that follows prometheus standard
+ * @param metricName Name of the counter metric
+ */
+ private String normalizeCounterMetricName(String metricName) {
+ return metricName + METRIC_NAME_DELIMITER + COUNTER_SUFFIX;
+ }
+
+ /**
+ * Create timer metric name that follows prometheus standard
+ * @param metricName Name of the timer metric
+ */
+ private String normalizeTimerMetricName(String metricName) {
+ return metricName + METRIC_NAME_DELIMITER + TIMER_UNIT;
+ }
+}
diff --git a/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactory.java b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactory.java
new file mode 100644
index 00000000000..af444a30a26
--- /dev/null
+++ b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactory.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+import java.io.IOException;
+import java.util.concurrent.ConcurrentHashMap;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.hotspot.*;
+
+import com.yahoo.athenz.common.metrics.Metric;
+import com.yahoo.athenz.common.metrics.MetricFactory;
+import com.yahoo.athenz.common.metrics.impl.NoOpMetric;
+
+public class PrometheusMetricFactory implements MetricFactory {
+
+ public static final String SYSTEM_PROP_PREFIX = "athenz.metrics.prometheus.";
+ public static final String ENABLE_PROP = "enable";
+
+ public static final String JVM_ENABLE_PROP = "jvm.enable";
+
+ public static final String HTTP_SERVER_ENABLE_PROP = "http_server.enable";
+ public static final String HTTP_SERVER_PORT_PROP = "http_server.port";
+
+ public static final String NAMESPACE_PROP = "namespace";
+ public static final String LABEL_REQUEST_DOMAIN_NAME_ENABLE_PROP = "label.request_domain_name.enable";
+ public static final String LABEL_PRINCIPAL_DOMAIN_NAME_ENABLE_PROP = "label.principal_domain_name.enable";
+
+ @Override
+ public Metric create() {
+ boolean isEnable = Boolean.valueOf(getProperty(ENABLE_PROP, "true"));
+ if (!isEnable) {
+ return new NoOpMetric();
+ }
+
+ // metric registry, should have 1-to-1 relationship with ConcurrentHashMap namesToCollectors for collector lookup
+ CollectorRegistry registry = new CollectorRegistry();
+ ConcurrentHashMap namesToCollectors = new ConcurrentHashMap<>();
+
+ // register JVM metrics
+ if (Boolean.valueOf(getProperty(JVM_ENABLE_PROP, "false"))) {
+ // for version = 0.6.1
+ // DefaultExports.register(registry);
+
+ // for version <= 0.6.0
+ new StandardExports().register(registry);
+ new MemoryPoolsExports().register(registry);
+ new MemoryAllocationExports().register(registry);
+ new BufferPoolsExports().register(registry);
+ new GarbageCollectorExports().register(registry);
+ new ThreadExports().register(registry);
+ new ClassLoadingExports().register(registry);
+ new VersionInfoExports().register(registry);
+ }
+
+ // exporter
+ PrometheusExporter exporter = null;
+ if (Boolean.valueOf(getProperty(HTTP_SERVER_ENABLE_PROP, "true"))) {
+ // HTTP server for pulling
+ int pullingPort = Integer.valueOf(getProperty(HTTP_SERVER_PORT_PROP, "8181"));
+ try {
+ exporter = new PrometheusPullServer(pullingPort, registry);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // prometheus metric class
+ String namespace = getProperty(NAMESPACE_PROP, "athenz_server");
+ boolean isLabelRequestDomainNameEnable = Boolean.valueOf(getProperty(LABEL_REQUEST_DOMAIN_NAME_ENABLE_PROP, "false"));
+ boolean isLabelPrincipalDomainNameEnable = Boolean.valueOf(getProperty(LABEL_PRINCIPAL_DOMAIN_NAME_ENABLE_PROP, "false"));
+ return new PrometheusMetric(registry, namesToCollectors, exporter, namespace, isLabelRequestDomainNameEnable, isLabelPrincipalDomainNameEnable);
+
+ }
+
+ /**
+ * Get system property related to PrometheusMetric. Property name: ${prefix}.${key}
+ * @param key key without prefix
+ * @param def default value
+ * @return system property value
+ */
+ public static String getProperty(String key, String def) {
+ return System.getProperty(SYSTEM_PROP_PREFIX + key, def);
+ }
+}
diff --git a/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServer.java b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServer.java
new file mode 100644
index 00000000000..7de29304ad8
--- /dev/null
+++ b/contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServer.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.exporter.HTTPServer;
+
+public class PrometheusPullServer implements PrometheusExporter {
+
+ private HTTPServer server;
+
+ public PrometheusPullServer(int pullingPort, CollectorRegistry registry) throws IOException {
+ boolean isDaemon = true;
+ this.server = new HTTPServer(new InetSocketAddress(pullingPort), registry, isDaemon);
+ }
+
+ @Override
+ public void flush() {
+ // should response to pull request from prometheus only, no action on flush
+ }
+
+ @Override
+ public void quit() {
+ this.server.stop();
+ }
+
+}
diff --git a/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactoryTest.java b/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactoryTest.java
new file mode 100644
index 00000000000..cf355c53a82
--- /dev/null
+++ b/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactoryTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import io.prometheus.client.CollectorRegistry;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.BindException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+import com.yahoo.athenz.common.metrics.Metric;
+import com.yahoo.athenz.common.metrics.impl.NoOpMetric;
+
+public class PrometheusMetricFactoryTest {
+
+ private static String setProperty(String key, String value) {
+ return System.setProperty(PrometheusMetricFactory.SYSTEM_PROP_PREFIX + key, value);
+ }
+
+ private static String clearProperty(String key) {
+ return System.clearProperty(PrometheusMetricFactory.SYSTEM_PROP_PREFIX + key);
+ }
+
+ @Test
+ public void testGetProperty() {
+ String expected = "false";
+
+ setProperty(PrometheusMetricFactory.ENABLE_PROP, expected);
+ String prop = PrometheusMetricFactory.getProperty(PrometheusMetricFactory.ENABLE_PROP, "true");
+ clearProperty(PrometheusMetricFactory.ENABLE_PROP);
+
+ // assertions
+ Assert.assertEquals(prop, expected);
+ }
+
+ @Test
+ public void testCreateMetricDisable() {
+ Class> expected = NoOpMetric.class;
+
+ setProperty(PrometheusMetricFactory.ENABLE_PROP, "false");
+ Metric metric = new PrometheusMetricFactory().create();
+ clearProperty(PrometheusMetricFactory.ENABLE_PROP);
+
+ // assertions
+ Assert.assertEquals(metric.getClass(), expected);
+ }
+
+ @Test
+ public void testCreateJvmMetricEnable()
+ throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+
+ setProperty(PrometheusMetricFactory.JVM_ENABLE_PROP, "true");
+ setProperty(PrometheusMetricFactory.HTTP_SERVER_ENABLE_PROP, "false");
+ PrometheusMetric metric = (PrometheusMetric) new PrometheusMetricFactory().create();
+ clearProperty(PrometheusMetricFactory.JVM_ENABLE_PROP);
+ clearProperty(PrometheusMetricFactory.HTTP_SERVER_ENABLE_PROP);
+
+ Field registryField = metric.getClass().getDeclaredField("registry");
+ registryField.setAccessible(true);
+ CollectorRegistry registry = (CollectorRegistry) registryField.get(metric);
+
+ // assertions
+ Assert.assertNotNull(registry.getSampleValue("process_cpu_seconds_total"));
+ }
+
+ @Test(expectedExceptions = { RuntimeException.class, BindException.class }, expectedExceptionsMessageRegExp = ".* Address already in use.*")
+ public void testCreateErrorUsedPort() throws IOException {
+ int port = 18181;
+ try (Socket socket = new Socket()) {
+ socket.bind(new InetSocketAddress(port));
+
+ setProperty(PrometheusMetricFactory.HTTP_SERVER_ENABLE_PROP, "true");
+ setProperty(PrometheusMetricFactory.HTTP_SERVER_PORT_PROP, String.valueOf(port));
+ new PrometheusMetricFactory().create();
+ clearProperty(PrometheusMetricFactory.HTTP_SERVER_ENABLE_PROP);
+ clearProperty(PrometheusMetricFactory.HTTP_SERVER_PORT_PROP);
+ }
+ }
+
+ @Test
+ public void testCreate() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
+ Class expectedExporterClass = PrometheusPullServer.class;
+ String expectedNamespace = "expected_athenz_server";
+ boolean expectedIsLabelRequestDomainNameEnable = true;
+ boolean expectedIsLabelPrincipalDomainNameEnable = true;
+
+ PrometheusMetric metric = null;
+ try {
+ setProperty(PrometheusMetricFactory.HTTP_SERVER_ENABLE_PROP, "true");
+ setProperty(PrometheusMetricFactory.NAMESPACE_PROP, expectedNamespace);
+ setProperty(PrometheusMetricFactory.LABEL_REQUEST_DOMAIN_NAME_ENABLE_PROP, String.valueOf(expectedIsLabelRequestDomainNameEnable));
+ setProperty(PrometheusMetricFactory.LABEL_PRINCIPAL_DOMAIN_NAME_ENABLE_PROP, String.valueOf(expectedIsLabelPrincipalDomainNameEnable));
+ metric = (PrometheusMetric) new PrometheusMetricFactory().create();
+ clearProperty(PrometheusMetricFactory.HTTP_SERVER_ENABLE_PROP);
+ clearProperty(PrometheusMetricFactory.NAMESPACE_PROP);
+ clearProperty(PrometheusMetricFactory.LABEL_REQUEST_DOMAIN_NAME_ENABLE_PROP);
+ clearProperty(PrometheusMetricFactory.LABEL_PRINCIPAL_DOMAIN_NAME_ENABLE_PROP);
+
+ // assertions
+ Field exporterField = metric.getClass().getDeclaredField("exporter");
+ exporterField.setAccessible(true);
+ PrometheusExporter exporter = (PrometheusExporter) exporterField.get(metric);
+ Assert.assertEquals(exporter.getClass(), expectedExporterClass);
+
+ Field namespaceField = metric.getClass().getDeclaredField("namespace");
+ namespaceField.setAccessible(true);
+ String namespace = (String) namespaceField.get(metric);
+ Assert.assertEquals(namespace, expectedNamespace);
+
+ Field isLabelRequestDomainNameEnableField = metric.getClass().getDeclaredField("isLabelRequestDomainNameEnable");
+ isLabelRequestDomainNameEnableField.setAccessible(true);
+ boolean isLabelRequestDomainNameEnable = (Boolean) isLabelRequestDomainNameEnableField.get(metric);
+ Assert.assertEquals(isLabelRequestDomainNameEnable, expectedIsLabelRequestDomainNameEnable);
+
+ Field isLabelPrincipalDomainNameEnableField = metric.getClass().getDeclaredField("isLabelPrincipalDomainNameEnable");
+ isLabelPrincipalDomainNameEnableField.setAccessible(true);
+ boolean isLabelPrincipalDomainNameEnable = (Boolean) isLabelPrincipalDomainNameEnableField.get(metric);
+ Assert.assertEquals(isLabelPrincipalDomainNameEnable, expectedIsLabelPrincipalDomainNameEnable);
+ } finally {
+ // cleanup
+ if (metric != null) {
+ metric.quit();
+ }
+ }
+ }
+
+}
diff --git a/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricTest.java b/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricTest.java
new file mode 100644
index 00000000000..b0a40c1bb6d
--- /dev/null
+++ b/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricTest.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.Counter;
+import io.prometheus.client.SimpleCollector;
+import io.prometheus.client.Summary;
+
+public class PrometheusMetricTest {
+
+ private String[] labelNames = {
+ PrometheusMetric.REQUEST_DOMAIN_LABEL_NAME,
+ PrometheusMetric.PRINCIPAL_DOMAIN_LABEL_NAME
+ };
+
+ @Test
+ public void testConstructor() {
+ CollectorRegistry registry = mock(CollectorRegistry.class);
+ ConcurrentHashMap namesToCollectors = new ConcurrentHashMap<>();
+ PrometheusExporter exporter = mock(PrometheusExporter.class);
+ String namespace = "constructor_test";
+ boolean isLabelRequestDomainNameEnable = true;
+ boolean isLabelPrincipalDomainNameEnable = true;
+
+ BiFunction getFieldValue = (f, object) -> {
+ try {
+ f.setAccessible(true);
+ return f.get(object);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ };
+
+ PrometheusMetric metric_1 = new PrometheusMetric(registry, namesToCollectors, exporter, namespace);
+ // assertions
+ for (Field f : metric_1.getClass().getDeclaredFields()) {
+ switch (f.getName()) {
+ case "registry":
+ Assert.assertSame(getFieldValue.apply(f, metric_1), registry);
+ break;
+ case "namesToCollectors":
+ Assert.assertSame(getFieldValue.apply(f, metric_1), namesToCollectors);
+ break;
+ case "exporter":
+ Assert.assertSame(getFieldValue.apply(f, metric_1), exporter);
+ break;
+ case "namespace":
+ Assert.assertSame(getFieldValue.apply(f, metric_1), namespace);
+ break;
+ case "isLabelRequestDomainNameEnable":
+ Assert.assertEquals(getFieldValue.apply(f, metric_1), false);
+ break;
+ case "isLabelPrincipalDomainNameEnable":
+ Assert.assertEquals(getFieldValue.apply(f, metric_1), false);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // different signature
+ PrometheusMetric metric_2 = new PrometheusMetric(registry, namesToCollectors, exporter, namespace,
+ isLabelRequestDomainNameEnable, isLabelPrincipalDomainNameEnable);
+ // assertions
+ for (Field f : metric_2.getClass().getDeclaredFields()) {
+ switch (f.getName()) {
+ case "registry":
+ Assert.assertSame(getFieldValue.apply(f, metric_2), registry);
+ break;
+ case "namesToCollectors":
+ Assert.assertSame(getFieldValue.apply(f, metric_2), namesToCollectors);
+ break;
+ case "exporter":
+ Assert.assertSame(getFieldValue.apply(f, metric_2), exporter);
+ break;
+ case "namespace":
+ Assert.assertSame(getFieldValue.apply(f, metric_2), namespace);
+ break;
+ case "isLabelRequestDomainNameEnable":
+ Assert.assertEquals(getFieldValue.apply(f, metric_2), isLabelRequestDomainNameEnable);
+ break;
+ case "isLabelPrincipalDomainNameEnable":
+ Assert.assertEquals(getFieldValue.apply(f, metric_2), isLabelPrincipalDomainNameEnable);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Test
+ public void testCreateOrGetCollector() throws NoSuchMethodException, SecurityException, IllegalAccessException,
+ IllegalArgumentException, InvocationTargetException {
+ CollectorRegistry registry = new CollectorRegistry();
+ ConcurrentHashMap namesToCollectors = new ConcurrentHashMap<>();
+ PrometheusMetric metric = new PrometheusMetric(registry, namesToCollectors, null, "");
+ Method createOrGetCollector = metric.getClass().getDeclaredMethod("createOrGetCollector", String.class, SimpleCollector.Builder.class);
+ createOrGetCollector.setAccessible(true);
+
+ // test create
+ String metricName = "metric_test";
+ Counter.Builder builder = Counter.build();
+ double countValue = 110.110d;
+ Counter counter = (Counter) createOrGetCollector.invoke(metric, metricName, builder);
+ counter.labels("", "").inc(countValue);
+ // assertions
+ Assert.assertSame(counter, namesToCollectors.get(metricName));
+ Assert.assertEquals(registry.getSampleValue(metricName, this.labelNames, new String[]{"", ""}), countValue);
+
+ // test get
+ Counter counter_2 = (Counter) createOrGetCollector.invoke(metric, metricName, builder);
+ // assertions
+ Assert.assertSame(counter_2, namesToCollectors.get(metricName));
+ Assert.assertSame(counter_2, counter);
+ }
+
+ @Test
+ public void testIncrement() {
+ CollectorRegistry registry = new CollectorRegistry();
+ ConcurrentHashMap namesToCollectors = new ConcurrentHashMap<>();
+ String namespace = "metric_test";
+ int count = 24;
+
+ // 1. no labels (default)
+ PrometheusMetric metric_1 = new PrometheusMetric(registry, namesToCollectors, null, namespace);
+ String metricName_1 = "test_counter_1";
+ String fullMetricName_1 = namespace + "_" + metricName_1 + "_" + PrometheusMetric.COUNTER_SUFFIX;
+ String requestDomainName_1 = "request_domain_1";
+ String principalDomainName_1 = "principal_domain_1";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", principalDomainName_1 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, principalDomainName_1 }));
+ metric_1.increment(metricName_1);
+ metric_1.increment(metricName_1, requestDomainName_1);
+ metric_1.increment(metricName_1, null, principalDomainName_1);
+ metric_1.increment(metricName_1, requestDomainName_1, principalDomainName_1);
+ metric_1.increment(metricName_1, null, count);
+ metric_1.increment(metricName_1, requestDomainName_1, count);
+ metric_1.increment(metricName_1, null, principalDomainName_1, count);
+ metric_1.increment(metricName_1, requestDomainName_1, principalDomainName_1, count);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", "" }), 4d + 24d * 4d, 0.1d);
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", principalDomainName_1 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, principalDomainName_1 }));
+
+ // 2. only request domain
+ PrometheusMetric metric_2 = new PrometheusMetric(registry, namesToCollectors, null, namespace, true, false);
+ String metricName_2 = "test_counter_2";
+ String fullMetricName_2 = namespace + "_" + metricName_2 + "_" + PrometheusMetric.COUNTER_SUFFIX;
+ String requestDomainName_2 = "request_domain_2";
+ String principalDomainName_2 = "principal_domain_2";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", principalDomainName_2 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, principalDomainName_2 }));
+ metric_2.increment(metricName_2);
+ metric_2.increment(metricName_2, requestDomainName_2);
+ metric_2.increment(metricName_2, null, principalDomainName_2);
+ metric_2.increment(metricName_2, requestDomainName_2, principalDomainName_2);
+ metric_2.increment(metricName_2, null, count);
+ metric_2.increment(metricName_2, requestDomainName_2, count);
+ metric_2.increment(metricName_2, null, principalDomainName_2, count);
+ metric_2.increment(metricName_2, requestDomainName_2, principalDomainName_2, count);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", "" }), 2d + 24d * 2d, 0.1d);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, "" }), 2d + 24d * 2d, 0.1d);
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", principalDomainName_2 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, principalDomainName_2 }));
+
+ // 3. only principal domain
+ PrometheusMetric metric_3 = new PrometheusMetric(registry, namesToCollectors, null, namespace, false, true);
+ String metricName_3 = "test_counter_3";
+ String fullMetricName_3 = namespace + "_" + metricName_3 + "_" + PrometheusMetric.COUNTER_SUFFIX;
+ String requestDomainName_3 = "request_domain_3";
+ String principalDomainName_3 = "principal_domain_3";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", principalDomainName_3 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, principalDomainName_3 }));
+ metric_3.increment(metricName_3);
+ metric_3.increment(metricName_3, requestDomainName_3);
+ metric_3.increment(metricName_3, null, principalDomainName_3);
+ metric_3.increment(metricName_3, requestDomainName_3, principalDomainName_3);
+ metric_3.increment(metricName_3, null, count);
+ metric_3.increment(metricName_3, requestDomainName_3, count);
+ metric_3.increment(metricName_3, null, principalDomainName_3, count);
+ metric_3.increment(metricName_3, requestDomainName_3, principalDomainName_3, count);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", "" }), 2d + 24d * 2d, 0.1d);
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, "" }));
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", principalDomainName_3 }), 2d + 24d * 2d, 0.1d);
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, principalDomainName_3 }));
+
+ // 4. enable both labels
+ PrometheusMetric metric_4 = new PrometheusMetric(registry, namesToCollectors, null, namespace, true, true);
+ String metricName_4 = "test_counter_4";
+ String fullMetricName_4 = namespace + "_" + metricName_4 + "_" + PrometheusMetric.COUNTER_SUFFIX;
+ String requestDomainName_4 = "request_domain_4";
+ String principalDomainName_4 = "principal_domain_4";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", principalDomainName_4 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, principalDomainName_4 }));
+ metric_4.increment(metricName_4);
+ metric_4.increment(metricName_4, requestDomainName_4);
+ metric_4.increment(metricName_4, null, principalDomainName_4);
+ metric_4.increment(metricName_4, requestDomainName_4, principalDomainName_4);
+ metric_4.increment(metricName_4, null, count);
+ metric_4.increment(metricName_4, requestDomainName_4, count);
+ metric_4.increment(metricName_4, null, principalDomainName_4, count);
+ metric_4.increment(metricName_4, requestDomainName_4, principalDomainName_4, count);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", "" }), 1d + 24d, 0.1d);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, "" }), 1d + 24d, 0.1d);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", principalDomainName_4 }), 1d + 24d, 0.1d);
+ Assert.assertEquals(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, principalDomainName_4 }), 1d + 24d, 0.1d);
+ }
+
+ @Test
+ public void testStartTiming() {
+ CollectorRegistry registry = new CollectorRegistry();
+ ConcurrentHashMap namesToCollectors = new ConcurrentHashMap<>();
+ String namespace = "metric_test";
+
+ // 1. no labels (default)
+ PrometheusMetric metric_1 = new PrometheusMetric(registry, namesToCollectors, null, namespace);
+ String metricName_1 = "test_timer_1";
+ String fullMetricName_1 = namespace + "_" + metricName_1 + "_" + PrometheusMetric.TIMER_UNIT + "_sum";
+ String requestDomainName_1 = "request_domain_1";
+ String principalDomainName_1 = "principal_domain_1";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", principalDomainName_1 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, principalDomainName_1 }));
+ metric_1.stopTiming(metric_1.startTiming(metricName_1, null));
+ metric_1.stopTiming(metric_1.startTiming(metricName_1, requestDomainName_1));
+ metric_1.stopTiming(metric_1.startTiming(metricName_1, null, principalDomainName_1));
+ metric_1.stopTiming(metric_1.startTiming(metricName_1, requestDomainName_1, principalDomainName_1));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ "", principalDomainName_1 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_1, this.labelNames, new String[]{ requestDomainName_1, principalDomainName_1 }));
+
+ // 2. only request domain
+ PrometheusMetric metric_2 = new PrometheusMetric(registry, namesToCollectors, null, namespace, true, false);
+ String metricName_2 = "test_timer_2";
+ String fullMetricName_2 = namespace + "_" + metricName_2 + "_" + PrometheusMetric.TIMER_UNIT + "_sum";
+ String requestDomainName_2 = "request_domain_2";
+ String principalDomainName_2 = "principal_domain_2";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", principalDomainName_2 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, principalDomainName_2 }));
+ metric_2.stopTiming(metric_2.startTiming(metricName_2, null));
+ metric_2.stopTiming(metric_2.startTiming(metricName_2, requestDomainName_2));
+ metric_2.stopTiming(metric_2.startTiming(metricName_2, null, principalDomainName_2));
+ metric_2.stopTiming(metric_2.startTiming(metricName_2, requestDomainName_2, principalDomainName_2));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ "", principalDomainName_2 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_2, this.labelNames, new String[]{ requestDomainName_2, principalDomainName_2 }));
+
+ // 3. only principal domain
+ PrometheusMetric metric_3 = new PrometheusMetric(registry, namesToCollectors, null, namespace, false, true);
+ String metricName_3 = "test_timer_3";
+ String fullMetricName_3 = namespace + "_" + metricName_3 + "_" + PrometheusMetric.TIMER_UNIT + "_sum";
+ String requestDomainName_3 = "request_domain_3";
+ String principalDomainName_3 = "principal_domain_3";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", principalDomainName_3 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, principalDomainName_3 }));
+ metric_3.stopTiming(metric_3.startTiming(metricName_3, null));
+ metric_3.stopTiming(metric_3.startTiming(metricName_3, requestDomainName_3));
+ metric_3.stopTiming(metric_3.startTiming(metricName_3, null, principalDomainName_3));
+ metric_3.stopTiming(metric_3.startTiming(metricName_3, requestDomainName_3, principalDomainName_3));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, "" }));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ "", principalDomainName_3 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_3, this.labelNames, new String[]{ requestDomainName_3, principalDomainName_3 }));
+
+ // 4. enable both labels
+ PrometheusMetric metric_4 = new PrometheusMetric(registry, namesToCollectors, null, namespace, true, true);
+ String metricName_4 = "test_timer_4";
+ String fullMetricName_4 = namespace + "_" + metricName_4 + "_" + PrometheusMetric.TIMER_UNIT + "_sum";
+ String requestDomainName_4 = "request_domain_4";
+ String principalDomainName_4 = "principal_domain_4";
+ // assertions
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, "" }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", principalDomainName_4 }));
+ Assert.assertNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, principalDomainName_4 }));
+ metric_4.stopTiming(metric_4.startTiming(metricName_4, null));
+ metric_4.stopTiming(metric_4.startTiming(metricName_4, requestDomainName_4));
+ metric_4.stopTiming(metric_4.startTiming(metricName_4, null, principalDomainName_4));
+ metric_4.stopTiming(metric_4.startTiming(metricName_4, requestDomainName_4, principalDomainName_4));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", "" }));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, "" }));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ "", principalDomainName_4 }));
+ Assert.assertNotNull(registry.getSampleValue(fullMetricName_4, this.labelNames, new String[]{ requestDomainName_4, principalDomainName_4 }));
+ }
+
+ @Test
+ public void testStopTiming() {
+ Summary.Timer timer = mock(Summary.Timer.class);
+ PrometheusMetric metric = new PrometheusMetric(null, null, null, "", false, false);
+
+ metric.stopTiming(timer);
+ // assertions
+ verify(timer, times(1)).observeDuration();
+
+ // different signature
+ metric.stopTiming(timer, "request_domain", "principal_domain");
+ // assertions
+ verify(timer, times(2)).observeDuration();
+ }
+ @Test
+ public void testStopTimingOnNull() {
+ PrometheusMetric metric = new PrometheusMetric(null, null, null, "", false, false);
+ metric.stopTiming(null);
+
+ // no exceptions, and no actions
+ }
+
+ @Test
+ public void testFlush() {
+ PrometheusExporter exporter = mock(PrometheusExporter.class);
+
+ PrometheusMetric metric = new PrometheusMetric(null, null, exporter, "", false, false);
+ metric.flush();
+ // assertions
+ verify(exporter, times(1)).flush();
+
+ // test null exporter
+ metric = new PrometheusMetric(null, null, null, "", false, false);
+ metric.flush();
+ // no exceptions, and no actions
+ }
+
+ @Test
+ public void testQuit() {
+ PrometheusExporter exporter = mock(PrometheusExporter.class);
+
+ PrometheusMetric metric = new PrometheusMetric(null, null, exporter, "", false, false);
+ metric.quit();
+ // assertions
+ verify(exporter, times(1)).flush();
+ verify(exporter, times(1)).quit();
+
+ // test null exporter
+ metric = new PrometheusMetric(null, null, null, "", false, false);
+ metric.quit();
+ // no exceptions, and no actions
+ }
+
+}
diff --git a/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServerTest.java b/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServerTest.java
new file mode 100644
index 00000000000..1dc08d5c017
--- /dev/null
+++ b/contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServerTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019 Yahoo Inc.
+ *
+ * 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.
+ */
+package com.yahoo.athenz.common.metrics.impl.prometheus;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.HttpHostConnectException;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.Counter;
+
+public class PrometheusPullServerTest {
+
+ @Test
+ public void testConstructor() throws IOException {
+
+ int port = 8181;
+ String counterName = "constructor_test_total";
+ String counterHelp = "constructor_test_help";
+ double counterValue = 1234.6789;
+
+ CollectorRegistry registry = new CollectorRegistry();
+ Counter counter = Counter.build().name(counterName).help(counterHelp).register(registry);
+ counter.inc(counterValue);
+
+ // new
+ String expectedResponseText = String.join(
+ "\n",
+ String.format("# HELP %s %s", counterName, counterHelp),
+ String.format("# TYPE %s %s", counterName, counter.getClass().getSimpleName().toLowerCase()),
+ String.format("%s %.4f", counterName, counterValue)
+ );
+ PrometheusPullServer exporter = null;
+ try {
+ exporter = new PrometheusPullServer(port, registry);
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(String.format("http://localhost:%d/metrics", port));
+ HttpResponse response = client.execute(request);
+ BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
+ String responseText = rd.lines().collect(Collectors.joining("\n"));
+
+ // assertions
+ Assert.assertEquals(responseText, expectedResponseText);
+ } finally {
+ // cleanup
+ if (exporter != null) {
+ exporter.quit();
+ }
+ }
+ }
+
+ @Test(expectedExceptions = { HttpHostConnectException.class }, expectedExceptionsMessageRegExp = ".* failed: Connection refused \\(Connection refused\\)")
+ public void testQuit() throws IOException {
+ int port = 8181;
+ CollectorRegistry registry = new CollectorRegistry();
+ PrometheusPullServer exporter = new PrometheusPullServer(port, registry);
+ exporter.quit();
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(String.format("http://localhost:%d/metrics", port));
+ client.execute(request);
+ }
+
+}