From 3274dbce033539fd97f12eaeeeb6ae0b2797b1da Mon Sep 17 00:00:00 2001 From: Windz Date: Mon, 9 Sep 2019 06:23:07 +0900 Subject: [PATCH] PrometheusMetric class (#770) --- contributions/metric/prometheus/.gitignore | 27 ++ contributions/metric/prometheus/README.md | 86 ++++ .../jmeter/metric-2000-domain/summary.csv | 3 + .../assets/jmeter/metric-no-label/summary.csv | 3 + .../doc/assets/jmeter/no_ops/summary.csv | 5 + .../doc/assets/jmeter/prometheus/summary.csv | 5 + .../metric/prometheus/doc/design-concerns.md | 20 + .../metric/prometheus/doc/example-main.md | 70 ++++ .../metric/prometheus/doc/performance.md | 17 + contributions/metric/prometheus/pom.xml | 235 +++++++++++ .../impl/prometheus/PrometheusExporter.java | 30 ++ .../impl/prometheus/PrometheusMetric.java | 199 +++++++++ .../prometheus/PrometheusMetricFactory.java | 99 +++++ .../impl/prometheus/PrometheusPullServer.java | 43 ++ .../PrometheusMetricFactoryTest.java | 145 +++++++ .../impl/prometheus/PrometheusMetricTest.java | 389 ++++++++++++++++++ .../prometheus/PrometheusPullServerTest.java | 88 ++++ 17 files changed, 1464 insertions(+) create mode 100644 contributions/metric/prometheus/.gitignore create mode 100644 contributions/metric/prometheus/README.md create mode 100644 contributions/metric/prometheus/doc/assets/jmeter/metric-2000-domain/summary.csv create mode 100644 contributions/metric/prometheus/doc/assets/jmeter/metric-no-label/summary.csv create mode 100644 contributions/metric/prometheus/doc/assets/jmeter/no_ops/summary.csv create mode 100644 contributions/metric/prometheus/doc/assets/jmeter/prometheus/summary.csv create mode 100644 contributions/metric/prometheus/doc/design-concerns.md create mode 100644 contributions/metric/prometheus/doc/example-main.md create mode 100644 contributions/metric/prometheus/doc/performance.md create mode 100644 contributions/metric/prometheus/pom.xml create mode 100644 contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusExporter.java create mode 100644 contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetric.java create mode 100644 contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactory.java create mode 100644 contributions/metric/prometheus/src/main/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServer.java create mode 100644 contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricFactoryTest.java create mode 100644 contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusMetricTest.java create mode 100644 contributions/metric/prometheus/src/test/java/com/yahoo/athenz/common/metrics/impl/prometheus/PrometheusPullServerTest.java 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); + } + +}