You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by lh...@apache.org on 2026/04/20 08:34:11 UTC
(pulsar) branch branch-4.0 updated: [fix][sec][branch-4.0] Upgrade to Jetty 12.1.8 to address several CVEs (#25534)
This is an automated email from the ASF dual-hosted git repository.
lhotari pushed a commit to branch branch-4.0
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/branch-4.0 by this push:
new e428972aec0 [fix][sec][branch-4.0] Upgrade to Jetty 12.1.8 to address several CVEs (#25534)
e428972aec0 is described below
commit e428972aec0bcfd12fb0614bf89b53c7c6f71709
Author: Lari Hotari <lh...@users.noreply.github.com>
AuthorDate: Mon Apr 20 11:34:04 2026 +0300
[fix][sec][branch-4.0] Upgrade to Jetty 12.1.8 to address several CVEs (#25534)
---
conf/bookkeeper.conf | 2 +-
conf/zookeeper.conf | 4 +-
distribution/server/pom.xml | 43 +-
distribution/server/src/assemble/LICENSE.bin.txt | 72 +-
distribution/server/src/assemble/bin.xml | 2 +
distribution/shell/src/assemble/LICENSE.bin.txt | 21 +-
.../bookkeeper-prometheus-metrics-provider/pom.xml | 85 ++
.../bookkeeper/DataSketchesOpStatsLogger.java | 222 ++++
.../prometheus/bookkeeper/LongAdderCounter.java | 91 ++
.../bookkeeper/PrometheusMetricsProvider.java | 253 +++++
.../prometheus/bookkeeper/PrometheusServlet.java | 60 +
.../bookkeeper/PrometheusStatsLogger.java | 100 ++
.../bookkeeper/PrometheusTextFormat.java | 240 ++++
.../prometheus/bookkeeper/ScopeContext.java | 39 +-
.../metrics/prometheus/bookkeeper/SimpleGauge.java | 33 +-
.../ThreadScopedDataSketchesStatsLogger.java | 137 +++
.../bookkeeper/ThreadScopedLongAdderCounter.java | 111 ++
.../prometheus/bookkeeper/package-info.java | 31 +-
.../bookkeeper/TestPrometheusFormatter.java | 296 +++++
.../bookkeeper/TestPrometheusMetricsProvider.java | 166 +++
jetty-upgrade/pom.xml | 39 +
.../zookeeper-prometheus-metrics}/pom.xml | 78 +-
.../zookeeper/PrometheusMetricsProvider.java | 632 +++++++++++
.../metrics/prometheus/zookeeper/package-info.java | 31 +-
.../prometheus/zookeeper/ExportJvmInfoTest.java | 61 +
.../PrometheusMetricsProviderConfigTest.java | 66 ++
.../zookeeper/PrometheusMetricsProviderTest.java | 714 ++++++++++++
jetty-upgrade/zookeeper-with-patched-admin/pom.xml | 173 +++
.../apache/zookeeper/server/admin/AdminServer.java | 53 +
.../zookeeper/server/admin/AdminServerFactory.java | 64 ++
.../apache/zookeeper/server/admin/AuthRequest.java | 64 ++
.../org/apache/zookeeper/server/admin/Command.java | 90 ++
.../apache/zookeeper/server/admin/CommandBase.java | 83 ++
.../zookeeper/server/admin/CommandOutputter.java | 41 +-
.../zookeeper/server/admin/CommandResponse.java | 190 ++++
.../apache/zookeeper/server/admin/Commands.java | 1168 ++++++++++++++++++++
.../zookeeper/server/admin/DummyAdminServer.java | 27 +-
.../apache/zookeeper/server/admin/GetCommand.java | 30 +-
.../zookeeper/server/admin/JettyAdminServer.java | 397 +++++++
.../zookeeper/server/admin/JsonOutputter.java | 69 ++
.../apache/zookeeper/server/admin/PostCommand.java | 30 +-
.../zookeeper/server/admin/StreamOutputter.java | 52 +
.../zookeeper/server/admin/package-info.java | 31 +-
managed-ledger/pom.xml | 17 +-
.../bookkeeper/mledger/impl/ManagedLedgerTest.java | 6 +-
pom.xml | 180 +--
pulsar-broker-auth-athenz/pom.xml | 2 +-
pulsar-broker-auth-oidc/pom.xml | 2 +-
pulsar-broker-common/pom.xml | 25 +-
.../apache/pulsar/broker/web/GzipHandlerUtil.java | 11 +-
.../pulsar/broker/web/JettyRequestLogFactory.java | 58 +-
.../web/plugin/servlet/AdditionalServlet.java | 27 +-
.../servlet/AdditionalServletWithClassLoader.java | 16 +-
.../jetty/metrics/JettyStatisticsCollector.java | 111 ++
.../apache/pulsar/jetty/metrics/package-info.java} | 28 +-
.../pulsar/jetty/tls/JettySslContextFactory.java | 4 +
.../web/plugin/servlet/MockAdditionalServlet.java | 3 +-
.../jetty/tls/JettySslContextFactoryTest.java | 6 +-
pulsar-broker/pom.xml | 29 +-
.../org/apache/pulsar/broker/PulsarService.java | 87 +-
.../prometheus/PulsarPrometheusMetricsServlet.java | 2 +-
.../apache/pulsar/broker/web/ExceptionHandler.java | 37 +-
.../org/apache/pulsar/broker/web/WebService.java | 76 +-
.../apache/pulsar/PrometheusMetricsTestUtil.java | 2 +-
.../pulsar/broker/BrokerAdditionalServletTest.java | 15 +-
.../broker/admin/AdminApiGetLastMessageIdTest.java | 3 +-
.../org/apache/pulsar/broker/admin/AdminTest.java | 11 +-
.../pulsar/broker/admin/MockServletContext.java | 279 -----
.../apache/pulsar/broker/admin/NamespacesTest.java | 5 +-
.../pulsar/broker/admin/NamespacesV2Test.java | 3 +-
.../pulsar/broker/admin/PersistentTopicsTest.java | 7 +-
.../pulsar/broker/admin/ResourceGroupsTest.java | 4 +-
.../pulsar/broker/admin/TopicPoliciesTest.java | 29 +-
.../broker/intercept/BrokerInterceptorTest.java | 26 +-
.../broker/intercept/CounterBrokerInterceptor.java | 6 +-
.../pulsar/broker/service/BrokerServiceTest.java | 8 +-
.../pulsar/broker/web/ExceptionHandlerTest.java | 17 +-
.../broker/web/WebServiceOriginalClientIPTest.java | 10 +-
.../AdditionalServletWithClassLoaderTest.java | 15 +-
.../MockAdditionalServletWithClassLoader.java | 3 +-
.../pulsar/client/api/MockBrokerService.java | 12 +-
.../proxy/ClientSideEncryptionWssConsumer.java | 21 +-
.../proxy/ClientSideEncryptionWssProducer.java | 24 +-
.../websocket/proxy/ProxyAuthenticationTest.java | 16 +-
.../proxy/ProxyEncryptionPublishConsumeTest.java | 16 +-
.../websocket/proxy/ProxyIdleTimeoutTest.java | 4 +-
.../pulsar/websocket/proxy/ProxyPingTest.java | 4 +-
.../websocket/proxy/ProxyPublishConsumeTest.java | 156 ++-
.../proxy/ProxyPublishConsumeTlsTest.java | 27 +-
.../proxy/ProxyPublishConsumeWithoutZKTest.java | 8 +-
.../pulsar/websocket/proxy/ProxyRoleAuthTest.java | 18 +-
.../websocket/proxy/SimpleConsumerSocket.java | 22 +-
.../websocket/proxy/SimpleProducerSocket.java | 14 +-
.../proxy/v1/V1ProxyAuthenticationTest.java | 16 +-
pulsar-client-admin/pom.xml | 2 +-
.../client/admin/internal/PulsarAdminImpl.java | 6 +-
.../admin/internal/http/AsyncHttpConnector.java | 6 +-
.../internal/http/AsyncHttpConnectorProvider.java | 17 +-
pulsar-client-auth-athenz/pom.xml | 41 +-
pulsar-client-tools-test/pom.xml | 5 +
pulsar-client-tools/pom.xml | 7 +-
.../pulsar/client/cli/AbstractCmdConsume.java | 14 +-
.../org/apache/pulsar/client/cli/CmdConsume.java | 21 +-
.../org/apache/pulsar/client/cli/CmdProduce.java | 36 +-
.../java/org/apache/pulsar/client/cli/CmdRead.java | 20 +-
.../org/apache/pulsar/client/impl/HttpClient.java | 26 +-
pulsar-common/pom.xml | 5 +
pulsar-functions/utils/pom.xml | 2 +-
pulsar-functions/worker/pom.xml | 18 +-
.../pulsar/functions/worker/rest/WorkerServer.java | 66 +-
pulsar-io/alluxio/pom.xml | 7 +
pulsar-io/debezium/core/pom.xml | 4 +
pulsar-io/hbase/pom.xml | 6 +-
pulsar-io/http/pom.xml | 2 +-
pulsar-io/jdbc/openmldb/pom.xml | 6 +-
pulsar-io/kafka/pom.xml | 5 -
pulsar-io/solr/pom.xml | 7 +-
pulsar-metadata/pom.xml | 5 +-
.../bookkeeper-storage/pom.xml | 7 +
pulsar-proxy/pom.xml | 29 +-
.../pulsar/proxy/server/AdminProxyHandler.java | 177 ++-
.../pulsar/proxy/server/ProxyServiceStarter.java | 80 +-
.../org/apache/pulsar/proxy/server/WebServer.java | 71 +-
.../server/AdminProxyHandlerKeystoreTLSTest.java | 2 +-
.../pulsar/proxy/server/AdminProxyHandlerTest.java | 52 +-
.../proxy/server/AuthedAdminProxyHandlerTest.java | 2 +-
.../proxy/server/ProxyAdditionalServletTest.java | 7 +-
.../pulsar/proxy/server/ProxyIsAHttpProxyTest.java | 23 +-
.../proxy/server/ProxyMultiBrokerBaseTest.java | 128 +++
.../proxy/server/ProxyOriginalClientIPTest.java | 8 +-
.../proxy/server/ProxyPackagesUploadTest.java | 2 +-
.../pulsar/proxy/server/ProxyRedirectTest.java | 84 ++
.../proxy/server/ProxyServiceStarterTest.java | 47 +-
.../proxy/server/ProxyServiceTlsStarterTest.java | 41 +-
.../SuperUserAuthedAdminProxyHandlerTest.java | 2 +-
.../server/UnauthedAdminProxyHandlerTest.java | 2 +-
pulsar-testclient/pom.xml | 11 +-
.../proxy/socket/client/PerformanceClient.java | 9 +-
.../socket/client/SimpleTestProducerSocket.java | 20 +-
pulsar-websocket/pom.xml | 24 +-
.../pulsar/websocket/AbstractWebSocketHandler.java | 17 +-
.../apache/pulsar/websocket/ConsumerHandler.java | 29 +-
.../websocket/MultiTopicConsumerHandler.java | 4 +-
.../apache/pulsar/websocket/ProducerHandler.java | 15 +-
.../org/apache/pulsar/websocket/ReaderHandler.java | 19 +-
.../pulsar/websocket/WebSocketConsumerServlet.java | 20 +-
.../WebSocketHttpServletRequestWrapper.java | 3 +-
.../WebSocketMultiTopicConsumerServlet.java | 21 +-
.../pulsar/websocket/WebSocketProducerServlet.java | 19 +-
.../pulsar/websocket/WebSocketReaderServlet.java | 19 +-
.../apache/pulsar/websocket/WebSocketService.java | 4 +-
.../pulsar/websocket/service/ProxyServer.java | 69 +-
.../pulsar/websocket/stats/ProxyTopicStat.java | 6 +-
.../websocket/AbstractWebSocketHandlerTest.java | 27 +-
.../pulsar/websocket/PingPongSupportTest.java | 58 +-
.../pulsar/websocket/ProducerHandlerTest.java | 4 +-
.../apache/pulsar/websocket/ReaderHandlerTest.java | 14 +-
.../WebSocketHttpServletRequestWrapperTest.java | 8 +-
testmocks/pom.xml | 5 +-
.../plugins/RandomAdditionalServlet.java | 5 +-
tests/integration/pom.xml | 5 +
.../integration/websocket/WebSocketTestSuite.java | 18 +-
tiered-storage/file-system/pom.xml | 454 ++++----
163 files changed, 7835 insertions(+), 1889 deletions(-)
diff --git a/conf/bookkeeper.conf b/conf/bookkeeper.conf
index 562e37eec33..d680e411353 100644
--- a/conf/bookkeeper.conf
+++ b/conf/bookkeeper.conf
@@ -595,7 +595,7 @@ compactionRateByBytes=1000000
# enableStatistics=true
# Stats Provider Class (if statistics are enabled)
-statsProviderClass=org.apache.bookkeeper.stats.prometheus.PrometheusMetricsProvider
+statsProviderClass=org.apache.pulsar.metrics.prometheus.bookkeeper.PrometheusMetricsProvider
# Default port for Prometheus metrics exporter
prometheusStatsHttpPort=8000
diff --git a/conf/zookeeper.conf b/conf/zookeeper.conf
index db85e688e5c..c497ff77008 100644
--- a/conf/zookeeper.conf
+++ b/conf/zookeeper.conf
@@ -76,6 +76,6 @@ portUnification=false
## Metrics Providers
#
# https://prometheus.io Metrics Exporter
-metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
+metricsProvider.className=org.apache.pulsar.metrics.prometheus.zookeeper.PrometheusMetricsProvider
metricsProvider.httpPort=8000
-metricsProvider.exportJvmInfo=true
+metricsProvider.exportJvmInfo=true
\ No newline at end of file
diff --git a/distribution/server/pom.xml b/distribution/server/pom.xml
index f2caf97cc95..647c55a1866 100644
--- a/distribution/server/pom.xml
+++ b/distribution/server/pom.xml
@@ -82,29 +82,21 @@
</dependency>
<dependency>
- <groupId>jline</groupId>
- <artifactId>jline</artifactId>
- <version>${jline.version}</version>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>pulsar-bookkeeper-prometheus-metrics-provider</artifactId>
+ <version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.apache.zookeeper</groupId>
- <artifactId>zookeeper-prometheus-metrics</artifactId>
- <version>${zookeeper.version}</version>
- <exclusions>
- <exclusion>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-core</artifactId>
- </exclusion>
- <exclusion>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-classic</artifactId>
- </exclusion>
- <exclusion>
- <groupId>io.netty</groupId>
- <artifactId>netty-tcnative</artifactId>
- </exclusion>
- </exclusions>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>pulsar-zookeeper-prometheus-metrics</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>jline</groupId>
+ <artifactId>jline</artifactId>
+ <version>${jline.version}</version>
</dependency>
<dependency>
@@ -141,6 +133,12 @@
<groupId>${project.groupId}</groupId>
<artifactId>pulsar-testclient</artifactId>
<version>${project.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.zookeeper</groupId>
+ <artifactId>zookeeper</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<dependency>
@@ -199,11 +197,6 @@
<artifactId>log4j-slf4j2-impl</artifactId>
</dependency>
- <dependency>
- <groupId>org.apache.bookkeeper.stats</groupId>
- <artifactId>prometheus-metrics-provider</artifactId>
- </dependency>
-
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_log4j2</artifactId>
diff --git a/distribution/server/src/assemble/LICENSE.bin.txt b/distribution/server/src/assemble/LICENSE.bin.txt
index d6e03704578..de8de936a6d 100644
--- a/distribution/server/src/assemble/LICENSE.bin.txt
+++ b/distribution/server/src/assemble/LICENSE.bin.txt
@@ -330,7 +330,6 @@ The Apache Software License, Version 2.0
- io.prometheus-simpleclient_common-0.16.0.jar
- io.prometheus-simpleclient_hotspot-0.16.0.jar
- io.prometheus-simpleclient_httpserver-0.16.0.jar
- - io.prometheus-simpleclient_jetty-0.16.0.jar
- io.prometheus-simpleclient_log4j2-0.16.0.jar
- io.prometheus-simpleclient_servlet-0.16.0.jar
- io.prometheus-simpleclient_servlet_common-0.16.0.jar
@@ -354,8 +353,8 @@ The Apache Software License, Version 2.0
- org.apache.logging.log4j-log4j-web-2.25.4.jar
- org.apache.logging.log4j-log4j-layout-template-json-2.25.4.jar
* Java Native Access JNA
- - net.java.dev.jna-jna-jpms-5.12.1.jar
- - net.java.dev.jna-jna-platform-jpms-5.12.1.jar
+ - net.java.dev.jna-jna-jpms-5.18.1.jar
+ - net.java.dev.jna-jna-platform-jpms-5.18.1.jar
* BookKeeper
- org.apache.bookkeeper-bookkeeper-common-4.17.3.jar
- org.apache.bookkeeper-bookkeeper-common-allocator-4.17.3.jar
@@ -376,12 +375,10 @@ The Apache Software License, Version 2.0
- org.apache.bookkeeper.http-http-server-4.17.3.jar
- org.apache.bookkeeper.http-vertx-http-server-4.17.3.jar
- org.apache.bookkeeper.stats-bookkeeper-stats-api-4.17.3.jar
- - org.apache.bookkeeper.stats-prometheus-metrics-provider-4.17.3.jar
- org.apache.distributedlog-distributedlog-common-4.17.3.jar
- org.apache.distributedlog-distributedlog-core-4.17.3-tests.jar
- org.apache.distributedlog-distributedlog-core-4.17.3.jar
- org.apache.distributedlog-distributedlog-protocol-4.17.3.jar
- - org.apache.bookkeeper.stats-codahale-metrics-provider-4.17.3.jar
- org.apache.bookkeeper-bookkeeper-slogger-api-4.17.3.jar
- org.apache.bookkeeper-bookkeeper-slogger-slf4j-4.17.3.jar
- org.apache.bookkeeper-native-io-4.17.3.jar
@@ -395,25 +392,43 @@ The Apache Software License, Version 2.0
- org.asynchttpclient-async-http-client-2.14.5.jar
- org.asynchttpclient-async-http-client-netty-utils-2.14.5.jar
* Jetty
- - org.eclipse.jetty-jetty-client-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-continuation-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-http-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-io-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-proxy-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-security-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-server-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-servlet-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-servlets-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-util-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-util-ajax-9.4.58.v20250814.jar
- - org.eclipse.jetty.websocket-javax-websocket-client-impl-9.4.58.v20250814.jar
- - org.eclipse.jetty.websocket-websocket-api-9.4.58.v20250814.jar
- - org.eclipse.jetty.websocket-websocket-client-9.4.58.v20250814.jar
- - org.eclipse.jetty.websocket-websocket-common-9.4.58.v20250814.jar
- - org.eclipse.jetty.websocket-websocket-server-9.4.58.v20250814.jar
- - org.eclipse.jetty.websocket-websocket-servlet-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-alpn-conscrypt-server-9.4.58.v20250814.jar
- - org.eclipse.jetty-jetty-alpn-server-9.4.58.v20250814.jar
+ - org.eclipse.jetty-jetty-alpn-client-12.1.8.jar
+ - org.eclipse.jetty-jetty-alpn-conscrypt-server-12.1.8.jar
+ - org.eclipse.jetty-jetty-alpn-server-12.1.8.jar
+ - org.eclipse.jetty-jetty-annotations-12.1.8.jar
+ - org.eclipse.jetty-jetty-client-12.1.8.jar
+ - org.eclipse.jetty-jetty-http-12.1.8.jar
+ - org.eclipse.jetty-jetty-io-12.1.8.jar
+ - org.eclipse.jetty-jetty-jndi-12.1.8.jar
+ - org.eclipse.jetty-jetty-plus-12.1.8.jar
+ - org.eclipse.jetty-jetty-security-12.1.8.jar
+ - org.eclipse.jetty-jetty-server-12.1.8.jar
+ - org.eclipse.jetty-jetty-session-12.1.8.jar
+ - org.eclipse.jetty-jetty-util-12.1.8.jar
+ - org.eclipse.jetty-jetty-xml-12.1.8.jar
+ - org.eclipse.jetty.compression-jetty-compression-common-12.1.8.jar
+ - org.eclipse.jetty.compression-jetty-compression-gzip-12.1.8.jar
+ - org.eclipse.jetty.compression-jetty-compression-server-12.1.8.jar
+ - org.eclipse.jetty.ee-jetty-ee-webapp-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-annotations-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-nested-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-plus-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-proxy-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-security-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-servlet-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-servlets-12.1.8.jar
+ - org.eclipse.jetty.ee8-jetty-ee8-webapp-12.1.8.jar
+ - org.eclipse.jetty.ee8.websocket-jetty-ee8-websocket-jetty-api-12.1.8.jar
+ - org.eclipse.jetty.ee8.websocket-jetty-ee8-websocket-jetty-common-12.1.8.jar
+ - org.eclipse.jetty.ee8.websocket-jetty-ee8-websocket-jetty-server-12.1.8.jar
+ - org.eclipse.jetty.ee8.websocket-jetty-ee8-websocket-servlet-12.1.8.jar
+ - org.eclipse.jetty.toolchain-jetty-servlet-api-4.0.9.jar
+ - org.eclipse.jetty.websocket-jetty-websocket-core-client-12.1.8.jar
+ - org.eclipse.jetty.websocket-jetty-websocket-core-common-12.1.8.jar
+ - org.eclipse.jetty.websocket-jetty-websocket-core-server-12.1.8.jar
+ - org.eclipse.jetty.websocket-jetty-websocket-jetty-api-12.1.8.jar
+ - org.eclipse.jetty.websocket-jetty-websocket-jetty-client-12.1.8.jar
+ - org.eclipse.jetty.websocket-jetty-websocket-jetty-common-12.1.8.jar
* SnakeYaml -- org.yaml-snakeyaml-2.0.jar
* RocksDB - org.rocksdb-rocksdbjni-7.9.2.jar
* Google Error Prone Annotations - com.google.errorprone-error_prone_annotations-2.45.0.jar
@@ -480,7 +495,6 @@ The Apache Software License, Version 2.0
- io.dropwizard.metrics-metrics-core-4.1.12.1.jar
- io.dropwizard.metrics-metrics-graphite-4.1.12.1.jar
- io.dropwizard.metrics-metrics-jvm-4.1.12.1.jar
- - io.dropwizard.metrics-metrics-jmx-4.1.12.1.jar
* Prometheus
- io.prometheus-simpleclient_httpserver-0.16.0.jar
* Oxia
@@ -501,9 +515,7 @@ The Apache Software License, Version 2.0
- io.vertx-vertx-web-4.5.24.jar
- io.vertx-vertx-web-common-4.5.24.jar
* Apache ZooKeeper
- - org.apache.zookeeper-zookeeper-3.9.5.jar
- org.apache.zookeeper-zookeeper-jute-3.9.5.jar
- - org.apache.zookeeper-zookeeper-prometheus-metrics-3.9.5.jar
* Snappy Java
- org.xerial.snappy-snappy-java-1.1.10.8.jar
* Google HTTP Client
@@ -556,6 +568,10 @@ BSD 3-clause "New" or "Revised" License
* JSR305 -- com.google.code.findbugs-jsr305-3.0.2.jar -- ../licenses/LICENSE-JSR305.txt
* JLine -- jline-jline-2.14.6.jar -- ../licenses/LICENSE-JLine.txt
* JLine3 -- org.jline-jline-3.21.0.jar -- ../licenses/LICENSE-JLine.txt
+ * OW2 ASM
+ - org.ow2.asm-asm-9.9.1.jar -- ../licenses/LICENSE-ASM.txt
+ - org.ow2.asm-asm-commons-9.9.1.jar -- ../licenses/LICENSE-ASM.txt
+ - org.ow2.asm-asm-tree-9.9.1.jar -- ../licenses/LICENSE-ASM.txt
BSD 2-Clause License
* HdrHistogram -- org.hdrhistogram-HdrHistogram-2.1.9.jar -- ../licenses/LICENSE-HdrHistogram.txt
@@ -581,7 +597,6 @@ CDDL-1.1 -- ../licenses/LICENSE-CDDL-1.1.txt
* Java Annotations API
- com.sun.activation-jakarta.activation-1.2.2.jar
* Java Servlet API -- javax.servlet-javax.servlet-api-3.1.0.jar
- * WebSocket Server API -- javax.websocket-javax.websocket-client-api-1.0.jar
* HK2 - Dependency Injection Kernel
- org.glassfish.hk2-hk2-api-2.6.1.jar
- org.glassfish.hk2-hk2-locator-2.6.1.jar
@@ -609,6 +624,7 @@ Eclipse Public License - v2.0 -- ../licenses/LICENSE-EPL-2.0.txt
* Jakarta Annotations API -- jakarta.annotation-jakarta.annotation-api-1.3.5.jar
* Jakarta RESTful Web Services -- jakarta.ws.rs-jakarta.ws.rs-api-2.1.6.jar
* Jakarta Injection -- org.glassfish.hk2.external-jakarta.inject-2.6.1.jar
+ * Jakarta Transactions API -- jakarta.transaction-jakarta.transaction-api-1.3.3.jar
Public Domain (CC0) -- ../licenses/LICENSE-CC0.txt
* Reactive Streams -- org.reactivestreams-reactive-streams-1.0.4.jar
diff --git a/distribution/server/src/assemble/bin.xml b/distribution/server/src/assemble/bin.xml
index 4dfec015c0e..ea359c97363 100644
--- a/distribution/server/src/assemble/bin.xml
+++ b/distribution/server/src/assemble/bin.xml
@@ -128,6 +128,8 @@
<exclude>com.google.android:annotations</exclude>
<!-- Needed only in the pulsar-shell distro only -->
<exclude>net.java.dev.jna:jna</exclude>
+ <!-- Exclude org.apache.zookeeper:zookeeper since the patched version is included in the distribution -->
+ <exclude>org.apache.zookeeper:zookeeper</exclude>
</excludes>
</dependencySet>
</dependencySets>
diff --git a/distribution/shell/src/assemble/LICENSE.bin.txt b/distribution/shell/src/assemble/LICENSE.bin.txt
index 3afd6092a39..33ec6103672 100644
--- a/distribution/shell/src/assemble/LICENSE.bin.txt
+++ b/distribution/shell/src/assemble/LICENSE.bin.txt
@@ -401,14 +401,18 @@ The Apache Software License, Version 2.0
- async-http-client-2.14.5.jar
- async-http-client-netty-utils-2.14.5.jar
* Jetty
- - jetty-client-9.4.58.v20250814.jar
- - jetty-http-9.4.58.v20250814.jar
- - jetty-io-9.4.58.v20250814.jar
- - jetty-util-9.4.58.v20250814.jar
- - javax-websocket-client-impl-9.4.58.v20250814.jar
- - websocket-api-9.4.58.v20250814.jar
- - websocket-client-9.4.58.v20250814.jar
- - websocket-common-9.4.58.v20250814.jar
+ - jetty-alpn-client-12.1.8.jar
+ - jetty-client-12.1.8.jar
+ - jetty-compression-common-12.1.8.jar
+ - jetty-compression-gzip-12.1.8.jar
+ - jetty-http-12.1.8.jar
+ - jetty-io-12.1.8.jar
+ - jetty-util-12.1.8.jar
+ - jetty-websocket-core-client-12.1.8.jar
+ - jetty-websocket-core-common-12.1.8.jar
+ - jetty-websocket-jetty-api-12.1.8.jar
+ - jetty-websocket-jetty-client-12.1.8.jar
+ - jetty-websocket-jetty-common-12.1.8.jar
* SnakeYaml -- snakeyaml-2.0.jar
* Google Error Prone Annotations - error_prone_annotations-2.45.0.jar
* Javassist -- javassist-3.25.0-GA.jar
@@ -434,7 +438,6 @@ MIT License
CDDL-1.1 -- ../licenses/LICENSE-CDDL-1.1.txt
* Java Annotations API
- jakarta.activation-1.2.2.jar
- * WebSocket Server API -- javax.websocket-client-api-1.0.jar
* HK2 - Dependency Injection Kernel
- hk2-api-2.6.1.jar
- hk2-locator-2.6.1.jar
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/pom.xml b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/pom.xml
new file mode 100644
index 00000000000..e864323275a
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/pom.xml
@@ -0,0 +1,85 @@
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>jetty-upgrade</artifactId>
+ <version>4.0.10-SNAPSHOT</version>
+ </parent>
+ <artifactId>pulsar-bookkeeper-prometheus-metrics-provider</artifactId>
+ <name>Apache Pulsar :: BookKeeper Stats Providers :: Prometheus</name>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.bookkeeper.stats</groupId>
+ <artifactId>bookkeeper-stats-api</artifactId>
+ <version>${bookkeeper.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_hotspot</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_servlet</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-common</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-buffer</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-servlet</artifactId>
+ </dependency>
+
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.yahoo.datasketches</groupId>
+ <artifactId>sketches-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.bookkeeper</groupId>
+ <artifactId>testtools</artifactId>
+ <version>${bookkeeper.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/DataSketchesOpStatsLogger.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/DataSketchesOpStatsLogger.java
new file mode 100644
index 00000000000..5c5bf560799
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/DataSketchesOpStatsLogger.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import com.yahoo.sketches.quantiles.DoublesSketch;
+import com.yahoo.sketches.quantiles.DoublesSketchBuilder;
+import com.yahoo.sketches.quantiles.DoublesUnion;
+import com.yahoo.sketches.quantiles.DoublesUnionBuilder;
+import io.netty.util.concurrent.FastThreadLocal;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.LongAdder;
+import java.util.concurrent.locks.StampedLock;
+import org.apache.bookkeeper.stats.OpStatsData;
+import org.apache.bookkeeper.stats.OpStatsLogger;
+
+/**
+ * OpStatsLogger implementation that uses DataSketches library to calculate the approximated latency quantiles.
+ */
+public class DataSketchesOpStatsLogger implements OpStatsLogger {
+
+ /*
+ * Use 2 rotating thread local accessor so that we can safely swap them.
+ */
+ private volatile ThreadLocalAccessor current;
+ private volatile ThreadLocalAccessor replacement;
+
+ /*
+ * These are the sketches where all the aggregated results are published.
+ */
+ private volatile DoublesSketch successResult;
+ private volatile DoublesSketch failResult;
+
+ private final LongAdder successCountAdder = new LongAdder();
+ private final LongAdder failCountAdder = new LongAdder();
+
+ private final LongAdder successSumAdder = new LongAdder();
+ private final LongAdder failSumAdder = new LongAdder();
+
+ private Map<String, String> labels;
+
+ // used for lazy registration for thread scoped metrics
+ private boolean threadInitialized;
+
+ public DataSketchesOpStatsLogger(Map<String, String> labels) {
+ this.current = new ThreadLocalAccessor();
+ this.replacement = new ThreadLocalAccessor();
+ this.labels = labels;
+ }
+
+ @Override
+ public void registerFailedEvent(long eventLatency, TimeUnit unit) {
+ double valueMillis = unit.toMicros(eventLatency) / 1000.0;
+
+ failCountAdder.increment();
+ failSumAdder.add((long) valueMillis);
+
+ LocalData localData = current.localData.get();
+
+ long stamp = localData.lock.readLock();
+ try {
+ localData.failSketch.update(valueMillis);
+ } finally {
+ localData.lock.unlockRead(stamp);
+ }
+ }
+
+ @Override
+ public void registerSuccessfulEvent(long eventLatency, TimeUnit unit) {
+ double valueMillis = unit.toMicros(eventLatency) / 1000.0;
+
+ successCountAdder.increment();
+ successSumAdder.add((long) valueMillis);
+
+ LocalData localData = current.localData.get();
+
+ long stamp = localData.lock.readLock();
+ try {
+ localData.successSketch.update(valueMillis);
+ } finally {
+ localData.lock.unlockRead(stamp);
+ }
+ }
+
+ @Override
+ public void registerSuccessfulValue(long value) {
+ successCountAdder.increment();
+ successSumAdder.add(value);
+
+ LocalData localData = current.localData.get();
+
+ long stamp = localData.lock.readLock();
+ try {
+ localData.successSketch.update(value);
+ } finally {
+ localData.lock.unlockRead(stamp);
+ }
+ }
+
+ @Override
+ public void registerFailedValue(long value) {
+ failCountAdder.increment();
+ failSumAdder.add(value);
+
+ LocalData localData = current.localData.get();
+
+ long stamp = localData.lock.readLock();
+ try {
+ localData.failSketch.update(value);
+ } finally {
+ localData.lock.unlockRead(stamp);
+ }
+ }
+
+ @Override
+ public OpStatsData toOpStatsData() {
+ // Not relevant as we don't use JMX here
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ // Not relevant as we don't use JMX here
+ throw new UnsupportedOperationException();
+ }
+
+ public void rotateLatencyCollection() {
+ // Swap current with replacement
+ ThreadLocalAccessor local = current;
+ current = replacement;
+ replacement = local;
+
+ final DoublesUnion aggregateSuccesss = new DoublesUnionBuilder().build();
+ final DoublesUnion aggregateFail = new DoublesUnionBuilder().build();
+ local.map.forEach((localData, b) -> {
+ long stamp = localData.lock.writeLock();
+ try {
+ aggregateSuccesss.update(localData.successSketch);
+ localData.successSketch.reset();
+ aggregateFail.update(localData.failSketch);
+ localData.failSketch.reset();
+ } finally {
+ localData.lock.unlockWrite(stamp);
+ }
+ });
+
+ successResult = aggregateSuccesss.getResultAndReset();
+ failResult = aggregateFail.getResultAndReset();
+ }
+
+ public long getCount(boolean success) {
+ return success ? successCountAdder.sum() : failCountAdder.sum();
+ }
+
+ public long getSum(boolean success) {
+ return success ? successSumAdder.sum() : failSumAdder.sum();
+ }
+
+ public double getQuantileValue(boolean success, double quantile) {
+ DoublesSketch s = success ? successResult : failResult;
+ return s != null ? s.getQuantile(quantile) : Double.NaN;
+ }
+
+ public Map<String, String> getLabels() {
+ return labels;
+ }
+
+ public boolean isThreadInitialized() {
+ return threadInitialized;
+ }
+
+ public void initializeThread(Map<String, String> labels) {
+ this.labels = labels;
+ this.threadInitialized = true;
+ }
+
+ private static class LocalData {
+ private final DoublesSketch successSketch = new DoublesSketchBuilder().build();
+ private final DoublesSketch failSketch = new DoublesSketchBuilder().build();
+ private final StampedLock lock = new StampedLock();
+ }
+
+ private static class ThreadLocalAccessor {
+ private final Map<LocalData, Boolean> map = new ConcurrentHashMap<>();
+ private final FastThreadLocal<LocalData> localData = new FastThreadLocal<LocalData>() {
+
+ @Override
+ protected LocalData initialValue() throws Exception {
+ LocalData localData = new LocalData();
+ map.put(localData, Boolean.TRUE);
+ return localData;
+ }
+
+ @Override
+ protected void onRemoval(LocalData value) throws Exception {
+ map.remove(value);
+ }
+ };
+ }
+
+ @Override
+ public String toString() {
+ return "DataSketchesOpStatsLogger{labels=" + labels + ", id=" + System.identityHashCode(this) + "}";
+ }
+}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/LongAdderCounter.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/LongAdderCounter.java
new file mode 100644
index 00000000000..a5a8e4d2f02
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/LongAdderCounter.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.LongAdder;
+import org.apache.bookkeeper.stats.Counter;
+
+/**
+ * {@link Counter} implementation based on {@link LongAdder}.
+ *
+ * <p>LongAdder keeps a counter per-thread and then aggregates to get the result, in order to avoid contention between
+ * multiple threads.
+ */
+public class LongAdderCounter implements Counter {
+ private final LongAdder counter = new LongAdder();
+
+ private Map<String, String> labels;
+
+ // used for lazy registration for thread scoped metric
+ private boolean threadInitialized;
+
+ public LongAdderCounter(Map<String, String> labels) {
+ this.labels = labels;
+ }
+
+ @Override
+ public void clear() {
+ counter.reset();
+ }
+
+ @Override
+ public void inc() {
+ counter.increment();
+ }
+
+ @Override
+ public void dec() {
+ counter.decrement();
+ }
+
+ @Override
+ public void addCount(long delta) {
+ counter.add(delta);
+ }
+
+ /**
+ * When counter is used to count time.
+ * consistent with the {@link DataSketchesOpStatsLogger#registerSuccessfulEvent(long, TimeUnit)} 's logic
+ * */
+ @Override
+ public void addLatency(long eventLatency, TimeUnit unit) {
+ long valueMillis = unit.toMillis(eventLatency);
+ counter.add(valueMillis);
+ }
+
+ @Override
+ public Long get() {
+ return counter.sum();
+ }
+
+ public Map<String, String> getLabels() {
+ return labels;
+ }
+
+ public boolean isThreadInitialized() {
+ return threadInitialized;
+ }
+
+ public void initializeThread(Map<String, String> labels) {
+ this.labels = labels;
+ this.threadInitialized = true;
+ }
+}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusMetricsProvider.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusMetricsProvider.java
new file mode 100644
index 00000000000..e5568625328
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusMetricsProvider.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+// CHECKSTYLE.OFF: IllegalImport
+
+import com.google.common.annotations.VisibleForTesting;
+import io.netty.util.concurrent.DefaultThreadFactory;
+import io.netty.util.internal.PlatformDependent;
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.Gauge;
+import io.prometheus.client.Gauge.Child;
+import io.prometheus.client.hotspot.GarbageCollectorExports;
+import io.prometheus.client.hotspot.MemoryPoolsExports;
+import io.prometheus.client.hotspot.StandardExports;
+import io.prometheus.client.hotspot.ThreadExports;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.management.BufferPoolMXBean;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+import org.apache.bookkeeper.stats.StatsLogger;
+import org.apache.bookkeeper.stats.StatsProvider;
+import org.apache.bookkeeper.stats.ThreadRegistry;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.server.Server;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+// CHECKSTYLE.ON: IllegalImport
+
+/**
+ * A <i>Prometheus</i> based {@link StatsProvider} implementation.
+ */
+public class PrometheusMetricsProvider implements StatsProvider {
+
+ private ScheduledExecutorService executor;
+
+ public static final String PROMETHEUS_STATS_HTTP_ENABLE = "prometheusStatsHttpEnable";
+ public static final boolean DEFAULT_PROMETHEUS_STATS_HTTP_ENABLE = true;
+
+ public static final String PROMETHEUS_STATS_HTTP_ADDRESS = "prometheusStatsHttpAddress";
+ public static final String DEFAULT_PROMETHEUS_STATS_HTTP_ADDR = "0.0.0.0";
+
+ public static final String PROMETHEUS_STATS_HTTP_PORT = "prometheusStatsHttpPort";
+ public static final int DEFAULT_PROMETHEUS_STATS_HTTP_PORT = 8000;
+
+ public static final String PROMETHEUS_STATS_LATENCY_ROLLOVER_SECONDS = "prometheusStatsLatencyRolloverSeconds";
+ public static final int DEFAULT_PROMETHEUS_STATS_LATENCY_ROLLOVER_SECONDS = 60;
+
+ final CollectorRegistry registry;
+
+ Server server;
+
+ /*
+ * These acts a registry of the metrics defined in this provider
+ */
+ final ConcurrentMap<ScopeContext, LongAdderCounter> counters = new ConcurrentHashMap<>();
+ final ConcurrentMap<ScopeContext, SimpleGauge<? extends Number>> gauges = new ConcurrentHashMap<>();
+ final ConcurrentMap<ScopeContext, DataSketchesOpStatsLogger> opStats = new ConcurrentHashMap<>();
+ final ConcurrentMap<ScopeContext, ThreadScopedDataSketchesStatsLogger> threadScopedOpStats =
+ new ConcurrentHashMap<>();
+ final ConcurrentMap<ScopeContext, ThreadScopedLongAdderCounter> threadScopedCounters =
+ new ConcurrentHashMap<>();
+
+ public PrometheusMetricsProvider() {
+ this(CollectorRegistry.defaultRegistry);
+ }
+
+ public PrometheusMetricsProvider(CollectorRegistry registry) {
+ this.registry = registry;
+ }
+
+ @Override
+ public void start(Configuration conf) {
+ boolean httpEnabled = conf.getBoolean(PROMETHEUS_STATS_HTTP_ENABLE, DEFAULT_PROMETHEUS_STATS_HTTP_ENABLE);
+ boolean bkHttpServerEnabled = conf.getBoolean("httpServerEnabled", false);
+ boolean exposeDefaultJVMMetrics = conf.getBoolean("exposeDefaultJVMMetrics", true);
+ // only start its own http server when prometheus http is enabled and bk http server is not enabled.
+ if (httpEnabled && !bkHttpServerEnabled) {
+ String httpAddr = conf.getString(PROMETHEUS_STATS_HTTP_ADDRESS, DEFAULT_PROMETHEUS_STATS_HTTP_ADDR);
+ int httpPort = conf.getInt(PROMETHEUS_STATS_HTTP_PORT, DEFAULT_PROMETHEUS_STATS_HTTP_PORT);
+ InetSocketAddress httpEndpoint = InetSocketAddress.createUnresolved(httpAddr, httpPort);
+ this.server = new Server(httpEndpoint);
+ ServletContextHandler context = new ServletContextHandler();
+ context.setContextPath("/");
+ server.setHandler(context);
+
+ context.addServlet(new ServletHolder(new PrometheusServlet(this)), "/metrics");
+
+ try {
+ server.start();
+ log.info("Started Prometheus stats endpoint at {}", httpEndpoint);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ if (exposeDefaultJVMMetrics) {
+ // Include standard JVM stats
+ registerMetrics(new StandardExports());
+ registerMetrics(new MemoryPoolsExports());
+ registerMetrics(new GarbageCollectorExports());
+ registerMetrics(new ThreadExports());
+
+ // Add direct memory allocated through unsafe
+ registerMetrics(Gauge.build("jvm_memory_direct_bytes_used", "-").create().setChild(new Child() {
+ @Override
+ public double get() {
+ return getDirectMemoryUsage.get();
+ }
+ }));
+
+ registerMetrics(Gauge.build("jvm_memory_direct_bytes_max", "-").create().setChild(new Child() {
+ @Override
+ public double get() {
+ return PlatformDependent.estimateMaxDirectMemory();
+ }
+ }));
+ }
+
+ executor = Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory("metrics"));
+
+ int latencyRolloverSeconds = conf.getInt(PROMETHEUS_STATS_LATENCY_ROLLOVER_SECONDS,
+ DEFAULT_PROMETHEUS_STATS_LATENCY_ROLLOVER_SECONDS);
+
+ executor.scheduleAtFixedRate(() -> {
+ rotateLatencyCollection();
+ }, 1, latencyRolloverSeconds, TimeUnit.SECONDS);
+
+ }
+
+ @Override
+ public void stop() {
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ log.warn("Failed to shutdown Jetty server", e);
+ } finally {
+ ThreadRegistry.clear();
+ }
+ }
+ }
+
+ @Override
+ public StatsLogger getStatsLogger(String scope) {
+ return new PrometheusStatsLogger(PrometheusMetricsProvider.this, scope, Collections.emptyMap());
+ }
+
+ @Override
+ public void writeAllMetrics(Writer writer) throws IOException {
+ PrometheusTextFormat prometheusTextFormat = new PrometheusTextFormat();
+ PrometheusTextFormat.writeMetricsCollectedByPrometheusClient(writer, registry);
+
+ gauges.forEach((sc, gauge) -> prometheusTextFormat.writeGauge(writer, sc.getScope(), gauge));
+ counters.forEach((sc, counter) -> prometheusTextFormat.writeCounter(writer, sc.getScope(), counter));
+ opStats.forEach((sc, opStatLogger) ->
+ prometheusTextFormat.writeOpStat(writer, sc.getScope(), opStatLogger));
+ }
+
+ @Override
+ public String getStatsName(String... statsComponents) {
+ String completeName;
+ if (statsComponents.length == 0) {
+ return "";
+ } else if (statsComponents[0].isEmpty()) {
+ completeName = StringUtils.join(statsComponents, '_', 1, statsComponents.length);
+ } else {
+ completeName = StringUtils.join(statsComponents, '_');
+ }
+ return Collector.sanitizeMetricName(completeName);
+ }
+
+ @VisibleForTesting
+ void rotateLatencyCollection() {
+ opStats.forEach((name, metric) -> {
+ metric.rotateLatencyCollection();
+ });
+ }
+
+ private void registerMetrics(Collector collector) {
+ try {
+ collector.register(registry);
+ } catch (Exception e) {
+ // Ignore if these were already registered
+ if (log.isDebugEnabled()) {
+ log.debug("Failed to register Prometheus collector exports", e);
+ }
+ }
+ }
+
+ private static final Logger log = LoggerFactory.getLogger(PrometheusMetricsProvider.class);
+
+ /*
+ * Try to get Netty counter of used direct memory. This will be correct, unlike the JVM values.
+ */
+ private static final AtomicLong directMemoryUsage;
+ private static final Optional<BufferPoolMXBean> poolMxBeanOp;
+ private static final Supplier<Double> getDirectMemoryUsage;
+
+ static {
+ if (PlatformDependent.useDirectBufferNoCleaner()) {
+ poolMxBeanOp = Optional.empty();
+ AtomicLong tmpDirectMemoryUsage = null;
+ try {
+ Field field = PlatformDependent.class.getDeclaredField("DIRECT_MEMORY_COUNTER");
+ field.setAccessible(true);
+ tmpDirectMemoryUsage = (AtomicLong) field.get(null);
+ } catch (Throwable t) {
+ log.warn("Failed to access netty DIRECT_MEMORY_COUNTER field {}", t.getMessage());
+ }
+ directMemoryUsage = tmpDirectMemoryUsage;
+ getDirectMemoryUsage = () -> directMemoryUsage != null ? directMemoryUsage.get() : Double.NaN;
+ } else {
+ directMemoryUsage = null;
+ List<BufferPoolMXBean> platformMXBeans = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);
+ poolMxBeanOp = platformMXBeans.stream()
+ .filter(bufferPoolMXBean -> bufferPoolMXBean.getName().equals("direct")).findAny();
+ getDirectMemoryUsage = () -> poolMxBeanOp.isPresent() ? poolMxBeanOp.get().getMemoryUsed() : Double.NaN;
+ }
+ }
+}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusServlet.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusServlet.java
new file mode 100644
index 00000000000..88c572d8c4d
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusServlet.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import io.prometheus.client.exporter.common.TextFormat;
+import java.io.IOException;
+import java.io.Writer;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet used to export metrics in prometheus text format.
+ */
+public class PrometheusServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+
+ private final transient PrometheusMetricsProvider provider;
+
+ public PrometheusServlet(PrometheusMetricsProvider provider) {
+ this.provider = provider;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType(TextFormat.CONTENT_TYPE_004);
+
+ Writer writer = resp.getWriter();
+ try {
+ provider.writeAllMetrics(writer);
+ writer.flush();
+ } finally {
+ writer.close();
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ doGet(req, resp);
+ }
+
+}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusStatsLogger.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusStatsLogger.java
new file mode 100644
index 00000000000..b3f8098590f
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusStatsLogger.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import com.google.common.base.Joiner;
+import io.prometheus.client.Collector;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.bookkeeper.stats.Counter;
+import org.apache.bookkeeper.stats.Gauge;
+import org.apache.bookkeeper.stats.OpStatsLogger;
+import org.apache.bookkeeper.stats.StatsLogger;
+
+/**
+ * A {@code Prometheus} based {@link StatsLogger} implementation.
+ */
+public class PrometheusStatsLogger implements StatsLogger {
+
+ private final PrometheusMetricsProvider provider;
+ private final String scope;
+ private final Map<String, String> labels;
+
+ PrometheusStatsLogger(PrometheusMetricsProvider provider, String scope, Map<String, String> labels) {
+ this.provider = provider;
+ this.scope = scope;
+ this.labels = labels;
+ }
+
+ @Override
+ public OpStatsLogger getOpStatsLogger(String name) {
+ return provider.opStats.computeIfAbsent(scopeContext(name), x -> new DataSketchesOpStatsLogger(labels));
+ }
+
+ @Override
+ public OpStatsLogger getThreadScopedOpStatsLogger(String name) {
+ return provider.threadScopedOpStats.computeIfAbsent(scopeContext(name),
+ x -> new ThreadScopedDataSketchesStatsLogger(provider, x, labels));
+ }
+
+ @Override
+ public Counter getCounter(String name) {
+ return provider.counters.computeIfAbsent(scopeContext(name), x -> new LongAdderCounter(labels));
+ }
+
+ public Counter getThreadScopedCounter(String name) {
+ return provider.threadScopedCounters.computeIfAbsent(scopeContext(name),
+ x -> new ThreadScopedLongAdderCounter(provider, x, labels));
+ }
+
+ @Override
+ public <T extends Number> void registerGauge(String name, Gauge<T> gauge) {
+ provider.gauges.computeIfAbsent(scopeContext(name), x -> new SimpleGauge<T>(gauge, labels));
+ }
+
+ @Override
+ public <T extends Number> void unregisterGauge(String name, Gauge<T> gauge) {
+ // no-op
+ }
+
+ @Override
+ public void removeScope(String name, StatsLogger statsLogger) {
+ // no-op
+ }
+
+ @Override
+ public StatsLogger scope(String name) {
+ return new PrometheusStatsLogger(provider, completeName(name), labels);
+ }
+
+ @Override
+ public StatsLogger scopeLabel(String labelName, String labelValue) {
+ Map<String, String> newLabels = new TreeMap<>(labels);
+ newLabels.put(labelName, labelValue);
+ return new PrometheusStatsLogger(provider, scope, newLabels);
+ }
+
+ private ScopeContext scopeContext(String name) {
+ return new ScopeContext(completeName(name), labels);
+ }
+
+ private String completeName(String name) {
+ return Collector.sanitizeMetricName(scope.isEmpty() ? name : Joiner.on('_').join(scope, name));
+ }
+}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusTextFormat.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusTextFormat.java
new file mode 100644
index 00000000000..97ad0ea8fbe
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/PrometheusTextFormat.java
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.Collector.MetricFamilySamples;
+import io.prometheus.client.Collector.MetricFamilySamples.Sample;
+import io.prometheus.client.CollectorRegistry;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Logic to write metrics in Prometheus text format.
+ */
+public class PrometheusTextFormat {
+
+ Set<String> metricNameSet = new HashSet<>();
+
+ void writeGauge(Writer w, String name, SimpleGauge<? extends Number> gauge) {
+ // Example:
+ // # TYPE bookie_storage_entries_count gauge
+ // bookie_storage_entries_count 519
+ try {
+ writeType(w, name, "gauge");
+ w.append(name);
+ writeLabels(w, gauge.getLabels());
+ w.append(' ').append(gauge.getSample().toString()).append('\n');
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void writeCounter(Writer w, String name, LongAdderCounter counter) {
+ // Example:
+ // # TYPE jvm_threads_started_total counter
+ // jvm_threads_started_total 59
+ try {
+ writeType(w, name, "counter");
+ w.append(name);
+ writeLabels(w, counter.getLabels());
+ w.append(' ').append(counter.get().toString()).append('\n');
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void writeOpStat(Writer w, String name, DataSketchesOpStatsLogger opStat) {
+ // Example:
+ // # TYPE bookie_journal_JOURNAL_ADD_ENTRY summary
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="0.5",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="0.75",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="0.95",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="0.99",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="0.999",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="0.9999",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="false",quantile="1.0",} NaN
+ // bookie_journal_JOURNAL_ADD_ENTRY_count{success="false",} 0.0
+ // bookie_journal_JOURNAL_ADD_ENTRY_sum{success="false",} 0.0
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="0.5",} 1.706
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="0.75",} 1.89
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="0.95",} 2.121
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="0.99",} 10.708
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="0.999",} 10.902
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="0.9999",} 10.902
+ // bookie_journal_JOURNAL_ADD_ENTRY{success="true",quantile="1.0",} 10.902
+ // bookie_journal_JOURNAL_ADD_ENTRY_count{success="true",} 658.0
+ // bookie_journal_JOURNAL_ADD_ENTRY_sum{success="true",} 1265.0800000000002
+ try {
+ writeType(w, name, "summary");
+ writeQuantile(w, opStat, name, false, 0.5);
+ writeQuantile(w, opStat, name, false, 0.75);
+ writeQuantile(w, opStat, name, false, 0.95);
+ writeQuantile(w, opStat, name, false, 0.99);
+ writeQuantile(w, opStat, name, false, 0.999);
+ writeQuantile(w, opStat, name, false, 0.9999);
+ writeQuantile(w, opStat, name, false, 1.0);
+ writeCount(w, opStat, name, false);
+ writeSum(w, opStat, name, false);
+
+ writeQuantile(w, opStat, name, true, 0.5);
+ writeQuantile(w, opStat, name, true, 0.75);
+ writeQuantile(w, opStat, name, true, 0.95);
+ writeQuantile(w, opStat, name, true, 0.99);
+ writeQuantile(w, opStat, name, true, 0.999);
+ writeQuantile(w, opStat, name, true, 0.9999);
+ writeQuantile(w, opStat, name, true, 1.0);
+ writeCount(w, opStat, name, true);
+ writeSum(w, opStat, name, true);
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void writeLabels(Writer w, Map<String, String> labels) throws IOException {
+ if (labels.isEmpty()) {
+ return;
+ }
+
+ w.append('{');
+ writeLabelsNoBraces(w, labels);
+ w.append('}');
+ }
+
+ private void writeLabelsNoBraces(Writer w, Map<String, String> labels) throws IOException {
+ if (labels.isEmpty()) {
+ return;
+ }
+
+ boolean isFirst = true;
+ for (Map.Entry<String, String> e : labels.entrySet()) {
+ if (!isFirst) {
+ w.append(',');
+ }
+ isFirst = false;
+ w.append(e.getKey())
+ .append("=\"")
+ .append(e.getValue())
+ .append('"');
+ }
+ }
+
+ private void writeQuantile(Writer w, DataSketchesOpStatsLogger opStat, String name, Boolean success,
+ double quantile) throws IOException {
+ w.append(name)
+ .append("{success=\"").append(success.toString())
+ .append("\",quantile=\"").append(Double.toString(quantile))
+ .append("\"");
+ if (!opStat.getLabels().isEmpty()) {
+ w.append(", ");
+ writeLabelsNoBraces(w, opStat.getLabels());
+ }
+ w.append("} ")
+ .append(Double.toString(opStat.getQuantileValue(success, quantile))).append('\n');
+ }
+
+ private void writeCount(Writer w, DataSketchesOpStatsLogger opStat, String name, Boolean success)
+ throws IOException {
+ w.append(name).append("_count{success=\"").append(success.toString()).append("\"");
+ if (!opStat.getLabels().isEmpty()) {
+ w.append(", ");
+ writeLabelsNoBraces(w, opStat.getLabels());
+ }
+ w.append("} ")
+ .append(Long.toString(opStat.getCount(success))).append('\n');
+ }
+
+ private void writeSum(Writer w, DataSketchesOpStatsLogger opStat, String name, Boolean success)
+ throws IOException {
+ w.append(name).append("_sum{success=\"").append(success.toString()).append("\"");
+ if (!opStat.getLabels().isEmpty()) {
+ w.append(", ");
+ writeLabelsNoBraces(w, opStat.getLabels());
+ }
+ w.append("} ")
+ .append(Double.toString(opStat.getSum(success))).append('\n');
+ }
+
+ static void writeMetricsCollectedByPrometheusClient(Writer w, CollectorRegistry registry) throws IOException {
+ Enumeration<MetricFamilySamples> metricFamilySamples = registry.metricFamilySamples();
+ while (metricFamilySamples.hasMoreElements()) {
+ MetricFamilySamples metricFamily = metricFamilySamples.nextElement();
+ // Write type of metric
+ w.append("# TYPE ").append(metricFamily.name).append(getTypeNameSuffix(metricFamily.type)).append(' ')
+ .append(getTypeStr(metricFamily.type)).write('\n');
+
+ for (int i = 0; i < metricFamily.samples.size(); i++) {
+ Sample sample = metricFamily.samples.get(i);
+ w.write(sample.name);
+ w.write('{');
+ for (int j = 0; j < sample.labelNames.size(); j++) {
+ if (j != 0) {
+ w.write(", ");
+ }
+ w.write(sample.labelNames.get(j));
+ w.write("=\"");
+ w.write(sample.labelValues.get(j));
+ w.write('"');
+ }
+
+ w.write("} ");
+ w.write(Collector.doubleToGoString(sample.value));
+ w.write('\n');
+ }
+ }
+ }
+
+ static String getTypeNameSuffix(Collector.Type type) {
+ if (type.equals(Collector.Type.INFO)) {
+ return "_info";
+ }
+ return "";
+ }
+
+ static String getTypeStr(Collector.Type type) {
+ switch (type) {
+ case COUNTER:
+ return "counter";
+ case GAUGE:
+ case INFO:
+ return "gauge";
+ case SUMMARY:
+ return "summary";
+ case HISTOGRAM:
+ return "histogram";
+ case UNKNOWN:
+ default:
+ return "unknown";
+ }
+ }
+
+ void writeType(Writer w, String name, String type) throws IOException {
+ if (metricNameSet.contains(name)) {
+ return;
+ }
+ metricNameSet.add(name);
+ w.append("# TYPE ").append(name).append(" ").append(type).append("\n");
+ }
+
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ScopeContext.java
similarity index 50%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ScopeContext.java
index 65cc10e0d3b..4f15c006569 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ScopeContext.java
@@ -16,30 +16,41 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
+import java.util.Map;
+import java.util.Objects;
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
+/**
+ * Holder for a scope and a set of associated labels.
+ */
+public class ScopeContext {
+ private final String scope;
+ private final Map<String, String> labels;
+ public ScopeContext(String scope, Map<String, String> labels) {
+ this.scope = scope;
+ this.labels = labels;
}
- @Override
- public String getBasePath() {
- return null;
+ public String getScope() {
+ return scope;
}
@Override
- public ServletHolder getServletHolder() {
- return null;
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ScopeContext that = (ScopeContext) o;
+ return Objects.equals(scope, that.scope) && Objects.equals(labels, that.labels);
}
@Override
- public void close() {
- // no-op
+ public int hashCode() {
+ return Objects.hash(scope, labels);
}
}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/SimpleGauge.java
similarity index 58%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/SimpleGauge.java
index 65cc10e0d3b..384cc682c2f 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/SimpleGauge.java
@@ -16,30 +16,29 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
+import java.util.Map;
+import org.apache.bookkeeper.stats.Gauge;
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
+/**
+ * A {@link Gauge} implementation that forwards on the value supplier.
+ */
+public class SimpleGauge<T extends Number> {
- }
+ private final Map<String, String> labels;
+ private final Gauge<T> gauge;
- @Override
- public String getBasePath() {
- return null;
+ public SimpleGauge(final Gauge<T> gauge, Map<String, String> labels) {
+ this.gauge = gauge;
+ this.labels = labels;
}
- @Override
- public ServletHolder getServletHolder() {
- return null;
+ Number getSample() {
+ return gauge.getSample();
}
- @Override
- public void close() {
- // no-op
+ public Map<String, String> getLabels() {
+ return labels;
}
}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ThreadScopedDataSketchesStatsLogger.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ThreadScopedDataSketchesStatsLogger.java
new file mode 100644
index 00000000000..4d3d5662d9c
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ThreadScopedDataSketchesStatsLogger.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.bookkeeper.stats.OpStatsData;
+import org.apache.bookkeeper.stats.OpStatsLogger;
+import org.apache.bookkeeper.stats.ThreadRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * OpStatsLogger implementation that lazily registers OpStatsLoggers per thread
+ * with added labels for the threadpool/thresd name and thread no.
+ */
+public class ThreadScopedDataSketchesStatsLogger implements OpStatsLogger {
+
+ private static Logger logger = LoggerFactory.getLogger(ThreadScopedDataSketchesStatsLogger.class);
+
+ private ThreadLocal<DataSketchesOpStatsLogger> statsLoggers;
+ private DataSketchesOpStatsLogger defaultStatsLogger;
+ private Map<String, String> originalLabels;
+ private ScopeContext scopeContext;
+ private PrometheusMetricsProvider provider;
+
+ public ThreadScopedDataSketchesStatsLogger(PrometheusMetricsProvider provider,
+ ScopeContext scopeContext,
+ Map<String, String> labels) {
+ this.provider = provider;
+ this.scopeContext = scopeContext;
+ this.originalLabels = labels;
+ this.defaultStatsLogger = new DataSketchesOpStatsLogger(labels);
+
+ Map<String, String> defaultLabels = new HashMap<>(labels);
+ defaultLabels.put("threadPool", "?");
+ defaultLabels.put("thread", "?");
+ this.defaultStatsLogger.initializeThread(defaultLabels);
+
+ this.statsLoggers = ThreadLocal.withInitial(() -> {
+ return new DataSketchesOpStatsLogger(labels);
+ });
+ }
+
+ @Override
+ public void registerFailedEvent(long eventLatency, TimeUnit unit) {
+ getStatsLogger().registerFailedEvent(eventLatency, unit);
+ }
+
+ @Override
+ public void registerSuccessfulEvent(long eventLatency, TimeUnit unit) {
+ getStatsLogger().registerSuccessfulEvent(eventLatency, unit);
+ }
+
+ @Override
+ public void registerSuccessfulValue(long value) {
+ getStatsLogger().registerSuccessfulValue(value);
+ }
+
+ @Override
+ public void registerFailedValue(long value) {
+ getStatsLogger().registerFailedValue(value);
+ }
+
+ @Override
+ public OpStatsData toOpStatsData() {
+ // Not relevant as we don't use JMX here
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ // Not relevant as we don't use JMX here
+ throw new UnsupportedOperationException();
+ }
+
+ private DataSketchesOpStatsLogger getStatsLogger() {
+ DataSketchesOpStatsLogger statsLogger = statsLoggers.get();
+
+ // Lazy registration
+ // Update the stats logger with the thread labels then add to the provider
+ // If for some reason this thread did not get registered,
+ // then we fallback to a standard OpsStatsLogger (defaultStatsLogger)
+ if (!statsLogger.isThreadInitialized()) {
+ ThreadRegistry.ThreadPoolThread tpt = ThreadRegistry.get();
+ if (tpt == null) {
+ logger.warn("Thread {} was not registered in the thread registry. Using default stats logger {}.",
+ Thread.currentThread(), defaultStatsLogger);
+ statsLoggers.set(defaultStatsLogger);
+ DataSketchesOpStatsLogger previous = provider.opStats
+ .put(new ScopeContext(scopeContext.getScope(), originalLabels), defaultStatsLogger);
+ // If we overwrite a logger, metrics will not be collected correctly
+ if (previous != null && previous != defaultStatsLogger) {
+ logger.error("Invalid state for thead " + Thread.currentThread() + ". Overwrote a stats logger."
+ + "New is {}, previous was {}",
+ defaultStatsLogger, previous);
+ throw new IllegalStateException("Invalid state. Overwrote a stats logger.");
+ }
+ return defaultStatsLogger;
+ } else {
+ Map<String, String> threadScopedlabels = new HashMap<>(originalLabels);
+ threadScopedlabels.put("threadPool", tpt.getThreadPool());
+ threadScopedlabels.put("thread", String.valueOf(tpt.getOrdinal()));
+
+ statsLogger.initializeThread(threadScopedlabels);
+ DataSketchesOpStatsLogger previous = provider.opStats
+ .put(new ScopeContext(scopeContext.getScope(), threadScopedlabels), statsLogger);
+ // If we overwrite a logger, metrics will not be collected correctly
+ if (previous != null && previous != statsLogger) {
+ logger.error("Invalid state for thead " + Thread.currentThread() + ". Overwrote a stats logger."
+ + "New is {}, previous was {}",
+ defaultStatsLogger, previous);
+ throw new IllegalStateException("Invalid state. Overwrote a stats logger.");
+ }
+ }
+ }
+
+ return statsLogger;
+ }
+}
\ No newline at end of file
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ThreadScopedLongAdderCounter.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ThreadScopedLongAdderCounter.java
new file mode 100644
index 00000000000..1c2287f5b9c
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/ThreadScopedLongAdderCounter.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import org.apache.bookkeeper.stats.Counter;
+import org.apache.bookkeeper.stats.ThreadRegistry;
+
+/**
+ * {@link Counter} implementation that lazily registers LongAdderCounters per thread
+ * * with added labels for the threadpool/thread name and thread no.
+ */
+public class ThreadScopedLongAdderCounter implements Counter {
+ private ThreadLocal<LongAdderCounter> counters;
+ private LongAdderCounter defaultCounter;
+ private Map<String, String> originalLabels;
+ private ScopeContext scopeContext;
+ private PrometheusMetricsProvider provider;
+
+ public ThreadScopedLongAdderCounter(PrometheusMetricsProvider provider,
+ ScopeContext scopeContext,
+ Map<String, String> labels) {
+ this.provider = provider;
+ this.scopeContext = scopeContext;
+ this.originalLabels = new HashMap<>(labels);
+ this.defaultCounter = new LongAdderCounter(labels);
+ Map<String, String> defaultLabels = new HashMap<>(labels);
+ defaultLabels.put("threadPool", "?");
+ defaultLabels.put("thread", "?");
+ this.defaultCounter.initializeThread(defaultLabels);
+
+ this.counters = ThreadLocal.withInitial(() -> {
+ return new LongAdderCounter(labels);
+ });
+ }
+
+ @Override
+ public void clear() {
+ getCounter().clear();
+ }
+
+ @Override
+ public void inc() {
+ getCounter().inc();
+ }
+
+ @Override
+ public void dec() {
+ getCounter().dec();
+ }
+
+ @Override
+ public void addCount(long delta) {
+ getCounter().addCount(delta);
+ }
+
+ @Override
+ public void addLatency(long eventLatency, TimeUnit unit) {
+ getCounter().addLatency(eventLatency, unit);
+ }
+
+ @Override
+ public Long get() {
+ return getCounter().get();
+ }
+
+ private LongAdderCounter getCounter() {
+ LongAdderCounter counter = counters.get();
+
+ // Lazy registration
+ // Update the counter with the thread labels then add to the provider
+ // If for some reason this thread did not get registered,
+ // then we fallback to a standard counter (defaultCounter)
+ if (!counter.isThreadInitialized()) {
+ ThreadRegistry.ThreadPoolThread tpt = ThreadRegistry.get();
+
+ if (tpt == null) {
+ counters.set(defaultCounter);
+ provider.counters.put(new ScopeContext(scopeContext.getScope(), originalLabels), defaultCounter);
+ return defaultCounter;
+ } else {
+ Map<String, String> threadScopedlabels = new HashMap<>(originalLabels);
+ threadScopedlabels.put("threadPool", tpt.getThreadPool());
+ threadScopedlabels.put("thread", String.valueOf(tpt.getOrdinal()));
+
+ counter.initializeThread(threadScopedlabels);
+ provider.counters.put(new ScopeContext(scopeContext.getScope(), threadScopedlabels), counter);
+ }
+ }
+
+ return counter;
+ }
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/package-info.java
similarity index 59%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/package-info.java
index 65cc10e0d3b..fd5cf0e154a 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/main/java/org/apache/pulsar/metrics/prometheus/bookkeeper/package-info.java
@@ -16,30 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
-
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
-
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
-
- }
-
- @Override
- public String getBasePath() {
- return null;
- }
-
- @Override
- public ServletHolder getServletHolder() {
- return null;
- }
-
- @Override
- public void close() {
- // no-op
- }
-}
+/**
+ * A stats provider implementation for BookKeeper that uses Jetty 12.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/test/java/org/apache/pulsar/metrics/prometheus/bookkeeper/TestPrometheusFormatter.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/test/java/org/apache/pulsar/metrics/prometheus/bookkeeper/TestPrometheusFormatter.java
new file mode 100644
index 00000000000..fceee727270
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/test/java/org/apache/pulsar/metrics/prometheus/bookkeeper/TestPrometheusFormatter.java
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.Gauge;
+import io.prometheus.client.hotspot.GarbageCollectorExports;
+import io.prometheus.client.hotspot.MemoryPoolsExports;
+import io.prometheus.client.hotspot.StandardExports;
+import io.prometheus.client.hotspot.ThreadExports;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.bookkeeper.stats.Counter;
+import org.apache.bookkeeper.stats.OpStatsLogger;
+import org.apache.bookkeeper.stats.StatsLogger;
+import org.testng.annotations.Test;
+
+/**
+ * Test for {@link PrometheusMetricsProvider}.
+ */
+public class TestPrometheusFormatter {
+
+ @Test(timeOut = 30000)
+ public void testStatsOutput() throws Exception {
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ StatsLogger statsLogger = provider.getStatsLogger("test");
+ Counter counter = statsLogger.getCounter("my_counter");
+
+ counter.inc();
+ counter.inc();
+
+ OpStatsLogger opStats = statsLogger.getOpStatsLogger("op");
+ opStats.registerSuccessfulEvent(10, TimeUnit.MILLISECONDS);
+ opStats.registerSuccessfulEvent(5, TimeUnit.MILLISECONDS);
+
+ OpStatsLogger opStats1 = statsLogger.scopeLabel("test_label", "test_value")
+ .getOpStatsLogger("op_label");
+ opStats1.registerSuccessfulEvent(10, TimeUnit.MILLISECONDS);
+ opStats1.registerSuccessfulEvent(5, TimeUnit.MILLISECONDS);
+ opStats1.registerFailedEvent(1, TimeUnit.MILLISECONDS);
+
+ provider.rotateLatencyCollection();
+
+ StringWriter writer = new StringWriter();
+ provider.writeAllMetrics(writer);
+
+ writer.write("jvm_memory_direct_bytes_max{} 4.77626368E8\n");
+ writer.write("jvm_memory_pool_bytes_used{pool=\"Code Cache\"} 3347712.0\n");
+ writer.write("jvm_memory_pool_bytes_used{pool=\"CodeHeap 'non-nmethods'\"} 1207168.0\n");
+ System.out.println(writer);
+ Multimap<String, Metric> metrics = parseMetrics(writer.toString());
+ System.out.println(metrics);
+
+ List<Metric> cm = (List<Metric>) metrics.get("test_my_counter");
+ assertEquals(1, cm.size());
+ assertEquals(0, cm.get(0).tags.size());
+ assertEquals(2.0, cm.get(0).value, 0.0);
+
+ // test_op_sum
+ cm = (List<Metric>) metrics.get("test_op_sum");
+ assertEquals(2, cm.size());
+ Metric m = cm.get(0);
+ assertEquals(1, cm.get(0).tags.size());
+ assertEquals(0.0, m.value, 0.0);
+ assertEquals(1, m.tags.size());
+ assertEquals("false", m.tags.get("success"));
+
+ m = cm.get(1);
+ assertEquals(1, cm.get(0).tags.size());
+ assertEquals(15.0, m.value, 0.0);
+ assertEquals(1, m.tags.size());
+ assertEquals("true", m.tags.get("success"));
+
+ // test_op_count
+ cm = (List<Metric>) metrics.get("test_op_count");
+ assertEquals(2, cm.size());
+ m = cm.get(0);
+ assertEquals(1, cm.get(0).tags.size());
+ assertEquals(0.0, m.value, 0.0);
+ assertEquals(1, m.tags.size());
+ assertEquals("false", m.tags.get("success"));
+
+ m = cm.get(1);
+ assertEquals(1, cm.get(0).tags.size());
+ assertEquals(2.0, m.value, 0.0);
+ assertEquals(1, m.tags.size());
+ assertEquals("true", m.tags.get("success"));
+
+ // Latency
+ cm = (List<Metric>) metrics.get("test_op");
+ assertEquals(14, cm.size());
+
+ boolean found = false;
+ for (Metric mt : cm) {
+ if ("true".equals(mt.tags.get("success")) && "1.0".equals(mt.tags.get("quantile"))) {
+ assertEquals(10.0, mt.value, 0.0);
+ found = true;
+ }
+ }
+
+ assertTrue(found);
+
+ // test_op_label_sum
+ cm = (List<Metric>) metrics.get("test_op_label_sum");
+ assertEquals(2, cm.size());
+ m = cm.get(0);
+ assertEquals(2, m.tags.size());
+ assertEquals(1.0, m.value, 0.0);
+ assertEquals("false", m.tags.get("success"));
+ assertEquals("test_value", m.tags.get("test_label"));
+
+ m = cm.get(1);
+ assertEquals(15.0, m.value, 0.0);
+ assertEquals(2, m.tags.size());
+ assertEquals("true", m.tags.get("success"));
+ assertEquals("test_value", m.tags.get("test_label"));
+
+ // test_op_label_count
+ cm = (List<Metric>) metrics.get("test_op_label_count");
+ assertEquals(2, cm.size());
+ m = cm.get(0);
+ assertEquals(1, m.value, 0.0);
+ assertEquals(2, m.tags.size());
+ assertEquals("false", m.tags.get("success"));
+ assertEquals("test_value", m.tags.get("test_label"));
+
+ m = cm.get(1);
+ assertEquals(2.0, m.value, 0.0);
+ assertEquals(2, m.tags.size());
+ assertEquals("true", m.tags.get("success"));
+ assertEquals("test_value", m.tags.get("test_label"));
+
+ // Latency
+ cm = (List<Metric>) metrics.get("test_op_label");
+ assertEquals(14, cm.size());
+
+ found = false;
+ for (Metric mt : cm) {
+ if ("true".equals(mt.tags.get("success"))
+ && "test_value".equals(mt.tags.get("test_label"))
+ && "1.0".equals(mt.tags.get("quantile"))) {
+ assertEquals(10.0, mt.value, 0.0);
+ found = true;
+ }
+ }
+
+ assertTrue(found);
+ }
+
+ @Test
+ public void testWriteMetricsCollectedByPrometheusClient() {
+ CollectorRegistry registry = CollectorRegistry.defaultRegistry;
+ registry.register(new StandardExports());
+ registry.register(new MemoryPoolsExports());
+ registry.register(new GarbageCollectorExports());
+ registry.register(new ThreadExports());
+ registry.register(Gauge.build("jvm_memory_direct_bytes_used", "-").create().setChild(new Gauge.Child() {
+ @Override
+ public double get() {
+ return 1.0;
+ }
+ }));
+ registry.register(Gauge.build("jvm_memory_direct_bytes_max", "-").create().setChild(new Gauge.Child() {
+ @Override
+ public double get() {
+ return 100.0;
+ }
+ }));
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider(registry);
+ StringWriter writer = new StringWriter();
+ try {
+ provider.rotateLatencyCollection();
+ provider.writeAllMetrics(writer);
+ String output = writer.toString();
+ parseMetrics(output);
+ assertTrue(output.contains("# TYPE jvm_memory_direct_bytes_max gauge"));
+ assertTrue(output.contains("# TYPE jvm_memory_direct_bytes_used gauge"));
+ assertTrue(output.contains("# TYPE jvm_gc_collection_seconds summary"));
+ assertTrue(output.contains("# TYPE jvm_memory_pool_bytes_committed gauge"));
+ assertTrue(output.contains("# TYPE process_cpu_seconds counter"));
+ } catch (Exception e) {
+ fail();
+ }
+
+ }
+
+ @Test
+ public void testPrometheusTypeDuplicate() throws IOException {
+ PrometheusTextFormat prometheusTextFormat = new PrometheusTextFormat();
+ StringWriter writer = new StringWriter();
+ prometheusTextFormat.writeType(writer, "counter", "gauge");
+ prometheusTextFormat.writeType(writer, "counter", "gauge");
+ String string = writer.toString();
+ assertEquals("# TYPE counter gauge\n", string);
+ }
+
+
+ /**
+ * Hacky parsing of Prometheus text format. Sould be good enough for unit tests
+ */
+ private static Multimap<String, Metric> parseMetrics(String metrics) {
+ Multimap<String, Metric> parsed = ArrayListMultimap.create();
+
+ // Example of lines are
+ // jvm_threads_current{cluster="standalone",} 203.0
+ // or
+ // pulsar_subscriptions_count{cluster="standalone", namespace="sample/standalone/ns1",
+ // topic="persistent://sample/standalone/ns1/test-2"} 0.0 1517945780897
+ Pattern pattern = Pattern.compile("^(\\w+)(\\{([^\\}]*)\\})?\\s(-?[\\d\\w\\.]+)(\\s(\\d+))?$");
+ Pattern formatPattern =
+ Pattern.compile("^(\\w+)(\\{((\\w+=[-\\s\\\'\\\"\\.\\w]+(,\\s?\\w+=[\\\"\\.\\w]+)*))?\\})?"
+ + "\\s(-?[\\d\\w\\.]+)(\\s(\\d+))?$");
+ Pattern tagsPattern = Pattern.compile("(\\w+)=\"([^\"]+)\"(,\\s?)?");
+
+ Splitter.on("\n").split(metrics).forEach(line -> {
+ if (line.isEmpty() || line.startsWith("#")) {
+ return;
+ }
+
+ System.err.println("LINE: '" + line + "'");
+ Matcher matcher = pattern.matcher(line);
+ Matcher formatMatcher = formatPattern.matcher(line);
+ System.err.println("Matches: " + matcher.matches());
+ System.err.println(matcher);
+ assertTrue(matcher.matches());
+ assertTrue("failed to validate line: " + line, formatMatcher.matches());
+
+ assertEquals(6, matcher.groupCount());
+ System.err.println("groups: " + matcher.groupCount());
+ for (int i = 0; i < matcher.groupCount(); i++) {
+ System.err.println(" GROUP " + i + " -- " + matcher.group(i));
+ }
+
+ checkArgument(matcher.matches());
+ checkArgument(formatMatcher.matches());
+ String name = matcher.group(1);
+
+ Metric m = new Metric();
+ m.value = Double.parseDouble(matcher.group(4));
+
+ String tags = matcher.group(3);
+ if (tags != null) {
+ Matcher tagsMatcher = tagsPattern.matcher(tags);
+ while (tagsMatcher.find()) {
+ String tag = tagsMatcher.group(1);
+ String value = tagsMatcher.group(2);
+ m.tags.put(tag, value);
+ }
+ }
+
+ parsed.put(name, m);
+ });
+
+ return parsed;
+ }
+
+ static class Metric {
+ Map<String, String> tags = new TreeMap<>();
+ double value;
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("tags", tags).add("value", value).toString();
+ }
+ }
+}
diff --git a/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/test/java/org/apache/pulsar/metrics/prometheus/bookkeeper/TestPrometheusMetricsProvider.java b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/test/java/org/apache/pulsar/metrics/prometheus/bookkeeper/TestPrometheusMetricsProvider.java
new file mode 100644
index 00000000000..a92616062c3
--- /dev/null
+++ b/jetty-upgrade/bookkeeper-prometheus-metrics-provider/src/test/java/org/apache/pulsar/metrics/prometheus/bookkeeper/TestPrometheusMetricsProvider.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.bookkeeper;
+
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertNotNull;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertSame;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+import lombok.Cleanup;
+import org.apache.bookkeeper.stats.Counter;
+import org.apache.bookkeeper.stats.StatsLogger;
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.testng.annotations.Test;
+
+/**
+ * Unit test of {@link PrometheusMetricsProvider}.
+ */
+public class TestPrometheusMetricsProvider {
+
+ @Test
+ public void testStartNoHttp() {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ENABLE, false);
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ try {
+ provider.start(config);
+ assertNull(provider.server);
+ } finally {
+ provider.stop();
+ }
+ }
+
+ @Test
+ public void testStartNoHttpWhenBkHttpEnabled() {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ENABLE, true);
+ config.setProperty("httpServerEnabled", true);
+ @Cleanup("stop") PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ provider.start(config);
+ assertNull(provider.server);
+ }
+
+ @Test
+ public void testStartWithHttp() {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ENABLE, true);
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_PORT, 0); // ephemeral
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ try {
+ provider.start(config);
+ assertNotNull(provider.server);
+ } finally {
+ provider.stop();
+ }
+ }
+
+ @Test
+ public void testStartWithHttpSpecifyAddr() {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ENABLE, true);
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_PORT, 0); // ephemeral
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ADDRESS, "127.0.0.1");
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ try {
+ provider.start(config);
+ assertNotNull(provider.server);
+ } finally {
+ provider.stop();
+ }
+ }
+
+ @Test
+ public void testCounter() {
+ LongAdderCounter counter = new LongAdderCounter(Collections.emptyMap());
+ long value = counter.get();
+ assertEquals(0L, value);
+ counter.inc();
+ assertEquals(1L, counter.get().longValue());
+ counter.dec();
+ assertEquals(0L, counter.get().longValue());
+ counter.addCount(3);
+ assertEquals(3L, counter.get().longValue());
+ }
+
+ @Test
+ public void testCounter2() {
+ LongAdderCounter counter = new LongAdderCounter(Collections.emptyMap());
+ long value = counter.get();
+ assertEquals(0L, value);
+ counter.addLatency(3 * 1000 * 1000L, TimeUnit.NANOSECONDS);
+ assertEquals(3L, counter.get().longValue());
+ }
+
+ @Test
+ public void testTwoCounters() throws Exception {
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ StatsLogger statsLogger = provider.getStatsLogger("test");
+
+ Counter counter1 = statsLogger.getCounter("counter");
+ Counter counter2 = statsLogger.getCounter("counter");
+ assertEquals(counter1, counter2);
+ assertSame(counter1, counter2);
+
+ assertEquals(1, provider.counters.size());
+ }
+
+ @Test
+ public void testJvmDirectMemoryMetrics() throws Exception {
+ PropertiesConfiguration config = new PropertiesConfiguration();
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ENABLE, true);
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_PORT, 0);
+ config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ADDRESS, "127.0.0.1");
+ ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(25);
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ try {
+ provider.start(config);
+ assertNotNull(provider.server);
+ StringWriter writer = new StringWriter();
+ provider.writeAllMetrics(writer);
+ String s = writer.toString();
+ String[] split = s.split(System.lineSeparator());
+ HashMap<String, String> map = new HashMap<>();
+ for (String str : split) {
+ String[] aux = str.split(" ");
+ map.put(aux[0], aux[1]);
+ }
+ String directBytesMax = map.get("jvm_memory_direct_bytes_max{}");
+ assertNotNull(directBytesMax);
+ assertNotEquals("Nan", directBytesMax);
+ assertNotEquals("-1", directBytesMax);
+ String directBytesUsed = map.get("jvm_memory_direct_bytes_used{}");
+ assertNotNull(directBytesUsed);
+ assertNotEquals("Nan", directBytesUsed);
+ // this condition is flaky
+ //assertTrue(Double.parseDouble(directBytesUsed) > 25);
+ // ensure byteBuffer doesn't gc
+ byteBuf.release();
+ } finally {
+ provider.stop();
+ }
+ }
+
+}
diff --git a/jetty-upgrade/pom.xml b/jetty-upgrade/pom.xml
new file mode 100644
index 00000000000..4898d78fc21
--- /dev/null
+++ b/jetty-upgrade/pom.xml
@@ -0,0 +1,39 @@
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <packaging>pom</packaging>
+ <parent>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>pulsar</artifactId>
+ <version>4.0.10-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>jetty-upgrade</artifactId>
+ <name>Pulsar Jetty Upgrade :: Parent</name>
+
+ <modules>
+ <module>bookkeeper-prometheus-metrics-provider</module>
+ <module>zookeeper-prometheus-metrics</module>
+ <module>zookeeper-with-patched-admin</module>
+ </modules>
+</project>
diff --git a/testmocks/pom.xml b/jetty-upgrade/zookeeper-prometheus-metrics/pom.xml
old mode 100644
new mode 100755
similarity index 52%
copy from testmocks/pom.xml
copy to jetty-upgrade/zookeeper-prometheus-metrics/pom.xml
index 505b8cb9609..63768761ce1
--- a/testmocks/pom.xml
+++ b/jetty-upgrade/zookeeper-prometheus-metrics/pom.xml
@@ -19,78 +19,52 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
-
<parent>
- <artifactId>pulsar</artifactId>
<groupId>org.apache.pulsar</groupId>
+ <artifactId>jetty-upgrade</artifactId>
<version>4.0.10-SNAPSHOT</version>
</parent>
- <artifactId>testmocks</artifactId>
+ <artifactId>pulsar-zookeeper-prometheus-metrics</artifactId>
<packaging>jar</packaging>
- <name>Pulsar Test Mocks</name>
+ <name>Apache Pulsar :: Apache ZooKeeper Prometheus Metrics Provider</name>
+ <description>ZooKeeper Prometheus.io Metrics Provider implementation for Jetty 12</description>
<dependencies>
-
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
- <exclusions>
- <exclusion>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-core</artifactId>
- </exclusion>
- <exclusion>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-classic</artifactId>
- </exclusion>
- <exclusion>
- <groupId>io.netty</groupId>
- <artifactId>netty-tcnative</artifactId>
- </exclusion>
- </exclusions>
+ <version>${zookeeper.version}</version>
+ <scope>provided</scope>
</dependency>
-
<dependency>
- <groupId>org.apache.bookkeeper</groupId>
- <artifactId>bookkeeper-server</artifactId>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient</artifactId>
</dependency>
-
<dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-lang3</artifactId>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_hotspot</artifactId>
</dependency>
-
<dependency>
- <groupId>org.testng</groupId>
- <artifactId>testng</artifactId>
+ <groupId>io.prometheus</groupId>
+ <artifactId>simpleclient_servlet</artifactId>
</dependency>
-
<dependency>
- <groupId>org.objenesis</groupId>
- <artifactId>objenesis</artifactId>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-servlet</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
</dependency>
-
</dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-checkstyle-plugin</artifactId>
- <executions>
- <execution>
- <id>checkstyle</id>
- <phase>verify</phase>
- <goals>
- <goal>check</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
-
</project>
diff --git a/jetty-upgrade/zookeeper-prometheus-metrics/src/main/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProvider.java b/jetty-upgrade/zookeeper-prometheus-metrics/src/main/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProvider.java
new file mode 100644
index 00000000000..7b1999004ca
--- /dev/null
+++ b/jetty-upgrade/zookeeper-prometheus-metrics/src/main/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProvider.java
@@ -0,0 +1,632 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.zookeeper;
+
+import io.prometheus.client.Collector;
+import io.prometheus.client.CollectorRegistry;
+import io.prometheus.client.exporter.MetricsServlet;
+import io.prometheus.client.hotspot.DefaultExports;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Enumeration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.zookeeper.metrics.Counter;
+import org.apache.zookeeper.metrics.CounterSet;
+import org.apache.zookeeper.metrics.Gauge;
+import org.apache.zookeeper.metrics.GaugeSet;
+import org.apache.zookeeper.metrics.MetricsContext;
+import org.apache.zookeeper.metrics.MetricsProvider;
+import org.apache.zookeeper.metrics.MetricsProviderLifeCycleException;
+import org.apache.zookeeper.metrics.Summary;
+import org.apache.zookeeper.metrics.SummarySet;
+import org.apache.zookeeper.server.RateLogger;
+import org.eclipse.jetty.ee8.nested.ServletConstraint;
+import org.eclipse.jetty.ee8.security.ConstraintMapping;
+import org.eclipse.jetty.ee8.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.server.Server;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Metrics Provider implementation based on https://prometheus.io.
+ *
+ * @since 3.6.0
+ */
+public class PrometheusMetricsProvider implements MetricsProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PrometheusMetricsProvider.class);
+ private static final String LABEL = "key";
+ private static final String[] LABELS = {LABEL};
+
+ /**
+ * Number of worker threads for reporting Prometheus summary metrics.
+ * Default value is 1.
+ * If the number is less than 1, the main thread will be used.
+ */
+ static final String NUM_WORKER_THREADS = "numWorkerThreads";
+
+ /**
+ * The max queue size for Prometheus summary metrics reporting task.
+ * Default value is 1000000.
+ */
+ static final String MAX_QUEUE_SIZE = "maxQueueSize";
+
+ /**
+ * The timeout in ms for Prometheus worker threads shutdown.
+ * Default value is 1000ms.
+ */
+ static final String WORKER_SHUTDOWN_TIMEOUT_MS = "workerShutdownTimeoutMs";
+
+ /**
+ * We are using the 'defaultRegistry'.
+ * <p>
+ * When you are running ZooKeeper (server or client) together with other
+ * libraries every metrics will be expected as a single view.
+ * </p>
+ */
+ private final CollectorRegistry collectorRegistry = CollectorRegistry.defaultRegistry;
+ private final RateLogger rateLogger = new RateLogger(LOG, 60 * 1000);
+ private String host = "0.0.0.0";
+ private int port = 7000;
+ private boolean exportJvmInfo = true;
+ private Server server;
+ private final MetricsServletImpl servlet = new MetricsServletImpl();
+ private final Context rootContext = new Context();
+ private int numWorkerThreads = 1;
+ private int maxQueueSize = 1000000;
+ private long workerShutdownTimeoutMs = 1000;
+ private Optional<ExecutorService> executorOptional = Optional.empty();
+
+ @Override
+ public void configure(Properties configuration) throws MetricsProviderLifeCycleException {
+ LOG.info("Initializing metrics, configuration: {}", configuration);
+ this.host = configuration.getProperty("httpHost", "0.0.0.0");
+ this.port = Integer.parseInt(configuration.getProperty("httpPort", "7000"));
+ this.exportJvmInfo = Boolean.parseBoolean(configuration.getProperty("exportJvmInfo", "true"));
+ this.numWorkerThreads = Integer.parseInt(
+ configuration.getProperty(NUM_WORKER_THREADS, "1"));
+ this.maxQueueSize = Integer.parseInt(
+ configuration.getProperty(MAX_QUEUE_SIZE, "1000000"));
+ this.workerShutdownTimeoutMs = Long.parseLong(
+ configuration.getProperty(WORKER_SHUTDOWN_TIMEOUT_MS, "1000"));
+ }
+
+ @Override
+ public void start() throws MetricsProviderLifeCycleException {
+ this.executorOptional = createExecutor();
+ try {
+ LOG.info("Starting /metrics HTTP endpoint at host: {}, port: {}, exportJvmInfo: {}",
+ host, port, exportJvmInfo);
+ if (exportJvmInfo) {
+ DefaultExports.initialize();
+ }
+ server = new Server(new InetSocketAddress(host, port));
+ ServletContextHandler context = new ServletContextHandler();
+ context.setContextPath("/");
+ constrainTraceMethod(context);
+ server.setHandler(context);
+ context.addServlet(new ServletHolder(servlet), "/metrics");
+ server.start();
+ } catch (Exception err) {
+ LOG.error("Cannot start /metrics server", err);
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception suppressed) {
+ err.addSuppressed(suppressed);
+ } finally {
+ server = null;
+ }
+ }
+ throw new MetricsProviderLifeCycleException(err);
+ }
+ }
+
+ // for tests
+ MetricsServletImpl getServlet() {
+ return servlet;
+ }
+
+ @Override
+ public MetricsContext getRootContext() {
+ return rootContext;
+ }
+
+ @Override
+ public void stop() {
+ shutdownExecutor();
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception err) {
+ LOG.error("Cannot safely stop Jetty server", err);
+ } finally {
+ server = null;
+ }
+ }
+ }
+
+ /**
+ * Dump all values to the 4lw interface and to the Admin server.
+ * <p>
+ * This method is not expected to be used to serve metrics to Prometheus. We
+ * are using the MetricsServlet provided by Prometheus for that, leaving the
+ * real representation to the Prometheus Java client.
+ * </p>
+ *
+ * @param sink the receiver of data (4lw interface, Admin server or tests)
+ */
+ @Override
+ public void dump(BiConsumer<String, Object> sink) {
+ sampleGauges();
+ Enumeration<Collector.MetricFamilySamples> samplesFamilies = collectorRegistry.metricFamilySamples();
+ while (samplesFamilies.hasMoreElements()) {
+ Collector.MetricFamilySamples samples = samplesFamilies.nextElement();
+ samples.samples.forEach(sample -> {
+ String key = buildKeyForDump(sample);
+ sink.accept(key, sample.value);
+ });
+ }
+ }
+
+ private static String buildKeyForDump(Collector.MetricFamilySamples.Sample sample) {
+ StringBuilder keyBuilder = new StringBuilder();
+ keyBuilder.append(sample.name);
+ if (sample.labelNames.size() > 0) {
+ keyBuilder.append('{');
+ for (int i = 0; i < sample.labelNames.size(); ++i) {
+ if (i > 0) {
+ keyBuilder.append(',');
+ }
+ keyBuilder.append(sample.labelNames.get(i));
+ keyBuilder.append("=\"");
+ keyBuilder.append(sample.labelValues.get(i));
+ keyBuilder.append('"');
+ }
+ keyBuilder.append('}');
+ }
+ return keyBuilder.toString();
+ }
+
+ /**
+ * Update Gauges. In ZooKeeper Metrics API Gauges are callbacks served by
+ * internal components and the value is not held by Prometheus structures.
+ */
+ private void sampleGauges() {
+ rootContext.gauges.values()
+ .forEach(PrometheusGaugeWrapper::sample);
+
+ rootContext.gaugeSets.values()
+ .forEach(PrometheusLabelledGaugeWrapper::sample);
+ }
+
+ @Override
+ public void resetAllValues() {
+ // not supported on Prometheus
+ }
+
+ /**
+ * Add constraint to a given context to disallow TRACE method.
+ * @param ctxHandler the context to modify
+ */
+ private void constrainTraceMethod(ServletContextHandler ctxHandler) {
+ ServletConstraint c = new ServletConstraint();
+ c.setAuthenticate(true);
+
+ ConstraintMapping cmt = new ConstraintMapping();
+ cmt.setConstraint(c);
+ cmt.setMethod("TRACE");
+ cmt.setPathSpec("/*");
+
+ ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
+ securityHandler.setConstraintMappings(new ConstraintMapping[] {cmt});
+
+ ctxHandler.setSecurityHandler(securityHandler);
+ }
+
+ private class Context implements MetricsContext {
+
+ private final ConcurrentMap<String, PrometheusGaugeWrapper> gauges = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusLabelledGaugeWrapper> gaugeSets = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusCounter> counters = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusLabelledCounter> counterSets = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusSummary> basicSummaries = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusSummary> summaries = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusLabelledSummary> basicSummarySets = new ConcurrentHashMap<>();
+ private final ConcurrentMap<String, PrometheusLabelledSummary> summarySets = new ConcurrentHashMap<>();
+
+ @Override
+ public MetricsContext getContext(String name) {
+ // no hierarchy yet
+ return this;
+ }
+
+ @Override
+ public Counter getCounter(String name) {
+ return counters.computeIfAbsent(name, PrometheusCounter::new);
+ }
+
+ @Override
+ public CounterSet getCounterSet(final String name) {
+ Objects.requireNonNull(name, "Cannot register a CounterSet with null name");
+ return counterSets.computeIfAbsent(name, PrometheusLabelledCounter::new);
+ }
+
+ /**
+ * Gauges may go up and down, in ZooKeeper they are a way to export
+ * internal values with a callback.
+ *
+ * @param name the name of the gauge
+ * @param gauge the callback
+ */
+ @Override
+ public void registerGauge(String name, Gauge gauge) {
+ Objects.requireNonNull(name);
+ gauges.compute(name, (id, prev) ->
+ new PrometheusGaugeWrapper(id, gauge, prev != null ? prev.inner : null));
+ }
+
+ @Override
+ public void unregisterGauge(String name) {
+ PrometheusGaugeWrapper existing = gauges.remove(name);
+ if (existing != null) {
+ existing.unregister();
+ }
+ }
+
+ @Override
+ public void registerGaugeSet(final String name, final GaugeSet gaugeSet) {
+ Objects.requireNonNull(name, "Cannot register a GaugeSet with null name");
+ Objects.requireNonNull(gaugeSet, "Cannot register a null GaugeSet for " + name);
+
+ gaugeSets.compute(name, (id, prev) ->
+ new PrometheusLabelledGaugeWrapper(name, gaugeSet, prev != null ? prev.inner : null));
+ }
+
+ @Override
+ public void unregisterGaugeSet(final String name) {
+ Objects.requireNonNull(name, "Cannot unregister GaugeSet with null name");
+
+ final PrometheusLabelledGaugeWrapper existing = gaugeSets.remove(name);
+ if (existing != null) {
+ existing.unregister();
+ }
+ }
+
+ @Override
+ public Summary getSummary(String name, DetailLevel detailLevel) {
+ if (detailLevel == DetailLevel.BASIC) {
+ return basicSummaries.computeIfAbsent(name, (n) -> {
+ if (summaries.containsKey(n)) {
+ throw new IllegalArgumentException("Already registered a non basic summary as " + n);
+ }
+ return new PrometheusSummary(name, detailLevel);
+ });
+ } else {
+ return summaries.computeIfAbsent(name, (n) -> {
+ if (basicSummaries.containsKey(n)) {
+ throw new IllegalArgumentException("Already registered a basic summary as " + n);
+ }
+ return new PrometheusSummary(name, detailLevel);
+ });
+ }
+ }
+
+ @Override
+ public SummarySet getSummarySet(String name, DetailLevel detailLevel) {
+ if (detailLevel == DetailLevel.BASIC) {
+ return basicSummarySets.computeIfAbsent(name, (n) -> {
+ if (summarySets.containsKey(n)) {
+ throw new IllegalArgumentException("Already registered a non basic summary set as " + n);
+ }
+ return new PrometheusLabelledSummary(name, detailLevel);
+ });
+ } else {
+ return summarySets.computeIfAbsent(name, (n) -> {
+ if (basicSummarySets.containsKey(n)) {
+ throw new IllegalArgumentException("Already registered a basic summary set as " + n);
+ }
+ return new PrometheusLabelledSummary(name, detailLevel);
+ });
+ }
+ }
+
+ }
+
+ private class PrometheusCounter implements Counter {
+
+ private final io.prometheus.client.Counter inner;
+ private final String name;
+
+ public PrometheusCounter(String name) {
+ this.name = name;
+ this.inner = io.prometheus.client.Counter
+ .build(name, name)
+ .register(collectorRegistry);
+ }
+
+ @Override
+ public void add(long delta) {
+ try {
+ inner.inc(delta);
+ } catch (IllegalArgumentException err) {
+ LOG.error("invalid delta {} for metric {}", delta, name, err);
+ }
+ }
+
+ @Override
+ public long get() {
+ // this method is used only for tests
+ // Prometheus returns a "double"
+ // it is safe to fine to a long
+ // we are never setting non-integer values
+ return (long) inner.get();
+ }
+
+ }
+
+ private class PrometheusLabelledCounter implements CounterSet {
+ private final String name;
+ private final io.prometheus.client.Counter inner;
+
+ public PrometheusLabelledCounter(final String name) {
+ this.name = name;
+ this.inner = io.prometheus.client.Counter
+ .build(name, name)
+ .labelNames(LABELS)
+ .register(collectorRegistry);
+ }
+
+ @Override
+ public void add(final String key, final long delta) {
+ try {
+ inner.labels(key).inc(delta);
+ } catch (final IllegalArgumentException e) {
+ LOG.error("invalid delta {} for metric {} with key {}", delta, name, key, e);
+ }
+ }
+ }
+
+ private class PrometheusGaugeWrapper {
+
+ private final io.prometheus.client.Gauge inner;
+ private final Gauge gauge;
+ private final String name;
+
+ public PrometheusGaugeWrapper(String name, Gauge gauge, io.prometheus.client.Gauge prev) {
+ this.name = name;
+ this.gauge = gauge;
+ this.inner = prev != null ? prev
+ : io.prometheus.client.Gauge
+ .build(name, name)
+ .register(collectorRegistry);
+ }
+
+ /**
+ * Call the callack and update Prometheus Gauge. This method is called
+ * when the server is polling for a value.
+ */
+ private void sample() {
+ Number value = gauge.get();
+ this.inner.set(value != null ? value.doubleValue() : 0);
+ }
+
+ private void unregister() {
+ collectorRegistry.unregister(inner);
+ }
+ }
+
+ /**
+ * Prometheus implementation of GaugeSet interface. It wraps the GaugeSet object and
+ * uses the callback API to update the Prometheus Gauge.
+ */
+ private class PrometheusLabelledGaugeWrapper {
+ private final GaugeSet gaugeSet;
+ private final io.prometheus.client.Gauge inner;
+
+ private PrometheusLabelledGaugeWrapper(final String name,
+ final GaugeSet gaugeSet,
+ final io.prometheus.client.Gauge prev) {
+ this.gaugeSet = gaugeSet;
+ this.inner = prev != null ? prev :
+ io.prometheus.client.Gauge
+ .build(name, name)
+ .labelNames(LABELS)
+ .register(collectorRegistry);
+ }
+
+ /**
+ * Call the callback provided by the GaugeSet and update Prometheus Gauge.
+ * This method is called when the server is polling for a value.
+ */
+ private void sample() {
+ gaugeSet.values().forEach((key, value) ->
+ this.inner.labels(key).set(value != null ? value.doubleValue() : 0));
+ }
+
+ private void unregister() {
+ collectorRegistry.unregister(inner);
+ }
+ }
+
+ private class PrometheusSummary implements Summary {
+
+ private final io.prometheus.client.Summary inner;
+ private final String name;
+
+ public PrometheusSummary(String name, MetricsContext.DetailLevel level) {
+ this.name = name;
+ if (level == MetricsContext.DetailLevel.ADVANCED) {
+ this.inner = io.prometheus.client.Summary
+ .build(name, name)
+ .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error
+ .quantile(0.9, 0.01) // Add 90th percentile with 1% tolerated error
+ .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error
+ .register(collectorRegistry);
+ } else {
+ this.inner = io.prometheus.client.Summary
+ .build(name, name)
+ .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error
+ .register(collectorRegistry);
+ }
+ }
+
+ @Override
+ public void add(long delta) {
+ reportMetrics(() -> observe(delta));
+ }
+
+ private void observe(final long delta) {
+ try {
+ inner.observe(delta);
+ } catch (final IllegalArgumentException err) {
+ LOG.error("invalid delta {} for metric {}", delta, name, err);
+ }
+ }
+ }
+
+ private class PrometheusLabelledSummary implements SummarySet {
+
+ private final io.prometheus.client.Summary inner;
+ private final String name;
+
+ public PrometheusLabelledSummary(String name, MetricsContext.DetailLevel level) {
+ this.name = name;
+ if (level == MetricsContext.DetailLevel.ADVANCED) {
+ this.inner = io.prometheus.client.Summary
+ .build(name, name)
+ .labelNames(LABELS)
+ .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error
+ .quantile(0.9, 0.01) // Add 90th percentile with 1% tolerated error
+ .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error
+ .register(collectorRegistry);
+ } else {
+ this.inner = io.prometheus.client.Summary
+ .build(name, name)
+ .labelNames(LABELS)
+ .quantile(0.5, 0.05) // Add 50th percentile (= median) with 5% tolerated error
+ .register(collectorRegistry);
+ }
+ }
+
+ @Override
+ public void add(String key, long value) {
+ reportMetrics(() -> observe(key, value));
+ }
+
+ private void observe(final String key, final long value) {
+ try {
+ inner.labels(key).observe(value);
+ } catch (final IllegalArgumentException err) {
+ LOG.error("invalid value {} for metric {} with key {}", value, name, key, err);
+ }
+ }
+
+ }
+
+ class MetricsServletImpl extends MetricsServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ // little trick: update the Gauges before serving data
+ // from Prometheus CollectorRegistry
+ sampleGauges();
+ // serve data using Prometheus built in client.
+ super.doGet(req, resp);
+ }
+ }
+
+ private Optional<ExecutorService> createExecutor() {
+ if (numWorkerThreads < 1) {
+ LOG.info("Executor service was not created as numWorkerThreads {} is less than 1", numWorkerThreads);
+ return Optional.empty();
+ }
+
+ final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(maxQueueSize);
+ final ThreadPoolExecutor executor = new ThreadPoolExecutor(numWorkerThreads,
+ numWorkerThreads,
+ 0L,
+ TimeUnit.MILLISECONDS,
+ queue, new PrometheusWorkerThreadFactory());
+ LOG.info("Executor service was created with numWorkerThreads {} and maxQueueSize {}",
+ numWorkerThreads,
+ maxQueueSize);
+ return Optional.of(executor);
+ }
+
+ private void shutdownExecutor() {
+ if (executorOptional.isPresent()) {
+ LOG.info("Shutdown executor service with timeout {}", workerShutdownTimeoutMs);
+ final ExecutorService executor = executorOptional.get();
+ executor.shutdown();
+ try {
+ if (!executor.awaitTermination(workerShutdownTimeoutMs, TimeUnit.MILLISECONDS)) {
+ LOG.error("Not all the Prometheus worker threads terminated properly after {} timeout",
+ workerShutdownTimeoutMs);
+ executor.shutdownNow();
+ }
+ } catch (final Exception e) {
+ LOG.error("Error occurred while terminating Prometheus worker threads", e);
+ executor.shutdownNow();
+ }
+ }
+ }
+
+ private static class PrometheusWorkerThreadFactory implements ThreadFactory {
+ private static final AtomicInteger workerCounter = new AtomicInteger(1);
+
+ @Override
+ public Thread newThread(final Runnable runnable) {
+ final String threadName = "PrometheusMetricsProviderWorker-" + workerCounter.getAndIncrement();
+ final Thread thread = new Thread(runnable, threadName);
+ thread.setDaemon(true);
+ return thread;
+ }
+ }
+
+ private void reportMetrics(final Runnable task) {
+ if (executorOptional.isPresent()) {
+ try {
+ executorOptional.get().submit(task);
+ } catch (final RejectedExecutionException e) {
+ rateLogger.rateLimitLog("Prometheus metrics reporting task queue size exceeded the max",
+ String.valueOf(maxQueueSize));
+ }
+ } else {
+ task.run();
+ }
+ }
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/zookeeper-prometheus-metrics/src/main/java/org/apache/pulsar/metrics/prometheus/zookeeper/package-info.java
similarity index 59%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/zookeeper-prometheus-metrics/src/main/java/org/apache/pulsar/metrics/prometheus/zookeeper/package-info.java
index 65cc10e0d3b..59e2add5b97 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/zookeeper-prometheus-metrics/src/main/java/org/apache/pulsar/metrics/prometheus/zookeeper/package-info.java
@@ -16,30 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
-
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
-
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
-
- }
-
- @Override
- public String getBasePath() {
- return null;
- }
-
- @Override
- public ServletHolder getServletHolder() {
- return null;
- }
-
- @Override
- public void close() {
- // no-op
- }
-}
+/**
+ * Prometheus metrics provider for ZooKeeper that is using Jetty 12.
+ */
+package org.apache.pulsar.metrics.prometheus.zookeeper;
diff --git a/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/ExportJvmInfoTest.java b/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/ExportJvmInfoTest.java
new file mode 100644
index 00000000000..635fe1b0833
--- /dev/null
+++ b/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/ExportJvmInfoTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.zookeeper;
+
+import static org.testng.AssertJUnit.assertEquals;
+import io.prometheus.client.CollectorRegistry;
+import java.util.Properties;
+import org.testng.annotations.Test;
+
+/**
+ * Tests about Prometheus Metrics Provider. Please note that we are not testing
+ * Prometheus but our integration.
+ */
+public class ExportJvmInfoTest {
+
+ @Test
+ public void exportInfo() throws Exception {
+ runTest(true);
+ }
+
+ @Test
+ public void doNotExportInfo() throws Exception {
+ runTest(false);
+ }
+
+ private void runTest(boolean exportJvmInfo) throws Exception {
+ CollectorRegistry.defaultRegistry.clear();
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ try {
+ Properties configuration = new Properties();
+ configuration.setProperty("httpPort", "0"); // ephemeral port
+ configuration.setProperty("exportJvmInfo", exportJvmInfo + "");
+ provider.configure(configuration);
+ provider.start();
+ boolean[] found = {false};
+ provider.dump((k, v) -> {
+ found[0] = found[0] || k.contains("heap");
+ });
+ assertEquals(exportJvmInfo, found[0]);
+ } finally {
+ provider.stop();
+ }
+ }
+
+}
diff --git a/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProviderConfigTest.java b/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProviderConfigTest.java
new file mode 100644
index 00000000000..f076a15db93
--- /dev/null
+++ b/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProviderConfigTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.zookeeper;
+
+import io.prometheus.client.CollectorRegistry;
+import java.util.Properties;
+import org.apache.zookeeper.metrics.MetricsProviderLifeCycleException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+public class PrometheusMetricsProviderConfigTest {
+
+ @Test
+ public void testInvalidPort() {
+ Assert.assertThrows(MetricsProviderLifeCycleException.class, () -> {
+ CollectorRegistry.defaultRegistry.clear();
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ configuration.setProperty("httpPort", "65536");
+ configuration.setProperty("exportJvmInfo", "false");
+ provider.configure(configuration);
+ provider.start();
+ });
+ }
+
+ @Test
+ public void testInvalidAddr() {
+ Assert.assertThrows(MetricsProviderLifeCycleException.class, () -> {
+ CollectorRegistry.defaultRegistry.clear();
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ configuration.setProperty("httpHost", "master");
+ provider.configure(configuration);
+ provider.start();
+ });
+ }
+
+ @Test
+ public void testValidConfig() throws MetricsProviderLifeCycleException {
+ CollectorRegistry.defaultRegistry.clear();
+ PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ configuration.setProperty("httpHost", "0.0.0.0");
+ configuration.setProperty("httpPort", "0");
+ provider.configure(configuration);
+ provider.start();
+ }
+
+}
diff --git a/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProviderTest.java b/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProviderTest.java
new file mode 100644
index 00000000000..55b993b3ab5
--- /dev/null
+++ b/jetty-upgrade/zookeeper-prometheus-metrics/src/test/java/org/apache/pulsar/metrics/prometheus/zookeeper/PrometheusMetricsProviderTest.java
@@ -0,0 +1,714 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pulsar.metrics.prometheus.zookeeper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertSame;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+import io.prometheus.client.CollectorRegistry;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.zookeeper.metrics.Counter;
+import org.apache.zookeeper.metrics.CounterSet;
+import org.apache.zookeeper.metrics.Gauge;
+import org.apache.zookeeper.metrics.GaugeSet;
+import org.apache.zookeeper.metrics.MetricsContext;
+import org.apache.zookeeper.metrics.Summary;
+import org.apache.zookeeper.metrics.SummarySet;
+import org.apache.zookeeper.server.util.QuotaMetricsUtils;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+
+/**
+ * Tests about Prometheus Metrics Provider. Please note that we are not testing
+ * Prometheus but only our integration.
+ */
+public class PrometheusMetricsProviderTest {
+
+ private static final String URL_FORMAT = "http://localhost:%d/metrics";
+ private PrometheusMetricsProvider provider;
+
+ @BeforeMethod
+ public void setup() throws Exception {
+ CollectorRegistry.defaultRegistry.clear();
+ provider = new PrometheusMetricsProvider();
+ Properties configuration = new Properties();
+ configuration.setProperty("numWorkerThreads", "0"); // sync behavior for test
+ configuration.setProperty("httpHost", "127.0.0.1"); // local host for test
+ configuration.setProperty("httpPort", "0"); // ephemeral port
+ configuration.setProperty("exportJvmInfo", "false");
+ provider.configure(configuration);
+ provider.start();
+ }
+
+ @AfterMethod(alwaysRun = true)
+ public void tearDown() {
+ if (provider != null) {
+ provider.stop();
+ }
+ CollectorRegistry.defaultRegistry.clear();
+ }
+
+ @Test
+ public void testCounters() throws Exception {
+ Counter counter = provider.getRootContext().getCounter("cc");
+ counter.add(10);
+ int[] count = {0};
+ provider.dump((k, v) -> {
+ if (k.contains("_created")) {
+ return;
+ }
+ assertEquals("cc_total", k);
+ assertEquals(10, ((Number) v).intValue());
+ count[0]++;
+ }
+ );
+ assertEquals(1, count[0]);
+ count[0] = 0;
+
+ // this is not allowed but it must not throw errors
+ counter.add(-1);
+
+ provider.dump((k, v) -> {
+ if (k.contains("_created")) {
+ return;
+ }
+ assertEquals("cc_total", k);
+ assertEquals(10, ((Number) v).intValue());
+ count[0]++;
+ }
+ );
+ assertEquals(1, count[0]);
+
+ // we always must get the same object
+ assertSame(counter, provider.getRootContext().getCounter("cc"));
+
+ String res = callServlet();
+ assertThat(res).contains("# TYPE cc_total counter");
+ assertThat(res).contains("cc_total 10.0");
+ }
+
+ @Test
+ public void testCounterSet_single() throws Exception {
+ // create and register a CounterSet
+ final String name = QuotaMetricsUtils.QUOTA_EXCEEDED_ERROR_PER_NAMESPACE;
+ final CounterSet counterSet = provider.getRootContext().getCounterSet(name);
+ final String[] keys = {"ns1", "ns2"};
+ final int count = 3;
+
+ // update the CounterSet multiple times
+ for (int i = 0; i < count; i++) {
+ Arrays.asList(keys).forEach(key -> counterSet.inc(key));
+ Arrays.asList(keys).forEach(key -> counterSet.add(key, 2));
+ }
+
+ // validate with dump call
+ final Map<String, Number> expectedMetricsMap = new HashMap<>();
+ for (final String key : keys) {
+ expectedMetricsMap.put(String.format("%s_total{key=\"%s\"}", name, key), count * 3.0);
+ }
+ validateWithDump(expectedMetricsMap);
+
+ // validate with servlet call
+ final List<String> expectedNames = Collections.singletonList(String.format("# TYPE %s count", name));
+ final List<String> expectedMetrics = new ArrayList<>();
+ for (final String key : keys) {
+ expectedMetrics.add(String.format("%s{key=\"%s\",} %s", name, key, count * 3.0));
+ }
+ validateWithServletCall(expectedNames, expectedMetrics);
+
+ // validate registering with same name, no overwriting
+ assertSame(counterSet, provider.getRootContext().getCounterSet(name));
+ }
+
+ @Test
+ public void testCounterSet_multiple() throws Exception {
+ final String name = QuotaMetricsUtils.QUOTA_EXCEEDED_ERROR_PER_NAMESPACE;
+
+ final String[] names = new String[]{name + "_1", name + "_2"};
+ final String[] keys = new String[]{"ns21", "ns22"};
+ final int[] counts = new int[]{3, 5};
+
+ final int length = names.length;
+ final CounterSet[] counterSets = new CounterSet[length];
+
+ // create and register the CounterSets
+ for (int i = 0; i < length; i++) {
+ counterSets[i] = provider.getRootContext().getCounterSet(names[i]);
+ }
+
+ // update each CounterSet multiple times
+ for (int i = 0; i < length; i++) {
+ for (int j = 0; j < counts[i]; j++) {
+ counterSets[i].inc(keys[i]);
+ }
+ }
+
+ // validate with dump call
+ final Map<String, Number> expectedMetricsMap = new HashMap<>();
+ for (int i = 0; i < length; i++) {
+ expectedMetricsMap.put(String.format("%s_total{key=\"%s\"}", names[i], keys[i]), counts[i] * 1.0);
+ }
+ validateWithDump(expectedMetricsMap);
+
+ // validate with servlet call
+ final List<String> expectedNames = new ArrayList<>();
+ final List<String> expectedMetrics = new ArrayList<>();
+ for (int i = 0; i < length; i++) {
+ expectedNames.add(String.format("# TYPE %s count", names[i]));
+ expectedMetrics.add(String.format("%s{key=\"%s\",} %s", names[i], keys[i], counts[i] * 1.0));
+ }
+ validateWithServletCall(expectedNames, expectedMetrics);
+ }
+
+ @Test
+ public void testCounterSet_registerWithNullName() {
+ assertThrows(NullPointerException.class,
+ () -> provider.getRootContext().getCounterSet(null));
+ }
+
+ @Test
+ public void testCounterSet_negativeValue() {
+ // create and register a CounterSet
+ final String name = QuotaMetricsUtils.QUOTA_EXCEEDED_ERROR_PER_NAMESPACE;
+ final CounterSet counterSet = provider.getRootContext().getCounterSet(name);
+
+ // add negative value and make sure no exception is thrown
+ counterSet.add("ns1", -1);
+ }
+
+ @Test
+ public void testCounterSet_nullKey() {
+ // create and register a CounterSet
+ final String name = QuotaMetricsUtils.QUOTA_EXCEEDED_ERROR_PER_NAMESPACE;
+ final CounterSet counterSet = provider.getRootContext().getCounterSet(name);
+
+ // increment the count with null key and make sure no exception is thrown
+ counterSet.inc(null);
+ counterSet.add(null, 2);
+ }
+
+ @Test
+ public void testGauge() throws Exception {
+ int[] values = {78, -89};
+ int[] callCounts = {0, 0};
+ Gauge gauge0 = () -> {
+ callCounts[0]++;
+ return values[0];
+ };
+ Gauge gauge1 = () -> {
+ callCounts[1]++;
+ return values[1];
+ };
+ provider.getRootContext().registerGauge("gg", gauge0);
+
+ int[] count = {0};
+ provider.dump((k, v) -> {
+ assertEquals("gg", k);
+ assertEquals(values[0], ((Number) v).intValue());
+ count[0]++;
+ }
+ );
+ assertEquals(1, callCounts[0]);
+ assertEquals(0, callCounts[1]);
+ assertEquals(1, count[0]);
+ count[0] = 0;
+ String res2 = callServlet();
+ assertThat(res2).contains("# TYPE gg gauge");
+ assertThat(res2.contains("gg 78.0"));
+
+ provider.getRootContext().unregisterGauge("gg");
+ provider.dump((k, v) -> {
+ count[0]++;
+ }
+ );
+ assertEquals(2, callCounts[0]);
+ assertEquals(0, callCounts[1]);
+ assertEquals(0, count[0]);
+ String res3 = callServlet();
+ assertTrue(res3.isEmpty());
+
+ provider.getRootContext().registerGauge("gg", gauge1);
+
+ provider.dump((k, v) -> {
+ assertEquals("gg", k);
+ assertEquals(values[1], ((Number) v).intValue());
+ count[0]++;
+ }
+ );
+ assertEquals(2, callCounts[0]);
+ assertEquals(1, callCounts[1]);
+ assertEquals(1, count[0]);
+ count[0] = 0;
+
+ String res4 = callServlet();
+ assertThat(res4.contains("# TYPE gg gauge"));
+ assertThat(res4.contains("gg -89.0"));
+ assertEquals(2, callCounts[0]);
+ // the servlet must sample the value again (from gauge1)
+ assertEquals(2, callCounts[1]);
+
+ // override gauge, without unregister
+ provider.getRootContext().registerGauge("gg", gauge0);
+
+ provider.dump((k, v) -> {
+ count[0]++;
+ }
+ );
+ assertEquals(1, count[0]);
+ assertEquals(3, callCounts[0]);
+ assertEquals(2, callCounts[1]);
+ }
+
+ @Test
+ public void testBasicSummary() throws Exception {
+ Summary summary = provider.getRootContext()
+ .getSummary("cc", MetricsContext.DetailLevel.BASIC);
+ summary.add(10);
+ summary.add(10);
+ int[] count = {0};
+ provider.dump((k, v) -> {
+ count[0]++;
+ int value = ((Number) v).intValue();
+
+ switch (k) {
+ case "cc{quantile=\"0.5\"}":
+ assertEquals(10, value);
+ break;
+ case "cc_count":
+ assertEquals(2, value);
+ break;
+ case "cc_sum":
+ assertEquals(20, value);
+ break;
+ case "cc_created":
+ break;
+ default:
+ fail("unexpected key " + k);
+ break;
+ }
+ }
+ );
+ assertEquals(4, count[0]);
+ count[0] = 0;
+
+ // we always must get the same object
+ assertSame(summary, provider.getRootContext()
+ .getSummary("cc", MetricsContext.DetailLevel.BASIC));
+
+ try {
+ provider.getRootContext()
+ .getSummary("cc", MetricsContext.DetailLevel.ADVANCED);
+ fail("Can't get the same summary with a different DetailLevel");
+ } catch (IllegalArgumentException err) {
+ assertThat(err.getMessage()).contains("Already registered");
+ }
+
+ String res = callServlet();
+ assertThat(res).contains("# TYPE cc summary");
+ assertThat(res).contains("cc_sum 20.0");
+ assertThat(res).contains("cc_count 2.0");
+ assertThat(res).contains("cc{quantile=\"0.5\",} 10.0");
+ }
+
+ @Test
+ public void testAdvancedSummary() throws Exception {
+ Summary summary = provider.getRootContext()
+ .getSummary("cc", MetricsContext.DetailLevel.ADVANCED);
+ summary.add(10);
+ summary.add(10);
+ int[] count = {0};
+ provider.dump((k, v) -> {
+ count[0]++;
+ int value = ((Number) v).intValue();
+
+ switch (k) {
+ case "cc{quantile=\"0.5\"}":
+ assertEquals(10, value);
+ break;
+ case "cc{quantile=\"0.9\"}":
+ assertEquals(10, value);
+ break;
+ case "cc{quantile=\"0.99\"}":
+ assertEquals(10, value);
+ break;
+ case "cc_count":
+ assertEquals(2, value);
+ break;
+ case "cc_sum":
+ assertEquals(20, value);
+ break;
+ case "cc_created":
+ break;
+ default:
+ fail("unexpected key " + k);
+ break;
+ }
+ }
+ );
+ assertEquals(6, count[0]);
+ count[0] = 0;
+
+ // we always must get the same object
+ assertSame(summary, provider.getRootContext()
+ .getSummary("cc", MetricsContext.DetailLevel.ADVANCED));
+
+ try {
+ provider.getRootContext()
+ .getSummary("cc", MetricsContext.DetailLevel.BASIC);
+ fail("Can't get the same summary with a different DetailLevel");
+ } catch (IllegalArgumentException err) {
+ assertThat(err.getMessage()).contains("Already registered");
+ }
+
+ String res = callServlet();
+ assertThat(res).contains("# TYPE cc summary");
+ assertThat(res).contains("cc_sum 20.0");
+ assertThat(res).contains("cc_count 2.0");
+ assertThat(res).contains("cc{quantile=\"0.5\",} 10.0");
+ assertThat(res).contains("cc{quantile=\"0.9\",} 10.0");
+ assertThat(res).contains("cc{quantile=\"0.99\",} 10.0");
+ }
+
+ /**
+ * Using TRACE method to visit metrics provider, the response should be 403 forbidden.
+ */
+ @Test
+ public void testTraceCall() throws IOException, IllegalAccessException, NoSuchFieldException {
+ Field privateServerField = provider.getClass().getDeclaredField("server");
+ privateServerField.setAccessible(true);
+ Server server = (Server) privateServerField.get(provider);
+ int port = ((ServerConnector) server.getConnectors()[0]).getLocalPort();
+
+ String metricsUrl = String.format(URL_FORMAT, port);
+ HttpURLConnection conn = (HttpURLConnection) new URL(metricsUrl).openConnection();
+ conn.setRequestMethod("TRACE");
+ conn.connect();
+ assertEquals(HttpURLConnection.HTTP_FORBIDDEN, conn.getResponseCode());
+ }
+
+ @Test
+ public void testSummary_asyncAndExceedMaxQueueSize() throws Exception {
+ final Properties config = new Properties();
+ config.setProperty("numWorkerThreads", "1");
+ config.setProperty("maxQueueSize", "1");
+ config.setProperty("httpPort", "0"); // ephemeral port
+ config.setProperty("exportJvmInfo", "false");
+
+ PrometheusMetricsProvider metricsProvider = null;
+ try {
+ metricsProvider = new PrometheusMetricsProvider();
+ metricsProvider.configure(config);
+ metricsProvider.start();
+ final Summary summary =
+ metricsProvider.getRootContext().getSummary("cc", MetricsContext.DetailLevel.ADVANCED);
+
+ // make sure no error is thrown
+ for (int i = 0; i < 10; i++) {
+ summary.add(10);
+ }
+ } finally {
+ if (metricsProvider != null) {
+ metricsProvider.stop();
+ }
+ }
+ }
+
+ @Test
+ public void testSummarySet() throws Exception {
+ final String name = "ss";
+ final String[] keys = {"ns1", "ns2"};
+ final double count = 3.0;
+
+ // create and register a SummarySet
+ final SummarySet summarySet = provider.getRootContext()
+ .getSummarySet(name, MetricsContext.DetailLevel.BASIC);
+
+ // update the SummarySet multiple times
+ for (int i = 0; i < count; i++) {
+ Arrays.asList(keys).forEach(key -> summarySet.add(key, 1));
+ }
+
+ // validate with dump call
+ final Map<String, Number> expectedMetricsMap = new HashMap<>();
+ for (final String key : keys) {
+ expectedMetricsMap.put(String.format("%s{key=\"%s\",quantile=\"0.5\"}", name, key), 1.0);
+ expectedMetricsMap.put(String.format("%s_count{key=\"%s\"}", name, key), count);
+ expectedMetricsMap.put(String.format("%s_sum{key=\"%s\"}", name, key), count);
+ }
+ validateWithDump(expectedMetricsMap);
+
+ // validate with servlet call
+ final List<String> expectedNames = Collections.singletonList(String.format("# TYPE %s summary", name));
+ final List<String> expectedMetrics = new ArrayList<>();
+ for (final String key : keys) {
+ expectedMetrics.add(String.format("%s{key=\"%s\",quantile=\"0.5\",} %s", name, key, 1.0));
+ expectedMetrics.add(String.format("%s_count{key=\"%s\",} %s", name, key, count));
+ expectedMetrics.add(String.format("%s_sum{key=\"%s\",} %s", name, key, count));
+ }
+ validateWithServletCall(expectedNames, expectedMetrics);
+
+ // validate registering with same name, no overwriting
+ assertSame(summarySet, provider.getRootContext()
+ .getSummarySet(name, MetricsContext.DetailLevel.BASIC));
+
+ // validate registering with different DetailLevel, not allowed
+ try {
+ provider.getRootContext()
+ .getSummarySet(name, MetricsContext.DetailLevel.ADVANCED);
+ fail("Can't get the same summarySet with a different DetailLevel");
+ } catch (final IllegalArgumentException e) {
+ assertThat(e.getMessage()).contains("Already registered");
+ }
+ }
+
+ private String callServlet() throws ServletException, IOException {
+ // we are not performing an HTTP request
+ // but we are calling directly the servlet
+ StringWriter writer = new StringWriter();
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ when(response.getWriter()).thenReturn(new PrintWriter(writer));
+ HttpServletRequest req = mock(HttpServletRequest.class);
+ provider.getServlet().doGet(req, response);
+ String res = writer.toString();
+ return res;
+ }
+
+ @Test
+ public void testGaugeSet_singleGaugeSet() throws Exception {
+ final String name = QuotaMetricsUtils.QUOTA_BYTES_LIMIT_PER_NAMESPACE;
+ final Number[] values = {10.0, 100.0};
+ final String[] keys = {"ns11", "ns12"};
+ final Map<String, Number> metricsMap = new HashMap<>();
+ for (int i = 0; i < values.length; i++) {
+ metricsMap.put(keys[i], values[i]);
+ }
+ final AtomicInteger callCount = new AtomicInteger(0);
+
+ // create and register GaugeSet
+ createAndRegisterGaugeSet(name, metricsMap, callCount);
+
+ // validate with dump call
+ final Map<String, Number> expectedMetricsMap = new HashMap<>();
+ for (int i = 0; i < values.length; i++) {
+ expectedMetricsMap.put(String.format("%s{key=\"%s\"}", name, keys[i]), values[i]);
+ }
+ validateWithDump(expectedMetricsMap);
+ assertEquals(1, callCount.get());
+
+ // validate with servlet call
+ final List<String> expectedNames = Collections.singletonList(String.format("# TYPE %s gauge", name));
+ final List<String> expectedMetrics = new ArrayList<>();
+ for (int i = 0; i < values.length; i++) {
+ expectedMetrics.add(String.format("%s{key=\"%s\",} %s", name, keys[i], values[i]));
+ }
+ validateWithServletCall(expectedNames, expectedMetrics);
+ assertEquals(2, callCount.get());
+
+ // unregister the GaugeSet
+ callCount.set(0);
+ provider.getRootContext().unregisterGaugeSet(name);
+
+ // validate with dump call
+ validateWithDump(Collections.emptyMap());
+ assertEquals(0, callCount.get());
+
+ // validate with servlet call
+ validateWithServletCall(new ArrayList<>(), new ArrayList<>());
+ assertEquals(0, callCount.get());
+ }
+
+ @Test
+ public void testGaugeSet_multipleGaugeSets() throws Exception {
+ final String[] names = new String[]{
+ QuotaMetricsUtils.QUOTA_COUNT_LIMIT_PER_NAMESPACE,
+ QuotaMetricsUtils.QUOTA_COUNT_USAGE_PER_NAMESPACE
+ };
+
+ final Number[] values = new Number[]{20.0, 200.0};
+ final String[] keys = new String[]{"ns21", "ns22"};
+ final int count = names.length;
+ final AtomicInteger[] callCounts = new AtomicInteger[count];
+
+ // create and register the GaugeSets
+ for (int i = 0; i < count; i++) {
+ final Map<String, Number> metricsMap = new HashMap<>();
+ metricsMap.put(keys[i], values[i]);
+ callCounts[i] = new AtomicInteger(0);
+ createAndRegisterGaugeSet(names[i], metricsMap, callCounts[i]);
+ }
+
+ // validate with dump call
+ final Map<String, Number> expectedMetricsMap = new HashMap<>();
+ for (int i = 0; i < count; i++) {
+ expectedMetricsMap.put(String.format("%s{key=\"%s\"}", names[i], keys[i]), values[i]);
+ }
+ validateWithDump(expectedMetricsMap);
+ for (int i = 0; i < count; i++) {
+ assertEquals(1, callCounts[i].get());
+ }
+
+ // validate with servlet call
+ final List<String> expectedNames = new ArrayList<>();
+ final List<String> expectedMetrics = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ expectedNames.add(String.format("# TYPE %s gauge", names[i]));
+ expectedMetrics.add(String.format("%s{key=\"%s\",} %s", names[i], keys[i], values[i]));
+ }
+ validateWithServletCall(expectedNames, expectedMetrics);
+ for (int i = 0; i < count; i++) {
+ assertEquals(2, callCounts[i].get());
+ }
+
+ // unregister the GaugeSets
+ for (int i = 0; i < count; i++) {
+ callCounts[i].set(0);
+ provider.getRootContext().unregisterGaugeSet(names[i]);
+ }
+
+ // validate with dump call
+ validateWithDump(Collections.emptyMap());
+ for (int i = 0; i < count; i++) {
+ assertEquals(0, callCounts[i].get());
+ }
+
+ // validate with servlet call
+ validateWithServletCall(new ArrayList<>(), new ArrayList<>());
+ for (int i = 0; i < count; i++) {
+ assertEquals(0, callCounts[i].get());
+ }
+ }
+
+ @Test
+ public void testGaugeSet_overwriteRegister() {
+ final String[] names = new String[]{
+ QuotaMetricsUtils.QUOTA_COUNT_LIMIT_PER_NAMESPACE,
+ QuotaMetricsUtils.QUOTA_COUNT_USAGE_PER_NAMESPACE
+ };
+
+ final int count = names.length;
+ final Number[] values = new Number[]{30.0, 300.0};
+ final String[] keys = new String[]{"ns31", "ns32"};
+ final AtomicInteger[] callCounts = new AtomicInteger[count];
+
+ // create and register the GaugeSets
+ for (int i = 0; i < count; i++) {
+ final Map<String, Number> metricsMap = new HashMap<>();
+ metricsMap.put(keys[i], values[i]);
+ callCounts[i] = new AtomicInteger(0);
+ // use the same name so the first GaugeSet got overwrite
+ createAndRegisterGaugeSet(names[0], metricsMap, callCounts[i]);
+ }
+
+ // validate with dump call to make sure the second GaugeSet overwrites the first
+ final Map<String, Number> expectedMetricsMap = new HashMap<>();
+ expectedMetricsMap.put(String.format("%s{key=\"%s\"}", names[0], keys[1]), values[1]);
+ validateWithDump(expectedMetricsMap);
+ assertEquals(0, callCounts[0].get());
+ assertEquals(1, callCounts[1].get());
+ }
+
+ @Test
+ public void testGaugeSet_nullKey() {
+ final String name = QuotaMetricsUtils.QUOTA_COUNT_LIMIT_PER_NAMESPACE;
+ final Map<String, Number> metricsMap = new HashMap<>();
+ metricsMap.put(null, 10.0);
+
+ final AtomicInteger callCount = new AtomicInteger(0);
+
+ // create and register GaugeSet
+ createAndRegisterGaugeSet(name, metricsMap, callCount);
+
+ // validate with dump call
+ assertThrows(IllegalArgumentException.class, () -> provider.dump(new HashMap<>()::put));
+
+ // validate with servlet call
+ assertThrows(IllegalArgumentException.class, this::callServlet);
+ }
+
+ @Test
+ public void testGaugeSet_registerWithNullGaugeSet() {
+ assertThrows(NullPointerException.class,
+ () -> provider.getRootContext().registerGaugeSet("name", null));
+
+ assertThrows(NullPointerException.class,
+ () -> provider.getRootContext().registerGaugeSet(null, HashMap::new));
+ }
+
+ @Test
+ public void testGaugeSet_unregisterNull() {
+ assertThrows(NullPointerException.class,
+ () -> provider.getRootContext().unregisterGaugeSet(null));
+ }
+
+ private void createAndRegisterGaugeSet(final String name,
+ final Map<String, Number> metricsMap,
+ final AtomicInteger callCount) {
+ final GaugeSet gaugeSet = () -> {
+ callCount.addAndGet(1);
+ return metricsMap;
+ };
+ provider.getRootContext().registerGaugeSet(name, gaugeSet);
+ }
+
+ private void validateWithDump(final Map<String, Number> expectedMetrics) {
+ final Map<String, Object> returnedMetrics = new HashMap<>();
+ provider.dump((key, value) -> {
+ if (!key.contains("_created{")) {
+ returnedMetrics.put(key, value);
+ }
+ });
+ assertThat(returnedMetrics).isEqualTo(expectedMetrics);
+ }
+
+ private void validateWithServletCall(final List<String> expectedNames,
+ final List<String> expectedMetrics) throws Exception {
+ final String response = callServlet();
+ if (expectedNames.isEmpty() && expectedMetrics.isEmpty()) {
+ assertTrue(response.isEmpty());
+ } else {
+ expectedNames.forEach(name -> assertThat(response.contains(name)));
+ expectedMetrics.forEach(metric -> assertThat(response.contains(metric)));
+ }
+ }
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/pom.xml b/jetty-upgrade/zookeeper-with-patched-admin/pom.xml
new file mode 100644
index 00000000000..602db860d2e
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/pom.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>jetty-upgrade</artifactId>
+ <version>4.0.10-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>zookeeper-with-patched-admin</artifactId>
+ <name>Apache Pulsar :: patched Apache ZooKeeper with Admin API using Jetty 12.1.x</name>
+ <description>Apache ZooKeeper with patched admin servlet using Jetty 12.1.x API</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.zookeeper</groupId>
+ <artifactId>zookeeper</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.zookeeper</groupId>
+ <artifactId>zookeeper-jute</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-servlet</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.github.spotbugs</groupId>
+ <artifactId>spotbugs-annotations</artifactId>
+ <scope>provided</scope>
+ <optional>true</optional>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>${shadePluginPhase}</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <createDependencyReducedPom>true</createDependencyReducedPom>
+ <useDependencyReducedPomInJar>true</useDependencyReducedPomInJar>
+ <promoteTransitiveDependencies>true</promoteTransitiveDependencies>
+ <artifactSet>
+ <includes>
+ <include>org.apache.zookeeper:zookeeper</include>
+ </includes>
+ </artifactSet>
+ <filters>
+ <filter>
+ <artifact>org.apache.zookeeper:zookeeper</artifact>
+ <includes>
+ <include>**</include>
+ </includes>
+ <excludes>
+ <exclude>org/apache/zookeeper/server/admin/**</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ <profiles>
+ <profile>
+ <!-- Profile that is activated when using IntelliJ IDEA -->
+ <id>intellij</id>
+ <activation>
+ <property>
+ <name>idea.maven.embedder.version</name>
+ </property>
+ </activation>
+ <build>
+ <plugins>
+ <!-- Add shaded jar to artifacts for IntelliJ IDEA so that IntelliJ builds
+ and running tests work -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-shade-jar</id>
+ <phase>package</phase>
+ <goals>
+ <goal>attach-artifact</goal>
+ </goals>
+ <configuration>
+ <artifacts>
+ <artifact>
+ <file>${project.build.directory}/${project.build.finalName}.jar</file>
+ <type>jar</type>
+ <classifier>shaded</classifier>
+ </artifact>
+ </artifacts>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>eclipse</id>
+ <activation>
+ <property>
+ <name>m2e.version</name>
+ </property>
+ </activation>
+ <build>
+ <!-- skip shading and compilation in Eclipse/VSCode -->
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </profile>
+ </profiles>
+</project>
\ No newline at end of file
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AdminServer.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AdminServer.java
new file mode 100644
index 00000000000..095b4e0ddf0
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AdminServer.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import org.apache.yetus.audience.InterfaceAudience;
+import org.apache.zookeeper.server.ZooKeeperServer;
+
+/**
+ * Interface for an embedded admin server that runs Commands. There is only one
+ * functional implementation, JettyAdminServer. DummyAdminServer, which does
+ * nothing, is used when we do not wish to run a server.
+ */
+@InterfaceAudience.Public
+public interface AdminServer {
+
+ void start() throws AdminServerException;
+
+ void shutdown() throws AdminServerException;
+
+ void setZooKeeperServer(ZooKeeperServer zkServer);
+
+ @InterfaceAudience.Public
+ class AdminServerException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ public AdminServerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AdminServerException(Throwable cause) {
+ super(cause);
+ }
+
+ }
+
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AdminServerFactory.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AdminServerFactory.java
new file mode 100644
index 00000000000..cdf83486e69
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AdminServerFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import java.lang.reflect.InvocationTargetException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Factory class for creating an AdminServer.
+ */
+public class AdminServerFactory {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AdminServerFactory.class);
+
+ /**
+ * This method encapsulates the logic for whether we should use a
+ * JettyAdminServer (i.e., the AdminServer is enabled) or a DummyAdminServer
+ * (i.e., the AdminServer is disabled). It uses reflection when attempting
+ * to create a JettyAdminServer, rather than referencing the class directly,
+ * so that it's ok to omit Jetty from the classpath if a user doesn't wish
+ * to pull in Jetty with ZooKeeper.
+ */
+ public static AdminServer createAdminServer() {
+ if (!"false".equals(System.getProperty("zookeeper.admin.enableServer"))) {
+ try {
+ Class<?> jettyAdminServerC = Class.forName("org.apache.zookeeper.server.admin.JettyAdminServer");
+ Object adminServer = jettyAdminServerC.getConstructor().newInstance();
+ return (AdminServer) adminServer;
+
+ } catch (ClassNotFoundException e) {
+ LOG.warn("Unable to start JettyAdminServer", e);
+ } catch (InstantiationException e) {
+ LOG.warn("Unable to start JettyAdminServer", e);
+ } catch (IllegalAccessException e) {
+ LOG.warn("Unable to start JettyAdminServer", e);
+ } catch (InvocationTargetException e) {
+ LOG.warn("Unable to start JettyAdminServer", e);
+ } catch (NoSuchMethodException e) {
+ LOG.warn("Unable to start JettyAdminServer", e);
+ } catch (NoClassDefFoundError e) {
+ LOG.warn("Unable to load jetty, not starting JettyAdminServer", e);
+ }
+ }
+ return new DummyAdminServer();
+ }
+
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AuthRequest.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AuthRequest.java
new file mode 100644
index 00000000000..421d4f92931
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/AuthRequest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import org.apache.zookeeper.ZooDefs;
+
+/**
+ * Class representing auth data for performing ACL check on admin server commands.
+ *
+ * For example, SnapshotCommand requires {@link ZooDefs.Perms.ALL} permission on
+ * the root node.
+ *
+ */
+public class AuthRequest {
+ private final int permission;
+ private final String path;
+
+ /**
+ * @param permission the required permission for auth check
+ * @param path the ZNode path for auth check
+ */
+ public AuthRequest(final int permission, final String path) {
+ this.permission = permission;
+ this.path = path;
+ }
+
+ /**
+ * @return permission
+ */
+ public int getPermission() {
+ return permission;
+ }
+
+ /**
+ * @return ZNode path
+ */
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public String toString() {
+ return "AuthRequest{"
+ + "permission=" + permission
+ + ", path='" + path + '\''
+ + '}';
+ }
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/Command.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/Command.java
new file mode 100644
index 00000000000..8a8089e2d60
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/Command.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Set;
+import org.apache.zookeeper.server.ZooKeeperServer;
+
+/**
+ * Interface implemented by all commands runnable by JettyAdminServer.
+ *
+ * @see CommandBase
+ * @see Commands
+ * @see JettyAdminServer
+ */
+public interface Command {
+
+ /**
+ * The set of all names that can be used to refer to this command (e.g.,
+ * "configuration", "config", and "conf").
+ */
+ Set<String> getNames();
+
+ /**
+ * The name that is returned with the command response and that appears in
+ * the list of all commands. This should be a member of the set returned by
+ * getNames().
+ */
+ String getPrimaryName();
+
+ /**
+ * @return true if the command requires an active ZooKeeperServer or a
+ * synced peer in order to resolve
+ */
+ boolean isServerRequired();
+
+ /**
+ * @return AuthRequest associated to the command. Null means auth check is not required.
+ */
+ AuthRequest getAuthRequest();
+
+ /**
+ * Run this command for HTTP GET request. Commands take a ZooKeeperServer, String-valued keyword
+ * arguments and return a CommandResponse object containing any information
+ * constituting the response to the command. Commands are responsible for
+ * parsing keyword arguments and performing any error handling if necessary.
+ * Errors should be reported by setting the "error" entry of the returned
+ * map with an appropriate message rather than throwing an exception.
+ *
+ * @param zkServer ZooKeeper server
+ * @param kwargs keyword -> argument value mapping
+ * @return CommandResponse representing response to command containing at minimum:
+ * - "command" key containing the command's primary name
+ * - "error" key containing a String error message or null if no error
+ */
+ CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs);
+
+ /**
+ * Run this command for HTTP POST. Commands take a ZooKeeperServer and InputStream and
+ * return a CommandResponse object containing any information
+ * constituting the response to the command. Commands are responsible for
+ * parsing keyword arguments and performing any error handling if necessary.
+ * Errors should be reported by setting the "error" entry of the returned
+ * map with an appropriate message rather than throwing an exception.
+ *
+ * @param zkServer ZooKeeper server
+ * @param inputStream InputStream from request
+ * @return CommandResponse representing response to command containing at minimum:
+ * - "command" key containing the command's primary name
+ * - "error" key containing a String error message or null if no error
+ */
+ CommandResponse runPost(ZooKeeperServer zkServer, InputStream inputStream);
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandBase.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandBase.java
new file mode 100644
index 00000000000..f6cb35066c3
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandBase.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class CommandBase implements Command {
+
+ private final String primaryName;
+ private final Set<String> names;
+ private final boolean serverRequired;
+ private final AuthRequest authRequest;
+
+ /**
+ * @param names The possible names of this command, with the primary name first.
+ */
+ protected CommandBase(List<String> names) {
+ this(names, true);
+ }
+
+ protected CommandBase(List<String> names, boolean serverRequired) {
+ this(names, serverRequired, null);
+ }
+
+ protected CommandBase(List<String> names, boolean serverRequired, AuthRequest authRequest) {
+ if (authRequest != null && !serverRequired) {
+ throw new IllegalArgumentException("An active server is required for auth check");
+ }
+ this.primaryName = names.get(0);
+ this.names = new HashSet<>(names);
+ this.serverRequired = serverRequired;
+ this.authRequest = authRequest;
+ }
+
+ @Override
+ public String getPrimaryName() {
+ return primaryName;
+ }
+
+ @Override
+ public Set<String> getNames() {
+ return names;
+ }
+
+
+ @Override
+ public boolean isServerRequired() {
+ return serverRequired;
+ }
+
+ @Override
+ public AuthRequest getAuthRequest() {
+ return authRequest;
+ }
+
+ /**
+ * @return A response with the command set to the primary name and the
+ * error set to null (these are the two entries that all command
+ * responses are required to include).
+ */
+ protected CommandResponse initializeResponse() {
+ return new CommandResponse(primaryName);
+ }
+
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandOutputter.java
similarity index 57%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandOutputter.java
index 65cc10e0d3b..2571605c5d5 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandOutputter.java
@@ -16,30 +16,33 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
+package org.apache.zookeeper.server.admin;
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
+import java.io.OutputStream;
+import java.io.PrintWriter;
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
-
- }
+/**
+ * CommandOutputters are used to format the responses from Commands.
+ *
+ * @see Command
+ * @see JettyAdminServer
+ */
+public interface CommandOutputter {
- @Override
- public String getBasePath() {
- return null;
- }
+ /**
+ * The MIME type of this output (e.g., "application/json").
+ */
+ String getContentType();
- @Override
- public ServletHolder getServletHolder() {
- return null;
+ /**
+ * Print out data as output.
+ */
+ default void output(CommandResponse response, PrintWriter pw) {
}
- @Override
- public void close() {
- // no-op
+ /**
+ * Stream out data as output.
+ */
+ default void output(final CommandResponse response, final OutputStream os) {
}
}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandResponse.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandResponse.java
new file mode 100644
index 00000000000..35ba6707650
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/CommandResponse.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A response from running a {@link Command}.
+ */
+public class CommandResponse {
+
+ /**
+ * The key in the map returned by {@link #toMap()} for the command name.
+ */
+ public static final String KEY_COMMAND = "command";
+ /**
+ * The key in the map returned by {@link #toMap()} for the error string.
+ */
+ public static final String KEY_ERROR = "error";
+
+ private final String command;
+ private final String error;
+ private final Map<String, Object> data;
+ private final Map<String, String> headers;
+ private int statusCode;
+ private InputStream inputStream;
+
+ /**
+ * Creates a new response with no error string.
+ *
+ * @param command command name
+ */
+ public CommandResponse(String command) {
+ this(command, null, HttpServletResponse.SC_OK);
+ }
+
+ /**
+ * Creates a new response.
+ *
+ * @param command command name
+ * @param error error string (may be null)
+ * @param statusCode http status code
+ */
+ public CommandResponse(String command, String error, int statusCode) {
+ this(command, error, statusCode, null);
+ }
+
+
+ /**
+ * Creates a new response.
+ *
+ * @param command command name
+ * @param error error string (may be null)
+ * @param statusCode http status code
+ * @param inputStream inputStream to send out data (may be null)
+ */
+ public CommandResponse(final String command, final String error, final int statusCode,
+ final InputStream inputStream) {
+ this.command = command;
+ this.error = error;
+ data = new LinkedHashMap<>();
+ headers = new HashMap<>();
+ this.statusCode = statusCode;
+ this.inputStream = inputStream;
+ }
+
+ /**
+ * Gets the command name.
+ *
+ * @return command name
+ */
+ public String getCommand() {
+ return command;
+ }
+
+ /**
+ * Gets the error string (may be null).
+ *
+ * @return error string
+ */
+ public String getError() {
+ return error;
+ }
+
+ /**
+ * Gets the http status code.
+ *
+ * @return http status code
+ */
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ /**
+ * Sets the http status code.
+ */
+ public void setStatusCode(int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ /**
+ * Gets the InputStream (may be null).
+ *
+ * @return InputStream
+ */
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ /**
+ * Sets the InputStream.
+ */
+ public void setInputStream(final InputStream inputStream) {
+ this.inputStream = inputStream;
+ }
+
+ /**
+ * Adds a key/value pair to this response.
+ *
+ * @param key key
+ * @param value value
+ * @return prior value for key, or null if none
+ */
+ public Object put(String key, Object value) {
+ return data.put(key, value);
+ }
+
+ /**
+ * Adds all key/value pairs in the given map to this response.
+ *
+ * @param m map of key/value pairs
+ */
+ public void putAll(Map<? extends String, ?> m) {
+ data.putAll(m);
+ }
+
+ /**
+ * Adds a header to this response.
+ *
+ * @param name name of the header
+ * @param value value of the header
+ */
+ public void addHeader(final String name, final String value) {
+ headers.put(name, value);
+ }
+
+ /**
+ * Returns all headers.
+ *
+ * @return map representation of all headers
+ */
+ public Map<String, String> getHeaders() {
+ return headers;
+ }
+
+ /**
+ * Converts this response to a map. The returned map is mutable, and
+ * changes to it do not reflect back into this response.
+ *
+ * @return map representation of response
+ */
+ public Map<String, Object> toMap() {
+ Map<String, Object> m = new LinkedHashMap<>(data);
+ m.put(KEY_COMMAND, command);
+ m.put(KEY_ERROR, error);
+ m.putAll(data);
+ return m;
+ }
+
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/Commands.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/Commands.java
new file mode 100644
index 00000000000..2b4b89a422f
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/Commands.java
@@ -0,0 +1,1168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import static org.apache.zookeeper.server.persistence.FileSnap.SNAPSHOT_FILE_PREFIX;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.zookeeper.Environment;
+import org.apache.zookeeper.Environment.Entry;
+import org.apache.zookeeper.KeeperException;
+import org.apache.zookeeper.Version;
+import org.apache.zookeeper.ZooDefs;
+import org.apache.zookeeper.data.ACL;
+import org.apache.zookeeper.data.Id;
+import org.apache.zookeeper.server.DataNode;
+import org.apache.zookeeper.server.DataTree;
+import org.apache.zookeeper.server.ServerCnxnFactory;
+import org.apache.zookeeper.server.ServerMetrics;
+import org.apache.zookeeper.server.ZooKeeperServer;
+import org.apache.zookeeper.server.ZooTrace;
+import org.apache.zookeeper.server.auth.ProviderRegistry;
+import org.apache.zookeeper.server.auth.ServerAuthenticationProvider;
+import org.apache.zookeeper.server.persistence.SnapshotInfo;
+import org.apache.zookeeper.server.persistence.Util;
+import org.apache.zookeeper.server.quorum.Follower;
+import org.apache.zookeeper.server.quorum.FollowerZooKeeperServer;
+import org.apache.zookeeper.server.quorum.Leader;
+import org.apache.zookeeper.server.quorum.LeaderZooKeeperServer;
+import org.apache.zookeeper.server.quorum.MultipleAddresses;
+import org.apache.zookeeper.server.quorum.QuorumPeer;
+import org.apache.zookeeper.server.quorum.QuorumPeer.LearnerType;
+import org.apache.zookeeper.server.quorum.QuorumZooKeeperServer;
+import org.apache.zookeeper.server.quorum.ReadOnlyZooKeeperServer;
+import org.apache.zookeeper.server.quorum.flexible.QuorumVerifier;
+import org.apache.zookeeper.server.util.RateLimiter;
+import org.apache.zookeeper.server.util.ZxidUtils;
+import org.eclipse.jetty.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class containing static methods for registering and running Commands, as well
+ * as default Command definitions.
+ *
+ * @see Command
+ * @see JettyAdminServer
+ */
+public class Commands {
+
+ static final Logger LOG = LoggerFactory.getLogger(Commands.class);
+ // VisibleForTesting
+ static final String ADMIN_RATE_LIMITER_INTERVAL = "zookeeper.admin.rateLimiterIntervalInMS";
+ private static final long rateLimiterInterval =
+ Integer.parseInt(System.getProperty(ADMIN_RATE_LIMITER_INTERVAL, "300000"));
+ // VisibleForTesting
+ static final String AUTH_INFO_SEPARATOR = " ";
+ // VisibleForTesting
+ static final String ROOT_PATH = "/";
+
+ /**
+ * Maps command names to Command instances.
+ */
+ private static Map<String, Command> commands = new HashMap<>();
+ private static Set<String> primaryNames = new HashSet<>();
+
+ /**
+ * Registers the given command. Registered commands can be run by passing
+ * any of their names to runCommand.
+ */
+ public static void registerCommand(Command command) {
+ for (String name : command.getNames()) {
+ Command prev = commands.put(name, command);
+ if (prev != null) {
+ LOG.warn("Re-registering command {} (primary name = {})", name, command.getPrimaryName());
+ }
+ }
+ primaryNames.add(command.getPrimaryName());
+ }
+
+ /**
+ * Run the registered command with name cmdName. Commands should not produce
+ * any exceptions; any (anticipated) errors should be reported in the
+ * "error" entry of the returned map. Likewise, if no command with the given
+ * name is registered, this will be noted in the "error" entry.
+ *
+ * @param cmdName
+ * @param zkServer
+ * @param kwargs String-valued keyword arguments to the command from HTTP GET request
+ * (may be null if command requires no additional arguments)
+ * @param authInfo auth info for auth check
+ * (null if command requires no auth check)
+ * @param request HTTP request
+ * @return Map representing response to command containing at minimum:
+ * - "command" key containing the command's primary name
+ * - "error" key containing a String error message or null if no error
+ */
+ public static CommandResponse runGetCommand(
+ String cmdName,
+ ZooKeeperServer zkServer,
+ Map<String, String> kwargs,
+ String authInfo,
+ HttpServletRequest request) {
+ return runCommand(cmdName, zkServer, kwargs, null, authInfo, request, true);
+ }
+
+ /**
+ * Run the registered command with name cmdName. Commands should not produce
+ * any exceptions; any (anticipated) errors should be reported in the
+ * "error" entry of the returned map. Likewise, if no command with the given
+ * name is registered, this will be noted in the "error" entry.
+ *
+ * @param cmdName
+ * @param zkServer
+ * @param inputStream InputStream from HTTP POST request
+ * @return Map representing response to command containing at minimum:
+ * - "command" key containing the command's primary name
+ * - "error" key containing a String error message or null if no error
+ */
+ public static CommandResponse runPostCommand(
+ String cmdName,
+ ZooKeeperServer zkServer,
+ InputStream inputStream,
+ String authInfo,
+ HttpServletRequest request) {
+ return runCommand(cmdName, zkServer, null, inputStream, authInfo, request, false);
+ }
+
+ private static CommandResponse runCommand(
+ String cmdName,
+ ZooKeeperServer zkServer,
+ Map<String, String> kwargs,
+ InputStream inputStream,
+ String authInfo,
+ HttpServletRequest request,
+ boolean isGet) {
+ Command command = getCommand(cmdName);
+ if (command == null) {
+ // set the status code to 200 to keep the current behavior of existing commands
+ LOG.warn("Unknown command");
+ return new CommandResponse(cmdName, "Unknown command: " + cmdName, HttpServletResponse.SC_OK);
+ }
+ if (command.isServerRequired() && (zkServer == null || !zkServer.isRunning())) {
+ // set the status code to 200 to keep the current behavior of existing commands
+ LOG.warn("This ZooKeeper instance is not currently serving requests for command");
+ return new CommandResponse(cmdName, "This ZooKeeper instance is not currently serving requests",
+ HttpServletResponse.SC_OK);
+ }
+
+ final AuthRequest authRequest = command.getAuthRequest();
+ if (authRequest != null) {
+ if (authInfo == null) {
+ LOG.warn("Auth info is missing for command");
+ return new CommandResponse(cmdName, "Auth info is missing for the command",
+ HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ try {
+ final List<Id> ids = handleAuthentication(request, authInfo);
+ handleAuthorization(zkServer, ids, authRequest.getPermission(), authRequest.getPath());
+ } catch (final KeeperException.AuthFailedException e) {
+ return new CommandResponse(cmdName, "Not authenticated", HttpServletResponse.SC_UNAUTHORIZED);
+ } catch (final KeeperException.NoAuthException e) {
+ return new CommandResponse(cmdName, "Not authorized", HttpServletResponse.SC_FORBIDDEN);
+ } catch (final Exception e) {
+ LOG.warn("Error occurred during auth for command", e);
+ return new CommandResponse(cmdName, "Error occurred during auth",
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ return isGet ? command.runGet(zkServer, kwargs) : command.runPost(zkServer, inputStream);
+ }
+
+ private static List<Id> handleAuthentication(final HttpServletRequest request, final String authInfo)
+ throws KeeperException.AuthFailedException {
+ final String[] authData = authInfo.split(AUTH_INFO_SEPARATOR);
+ // for IP and x509, auth info only contains the schema and Auth Id will be extracted from HTTP request
+ if (authData.length != 1 && authData.length != 2) {
+ LOG.warn("Invalid auth info length");
+ throw new KeeperException.AuthFailedException();
+ }
+
+ final String schema = authData[0];
+ final ServerAuthenticationProvider authProvider = ProviderRegistry.getServerProvider(schema);
+ if (authProvider != null) {
+ try {
+ final byte[] auth = authData.length == 2 ? authData[1].getBytes(StandardCharsets.UTF_8) : null;
+ final List<Id> ids = authProvider.handleAuthentication(request, auth);
+ if (ids.isEmpty()) {
+ LOG.warn("Auth Id list is empty");
+ throw new KeeperException.AuthFailedException();
+ }
+ return ids;
+ } catch (final RuntimeException e) {
+ LOG.warn("Caught runtime exception from AuthenticationProvider", e);
+ throw new KeeperException.AuthFailedException();
+ }
+ } else {
+ LOG.warn("Auth provider not found for schema");
+ throw new KeeperException.AuthFailedException();
+ }
+ }
+
+ /**
+ * Grant or deny authorization for a command by matching
+ * request-provided credentials with the ACLs present on a node.
+ *
+ * @param zkServer the ZooKeeper server object.
+ * @param ids the credentials extracted from the Authorization header.
+ * @param perm the set of permission bits required by the command.
+ * @param path the ZooKeeper node path whose ACLs should be used
+ * to satisfy the perm bits.
+ * @throws KeeperException.NoAuthException if one or more perm
+ * bits could not be satisfied.
+ */
+ private static void handleAuthorization(final ZooKeeperServer zkServer,
+ final List<Id> ids,
+ final int perm,
+ final String path)
+ throws KeeperException.NoNodeException, KeeperException.NoAuthException {
+ final DataNode dataNode = zkServer.getZKDatabase().getNode(path);
+ if (dataNode == null) {
+ throw new KeeperException.NoNodeException(path);
+ }
+ final List<ACL> acls = zkServer.getZKDatabase().aclForNode(dataNode);
+ // Check the individual bits of perm.
+ final int bitWidth = Integer.SIZE - Integer.numberOfLeadingZeros(perm);
+ for (int b = 0; b < bitWidth; b++) {
+ final int permBit = 1 << b;
+ if ((perm & permBit) != 0) {
+ zkServer.checkACL(null, acls, permBit, ids, path, null);
+ }
+ }
+ }
+
+ /**
+ * Returns the primary names of all registered commands.
+ */
+ public static Set<String> getPrimaryNames() {
+ return primaryNames;
+ }
+
+ /**
+ * Returns the commands registered under cmdName with registerCommand, or
+ * null if no command is registered with that name.
+ */
+ public static Command getCommand(String cmdName) {
+ return commands.get(cmdName);
+ }
+
+ static {
+ registerCommand(new CnxnStatResetCommand());
+ registerCommand(new ConfCommand());
+ registerCommand(new ConsCommand());
+ registerCommand(new DigestCommand());
+ registerCommand(new DirsCommand());
+ registerCommand(new DumpCommand());
+ registerCommand(new EnvCommand());
+ registerCommand(new GetTraceMaskCommand());
+ registerCommand(new InitialConfigurationCommand());
+ registerCommand(new IsroCommand());
+ registerCommand(new LastSnapshotCommand());
+ registerCommand(new LeaderCommand());
+ registerCommand(new MonitorCommand());
+ registerCommand(new ObserverCnxnStatResetCommand());
+ registerCommand(new RestoreCommand());
+ registerCommand(new RuokCommand());
+ registerCommand(new SetTraceMaskCommand());
+ registerCommand(new SnapshotCommand());
+ registerCommand(new SrvrCommand());
+ registerCommand(new StatCommand());
+ registerCommand(new StatResetCommand());
+ registerCommand(new SyncedObserverConsCommand());
+ registerCommand(new SystemPropertiesCommand());
+ registerCommand(new VotingViewCommand());
+ registerCommand(new WatchCommand());
+ registerCommand(new WatchesByPathCommand());
+ registerCommand(new WatchSummaryCommand());
+ registerCommand(new ZabStateCommand());
+ }
+
+ /**
+ * Reset all connection statistics.
+ */
+ public static class CnxnStatResetCommand extends GetCommand {
+
+ public CnxnStatResetCommand() {
+ super(Arrays.asList("connection_stat_reset", "crst"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ zkServer.getServerCnxnFactory().resetAllConnectionStats();
+ return response;
+
+ }
+
+ }
+
+ /**
+ * Server configuration parameters.
+ *
+ * @see ZooKeeperServer#getConf()
+ */
+ public static class ConfCommand extends GetCommand {
+
+ public ConfCommand() {
+ super(Arrays.asList("configuration", "conf", "config"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.putAll(zkServer.getConf().toMap());
+ return response;
+ }
+
+ }
+
+ /**
+ * Information on client connections to server. Returned Map contains:
+ * - "connections": list of connection info objects
+ *
+ * @see org.apache.zookeeper.server.ServerCnxn#getConnectionInfo(boolean)
+ */
+ public static class ConsCommand extends GetCommand {
+
+ public ConsCommand() {
+ super(Arrays.asList("connections", "cons"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ ServerCnxnFactory serverCnxnFactory = zkServer.getServerCnxnFactory();
+ if (serverCnxnFactory != null) {
+ response.put("connections", serverCnxnFactory.getAllConnectionInfo(false));
+ } else {
+ response.put("connections", Collections.emptyList());
+ }
+ ServerCnxnFactory secureServerCnxnFactory = zkServer.getSecureServerCnxnFactory();
+ if (secureServerCnxnFactory != null) {
+ response.put("secure_connections", secureServerCnxnFactory.getAllConnectionInfo(false));
+ } else {
+ response.put("secure_connections", Collections.emptyList());
+ }
+ return response;
+ }
+
+ }
+
+ /**
+ * Information on ZK datadir and snapdir size in bytes.
+ */
+ public static class DirsCommand extends GetCommand {
+
+ public DirsCommand() {
+ super(Arrays.asList("dirs"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.put("datadir_size", zkServer.getDataDirSize());
+ response.put("logdir_size", zkServer.getLogDirSize());
+ return response;
+ }
+
+ }
+
+ /**
+ * Information on session expirations and ephemerals. Returned map contains:
+ * - "expiry_time_to_session_ids": Map<Long, Set<Long>>
+ * time -> sessions IDs of sessions that expire at time
+ * - "session_id_to_ephemeral_paths": Map<Long, Set<String>>
+ * session ID -> ephemeral paths created by that session
+ *
+ * @see ZooKeeperServer#getSessionExpiryMap()
+ * @see ZooKeeperServer#getEphemerals()
+ */
+ public static class DumpCommand extends GetCommand {
+
+ public DumpCommand() {
+ super(Arrays.asList("dump"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.put("expiry_time_to_session_ids", zkServer.getSessionExpiryMap());
+ response.put("session_id_to_ephemeral_paths", zkServer.getEphemerals());
+ return response;
+ }
+
+ }
+
+ /**
+ * All defined environment variables.
+ */
+ public static class EnvCommand extends GetCommand {
+
+ public EnvCommand() {
+ super(Arrays.asList("environment", "env", "envi"), false);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ for (Entry e : Environment.list()) {
+ response.put(e.getKey(), e.getValue());
+ }
+ return response;
+ }
+
+ }
+
+ /**
+ * Digest histories for every specific number of txns.
+ */
+ public static class DigestCommand extends GetCommand {
+
+ public DigestCommand() {
+ super(Arrays.asList("hash"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.put("digests", zkServer.getZKDatabase().getDataTree().getDigestLog());
+ return response;
+ }
+
+ }
+
+ /**
+ * The current trace mask. Returned map contains:
+ * - "tracemask": Long
+ */
+ public static class GetTraceMaskCommand extends GetCommand {
+
+ public GetTraceMaskCommand() {
+ super(Arrays.asList("get_trace_mask", "gtmk"), false);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.put("tracemask", ZooTrace.getTextTraceLevel());
+ return response;
+ }
+
+ }
+
+ public static class InitialConfigurationCommand extends GetCommand {
+
+ public InitialConfigurationCommand() {
+ super(Arrays.asList("initial_configuration", "icfg"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.put("initial_configuration", zkServer.getInitialConfig());
+ return response;
+ }
+
+ }
+
+ /**
+ * Is this server in read-only mode. Returned map contains:
+ * - "is_read_only": Boolean
+ */
+ public static class IsroCommand extends GetCommand {
+
+ public IsroCommand() {
+ super(Arrays.asList("is_read_only", "isro"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ response.put("read_only", zkServer instanceof ReadOnlyZooKeeperServer);
+ return response;
+ }
+
+ }
+
+ /**
+ * Command returns information of the last snapshot that zookeeper server
+ * has finished saving to disk. During the time between the server starts up
+ * and it finishes saving its first snapshot, the command returns the zxid
+ * and last modified time of the snapshot file used for restoration at
+ * server startup. Returned map contains:
+ * - "zxid": String
+ * - "timestamp": Long
+ */
+ public static class LastSnapshotCommand extends GetCommand {
+
+ public LastSnapshotCommand() {
+ super(Arrays.asList("last_snapshot", "lsnp"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ SnapshotInfo info = zkServer.getTxnLogFactory().getLastSnapshotInfo();
+ response.put("zxid", Long.toHexString(info == null ? -1L : info.zxid));
+ response.put("timestamp", info == null ? -1L : info.timestamp);
+ return response;
+ }
+
+ }
+
+ /**
+ * Returns the leader status of this instance and the leader host string.
+ */
+ public static class LeaderCommand extends GetCommand {
+
+ public LeaderCommand() {
+ super(Arrays.asList("leader", "lead"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ if (zkServer instanceof QuorumZooKeeperServer) {
+ response.put("is_leader", zkServer instanceof LeaderZooKeeperServer);
+ QuorumPeer peer = ((QuorumZooKeeperServer) zkServer).self;
+ response.put("leader_id", peer.getLeaderId());
+ String leaderAddress = peer.getLeaderAddress();
+ response.put("leader_ip", leaderAddress != null ? leaderAddress : "");
+ } else {
+ response.put("error", "server is not initialized");
+ }
+ return response;
+ }
+
+ }
+
+ /**
+ * Some useful info for monitoring. Returned map contains:
+ * - "version": String
+ * server version
+ * - "avg_latency": Long
+ * - "max_latency": Long
+ * - "min_latency": Long
+ * - "packets_received": Long
+ * - "packets_sents": Long
+ * - "num_alive_connections": Integer
+ * - "outstanding_requests": Long
+ * number of unprocessed requests
+ * - "server_state": "leader", "follower", or "standalone"
+ * - "znode_count": Integer
+ * - "watch_count": Integer
+ * - "ephemerals_count": Integer
+ * - "approximate_data_size": Long
+ * - "open_file_descriptor_count": Long (unix only)
+ * - "max_file_descriptor_count": Long (unix only)
+ * - "fsync_threshold_exceed_count": Long
+ * - "non_mtls_conn_count": Long
+ * - "non_mtls_remote_conn_count": Long
+ * - "non_mtls_local_conn_count": Long
+ * - "followers": Integer (leader only)
+ * - "synced_followers": Integer (leader only)
+ * - "pending_syncs": Integer (leader only)
+ */
+ public static class MonitorCommand extends GetCommand {
+
+ public MonitorCommand() {
+ super(Arrays.asList("monitor", "mntr"), false);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ zkServer.dumpMonitorValues(response::put);
+ ServerMetrics.getMetrics().getMetricsProvider().dump(response::put);
+ return response;
+
+ }
+
+ }
+
+ /**
+ * Reset all observer connection statistics.
+ */
+ public static class ObserverCnxnStatResetCommand extends GetCommand {
+
+ public ObserverCnxnStatResetCommand() {
+ super(Arrays.asList("observer_connection_stat_reset", "orst"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ if (zkServer instanceof LeaderZooKeeperServer) {
+ Leader leader = ((LeaderZooKeeperServer) zkServer).getLeader();
+ leader.resetObserverConnectionStats();
+ } else if (zkServer instanceof FollowerZooKeeperServer) {
+ Follower follower = ((FollowerZooKeeperServer) zkServer).getFollower();
+ follower.resetObserverConnectionStats();
+ }
+ return response;
+ }
+
+ }
+
+ /**
+ * Restore from snapshot on the current server.
+ *
+ * Returned map contains:
+ * - "last_zxid": String
+ */
+ public static class RestoreCommand extends PostCommand {
+ static final String RESPONSE_DATA_LAST_ZXID = "last_zxid";
+ static final String ADMIN_RESTORE_ENABLED = "zookeeper.admin.restore.enabled";
+
+ private RateLimiter rateLimiter;
+
+ public RestoreCommand() {
+ super(Arrays.asList("restore", "rest"), true, new AuthRequest(ZooDefs.Perms.ALL, ROOT_PATH));
+ rateLimiter = new RateLimiter(1, rateLimiterInterval, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public CommandResponse runPost(final ZooKeeperServer zkServer, final InputStream inputStream) {
+ final CommandResponse response = initializeResponse();
+
+ // check feature flag
+ final boolean restoreEnabled = Boolean.parseBoolean(System.getProperty(ADMIN_RESTORE_ENABLED, "true"));
+ if (!restoreEnabled) {
+ response.setStatusCode(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ LOG.warn("Restore command is disabled");
+ return response;
+ }
+
+ if (!zkServer.isSerializeLastProcessedZxidEnabled()) {
+ response.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ LOG.warn("Restore command requires serializeLastProcessedZxidEnable flag is set to true");
+ return response;
+ }
+
+ if (inputStream == null) {
+ response.setStatusCode(HttpServletResponse.SC_BAD_REQUEST);
+ LOG.warn("InputStream from restore request is null");
+ return response;
+ }
+
+ // check rate limiting
+ if (!rateLimiter.allow()) {
+ response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS_429);
+ ServerMetrics.getMetrics().RESTORE_RATE_LIMITED_COUNT.add(1);
+ LOG.warn("Restore request was rate limited");
+ return response;
+ }
+
+ // restore from snapshot InputStream
+ try {
+ final long lastZxid = zkServer.restoreFromSnapshot(inputStream);
+ response.put(RESPONSE_DATA_LAST_ZXID, lastZxid);
+ } catch (final Exception e) {
+ response.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ ServerMetrics.getMetrics().RESTORE_ERROR_COUNT.add(1);
+ LOG.warn("Exception occurred when restore snapshot via the restore command", e);
+ }
+ return response;
+ }
+ }
+
+ /**
+ * No-op command, check if the server is running.
+ */
+ public static class RuokCommand extends GetCommand {
+
+ public RuokCommand() {
+ super(Arrays.asList("ruok"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ return initializeResponse();
+ }
+
+ }
+
+ /**
+ * Sets the trace mask. Required arguments:
+ * - "traceMask": Long
+ * Returned Map contains:
+ * - "tracemask": Long
+ */
+ public static class SetTraceMaskCommand extends GetCommand {
+
+ public SetTraceMaskCommand() {
+ super(Arrays.asList("set_trace_mask", "stmk"), false);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ long traceMask;
+ if (!kwargs.containsKey("traceMask")) {
+ response.put("error", "setTraceMask requires long traceMask argument");
+ return response;
+ }
+ try {
+ traceMask = Long.parseLong(kwargs.get("traceMask"));
+ } catch (NumberFormatException e) {
+ response.put("error", "setTraceMask requires long traceMask argument, got " + kwargs.get("traceMask"));
+ return response;
+ }
+
+ ZooTrace.setTextTraceLevel(traceMask);
+ response.put("tracemask", traceMask);
+ return response;
+ }
+
+ }
+
+ /**
+ * Take a snapshot of current server and stream out the data.
+ *
+ * Argument:
+ * - "streaming": optional String to indicate whether streaming out data
+ *
+ * Returned snapshot as stream if streaming is true and metadata of the snapshot
+ * - "last_zxid": String
+ * - "snapshot_size": String
+ */
+ public static class SnapshotCommand extends GetCommand {
+ static final String REQUEST_QUERY_PARAM_STREAMING = "streaming";
+
+ static final String RESPONSE_HEADER_LAST_ZXID = "last_zxid";
+ static final String RESPONSE_HEADER_SNAPSHOT_SIZE = "snapshot_size";
+
+ static final String ADMIN_SNAPSHOT_ENABLED = "zookeeper.admin.snapshot.enabled";
+
+ private final RateLimiter rateLimiter;
+
+ public SnapshotCommand() {
+ super(Arrays.asList("snapshot", "snap"), true, new AuthRequest(ZooDefs.Perms.ALL, ROOT_PATH));
+ rateLimiter = new RateLimiter(1, rateLimiterInterval, TimeUnit.MICROSECONDS);
+ }
+
+ @SuppressFBWarnings(value = "OBL_UNSATISFIED_OBLIGATION",
+ justification = "FileInputStream is passed to CommandResponse and closed in StreamOutputter")
+ @Override
+ public CommandResponse runGet(final ZooKeeperServer zkServer, final Map<String, String> kwargs) {
+ final CommandResponse response = initializeResponse();
+
+ // check feature flag
+ final boolean snapshotEnabled = Boolean.parseBoolean(System.getProperty(ADMIN_SNAPSHOT_ENABLED, "true"));
+ if (!snapshotEnabled) {
+ response.setStatusCode(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ LOG.warn("Snapshot command is disabled");
+ return response;
+ }
+
+ if (!zkServer.isSerializeLastProcessedZxidEnabled()) {
+ response.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ LOG.warn("Snapshot command requires serializeLastProcessedZxidEnable flag is set to true");
+ return response;
+ }
+
+ // check rate limiting
+ if (!rateLimiter.allow()) {
+ response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS_429);
+ ServerMetrics.getMetrics().SNAPSHOT_RATE_LIMITED_COUNT.add(1);
+ LOG.warn("Snapshot request was rate limited");
+ return response;
+ }
+
+ // check the streaming query param
+ boolean streaming = true;
+ if (kwargs.containsKey(REQUEST_QUERY_PARAM_STREAMING)) {
+ streaming = Boolean.parseBoolean(kwargs.get(REQUEST_QUERY_PARAM_STREAMING));
+ }
+
+ // take snapshot and stream out data if needed
+ try {
+ final File snapshotFile = zkServer.takeSnapshot(false, false);
+ final long lastZxid = Util.getZxidFromName(snapshotFile.getName(), SNAPSHOT_FILE_PREFIX);
+ response.addHeader(RESPONSE_HEADER_LAST_ZXID, "0x" + ZxidUtils.zxidToString(lastZxid));
+
+ final long size = snapshotFile.length();
+ response.addHeader(RESPONSE_HEADER_SNAPSHOT_SIZE, String.valueOf(size));
+
+ if (size == 0) {
+ response.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ ServerMetrics.getMetrics().SNAPSHOT_ERROR_COUNT.add(1);
+ LOG.warn("Snapshot file {} is empty", snapshotFile);
+ } else if (streaming) {
+ response.setInputStream(new FileInputStream(snapshotFile));
+ }
+ } catch (final Exception e) {
+ response.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ ServerMetrics.getMetrics().SNAPSHOT_ERROR_COUNT.add(1);
+ LOG.warn("Exception occurred when taking the snapshot via the snapshot admin command", e);
+ }
+ return response;
+ }
+ }
+
+ /**
+ * Server information. Returned map contains:
+ * - "version": String
+ * version of server
+ * - "read_only": Boolean
+ * is server in read-only mode
+ * - "server_stats": ServerStats object
+ * - "node_count": Integer
+ */
+ public static class SrvrCommand extends GetCommand {
+
+ public SrvrCommand() {
+ super(Arrays.asList("server_stats", "srvr"));
+ }
+
+ // Allow subclasses (e.g. StatCommand) to specify their own names
+ protected SrvrCommand(List<String> names) {
+ super(names);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ LOG.info("running stat");
+ response.put("version", Version.getFullVersion());
+ response.put("read_only", zkServer instanceof ReadOnlyZooKeeperServer);
+ response.put("server_stats", zkServer.serverStats());
+ response.put("client_response", zkServer.serverStats().getClientResponseStats());
+ if (zkServer instanceof LeaderZooKeeperServer) {
+ Leader leader = ((LeaderZooKeeperServer) zkServer).getLeader();
+ response.put("proposal_stats", leader.getProposalStats());
+ }
+ response.put("node_count", zkServer.getZKDatabase().getNodeCount());
+ return response;
+ }
+
+ }
+
+ /**
+ * Same as SrvrCommand but has extra "connections" entry.
+ */
+ public static class StatCommand extends SrvrCommand {
+
+ public StatCommand() {
+ super(Arrays.asList("stats", "stat"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = super.runGet(zkServer, kwargs);
+
+ final Iterable<Map<String, Object>> connections;
+ if (zkServer.getServerCnxnFactory() != null) {
+ connections = zkServer.getServerCnxnFactory().getAllConnectionInfo(true);
+ } else {
+ connections = Collections.emptyList();
+ }
+ response.put("connections", connections);
+
+ final Iterable<Map<String, Object>> secureConnections;
+ if (zkServer.getSecureServerCnxnFactory() != null) {
+ secureConnections = zkServer.getSecureServerCnxnFactory().getAllConnectionInfo(true);
+ } else {
+ secureConnections = Collections.emptyList();
+ }
+ response.put("secure_connections", secureConnections);
+ return response;
+ }
+
+ }
+
+ /**
+ * Resets server statistics.
+ */
+ public static class StatResetCommand extends GetCommand {
+
+ public StatResetCommand() {
+ super(Arrays.asList("stat_reset", "srst"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ zkServer.serverStats().reset();
+ return response;
+ }
+
+ }
+
+ /**
+ * Information on observer connections to server. Returned Map contains:
+ * - "synced_observers": Integer (leader/follower only)
+ * - "observers": list of observer learner handler info objects (leader/follower only)
+ *
+ * @see org.apache.zookeeper.server.quorum.LearnerHandler#getLearnerHandlerInfo()
+ */
+ public static class SyncedObserverConsCommand extends GetCommand {
+
+ public SyncedObserverConsCommand() {
+ super(Arrays.asList("observers", "obsr"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+
+ CommandResponse response = initializeResponse();
+
+ if (zkServer instanceof LeaderZooKeeperServer) {
+ Leader leader = ((LeaderZooKeeperServer) zkServer).getLeader();
+
+ response.put("synced_observers", leader.getObservingLearners().size());
+ response.put("observers", leader.getObservingLearnersInfo());
+ return response;
+ } else if (zkServer instanceof FollowerZooKeeperServer) {
+ Follower follower = ((FollowerZooKeeperServer) zkServer).getFollower();
+ Integer syncedObservers = follower.getSyncedObserverSize();
+ if (syncedObservers != null) {
+ response.put("synced_observers", syncedObservers);
+ response.put("observers", follower.getSyncedObserversInfo());
+ return response;
+ }
+ }
+
+ response.put("synced_observers", 0);
+ response.put("observers", Collections.emptySet());
+ return response;
+ }
+
+ }
+
+ /**
+ * All defined system properties.
+ */
+ public static class SystemPropertiesCommand extends GetCommand {
+
+ public SystemPropertiesCommand() {
+ super(Arrays.asList("system_properties", "sysp"), false);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ Properties systemProperties = System.getProperties();
+ SortedMap<String, String> sortedSystemProperties = new TreeMap<>();
+ systemProperties.forEach((k, v) -> sortedSystemProperties.put(k.toString(), v.toString()));
+ response.putAll(sortedSystemProperties);
+ return response;
+ }
+
+ }
+
+ /**
+ * Returns the current ensemble configuration information.
+ * It provides list of current voting members in the ensemble.
+ */
+ public static class VotingViewCommand extends GetCommand {
+
+ public VotingViewCommand() {
+ super(Arrays.asList("voting_view"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ if (zkServer instanceof QuorumZooKeeperServer) {
+ QuorumPeer peer = ((QuorumZooKeeperServer) zkServer).self;
+ Map<Long, QuorumServerView> votingView = peer.getVotingView().entrySet().stream()
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> new QuorumServerView(e.getValue())));
+ response.put("current_config", votingView);
+ } else {
+ response.put("current_config", Collections.emptyMap());
+ }
+ return response;
+ }
+
+ @SuppressFBWarnings(value = "URF_UNREAD_FIELD", justification = "class is used only for JSON serialization")
+ private static class QuorumServerView {
+
+ @JsonProperty
+ private List<String> serverAddresses;
+
+ @JsonProperty
+ private List<String> electionAddresses;
+
+ @JsonProperty
+ private String clientAddress;
+
+ @JsonProperty
+ private String learnerType;
+
+ public QuorumServerView(QuorumPeer.QuorumServer quorumServer) {
+ this.serverAddresses = getMultiAddressString(quorumServer.addr);
+ this.electionAddresses = getMultiAddressString(quorumServer.electionAddr);
+ this.learnerType = quorumServer.type.equals(LearnerType.PARTICIPANT) ? "participant" : "observer";
+ this.clientAddress = getAddressString(quorumServer.clientAddr);
+ }
+
+ private static List<String> getMultiAddressString(MultipleAddresses multipleAddresses) {
+ if (multipleAddresses == null) {
+ return Collections.emptyList();
+ }
+
+ return multipleAddresses.getAllAddresses().stream()
+ .map(QuorumServerView::getAddressString)
+ .collect(Collectors.toList());
+ }
+
+ private static String getAddressString(InetSocketAddress address) {
+ if (address == null) {
+ return "";
+ }
+ return String.format("%s:%d", QuorumPeer.QuorumServer.delimitedHostString(address), address.getPort());
+ }
+ }
+
+ }
+
+ /**
+ * Watch information aggregated by session. Returned Map contains:
+ * - "session_id_to_watched_paths": Map<Long, Set<String>> session ID -> watched paths
+ *
+ * @see DataTree#getWatches()
+ * @see DataTree#getWatches()
+ */
+ public static class WatchCommand extends GetCommand {
+
+ public WatchCommand() {
+ super(Arrays.asList("watches", "wchc"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ DataTree dt = zkServer.getZKDatabase().getDataTree();
+ CommandResponse response = initializeResponse();
+ response.put("session_id_to_watched_paths", dt.getWatches().toMap());
+ return response;
+ }
+
+ }
+
+ /**
+ * Watch information aggregated by path. Returned Map contains:
+ * - "path_to_session_ids": Map<String, Set<Long>> path -> session IDs of sessions watching path
+ *
+ * @see DataTree#getWatchesByPath()
+ */
+ public static class WatchesByPathCommand extends GetCommand {
+
+ public WatchesByPathCommand() {
+ super(Arrays.asList("watches_by_path", "wchp"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ DataTree dt = zkServer.getZKDatabase().getDataTree();
+ CommandResponse response = initializeResponse();
+ response.put("path_to_session_ids", dt.getWatchesByPath().toMap());
+ return response;
+ }
+
+ }
+
+ /**
+ * Summarized watch information.
+ *
+ * @see DataTree#getWatchesSummary()
+ */
+ public static class WatchSummaryCommand extends GetCommand {
+
+ public WatchSummaryCommand() {
+ super(Arrays.asList("watch_summary", "wchs"));
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ DataTree dt = zkServer.getZKDatabase().getDataTree();
+ CommandResponse response = initializeResponse();
+ response.putAll(dt.getWatchesSummary().toMap());
+ return response;
+ }
+
+ }
+
+ /**
+ * Returns the current phase of Zab protocol that peer is running.
+ * It can be in one of these phases: ELECTION, DISCOVERY, SYNCHRONIZATION, BROADCAST
+ */
+ public static class ZabStateCommand extends GetCommand {
+
+ public ZabStateCommand() {
+ super(Arrays.asList("zabstate"), false);
+ }
+
+ @Override
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
+ CommandResponse response = initializeResponse();
+ if (zkServer instanceof QuorumZooKeeperServer) {
+ QuorumPeer peer = ((QuorumZooKeeperServer) zkServer).self;
+ QuorumPeer.ZabState zabState = peer.getZabState();
+ QuorumVerifier qv = peer.getQuorumVerifier();
+
+ QuorumPeer.QuorumServer voter = qv.getVotingMembers().get(peer.getMyId());
+ boolean voting = (
+ voter != null
+ && voter.addr.equals(peer.getQuorumAddress())
+ && voter.electionAddr.equals(peer.getElectionAddress())
+ );
+ response.put("myid", zkServer.getConf().getServerId());
+ response.put("is_leader", zkServer instanceof LeaderZooKeeperServer);
+ response.put("quorum_address", peer.getQuorumAddress());
+ response.put("election_address", peer.getElectionAddress());
+ response.put("client_address", peer.getClientAddress());
+ response.put("voting", voting);
+ long lastProcessedZxid = zkServer.getZKDatabase().getDataTreeLastProcessedZxid();
+ response.put("last_zxid", "0x" + ZxidUtils.zxidToString(lastProcessedZxid));
+ response.put("zab_epoch", ZxidUtils.getEpochFromZxid(lastProcessedZxid));
+ response.put("zab_counter", ZxidUtils.getCounterFromZxid(lastProcessedZxid));
+ response.put("zabstate", zabState.name().toLowerCase());
+ } else {
+ response.put("voting", false);
+ response.put("zabstate", "");
+ }
+ return response;
+ }
+
+ }
+
+ private Commands() {
+ }
+
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/DummyAdminServer.java
similarity index 60%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/DummyAdminServer.java
index 65cc10e0d3b..4750ed2215f 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/DummyAdminServer.java
@@ -16,30 +16,29 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
+package org.apache.zookeeper.server.admin;
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
+import org.apache.zookeeper.server.ZooKeeperServer;
-public class MockAdditionalServlet implements AdditionalServlet {
+/**
+ * An AdminServer that does nothing.
+ *
+ * We use this class when we wish to disable the AdminServer. (This way we only
+ * have to consider whether the server is enabled when we create the
+ * AdminServer, which is handled by AdminServerFactory.)
+ */
+public class DummyAdminServer implements AdminServer {
@Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
-
+ public void start() throws AdminServerException {
}
@Override
- public String getBasePath() {
- return null;
+ public void shutdown() throws AdminServerException {
}
@Override
- public ServletHolder getServletHolder() {
- return null;
+ public void setZooKeeperServer(ZooKeeperServer zkServer) {
}
- @Override
- public void close() {
- // no-op
- }
}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/GetCommand.java
similarity index 56%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/GetCommand.java
index 65cc10e0d3b..90673a93066 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/GetCommand.java
@@ -16,30 +16,32 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
+package org.apache.zookeeper.server.admin;
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
+import java.io.InputStream;
+import java.util.List;
+import org.apache.zookeeper.server.ZooKeeperServer;
-public class MockAdditionalServlet implements AdditionalServlet {
+/**
+ * Command that represents HTTP GET request.
+ */
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
+public abstract class GetCommand extends CommandBase {
+ protected GetCommand(List<String> names) {
+ super(names);
}
- @Override
- public String getBasePath() {
- return null;
+ protected GetCommand(List<String> names, boolean serverRequired) {
+ super(names, serverRequired);
}
- @Override
- public ServletHolder getServletHolder() {
- return null;
+ protected GetCommand(List<String> names, boolean serverRequired, AuthRequest authRequest) {
+ super(names, serverRequired, authRequest);
}
@Override
- public void close() {
- // no-op
+ public CommandResponse runPost(ZooKeeperServer zkServer, InputStream inputStream) {
+ return null;
}
}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java
new file mode 100644
index 00000000000..1a487c00fd0
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/JettyAdminServer.java
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.zookeeper.common.QuorumX509Util;
+import org.apache.zookeeper.common.SecretUtils;
+import org.apache.zookeeper.common.X509Util;
+import org.apache.zookeeper.server.ZooKeeperServer;
+import org.apache.zookeeper.server.auth.IPAuthenticationProvider;
+import org.eclipse.jetty.ee8.nested.ServletConstraint;
+import org.eclipse.jetty.ee8.security.ConstraintMapping;
+import org.eclipse.jetty.ee8.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.server.DetectorConnectionFactory;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class encapsulates a Jetty server for running Commands.
+ *
+ * Given the default settings, start a ZooKeeper server and visit
+ * http://hostname:8080/commands for links to all registered commands. Visiting
+ * http://hostname:8080/commands/commandname will execute the associated
+ * Command and return the result in the body of the response. Any keyword
+ * arguments to the command are specified with URL parameters (e.g.,
+ * http://localhost:8080/commands/set_trace_mask?traceMask=306).
+ *
+ * @see Commands
+ * @see CommandOutputter
+ */
+public class JettyAdminServer implements AdminServer {
+
+ static final Logger LOG = LoggerFactory.getLogger(JettyAdminServer.class);
+
+ public static final int DEFAULT_PORT = 8080;
+ public static final int DEFAULT_IDLE_TIMEOUT = 30000;
+ public static final String DEFAULT_COMMAND_URL = "/commands";
+ private static final String DEFAULT_ADDRESS = "0.0.0.0";
+ public static final int DEFAULT_STS_MAX_AGE = 1 * 24 * 60 * 60; // seconds in a day
+ public static final int DEFAULT_HTTP_VERSION = 11; // based on HttpVersion.java in jetty
+
+ private final Server server;
+ private final String address;
+ private final int port;
+ private final int idleTimeout;
+ private final String commandUrl;
+ private ZooKeeperServer zkServer;
+
+ public JettyAdminServer() throws AdminServerException, IOException, GeneralSecurityException {
+ this(
+ System.getProperty("zookeeper.admin.serverAddress", DEFAULT_ADDRESS),
+ Integer.getInteger("zookeeper.admin.serverPort", DEFAULT_PORT),
+ Integer.getInteger("zookeeper.admin.idleTimeout", DEFAULT_IDLE_TIMEOUT),
+ System.getProperty("zookeeper.admin.commandURL", DEFAULT_COMMAND_URL),
+ Integer.getInteger("zookeeper.admin.httpVersion", DEFAULT_HTTP_VERSION),
+ Boolean.getBoolean("zookeeper.admin.portUnification"),
+ Boolean.getBoolean("zookeeper.admin.forceHttps"),
+ Boolean.getBoolean("zookeeper.admin.needClientAuth"));
+ }
+
+ public JettyAdminServer(
+ String address,
+ int port,
+ int timeout,
+ String commandUrl,
+ int httpVersion,
+ boolean portUnification,
+ boolean forceHttps,
+ boolean needClientAuth) throws IOException, GeneralSecurityException {
+
+ this.port = port;
+ this.idleTimeout = timeout;
+ this.commandUrl = commandUrl;
+ this.address = address;
+
+ server = new Server();
+ ServerConnector connector = null;
+
+ if (!portUnification && !forceHttps) {
+ connector = new ServerConnector(server);
+ } else {
+ SecureRequestCustomizer customizer = new SecureRequestCustomizer();
+ customizer.setStsMaxAge(DEFAULT_STS_MAX_AGE);
+ customizer.setStsIncludeSubDomains(true);
+
+ HttpConfiguration config = new HttpConfiguration();
+ config.setSecureScheme("https");
+ config.addCustomizer(customizer);
+
+ try (QuorumX509Util x509Util = new QuorumX509Util()) {
+ String privateKeyType = System.getProperty(x509Util.getSslKeystoreTypeProperty(), "");
+ String privateKeyPath = System.getProperty(x509Util.getSslKeystoreLocationProperty(), "");
+ String privateKeyPassword = getPasswordFromSystemPropertyOrFile(
+ x509Util.getSslKeystorePasswdProperty(),
+ x509Util.getSslKeystorePasswdPathProperty());
+
+ String certAuthType = System.getProperty(x509Util.getSslTruststoreTypeProperty(), "");
+ String certAuthPath = System.getProperty(x509Util.getSslTruststoreLocationProperty(), "");
+ String certAuthPassword = getPasswordFromSystemPropertyOrFile(
+ x509Util.getSslTruststorePasswdProperty(),
+ x509Util.getSslTruststorePasswdPathProperty());
+ KeyStore keyStore = null, trustStore = null;
+
+ try {
+ keyStore = X509Util.loadKeyStore(privateKeyPath, privateKeyPassword, privateKeyType);
+ trustStore = X509Util.loadTrustStore(certAuthPath, certAuthPassword, certAuthType);
+ LOG.info("Successfully loaded private key from {}", privateKeyPath);
+ LOG.info("Successfully loaded certificate authority from {}", certAuthPath);
+ } catch (Exception e) {
+ LOG.error("Failed to load authentication certificates for admin server.", e);
+ throw e;
+ }
+
+ SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
+ sslContextFactory.setKeyStore(keyStore);
+ sslContextFactory.setKeyStorePassword(privateKeyPassword);
+ sslContextFactory.setTrustStore(trustStore);
+ sslContextFactory.setTrustStorePassword(certAuthPassword);
+ sslContextFactory.setNeedClientAuth(needClientAuth);
+
+ SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory,
+ HttpVersion.fromVersion(httpVersion).asString());
+ if (forceHttps) {
+ connector = new ServerConnector(server,
+ sslConnectionFactory,
+ new HttpConnectionFactory(config));
+ } else {
+ connector = new ServerConnector(
+ server,
+ new DetectorConnectionFactory(sslConnectionFactory),
+ new HttpConnectionFactory(config));
+ connector.setDefaultProtocol(sslConnectionFactory.getProtocol());
+ }
+ }
+ }
+
+ connector.setHost(address);
+ connector.setPort(port);
+ connector.setIdleTimeout(idleTimeout);
+
+ server.addConnector(connector);
+
+ ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ context.setContextPath("/*");
+ constrainTraceMethod(context);
+ server.setHandler(context);
+
+ context.addServlet(new ServletHolder(new CommandServlet()), commandUrl + "/*");
+ }
+
+ /**
+ * Start the embedded Jetty server.
+ */
+ @Override
+ public void start() throws AdminServerException {
+ try {
+ server.start();
+ } catch (Exception e) {
+ // Server.start() only throws Exception, so let's at least wrap it
+ // in an identifiable subclass
+ String message = String.format(
+ "Problem starting AdminServer on address %s, port %d and command URL %s",
+ address,
+ port,
+ commandUrl);
+ throw new AdminServerException(message, e);
+ }
+ LOG.info("Started AdminServer on address {}, port {} and command URL {}", address, port, commandUrl);
+ }
+
+ /**
+ * Stop the embedded Jetty server.
+ *
+ * This is not very important except for tests where multiple
+ * JettyAdminServers are started and may try to bind to the same ports if
+ * previous servers aren't shut down.
+ */
+ @Override
+ public void shutdown() throws AdminServerException {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ String message = String.format(
+ "Problem stopping AdminServer on address %s, port %d and command URL %s",
+ address,
+ port,
+ commandUrl);
+ throw new AdminServerException(message, e);
+ }
+ }
+
+ /**
+ * Set the ZooKeeperServer that will be used to run Commands.
+ *
+ * It is not necessary to set the ZK server before calling
+ * AdminServer.start(), and the ZK server can be set to null when, e.g.,
+ * that server is being shut down. If the ZK server is not set or set to
+ * null, the AdminServer will still be able to issue Commands, but they will
+ * return an error until a ZK server is set.
+ */
+ @Override
+ public void setZooKeeperServer(ZooKeeperServer zkServer) {
+ this.zkServer = zkServer;
+ }
+
+ private class CommandServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void doGet(
+ HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ // Capture the command name from the URL
+ String cmd = request.getPathInfo();
+ if (cmd == null || cmd.equals("/")) {
+ // No command specified, print links to all commands instead
+ for (String link : commandLinks()) {
+ response.getWriter().println(link);
+ response.getWriter().println("<br/>");
+ }
+ return;
+ }
+ // Strip leading "/"
+ cmd = cmd.substring(1);
+
+ // Extract keyword arguments to command from request parameters
+ @SuppressWarnings("unchecked") Map<String, String[]> parameterMap = request.getParameterMap();
+ Map<String, String> kwargs = new HashMap<>();
+ for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
+ kwargs.put(entry.getKey(), entry.getValue()[0]);
+ }
+ final String authInfo = request.getHeader(HttpHeader.AUTHORIZATION.asString());
+
+ // Run the command
+ final CommandResponse cmdResponse = Commands.runGetCommand(cmd, zkServer, kwargs, authInfo, request);
+ response.setStatus(cmdResponse.getStatusCode());
+
+ final Map<String, String> headers = cmdResponse.getHeaders();
+ for (final Map.Entry<String, String> header : headers.entrySet()) {
+ response.addHeader(header.getKey(), header.getValue());
+ }
+ final String clientIP = IPAuthenticationProvider.getClientIPAddress(request);
+ if (cmdResponse.getInputStream() == null) {
+ // Format and print the output of the command
+ CommandOutputter outputter = new JsonOutputter(clientIP);
+ response.setContentType(outputter.getContentType());
+ outputter.output(cmdResponse, response.getWriter());
+ } else {
+ // Stream out the output of the command
+ CommandOutputter outputter = new StreamOutputter(clientIP);
+ response.setContentType(outputter.getContentType());
+ outputter.output(cmdResponse, response.getOutputStream());
+ }
+ }
+
+ /**
+ * Serves HTTP POST requests. It reads request payload as raw data.
+ * It's up to each command to process the payload accordingly.
+ * For example, RestoreCommand uses the payload InputStream directly
+ * to read snapshot data.
+ */
+ @Override
+ protected void doPost(final HttpServletRequest request,
+ final HttpServletResponse response) throws ServletException, IOException {
+ final String cmdName = extractCommandNameFromURL(request, response);
+ if (cmdName != null) {
+ final String authInfo = request.getHeader(HttpHeader.AUTHORIZATION.asString());
+ final CommandResponse cmdResponse =
+ Commands.runPostCommand(cmdName, zkServer, request.getInputStream(), authInfo, request);
+ final String clientIP = IPAuthenticationProvider.getClientIPAddress(request);
+ sendJSONResponse(response, cmdResponse, clientIP);
+ }
+ }
+
+ /**
+ * Extracts the command name from URL if it exists otherwise null.
+ */
+ private String extractCommandNameFromURL(final HttpServletRequest request,
+ final HttpServletResponse response) throws IOException {
+ String cmd = request.getPathInfo();
+ if (cmd == null || cmd.equals("/")) {
+ printCommandLinks(response);
+ return null;
+ }
+ // Strip leading "/"
+ return cmd.substring(1);
+ }
+
+ /**
+ * Prints the list of URLs to each registered command as response.
+ */
+ private void printCommandLinks(final HttpServletResponse response) throws IOException {
+ for (final String link : commandLinks()) {
+ response.getWriter().println(link);
+ response.getWriter().println("<br/>");
+ }
+ }
+
+ /**
+ * Send JSON string as the response.
+ */
+ private void sendJSONResponse(final HttpServletResponse response,
+ final CommandResponse cmdResponse,
+ final String clientIP) throws IOException {
+ final CommandOutputter outputter = new JsonOutputter(clientIP);
+
+ response.setStatus(cmdResponse.getStatusCode());
+ response.setContentType(outputter.getContentType());
+ outputter.output(cmdResponse, response.getWriter());
+ }
+ }
+
+ /**
+ * Returns a list of URLs to each registered Command.
+ */
+ private List<String> commandLinks() {
+ return Commands.getPrimaryNames().stream().sorted()
+ .map(command -> String.format("<a href=\"%s\">%s</a>", commandUrl + "/" + command, command))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Add constraint to a given context to disallow TRACE method.
+ *
+ * @param ctxHandler the context to modify
+ */
+ private void constrainTraceMethod(ServletContextHandler ctxHandler) {
+ ServletConstraint c = new ServletConstraint();
+ c.setAuthenticate(true);
+
+ ConstraintMapping cmt = new ConstraintMapping();
+ cmt.setConstraint(c);
+ cmt.setMethod("TRACE");
+ cmt.setPathSpec("/*");
+
+ ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
+ securityHandler.setConstraintMappings(new ConstraintMapping[]{cmt});
+
+ ctxHandler.setSecurityHandler(securityHandler);
+ }
+
+ /**
+ * Returns the password specified by the given property or stored in the file specified by the
+ * given path property. If both are specified, the password stored in the file will be returned.
+ *
+ * @param propertyName the name of the property
+ * @param pathPropertyName the name of the path property
+ * @return password value
+ */
+ private String getPasswordFromSystemPropertyOrFile(final String propertyName,
+ final String pathPropertyName) {
+ String value = System.getProperty(propertyName, "");
+ final String pathValue = System.getProperty(pathPropertyName, "");
+ if (!pathValue.isEmpty()) {
+ value = String.valueOf(SecretUtils.readSecret(pathValue));
+ }
+ return value;
+ }
+}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/JsonOutputter.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/JsonOutputter.java
new file mode 100644
index 00000000000..631f43508c7
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/JsonOutputter.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import java.io.IOException;
+import java.io.PrintWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JsonOutputter implements CommandOutputter {
+
+ static final Logger LOG = LoggerFactory.getLogger(JsonOutputter.class);
+
+ public static final String ERROR_RESPONSE = "{\"error\": \"Exception writing command response to JSON\"}";
+
+ private ObjectMapper mapper;
+ private final String clientIP;
+
+ public JsonOutputter(final String clientIP) {
+ mapper = new ObjectMapper();
+ mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
+ mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+ mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+ this.clientIP = clientIP;
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/json";
+ }
+
+ @Override
+ public void output(CommandResponse response, PrintWriter pw) {
+ try {
+ mapper.writeValue(pw, response.toMap());
+ } catch (JsonGenerationException e) {
+ LOG.warn("Exception writing command response to JSON:", e);
+ pw.write(ERROR_RESPONSE);
+ } catch (JsonMappingException e) {
+ LOG.warn("Exception writing command response to JSON:", e);
+ pw.write(ERROR_RESPONSE);
+ } catch (IOException e) {
+ LOG.warn("Exception writing command response as JSON to {}", clientIP, e);
+ pw.write(ERROR_RESPONSE);
+ }
+ }
+
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/PostCommand.java
similarity index 62%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/PostCommand.java
index 65cc10e0d3b..23795ed34ae 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/PostCommand.java
@@ -16,30 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
-
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
-
-public class MockAdditionalServlet implements AdditionalServlet {
+/**
+ * Command that represents HTTP POST request
+ */
+package org.apache.zookeeper.server.admin;
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
+import java.util.List;
+import java.util.Map;
+import org.apache.zookeeper.server.ZooKeeperServer;
+public abstract class PostCommand extends CommandBase {
+ protected PostCommand(List<String> names, boolean serverRequired, AuthRequest authRequest) {
+ super(names, serverRequired, authRequest);
}
@Override
- public String getBasePath() {
+ public CommandResponse runGet(ZooKeeperServer zkServer, Map<String, String> kwargs) {
return null;
}
-
- @Override
- public ServletHolder getServletHolder() {
- return null;
- }
-
- @Override
- public void close() {
- // no-op
- }
}
diff --git a/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/StreamOutputter.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/StreamOutputter.java
new file mode 100644
index 00000000000..f4d9ec5c13d
--- /dev/null
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/StreamOutputter.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.zookeeper.server.admin;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.apache.zookeeper.common.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A class for streaming data out.
+ */
+public class StreamOutputter implements CommandOutputter {
+ private static final Logger LOG = LoggerFactory.getLogger(StreamOutputter.class);
+ private final String clientIP;
+
+ public StreamOutputter(final String clientIP) {
+ this.clientIP = clientIP;
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/octet-stream";
+ }
+
+ @Override
+ public void output(final CommandResponse response, final OutputStream os) {
+ try (final InputStream is = response.getInputStream()) {
+ IOUtils.copyBytes(is, os, 1024, true);
+ } catch (final IOException e) {
+ LOG.warn("Exception streaming out data to {}", clientIP, e);
+ }
+ }
+}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/package-info.java
similarity index 59%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/package-info.java
index 65cc10e0d3b..85e5a06e706 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/jetty-upgrade/zookeeper-with-patched-admin/src/main/java/org/apache/zookeeper/server/admin/package-info.java
@@ -16,30 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
-
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
-
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
-
- }
-
- @Override
- public String getBasePath() {
- return null;
- }
-
- @Override
- public ServletHolder getServletHolder() {
- return null;
- }
-
- @Override
- public void close() {
- // no-op
- }
-}
+/**
+ * Admin server for ZooKeeper that is using Jetty 12.
+ */
+package org.apache.zookeeper.server.admin;
diff --git a/managed-ledger/pom.xml b/managed-ledger/pom.xml
index eac0882acda..25dd4b6853e 100644
--- a/managed-ledger/pom.xml
+++ b/managed-ledger/pom.xml
@@ -36,23 +36,12 @@
<groupId>org.apache.bookkeeper</groupId>
<artifactId>bookkeeper-server</artifactId>
</dependency>
-
<dependency>
- <groupId>org.apache.bookkeeper.stats</groupId>
- <artifactId>prometheus-metrics-provider</artifactId>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>zookeeper-with-patched-admin</artifactId>
+ <version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.apache.bookkeeper.stats</groupId>
- <artifactId>codahale-metrics-provider</artifactId>
- <version>${bookkeeper.version}</version>
- <exclusions>
- <exclusion>
- <artifactId>amqp-client</artifactId>
- <groupId>com.rabbitmq</groupId>
- </exclusion>
- </exclusions>
- </dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
diff --git a/managed-ledger/src/test/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerTest.java b/managed-ledger/src/test/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerTest.java
index c5f75150495..b3201a6def5 100644
--- a/managed-ledger/src/test/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerTest.java
+++ b/managed-ledger/src/test/java/org/apache/bookkeeper/mledger/impl/ManagedLedgerTest.java
@@ -72,6 +72,7 @@ import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
@@ -153,7 +154,6 @@ import org.apache.pulsar.metadata.api.extended.SessionEvent;
import org.apache.pulsar.metadata.impl.FaultInjectionMetadataStore;
import org.awaitility.Awaitility;
import org.awaitility.reflect.WhiteboxImpl;
-import org.eclipse.jetty.util.BlockingArrayQueue;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
@@ -3274,8 +3274,8 @@ public class ManagedLedgerTest extends MockedBookKeeperTestCase {
private static class CollectCtxAddEntryCallback implements AddEntryCallback {
- public List<Object> addCompleteCtxList = new BlockingArrayQueue<>();
- public List<Object> addFailedCtxList = new BlockingArrayQueue<>();
+ public List<Object> addCompleteCtxList = new CopyOnWriteArrayList<>();
+ public List<Object> addFailedCtxList = new CopyOnWriteArrayList<>();
@Override
public void addComplete(Position position, ByteBuf entryData, Object ctx) {
diff --git a/pom.xml b/pom.xml
index 86ad3265bb1..635da2219b2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -186,7 +186,9 @@ flexible messaging model and an intuitive client API.</description>
<curator.version>5.7.1</curator.version>
<netty.version>4.1.132.Final</netty.version>
<netty-iouring.version>0.0.26.Final</netty-iouring.version>
- <jetty.version>9.4.58.v20250814</jetty.version>
+ <jetty.version>12.1.8</jetty.version>
+ <!-- Jetty 9 is used in tiered-storage/file-system tests and pulsar-io/alluxio -->
+ <jetty9.version>9.4.58.v20250814</jetty9.version>
<conscrypt.version>2.5.2</conscrypt.version>
<jersey.version>2.42</jersey.version>
<athenz.version>1.10.62</athenz.version>
@@ -199,6 +201,7 @@ flexible messaging model and an intuitive client API.</description>
<!-- bouncycastle dependencies aren't necessarily aligned -->
<bouncycastle.bcprov-jdk18on.version>1.78.1</bouncycastle.bcprov-jdk18on.version>
<bouncycastle.bcpkix-jdk18on.version>1.81</bouncycastle.bcpkix-jdk18on.version>
+ <bouncycastle.bcutil-jdk18on.version>1.81</bouncycastle.bcutil-jdk18on.version>
<bouncycastle.bcprov-ext-jdk18on.version>1.78.1</bouncycastle.bcprov-ext-jdk18on.version>
<bouncycastle.bcpkix-fips.version>2.0.10</bouncycastle.bcpkix-fips.version>
<bouncycastle.bc-fips.version>2.0.1</bouncycastle.bc-fips.version>
@@ -279,7 +282,7 @@ flexible messaging model and an intuitive client API.</description>
<jakarta.activation.version>1.2.2</jakarta.activation.version>
<jakarta.xml.bind.version>2.3.3</jakarta.xml.bind.version>
<jakarta.validation.version>2.0.2</jakarta.validation.version>
- <jna.version>5.12.1</jna.version>
+ <jna.version>5.18.1</jna.version>
<kubernetesclient.version>18.0.0</kubernetesclient.version>
<jose4j.version>0.9.6</jose4j.version>
<okhttp3.version>5.3.1</okhttp3.version>
@@ -553,6 +556,11 @@ flexible messaging model and an intuitive client API.</description>
<groupId>io.netty</groupId>
<artifactId>netty-*</artifactId>
</exclusion>
+ <!-- exclude zookeeper dependency, use patched version -->
+ <exclusion>
+ <groupId>org.apache.zookeeper</groupId>
+ <artifactId>zookeeper</artifactId>
+ </exclusion>
</exclusions>
</dependency>
@@ -719,12 +727,6 @@ flexible messaging model and an intuitive client API.</description>
</exclusions>
</dependency>
- <dependency>
- <groupId>org.apache.bookkeeper.stats</groupId>
- <artifactId>prometheus-metrics-provider</artifactId>
- <version>${bookkeeper.version}</version>
- </dependency>
-
<dependency>
<groupId>com.google.re2j</groupId>
<artifactId>re2j</artifactId>
@@ -749,18 +751,6 @@ flexible messaging model and an intuitive client API.</description>
<version>${jose4j.version}</version>
</dependency>
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-server</artifactId>
- <version>${jetty.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-alpn-conscrypt-server</artifactId>
- <version>${jetty.version}</version>
- </dependency>
-
<dependency>
<groupId>org.conscrypt</groupId>
<artifactId>conscrypt-openjdk-uber</artifactId>
@@ -768,17 +758,17 @@ flexible messaging model and an intuitive client API.</description>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-bom</artifactId>
- <version>${jetty.version}</version>
+ <groupId>io.netty</groupId>
+ <artifactId>netty-bom</artifactId>
+ <version>${netty.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
- <groupId>io.netty</groupId>
- <artifactId>netty-bom</artifactId>
- <version>${netty.version}</version>
+ <groupId>com.fasterxml.jackson</groupId>
+ <artifactId>jackson-bom</artifactId>
+ <version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -888,36 +878,6 @@ flexible messaging model and an intuitive client API.</description>
<version>${commons-codec.version}</version>
</dependency>
- <dependency>
- <groupId>org.glassfish.jersey.core</groupId>
- <artifactId>jersey-server</artifactId>
- <version>${jersey.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.glassfish.jersey.core</groupId>
- <artifactId>jersey-client</artifactId>
- <version>${jersey.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.glassfish.jersey.inject</groupId>
- <artifactId>jersey-hk2</artifactId>
- <version>${jersey.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.glassfish.jersey.containers</groupId>
- <artifactId>jersey-container-servlet-core</artifactId>
- <version>${jersey.version}</version>
- </dependency>
-
- <dependency>
- <groupId>org.glassfish.jersey.containers</groupId>
- <artifactId>jersey-container-servlet</artifactId>
- <version>${jersey.version}</version>
- </dependency>
-
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
@@ -925,30 +885,23 @@ flexible messaging model and an intuitive client API.</description>
</dependency>
<dependency>
- <groupId>org.glassfish.jersey.media</groupId>
- <artifactId>jersey-media-json-jackson</artifactId>
- <version>${jersey.version}</version>
- <exclusions>
- <exclusion>
- <groupId>jakarta.activation</groupId>
- <artifactId>jakarta.activation-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>javax.xml.bind</groupId>
- <artifactId>jaxb-api</artifactId>
- </exclusion>
- </exclusions>
+ <groupId>net.java.dev.jna</groupId>
+ <artifactId>jna</artifactId>
+ <version>${jna.version}</version>
</dependency>
-
<dependency>
- <groupId>org.glassfish.jersey.media</groupId>
- <artifactId>jersey-media-multipart</artifactId>
- <version>${jersey.version}</version>
+ <groupId>net.java.dev.jna</groupId>
+ <artifactId>jna-platform</artifactId>
+ <version>${jna.version}</version>
</dependency>
-
<dependency>
<groupId>net.java.dev.jna</groupId>
- <artifactId>jna</artifactId>
+ <artifactId>jna-jpms</artifactId>
+ <version>${jna.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>net.java.dev.jna</groupId>
+ <artifactId>jna-platform-jpms</artifactId>
<version>${jna.version}</version>
</dependency>
@@ -974,14 +927,6 @@ flexible messaging model and an intuitive client API.</description>
<version>${docker-java.version}</version>
</dependency>
- <dependency>
- <groupId>com.fasterxml.jackson</groupId>
- <artifactId>jackson-bom</artifactId>
- <version>${jackson.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
-
<dependency>
<groupId>it.unimi.dsi</groupId>
<artifactId>fastutil</artifactId>
@@ -1043,6 +988,12 @@ flexible messaging model and an intuitive client API.</description>
<version>${bouncycastle.bcpkix-jdk18on.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcutil-jdk18on</artifactId>
+ <version>${bouncycastle.bcutil-jdk18on.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk18on</artifactId>
@@ -1381,6 +1332,10 @@ flexible messaging model and an intuitive client API.</description>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</exclusion>
+ <exclusion>
+ <groupId>org.apache.zookeeper</groupId>
+ <artifactId>zookeeper</artifactId>
+ </exclusion>
</exclusions>
</dependency>
@@ -1767,6 +1722,45 @@ flexible messaging model and an intuitive client API.</description>
<artifactId>lz4-java</artifactId>
<version>${lz4java.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey</groupId>
+ <artifactId>jersey-bom</artifactId>
+ <version>${jersey.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ <version>${jersey.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>javax.activation</groupId>
+ <artifactId>javax.activation-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>javax.xml.bind</groupId>
+ <artifactId>jaxb-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-bom</artifactId>
+ <version>${jetty.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-bom</artifactId>
+ <version>${jetty.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
</dependencies>
</dependencyManagement>
@@ -2694,6 +2688,7 @@ flexible messaging model and an intuitive client API.</description>
<module>tests</module>
<module>microbench</module>
+ <module>jetty-upgrade</module>
</modules>
</profile>
@@ -2752,6 +2747,7 @@ flexible messaging model and an intuitive client API.</description>
<!-- package management releated modules (begin) -->
<module>pulsar-package-management</module>
<!-- package management releated modules (end) -->
+ <module>jetty-upgrade</module>
</modules>
</profile>
@@ -3071,6 +3067,24 @@ flexible messaging model and an intuitive client API.</description>
<test.netty.leakdetection.args></test.netty.leakdetection.args>
</properties>
</profile>
+ <profile>
+ <id>eclipse</id>
+ <activation>
+ <property>
+ <name>m2e.version</name>
+ </property>
+ </activation>
+ <dependencyManagement>
+ <dependencies>
+ <!-- don't exclude org.apache.zookeeper:zookeeper dependency when using Eclipse/VSCode -->
+ <dependency>
+ <groupId>org.apache.pulsar</groupId>
+ <artifactId>zookeeper-with-patched-admin</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+ </profile>
</profiles>
<repositories>
diff --git a/pulsar-broker-auth-athenz/pom.xml b/pulsar-broker-auth-athenz/pom.xml
index 2eb0a52d788..9557f3b92ae 100644
--- a/pulsar-broker-auth-athenz/pom.xml
+++ b/pulsar-broker-auth-athenz/pom.xml
@@ -68,7 +68,7 @@
<artifactId>modernizer-maven-plugin</artifactId>
<configuration>
<failOnViolations>true</failOnViolations>
- <javaVersion>8</javaVersion>
+ <javaVersion>17</javaVersion>
</configuration>
<executions>
<execution>
diff --git a/pulsar-broker-auth-oidc/pom.xml b/pulsar-broker-auth-oidc/pom.xml
index 08a5fa95de8..cb58ca6d0f8 100644
--- a/pulsar-broker-auth-oidc/pom.xml
+++ b/pulsar-broker-auth-oidc/pom.xml
@@ -118,7 +118,7 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
- <artifactId>wiremock-jre8</artifactId>
+ <artifactId>wiremock-jre8-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
diff --git a/pulsar-broker-common/pom.xml b/pulsar-broker-common/pom.xml
index 82484191a43..6a59e69aaef 100644
--- a/pulsar-broker-common/pom.xml
+++ b/pulsar-broker-common/pom.xml
@@ -45,11 +45,6 @@
<artifactId>guava</artifactId>
</dependency>
- <dependency>
- <groupId>io.prometheus</groupId>
- <artifactId>simpleclient_jetty</artifactId>
- </dependency>
-
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
@@ -75,6 +70,26 @@
<artifactId>jjwt-jackson</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty.compression</groupId>
+ <artifactId>jetty-compression-server</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty.compression</groupId>
+ <artifactId>jetty-compression-gzip</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-servlet</artifactId>
+ </dependency>
+
<!-- test -->
<dependency>
<groupId>org.bouncycastle</groupId>
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/GzipHandlerUtil.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/GzipHandlerUtil.java
index 9e980cecb79..a1e28c864b4 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/GzipHandlerUtil.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/GzipHandlerUtil.java
@@ -19,9 +19,11 @@
package org.apache.pulsar.broker.web;
import java.util.List;
+import org.eclipse.jetty.compression.gzip.GzipCompression;
+import org.eclipse.jetty.compression.server.CompressionConfig;
+import org.eclipse.jetty.compression.server.CompressionHandler;
import org.eclipse.jetty.http.pathmap.PathSpecSet;
import org.eclipse.jetty.server.Handler;
-import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.IncludeExclude;
public class GzipHandlerUtil {
@@ -32,10 +34,13 @@ public class GzipHandlerUtil {
wrappedHandler = innerHandler;
} else {
// add GZIP handler which is active when the request contains "Accept-Encoding: gzip" header
- GzipHandler gzipHandler = new GzipHandler();
+ CompressionHandler gzipHandler = new CompressionHandler();
+ gzipHandler.putCompression(new GzipCompression());
gzipHandler.setHandler(innerHandler);
if (gzipCompressionExcludedPaths != null && gzipCompressionExcludedPaths.size() > 0) {
- gzipHandler.setExcludedPaths(gzipCompressionExcludedPaths.toArray(new String[0]));
+ CompressionConfig.Builder compressConfigBuilder = CompressionConfig.builder().defaults();
+ gzipCompressionExcludedPaths.forEach(compressConfigBuilder::compressExcludePath);
+ gzipHandler.putConfiguration("/", compressConfigBuilder.build());
}
wrappedHandler = gzipHandler;
}
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/JettyRequestLogFactory.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/JettyRequestLogFactory.java
index fc88647eb49..b8a777dbe88 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/JettyRequestLogFactory.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/JettyRequestLogFactory.java
@@ -19,12 +19,14 @@
package org.apache.pulsar.broker.web;
import java.net.InetSocketAddress;
+import java.net.SocketAddress;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.server.ConnectionMetaData;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.ProxyConnectionFactory;
@@ -33,6 +35,7 @@ import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Slf4jRequestLogWriter;
+import org.eclipse.jetty.util.Attributes;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
@@ -128,14 +131,18 @@ public class JettyRequestLogFactory {
delegate.log(request, response);
StringBuilder sb = requestLogStringBuilder.get();
sb.append(" [R:");
- sb.append(request.getRemoteHost());
+ String remoteAddr = Request.getRemoteAddr(request);
+ sb.append(remoteAddr);
sb.append(':');
- sb.append(request.getRemotePort());
- InetSocketAddress realRemoteAddress = lookupRealAddress(request.getHttpChannel().getRemoteAddress());
+ int remotePort = Request.getRemotePort(request);
+ sb.append(remotePort);
+
+ InetSocketAddress realRemoteAddress =
+ lookupRealAddress(unwrap(request.getConnectionMetaData()).getRemoteSocketAddress());
if (realRemoteAddress != null) {
String realRemoteHost = HostPort.normalizeHost(realRemoteAddress.getHostString());
int realRemotePort = realRemoteAddress.getPort();
- if (!realRemoteHost.equals(request.getRemoteHost()) || realRemotePort != request.getRemotePort()) {
+ if (!realRemoteHost.equals(remoteAddr) || realRemotePort != remotePort) {
sb.append(" via ");
sb.append(realRemoteHost);
sb.append(':');
@@ -143,23 +150,26 @@ public class JettyRequestLogFactory {
}
}
sb.append("]->[L:");
- InetSocketAddress realLocalAddress = lookupRealAddress(request.getHttpChannel().getLocalAddress());
+ InetSocketAddress realLocalAddress = lookupRealAddress(unwrap(request.getConnectionMetaData())
+ .getLocalSocketAddress());
+ String localAddr = Request.getLocalAddr(request);
+ int localPort = Request.getLocalPort(request);
if (realLocalAddress != null) {
String realLocalHost = HostPort.normalizeHost(realLocalAddress.getHostString());
int realLocalPort = realLocalAddress.getPort();
sb.append(realLocalHost);
sb.append(':');
sb.append(realLocalPort);
- if (!realLocalHost.equals(request.getLocalAddr()) || realLocalPort != request.getLocalPort()) {
+ if (!realLocalHost.equals(localAddr) || realLocalPort != localPort) {
sb.append(" dst ");
- sb.append(request.getLocalAddr());
+ sb.append(localAddr);
sb.append(':');
- sb.append(request.getLocalPort());
+ sb.append(localPort);
}
} else {
- sb.append(request.getLocalAddr());
+ sb.append(localAddr);
sb.append(':');
- sb.append(request.getLocalPort());
+ sb.append(localPort);
}
sb.append(']');
try {
@@ -169,19 +179,27 @@ public class JettyRequestLogFactory {
}
}
- private InetSocketAddress lookupRealAddress(InetSocketAddress socketAddress) {
- if (socketAddress == null) {
+ private ConnectionMetaData unwrap(ConnectionMetaData connectionMetaData) {
+ if (connectionMetaData instanceof Attributes) {
+ return (ConnectionMetaData) Attributes.unwrap((Attributes) connectionMetaData);
+ }
+ return connectionMetaData;
+ }
+
+ private InetSocketAddress lookupRealAddress(SocketAddress socketAddress) {
+ if (socketAddress == null || !(socketAddress instanceof InetSocketAddress)) {
return null;
}
+ InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
if (proxyProtocolRealAddressMapping.isEmpty()) {
- return socketAddress;
+ return inetSocketAddress;
}
- AddressEntry entry = proxyProtocolRealAddressMapping.get(new AddressKey(socketAddress.getHostString(),
- socketAddress.getPort()));
+ AddressEntry entry = proxyProtocolRealAddressMapping.get(new AddressKey(inetSocketAddress.getHostString(),
+ inetSocketAddress.getPort()));
if (entry != null) {
return entry.realAddress;
} else {
- return socketAddress;
+ return inetSocketAddress;
}
}
@@ -221,8 +239,12 @@ public class JettyRequestLogFactory {
ProxyConnectionFactory.ProxyEndPoint proxyEndPoint =
(ProxyConnectionFactory.ProxyEndPoint) connection.getEndPoint();
EndPoint originalEndpoint = proxyEndPoint.unwrap();
- mapAddress(proxyEndPoint.getLocalAddress(), originalEndpoint.getLocalAddress(), increment);
- mapAddress(proxyEndPoint.getRemoteAddress(), originalEndpoint.getRemoteAddress(), increment);
+ mapAddress((InetSocketAddress) proxyEndPoint.getLocalSocketAddress(),
+ (InetSocketAddress) originalEndpoint.getLocalSocketAddress(),
+ increment);
+ mapAddress((InetSocketAddress) proxyEndPoint.getRemoteSocketAddress(),
+ (InetSocketAddress) originalEndpoint.getRemoteSocketAddress(),
+ increment);
}
}
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServlet.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServlet.java
index 1ecf4c4e539..6d817bfb9bd 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServlet.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServlet.java
@@ -20,13 +20,19 @@ package org.apache.pulsar.broker.web.plugin.servlet;
import com.google.common.annotations.Beta;
import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
/**
* The additional servlet interface for support additional servlet.
*/
@Beta
public interface AdditionalServlet extends AutoCloseable {
+ /**
+ * The servlet implementation type enum.
+ * Currently, only {@link AdditionalServletType#JAVAX_SERVLET} is supported.
+ */
+ enum AdditionalServletType {
+ JAVAX_SERVLET
+ }
/**
* load plugin config.
@@ -43,11 +49,24 @@ public interface AdditionalServlet extends AutoCloseable {
String getBasePath();
/**
- * Get the servlet holder.
+ * Get the servlet type that this implementation uses.
+ */
+ default AdditionalServletType getServletType() {
+ return AdditionalServletType.JAVAX_SERVLET;
+ }
+
+ /**
+ * Retrieves the servlet instance for this additional servlet implementation.
+ * <p>
+ * The returned object's type must be compatible with the servlet interface class
+ * specified by the {@link AdditionalServletType} returned from {@link #getServletType()}.
+ * For example, if {@link #getServletType()} returns {@link AdditionalServletType#JAVAX_SERVLET},
+ * the returned object must implement {@code javax.servlet.Servlet}.
+ * </p>
*
- * @return the servlet holder
+ * @return the servlet instance implementing the appropriate servlet interface
*/
- ServletHolder getServletHolder();
+ Object getServletInstance();
@Override
void close();
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoader.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoader.java
index bc1f25c5af9..7fc261ecb7b 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoader.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoader.java
@@ -24,7 +24,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.common.configuration.PulsarConfiguration;
import org.apache.pulsar.common.nar.NarClassLoader;
-import org.eclipse.jetty.servlet.ServletHolder;
/**
* An additional servlet with it's classloader.
@@ -60,11 +59,22 @@ public class AdditionalServletWithClassLoader implements AdditionalServlet {
}
@Override
- public ServletHolder getServletHolder() {
+ public AdditionalServletType getServletType() {
ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(classLoader);
- return servlet.getServletHolder();
+ return servlet.getServletType();
+ } finally {
+ Thread.currentThread().setContextClassLoader(prevClassLoader);
+ }
+ }
+
+ @Override
+ public Object getServletInstance() {
+ ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(classLoader);
+ return servlet.getServletInstance();
} finally {
Thread.currentThread().setContextClassLoader(prevClassLoader);
}
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/metrics/JettyStatisticsCollector.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/metrics/JettyStatisticsCollector.java
new file mode 100644
index 00000000000..c4facc31515
--- /dev/null
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/metrics/JettyStatisticsCollector.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.pulsar.jetty.metrics;
+
+import io.prometheus.client.Collector;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+
+/**
+ * Collect Prometheus metrics from jetty's org.eclipse.jetty.server.handler.StatisticsHandler.
+ *
+ * This is ported from prometheus client_java 0.16 version of JettyStatisticsCollector which is using Jetty 9.x.
+ * This supports Jetty 12.x.
+ */
+public class JettyStatisticsCollector extends Collector {
+ private final StatisticsHandler statisticsHandler;
+ private static final List<String> EMPTY_LIST = new ArrayList<String>();
+
+ public JettyStatisticsCollector(StatisticsHandler statisticsHandler) {
+ this.statisticsHandler = statisticsHandler;
+ }
+
+ @Override
+ public List<MetricFamilySamples> collect() {
+ return Arrays.asList(
+ buildCounter("jetty_requests_total", "Number of requests", statisticsHandler.getRequests()),
+ buildGauge("jetty_requests_active", "Number of requests currently active",
+ statisticsHandler.getRequestsActive()),
+ buildGauge("jetty_requests_active_max", "Maximum number of requests that have been active at once",
+ statisticsHandler.getRequestsActiveMax()),
+ buildGauge("jetty_request_time_max_seconds", "Maximum time spent handling requests",
+ statisticsHandler.getRequestTimeMax() / 1000_000_000.0),
+ buildCounter("jetty_request_time_seconds_total", "Total time spent in all request handling",
+ statisticsHandler.getRequestTimeTotal() / 1000_000_000.0),
+ buildCounter("jetty_dispatched_total", "Number of dispatches", statisticsHandler.getHandleTotal()),
+ buildGauge("jetty_dispatched_active", "Number of dispatches currently active",
+ statisticsHandler.getHandleActive()),
+ buildGauge("jetty_dispatched_active_max", "Maximum number of active dispatches being handled",
+ statisticsHandler.getHandleActiveMax()),
+ buildGauge("jetty_dispatched_time_max", "Maximum time spent in dispatch handling",
+ statisticsHandler.getHandleTimeMax() / 1000_000_000.0),
+ buildCounter("jetty_dispatched_time_seconds_total", "Total time spent in dispatch handling",
+ statisticsHandler.getHandleTimeTotal() / 1000_000_000.0),
+ buildStatusCounter(),
+ buildGauge("jetty_stats_seconds", "Time in seconds stats have been collected for",
+ statisticsHandler.getStatisticsDuration().toNanos() / 1000_000_000.0),
+ buildCounter("jetty_responses_bytes_total", "Total number of bytes across all responses",
+ statisticsHandler.getBytesRead() + statisticsHandler.getBytesWritten())
+ );
+ }
+
+ private static MetricFamilySamples buildGauge(String name, String help, double value) {
+ return new MetricFamilySamples(
+ name,
+ Type.GAUGE,
+ help,
+ Collections.singletonList(new MetricFamilySamples.Sample(name, EMPTY_LIST, EMPTY_LIST, value)));
+ }
+
+ private static MetricFamilySamples buildCounter(String name, String help, double value) {
+ return new MetricFamilySamples(
+ name,
+ Type.COUNTER,
+ help,
+ Collections.singletonList(new MetricFamilySamples.Sample(name, EMPTY_LIST, EMPTY_LIST, value)));
+ }
+
+ private MetricFamilySamples buildStatusCounter() {
+ String name = "jetty_responses_total";
+ return new MetricFamilySamples(
+ name,
+ Type.COUNTER,
+ "Number of requests with response status",
+ Arrays.asList(
+ buildStatusSample(name, "1xx", statisticsHandler.getResponses1xx()),
+ buildStatusSample(name, "2xx", statisticsHandler.getResponses2xx()),
+ buildStatusSample(name, "3xx", statisticsHandler.getResponses3xx()),
+ buildStatusSample(name, "4xx", statisticsHandler.getResponses4xx()),
+ buildStatusSample(name, "5xx", statisticsHandler.getResponses5xx())
+ )
+ );
+ }
+
+ private static MetricFamilySamples.Sample buildStatusSample(String name, String status, double value) {
+ return new MetricFamilySamples.Sample(
+ name,
+ Collections.singletonList("code"),
+ Collections.singletonList(status),
+ value);
+ }
+}
\ No newline at end of file
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/metrics/package-info.java
similarity index 59%
copy from pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
copy to pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/metrics/package-info.java
index 65cc10e0d3b..53cc77e93ea 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/metrics/package-info.java
@@ -16,30 +16,4 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.pulsar.broker.web.plugin.servlet;
-
-import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
-
-public class MockAdditionalServlet implements AdditionalServlet {
-
- @Override
- public void loadConfig(PulsarConfiguration pulsarConfiguration) {
-
- }
-
- @Override
- public String getBasePath() {
- return null;
- }
-
- @Override
- public ServletHolder getServletHolder() {
- return null;
- }
-
- @Override
- public void close() {
- // no-op
- }
-}
+package org.apache.pulsar.jetty.metrics;
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/tls/JettySslContextFactory.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/tls/JettySslContextFactory.java
index 0ac1b78ca99..f99df06b73c 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/tls/JettySslContextFactory.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/jetty/tls/JettySslContextFactory.java
@@ -68,6 +68,10 @@ public class JettySslContextFactory {
this.setWantClientAuth(true);
this.setTrustAll(true);
}
+
+ // https://jetty.org/docs/jetty/12.1/operations-guide/protocols/index.html#ssl-sni
+ // Set to false for backwards compatibility with Jetty 9.x
+ setSniRequired(false);
}
@Override
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java b/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
index 65cc10e0d3b..492b541d6f9 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
+++ b/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServlet.java
@@ -19,7 +19,6 @@
package org.apache.pulsar.broker.web.plugin.servlet;
import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
public class MockAdditionalServlet implements AdditionalServlet {
@@ -34,7 +33,7 @@ public class MockAdditionalServlet implements AdditionalServlet {
}
@Override
- public ServletHolder getServletHolder() {
+ public Object getServletInstance() {
return null;
}
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/jetty/tls/JettySslContextFactoryTest.java b/pulsar-broker-common/src/test/java/org/apache/pulsar/jetty/tls/JettySslContextFactoryTest.java
index 3a4664c9208..6d8f38fbb50 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/jetty/tls/JettySslContextFactoryTest.java
+++ b/pulsar-broker-common/src/test/java/org/apache/pulsar/jetty/tls/JettySslContextFactoryTest.java
@@ -66,7 +66,7 @@ public class JettySslContextFactoryTest {
PulsarSslFactory sslFactory = new DefaultPulsarSslFactory();
sslFactory.initialize(sslConfiguration);
sslFactory.createInternalSslContext();
- SslContextFactory factory = JettySslContextFactory.createSslContextFactory(null,
+ SslContextFactory.Server factory = JettySslContextFactory.createSslContextFactory(null,
sslFactory, true, null, null);
ServerConnector connector = new ServerConnector(server, factory);
@@ -109,7 +109,7 @@ public class JettySslContextFactoryTest {
PulsarSslFactory sslFactory = new DefaultPulsarSslFactory();
sslFactory.initialize(sslConfiguration);
sslFactory.createInternalSslContext();
- SslContextFactory factory = JettySslContextFactory.createSslContextFactory(null,
+ SslContextFactory.Server factory = JettySslContextFactory.createSslContextFactory(null,
sslFactory, true, null,
new HashSet<String>() {
{
@@ -162,7 +162,7 @@ public class JettySslContextFactoryTest {
PulsarSslFactory sslFactory = new DefaultPulsarSslFactory();
sslFactory.initialize(sslConfiguration);
sslFactory.createInternalSslContext();
- SslContextFactory factory = JettySslContextFactory.createSslContextFactory(null,
+ SslContextFactory.Server factory = JettySslContextFactory.createSslContextFactory(null,
sslFactory, true,
new HashSet<String>() {
{
diff --git a/pulsar-broker/pom.xml b/pulsar-broker/pom.xml
index 5fe1d553eb4..86e1e7c43c5 100644
--- a/pulsar-broker/pom.xml
+++ b/pulsar-broker/pom.xml
@@ -169,7 +169,7 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
- <artifactId>wiremock-jre8</artifactId>
+ <artifactId>wiremock-jre8-standalone</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>
@@ -187,6 +187,18 @@
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.websocket</groupId>
+ <artifactId>jetty-websocket-jetty-client</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-proxy</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+
<!-- zookeeper server -->
<dependency>
<groupId>io.dropwizard.metrics</groupId>
@@ -247,13 +259,13 @@
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlet</artifactId>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-servlet</artifactId>
</dependency>
<dependency>
- <groupId>org.eclipse.jetty</groupId>
- <artifactId>jetty-servlets</artifactId>
+ <groupId>org.eclipse.jetty.ee8</groupId>
+ <artifactId>jetty-ee8-servlets</artifactId>
</dependency>
<dependency>
@@ -280,14 +292,12 @@
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>
<scope>test</scope>
- <version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<scope>test</scope>
- <version>${jersey.version}</version>
</dependency>
<dependency>
@@ -352,11 +362,6 @@
<artifactId>simpleclient</artifactId>
</dependency>
- <dependency>
- <groupId>io.prometheus</groupId>
- <artifactId>simpleclient_jetty</artifactId>
- </dependency>
-
<dependency>
<groupId>io.prometheus</groupId>
<artifactId>simpleclient_hotspot</artifactId>
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java
index bf5dc8a04f6..d643ed8b6fe 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/PulsarService.java
@@ -62,8 +62,8 @@ import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Supplier;
+import javax.servlet.Servlet;
import javax.servlet.ServletException;
-import javax.websocket.DeploymentException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@@ -197,8 +197,8 @@ import org.apache.pulsar.websocket.WebSocketProducerServlet;
import org.apache.pulsar.websocket.WebSocketReaderServlet;
import org.apache.pulsar.websocket.WebSocketService;
import org.apache.pulsar.zookeeper.DefaultMetadataNodeSizeStats;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
+import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -1169,8 +1169,7 @@ public class PulsarService implements AutoCloseable, ShutdownService {
private void addWebServerHandlers(WebService webService,
PulsarPrometheusMetricsServlet metricsServlet,
ServiceConfiguration config)
- throws PulsarServerException, PulsarClientException, MalformedURLException, ServletException,
- DeploymentException {
+ throws PulsarServerException, PulsarClientException, MalformedURLException, ServletException {
Map<String, Object> attributeMap = new HashMap<>();
attributeMap.put(WebService.ATTRIBUTE_PULSAR_NAME, this);
@@ -1236,9 +1235,37 @@ public class PulsarService implements AutoCloseable, ShutdownService {
if (additionalServlet instanceof AdditionalServletWithPulsarService) {
((AdditionalServletWithPulsarService) additionalServlet).setPulsarService(this);
}
- webService.addServlet(servletWithClassLoader.getBasePath(), servletWithClassLoader.getServletHolder(),
- config.isAuthenticationEnabled(), attributeMap);
- LOG.info("Broker add additional servlet basePath {} ", servletWithClassLoader.getBasePath());
+ switch (servletWithClassLoader.getServletType()) {
+ case JAVAX_SERVLET -> {
+ Object servletInstance = servletWithClassLoader.getServletInstance();
+ if (!(servletInstance instanceof javax.servlet.Servlet)) {
+ LOG.error("AdditionalServletWithClassLoader {} has invalid servlet instance type {} which "
+ + "doesn't match {}. Skipping.", servletWithClassLoader,
+ servletInstance.getClass().getName(), servletWithClassLoader.getServletType());
+ try {
+ servletWithClassLoader.close();
+ } catch (Exception e) {
+ LOG.error("Failed to close servlet {}.", servletWithClassLoader, e);
+ }
+ continue;
+ }
+ ServletHolder servletHolder =
+ new ServletHolder((Servlet) servletInstance);
+ webService.addServlet(servletWithClassLoader.getBasePath(), servletHolder,
+ config.isAuthenticationEnabled(), attributeMap);
+ LOG.info("Broker add additional servlet basePath {} ", servletWithClassLoader.getBasePath());
+ }
+ default -> {
+ LOG.error("AdditionalServletWithClassLoader {} has unsupported servlet type {}. Skipping.",
+ servletWithClassLoader, servletWithClassLoader.getServletType());
+ try {
+ servletWithClassLoader.close();
+ } catch (Exception e) {
+ LOG.error("Failed to close servlet {}.", servletWithClassLoader, e);
+ }
+ continue;
+ }
+ }
}
}
}
@@ -1246,35 +1273,31 @@ public class PulsarService implements AutoCloseable, ShutdownService {
private void addWebSocketServiceHandler(WebService webService,
Map<String, Object> attributeMap,
ServiceConfiguration config)
- throws PulsarServerException, PulsarClientException, MalformedURLException, ServletException,
- DeploymentException {
+ throws PulsarServerException, PulsarClientException, MalformedURLException, ServletException {
if (config.isWebSocketServiceEnabled()) {
// Use local broker address to avoid different IP address when using a VIP for service discovery
this.webSocketService = new WebSocketService(null, config);
this.webSocketService.start();
- final WebSocketServlet producerWebSocketServlet = new WebSocketProducerServlet(webSocketService);
- webService.addServlet(WebSocketProducerServlet.SERVLET_PATH,
- new ServletHolder(producerWebSocketServlet), true, attributeMap);
- webService.addServlet(WebSocketProducerServlet.SERVLET_PATH_V2,
- new ServletHolder(producerWebSocketServlet), true, attributeMap);
-
- final WebSocketServlet consumerWebSocketServlet = new WebSocketConsumerServlet(webSocketService);
- webService.addServlet(WebSocketConsumerServlet.SERVLET_PATH,
- new ServletHolder(consumerWebSocketServlet), true, attributeMap);
- webService.addServlet(WebSocketConsumerServlet.SERVLET_PATH_V2,
- new ServletHolder(consumerWebSocketServlet), true, attributeMap);
-
- final WebSocketServlet readerWebSocketServlet = new WebSocketReaderServlet(webSocketService);
- webService.addServlet(WebSocketReaderServlet.SERVLET_PATH,
- new ServletHolder(readerWebSocketServlet), true, attributeMap);
- webService.addServlet(WebSocketReaderServlet.SERVLET_PATH_V2,
- new ServletHolder(readerWebSocketServlet), true, attributeMap);
-
- final WebSocketMultiTopicConsumerServlet multiTopicConsumerWebSocketServlet =
- new WebSocketMultiTopicConsumerServlet(webSocketService);
- webService.addServlet(WebSocketMultiTopicConsumerServlet.SERVLET_PATH,
- new ServletHolder(multiTopicConsumerWebSocketServlet), true, attributeMap);
+ addWebSocketServlet(new WebSocketProducerServlet(webSocketService), attributeMap,
+ WebSocketProducerServlet.SERVLET_PATH, WebSocketProducerServlet.SERVLET_PATH_V2);
+
+ addWebSocketServlet(new WebSocketConsumerServlet(webSocketService), attributeMap,
+ WebSocketConsumerServlet.SERVLET_PATH, WebSocketConsumerServlet.SERVLET_PATH_V2);
+
+ addWebSocketServlet(new WebSocketReaderServlet(webSocketService), attributeMap,
+ WebSocketReaderServlet.SERVLET_PATH,
+ WebSocketReaderServlet.SERVLET_PATH_V2);
+
+ addWebSocketServlet(new WebSocketMultiTopicConsumerServlet(webSocketService), attributeMap,
+ WebSocketMultiTopicConsumerServlet.SERVLET_PATH);
+ }
+ }
+
+ private void addWebSocketServlet(JettyWebSocketServlet webSocketServlet, Map<String, Object> attributeMap,
+ String... servletPaths) {
+ for (String servletPath : servletPaths) {
+ webService.addWebSocketServlet(servletPath, webSocketServlet, attributeMap);
}
}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/stats/prometheus/PulsarPrometheusMetricsServlet.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/stats/prometheus/PulsarPrometheusMetricsServlet.java
index faabad8f8fe..7593c669725 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/stats/prometheus/PulsarPrometheusMetricsServlet.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/stats/prometheus/PulsarPrometheusMetricsServlet.java
@@ -35,7 +35,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.broker.PulsarService;
-import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.ee8.nested.HttpOutput;
@Slf4j
public class PulsarPrometheusMetricsServlet extends PrometheusMetricsServlet {
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/ExceptionHandler.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/ExceptionHandler.java
index 205e02ed75a..adf6123d72c 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/ExceptionHandler.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/ExceptionHandler.java
@@ -19,8 +19,6 @@
package org.apache.pulsar.broker.web;
import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Response;
@@ -28,11 +26,6 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.common.intercept.InterceptException;
import org.apache.pulsar.common.policies.data.ErrorData;
import org.apache.pulsar.common.util.ObjectMapperFactory;
-import org.eclipse.jetty.http.HttpField;
-import org.eclipse.jetty.http.HttpFields;
-import org.eclipse.jetty.http.HttpHeader;
-import org.eclipse.jetty.http.HttpVersion;
-import org.eclipse.jetty.http.MetaData;
/**
* Exception handler for handle exception.
@@ -41,29 +34,17 @@ import org.eclipse.jetty.http.MetaData;
public class ExceptionHandler {
public void handle(ServletResponse response, Exception ex) throws IOException {
+ HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (ex instanceof InterceptException) {
- if (response instanceof org.eclipse.jetty.server.Response) {
- String errorData = ObjectMapperFactory
- .getMapper().writer().writeValueAsString(new ErrorData(ex.getMessage()));
- byte[] errorBytes = errorData.getBytes(StandardCharsets.UTF_8);
- int errorCode = ((InterceptException) ex).getErrorCode();
- HttpFields httpFields = new HttpFields();
- HttpField httpField = new HttpField(HttpHeader.CONTENT_TYPE, "application/json;charset=utf-8");
- httpFields.add(httpField);
- MetaData.Response info = new MetaData.Response(HttpVersion.HTTP_1_1, errorCode, httpFields);
- info.setHttpVersion(HttpVersion.HTTP_1_1);
- info.setReason(errorData);
- info.setStatus(errorCode);
- info.setContentLength(errorBytes.length);
- ((org.eclipse.jetty.server.Response) response).getHttpChannel().sendResponse(info,
- ByteBuffer.wrap(errorBytes),
- true);
- } else {
- ((HttpServletResponse) response).sendError(((InterceptException) ex).getErrorCode(),
- ex.getMessage());
- }
+ byte[] errorBytes = ObjectMapperFactory
+ .getMapper().writer().writeValueAsBytes(new ErrorData(ex.getMessage()));
+ int errorCode = ((InterceptException) ex).getErrorCode();
+ httpServletResponse.setStatus(errorCode);
+ httpServletResponse.setContentType("application/json;charset=utf-8");
+ httpServletResponse.setContentLength(errorBytes.length);
+ httpServletResponse.getOutputStream().write(errorBytes);
} else {
- ((HttpServletResponse) response).sendError(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
+ httpServletResponse.sendError(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
ex.getMessage());
}
}
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java
index 7fa954948a8..21c99f8196f 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/broker/web/WebService.java
@@ -19,7 +19,6 @@
package org.apache.pulsar.broker.web;
import io.prometheus.client.CollectorRegistry;
-import io.prometheus.client.jetty.JettyStatisticsCollector;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
@@ -45,31 +44,33 @@ import org.apache.pulsar.broker.intercept.BrokerInterceptor;
import org.apache.pulsar.broker.intercept.BrokerInterceptors;
import org.apache.pulsar.common.util.PulsarSslConfiguration;
import org.apache.pulsar.common.util.PulsarSslFactory;
+import org.apache.pulsar.jetty.metrics.JettyStatisticsCollector;
import org.apache.pulsar.jetty.tls.JettySslContextFactory;
+import org.eclipse.jetty.ee8.nested.ContextHandler;
+import org.eclipse.jetty.ee8.nested.ResourceHandler;
+import org.eclipse.jetty.ee8.servlet.FilterHolder;
+import org.eclipse.jetty.ee8.servlet.ServletContextHandler;
+import org.eclipse.jetty.ee8.servlet.ServletHolder;
+import org.eclipse.jetty.ee8.websocket.server.JettyWebSocketServlet;
+import org.eclipse.jetty.ee8.websocket.server.config.JettyWebSocketServletContainerInitializer;
+import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.server.ConnectionFactory;
-import org.eclipse.jetty.server.ConnectionLimit;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.NetworkConnectionLimit;
import org.eclipse.jetty.server.ProxyConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
-import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.DefaultHandler;
-import org.eclipse.jetty.server.handler.HandlerCollection;
-import org.eclipse.jetty.server.handler.RequestLogHandler;
-import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.server.handler.QoSHandler;
import org.eclipse.jetty.server.handler.StatisticsHandler;
-import org.eclipse.jetty.servlet.FilterHolder;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.servlets.QoSFilter;
-import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
@@ -124,12 +125,14 @@ public class WebService implements AutoCloseable {
new WebExecutorThreadPoolStats(pulsar.getOpenTelemetry().getMeter(), webServiceExecutor);
this.server = new Server(webServiceExecutor);
if (config.getMaxHttpServerConnections() > 0) {
- server.addBean(new ConnectionLimit(config.getMaxHttpServerConnections(), server));
+ server.addBean(new NetworkConnectionLimit(config.getMaxHttpServerConnections(), server));
}
+ server.setStopTimeout(config.getBrokerShutdownTimeoutMs());
List<ServerConnector> connectors = new ArrayList<>();
Optional<Integer> port = config.getWebServicePort();
HttpConfiguration httpConfig = new HttpConfiguration();
+ httpConfig.setUriCompliance(UriCompliance.LEGACY);
if (config.isWebServiceTrustXForwardedFor()) {
httpConfig.addCustomizer(new ForwardedRequestCustomizer());
}
@@ -165,7 +168,7 @@ public class WebService implements AutoCloseable {
config.getTlsCertRefreshCheckDurationSec(),
TimeUnit.SECONDS);
}
- SslContextFactory sslCtxFactory =
+ SslContextFactory.Server sslCtxFactory =
JettySslContextFactory.createSslContextFactory(config.getWebServiceTlsProvider(),
this.sslFactory, config.isTlsRequireTrustedClientCertOnConnect(),
config.getTlsCiphers(), config.getTlsProtocols());
@@ -178,7 +181,9 @@ public class WebService implements AutoCloseable {
// org.eclipse.jetty.server.AbstractConnectionFactory.getFactories contains similar logic
// this is needed for TLS authentication
if (httpConfig.getCustomizer(SecureRequestCustomizer.class) == null) {
- httpConfig.addCustomizer(new SecureRequestCustomizer());
+ // disable SNI host check for backwards compatibility with Jetty 9.x
+ boolean sniHostCheck = false;
+ httpConfig.addCustomizer(new SecureRequestCustomizer(sniHostCheck));
}
httpsConnector = new ServerConnector(server, connectionFactories.toArray(new ConnectionFactory[0]));
httpsConnector.setPort(tlsPort.get());
@@ -242,12 +247,6 @@ public class WebService implements AutoCloseable {
FilterInitializer(PulsarService pulsarService) {
ServiceConfiguration config = pulsarService.getConfiguration();
- if (config.getMaxConcurrentHttpRequests() > 0) {
- FilterHolder filterHolder = new FilterHolder(QoSFilter.class);
- filterHolder.setInitParameter("maxRequests", String.valueOf(config.getMaxConcurrentHttpRequests()));
- filterHolders.add(filterHolder);
- }
-
if (config.isHttpRequestsLimitEnabled()) {
filterHolders.add(new FilterHolder(
new RateLimitingFilter(config.getHttpRequestsMaxPerSecond(),
@@ -358,39 +357,47 @@ public class WebService implements AutoCloseable {
}
filterInitializer.addFilters(servletContextHandler, requiresAuthentication);
- handlers.add(servletContextHandler);
+ handlers.add(servletContextHandler.get());
+ }
+
+ public void addWebSocketServlet(String path, JettyWebSocketServlet webSocketServlet,
+ Map<String, Object> attributeMap) {
+ ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
+ servletContextHandler.setContextPath(path);
+ JettyWebSocketServletContainerInitializer.configure(servletContextHandler, null);
+ ServletHolder servletHolder = new ServletHolder(webSocketServlet);
+ servletContextHandler.addServlet(servletHolder, MATCH_ALL);
+ handlers.add(servletContextHandler.get());
}
public void addStaticResources(String basePath, String resourcePath) {
ContextHandler capHandler = new ContextHandler();
capHandler.setContextPath(basePath);
ResourceHandler resHandler = new ResourceHandler();
- resHandler.setBaseResource(Resource.newClassPathResource(resourcePath));
+ ResourceFactory resourceFactory = ResourceFactory.root();
+ resHandler.setBaseResource(resourceFactory.newClassLoaderResource(resourcePath, true));
resHandler.setEtags(true);
resHandler.setCacheControl(WebService.HANDLER_CACHE_CONTROL);
capHandler.setHandler(resHandler);
- handlers.add(capHandler);
+ handlers.add(capHandler.get());
}
public void start() throws PulsarServerException {
try {
- RequestLogHandler requestLogHandler = new RequestLogHandler();
boolean showDetailedAddresses = pulsar.getConfiguration().getWebServiceLogDetailedAddresses() != null
? pulsar.getConfiguration().getWebServiceLogDetailedAddresses() :
(pulsar.getConfiguration().isWebServiceHaProxyProtocolEnabled()
|| pulsar.getConfiguration().isWebServiceTrustXForwardedFor());
RequestLog requestLogger = JettyRequestLogFactory.createRequestLogger(showDetailedAddresses, server);
- requestLogHandler.setRequestLog(requestLogger);
- handlers.add(0, new ContextHandlerCollection());
- handlers.add(requestLogHandler);
+ server.setRequestLog(requestLogger);
ContextHandlerCollection contexts = new ContextHandlerCollection();
- contexts.setHandlers(handlers.toArray(new Handler[handlers.size()]));
+ contexts.setHandlers(handlers);
Handler handlerForContexts = GzipHandlerUtil.wrapWithGzipHandler(contexts,
pulsar.getConfig().getHttpServerGzipCompressionExcludedPaths());
- HandlerCollection handlerCollection = new HandlerCollection();
- handlerCollection.setHandlers(new Handler[] {handlerForContexts, new DefaultHandler(), requestLogHandler});
+ Handler.Collection handlerCollection = new Handler.Sequence();
+ handlerCollection.setHandlers(handlerForContexts, new DefaultHandler());
// Metrics handler
StatisticsHandler stats = new StatisticsHandler();
@@ -402,7 +409,14 @@ public class WebService implements AutoCloseable {
// Already registered. Eg: in unit tests
}
- server.setHandler(stats);
+ Handler serverHandler = stats;
+ if (pulsar.getConfiguration().getMaxConcurrentHttpRequests() > 0) {
+ QoSHandler qoSHandler = new QoSHandler(serverHandler);
+ qoSHandler.setMaxRequestCount(pulsar.getConfiguration().getMaxConcurrentHttpRequests());
+ serverHandler = qoSHandler;
+ }
+ server.setHandler(serverHandler);
+
server.start();
if (httpConnector != null) {
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/PrometheusMetricsTestUtil.java b/pulsar-broker/src/test/java/org/apache/pulsar/PrometheusMetricsTestUtil.java
index 68826372b7b..50663af4f4f 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/PrometheusMetricsTestUtil.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/PrometheusMetricsTestUtil.java
@@ -32,7 +32,7 @@ import java.util.concurrent.TimeoutException;
import org.apache.pulsar.broker.PulsarService;
import org.apache.pulsar.broker.stats.prometheus.PrometheusMetricsGenerator;
import org.apache.pulsar.broker.stats.prometheus.PrometheusRawMetricsProvider;
-import org.eclipse.jetty.server.HttpOutput;
+import org.eclipse.jetty.ee8.nested.HttpOutput;
public class PrometheusMetricsTestUtil {
public static void generate(PulsarService pulsar, boolean includeTopicMetrics, boolean includeConsumerMetrics,
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/BrokerAdditionalServletTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/BrokerAdditionalServletTest.java
index dc21a6a6933..3457be30107 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/BrokerAdditionalServletTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/BrokerAdditionalServletTest.java
@@ -37,8 +37,7 @@ import org.apache.pulsar.broker.web.plugin.servlet.AdditionalServletWithClassLoa
import org.apache.pulsar.broker.web.plugin.servlet.AdditionalServletWithPulsarService;
import org.apache.pulsar.broker.web.plugin.servlet.AdditionalServlets;
import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.ee8.nested.Request;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
@@ -75,7 +74,9 @@ public class BrokerAdditionalServletTest extends MockedPulsarServiceBaseTest {
AdditionalServlet brokerAdditionalServlet = Mockito.mock(AdditionalServlet.class);
Mockito.when(brokerAdditionalServlet.getBasePath()).thenReturn(BASE_PATH);
- Mockito.when(brokerAdditionalServlet.getServletHolder()).thenReturn(new ServletHolder(servlet));
+ Mockito.when(brokerAdditionalServlet.getServletInstance()).thenReturn(servlet);
+ Mockito.when(brokerAdditionalServlet.getServletType())
+ .thenReturn(AdditionalServlet.AdditionalServletType.JAVAX_SERVLET);
AdditionalServletWithPulsarService brokerAdditionalServletWithPulsarService =
new AdditionalServletWithPulsarService() {
@@ -96,8 +97,8 @@ public class BrokerAdditionalServletTest extends MockedPulsarServiceBaseTest {
}
@Override
- public ServletHolder getServletHolder() {
- return new ServletHolder(new WithPulsarServiceServlet(pulsarService));
+ public Object getServletInstance() {
+ return new WithPulsarServiceServlet(pulsarService);
}
@Override
@@ -146,7 +147,7 @@ public class BrokerAdditionalServletTest extends MockedPulsarServiceBaseTest {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException,
IOException {
- log.info("[service] path: {}", ((Request) servletRequest).getOriginalURI());
+ log.info("[service] path: {}", ((Request) servletRequest).getHttpURI());
String value = servletRequest.getParameterMap().get(QUERY_PARAM)[0];
ServletOutputStream servletOutputStream = servletResponse.getOutputStream();
servletResponse.setContentLength(value.getBytes().length);
@@ -177,7 +178,7 @@ public class BrokerAdditionalServletTest extends MockedPulsarServiceBaseTest {
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException,
IOException {
- log.info("[service] path: {}", ((Request) servletRequest).getOriginalURI());
+ log.info("[service] path: {}", ((Request) servletRequest).getHttpURI());
String value = pulsarService == null ? "null" : PulsarService.class.getName();
ServletOutputStream servletOutputStream = servletResponse.getOutputStream();
servletResponse.setContentLength(value.getBytes().length);
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiGetLastMessageIdTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiGetLastMessageIdTest.java
index 1ec8f160dfb..3a99dbc251f 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiGetLastMessageIdTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminApiGetLastMessageIdTest.java
@@ -29,6 +29,7 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
+import javax.servlet.ServletContext;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.TimeoutHandler;
import org.apache.pulsar.broker.admin.v2.PersistentTopics;
@@ -69,7 +70,7 @@ public class AdminApiGetLastMessageIdTest extends MockedPulsarServiceBaseTest {
admin.namespaces().createNamespace("prop/ns-abc");
admin.namespaces().setNamespaceReplicationClusters("prop/ns-abc", Set.of("test"));
persistentTopics = spy(PersistentTopics.class);
- persistentTopics.setServletContext(new MockServletContext());
+ persistentTopics.setServletContext(mock(ServletContext.class));
persistentTopics.setPulsar(pulsar);
doReturn(false).when(persistentTopics).isRequestHttps();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java
index dbff93319a0..61b255e830d 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/AdminTest.java
@@ -45,6 +45,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import javax.servlet.ServletContext;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
@@ -137,7 +138,7 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
doNothing().when(properties).validateSuperUserAccess();
namespaces = spy(Namespaces.class);
- namespaces.setServletContext(new MockServletContext());
+ namespaces.setServletContext(mock(ServletContext.class));
namespaces.setPulsar(pulsar);
doReturn("test").when(namespaces).clientAppId();
doReturn(Set.of("use", "usw", "usc", "global")).when(namespaces).clusters();
@@ -154,7 +155,7 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
uriField.setAccessible(true);
persistentTopics = spy(PersistentTopics.class);
- persistentTopics.setServletContext(new MockServletContext());
+ persistentTopics.setServletContext(mock(ServletContext.class));
persistentTopics.setPulsar(pulsar);
doReturn("test").when(persistentTopics).clientAppId();
doReturn("persistent").when(persistentTopics).domain();
@@ -164,11 +165,11 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
doNothing().when(persistentTopics).validateAdminAccessForTenant("prop-xyz");
resourceQuotas = spy(ResourceQuotas.class);
- resourceQuotas.setServletContext(new MockServletContext());
+ resourceQuotas.setServletContext(mock(ServletContext.class));
resourceQuotas.setPulsar(pulsar);
brokerStats = spy(BrokerStats.class);
- brokerStats.setServletContext(new MockServletContext());
+ brokerStats.setServletContext(mock(ServletContext.class));
brokerStats.setPulsar(pulsar);
doReturn(false).when(persistentTopics).isRequestHttps();
@@ -177,7 +178,7 @@ public class AdminTest extends MockedPulsarServiceBaseTest {
doReturn(mock(AuthenticationDataHttps.class)).when(persistentTopics).clientAuthData();
schemasResource = spy(SchemasResource.class);
- schemasResource.setServletContext(new MockServletContext());
+ schemasResource.setServletContext(mock(ServletContext.class));
schemasResource.setPulsar(pulsar);
}
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/MockServletContext.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/MockServletContext.java
deleted file mode 100644
index 7ae63c500df..00000000000
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/MockServletContext.java
+++ /dev/null
@@ -1,279 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.pulsar.broker.admin;
-
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.util.Enumeration;
-import java.util.EventListener;
-import java.util.Map;
-import java.util.Set;
-import javax.servlet.Filter;
-import javax.servlet.FilterRegistration;
-import javax.servlet.RequestDispatcher;
-import javax.servlet.Servlet;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRegistration;
-import javax.servlet.ServletRegistration.Dynamic;
-import javax.servlet.SessionCookieConfig;
-import javax.servlet.SessionTrackingMode;
-import javax.servlet.descriptor.JspConfigDescriptor;
-import org.eclipse.jetty.util.AttributesMap;
-
-public class MockServletContext extends AttributesMap implements ServletContext {
-
- @Override
- public String getContextPath() {
- return null;
- }
-
- @Override
- public ServletContext getContext(String uripath) {
- return null;
- }
-
- @Override
- public int getMajorVersion() {
- return 0;
- }
-
- @Override
- public int getMinorVersion() {
- return 0;
- }
-
- @Override
- public int getEffectiveMajorVersion() {
- return 0;
- }
-
- @Override
- public int getEffectiveMinorVersion() {
- return 0;
- }
-
- @Override
- public String getMimeType(String file) {
- return null;
- }
-
- @Override
- public Set<String> getResourcePaths(String path) {
- return null;
- }
-
- @Override
- public URL getResource(String path) throws MalformedURLException {
- return null;
- }
-
- @Override
- public InputStream getResourceAsStream(String path) {
- return null;
- }
-
- @Override
- public RequestDispatcher getRequestDispatcher(String path) {
- return null;
- }
-
- @Override
- public RequestDispatcher getNamedDispatcher(String name) {
- return null;
- }
-
- @Override
- @Deprecated
- public Servlet getServlet(String name) throws ServletException {
- return null;
- }
-
- @Override
- @Deprecated
- public Enumeration<Servlet> getServlets() {
- return null;
- }
-
- @Override
- @Deprecated
- public Enumeration<String> getServletNames() {
- return null;
- }
-
- @Override
- public void log(String msg) {
- }
-
- @Override
- @Deprecated
- public void log(Exception exception, String msg) {
- }
-
- @Override
- public void log(String message, Throwable throwable) {
-
- }
-
- @Override
- public String getRealPath(String path) {
- return null;
- }
-
- @Override
- public String getServerInfo() {
- return null;
- }
-
- @Override
- public String getInitParameter(String name) {
- return null;
- }
-
- @Override
- public Enumeration<String> getInitParameterNames() {
- return null;
- }
-
- @Override
- public boolean setInitParameter(String name, String value) {
- return false;
- }
-
- @Override
- public String getServletContextName() {
- return null;
- }
-
- @Override
- public Dynamic addServlet(String servletName, String className) {
- return null;
- }
-
- @Override
- public Dynamic addServlet(String servletName, Servlet servlet) {
- return null;
- }
-
- @Override
- public Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) {
- return null;
- }
-
- @Override
- public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException {
- return null;
- }
-
- @Override
- public ServletRegistration getServletRegistration(String servletName) {
- return null;
- }
-
- @Override
- public Map<String, ? extends ServletRegistration> getServletRegistrations() {
- return null;
- }
-
- @Override
- public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, String className) {
- return null;
- }
-
- @Override
- public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Filter filter) {
- return null;
- }
-
- @Override
- public javax.servlet.FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) {
- return null;
- }
-
- @Override
- public <T extends Filter> T createFilter(Class<T> clazz) throws ServletException {
- return null;
- }
-
- @Override
- public FilterRegistration getFilterRegistration(String filterName) {
- return null;
- }
-
- @Override
- public Map<String, ? extends FilterRegistration> getFilterRegistrations() {
- return null;
- }
-
- @Override
- public SessionCookieConfig getSessionCookieConfig() {
- return null;
- }
-
- @Override
- public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes) {
- }
-
- @Override
- public Set<SessionTrackingMode> getDefaultSessionTrackingModes() {
- return null;
- }
-
- @Override
- public Set<SessionTrackingMode> getEffectiveSessionTrackingModes() {
- return null;
- }
-
- @Override
- public void addListener(String className) {
- }
-
- @Override
- public <T extends EventListener> void addListener(T t) {
- }
-
- @Override
- public void addListener(Class<? extends EventListener> listenerClass) {
- }
-
- @Override
- public <T extends EventListener> T createListener(Class<T> clazz) throws ServletException {
- return null;
- }
-
- @Override
- public JspConfigDescriptor getJspConfigDescriptor() {
- return null;
- }
-
- @Override
- public ClassLoader getClassLoader() {
- return null;
- }
-
- @Override
- public void declareRoles(String... roleNames) {
- }
-
- @Override
- public String getVirtualServerName() {
- return null;
- }
-}
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java
index 2f5b99bfd9d..da3ced47efc 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesTest.java
@@ -57,6 +57,7 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import javax.servlet.ServletContext;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.WebApplicationException;
@@ -211,7 +212,7 @@ public class NamespacesTest extends MockedPulsarServiceBaseTest {
super.internalSetup();
namespaces = spy(Namespaces.class);
- namespaces.setServletContext(new MockServletContext());
+ namespaces.setServletContext(mock(ServletContext.class));
namespaces.setPulsar(pulsar);
doReturn(false).when(namespaces).isRequestHttps();
doReturn("test").when(namespaces).clientAppId();
@@ -1230,7 +1231,7 @@ public class NamespacesTest extends MockedPulsarServiceBaseTest {
ownership.set(pulsar.getNamespaceService(), mockOwnershipCache);
TopicName topicName = TopicName.get(testNs.getPersistentTopicName("my-topic"));
PersistentTopics topics = spy(PersistentTopics.class);
- topics.setServletContext(new MockServletContext());
+ topics.setServletContext(mock(ServletContext.class));
topics.setPulsar(pulsar);
doReturn(false).when(topics).isRequestHttps();
doReturn("test").when(topics).clientAppId();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesV2Test.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesV2Test.java
index 44523a16435..a4320216089 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesV2Test.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/NamespacesV2Test.java
@@ -33,6 +33,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import javax.servlet.ServletContext;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.pulsar.broker.admin.v2.Namespaces;
@@ -96,7 +97,7 @@ public class NamespacesV2Test extends MockedPulsarServiceBaseTest {
super.internalSetup();
namespaces = spy(Namespaces.class);
- namespaces.setServletContext(new MockServletContext());
+ namespaces.setServletContext(mock(ServletContext.class));
namespaces.setPulsar(pulsar);
doReturn(false).when(namespaces).isRequestHttps();
doReturn("test").when(namespaces).clientAppId();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java
index a41fad63cf3..40df6b0b5ff 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/PersistentTopicsTest.java
@@ -48,6 +48,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
+import javax.servlet.ServletContext;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.AsyncResponse;
@@ -149,7 +150,7 @@ public class PersistentTopicsTest extends MockedPulsarServiceBaseTest {
conf.setTopicLevelPoliciesEnabled(false);
super.internalSetup();
persistentTopics = spy(PersistentTopics.class);
- persistentTopics.setServletContext(new MockServletContext());
+ persistentTopics.setServletContext(mock(ServletContext.class));
persistentTopics.setPulsar(pulsar);
doReturn(false).when(persistentTopics).isRequestHttps();
doReturn(null).when(persistentTopics).originalPrincipal();
@@ -159,7 +160,7 @@ public class PersistentTopicsTest extends MockedPulsarServiceBaseTest {
doReturn(mock(AuthenticationDataHttps.class)).when(persistentTopics).clientAuthData();
extPersistentTopics = spy(ExtPersistentTopics.class);
- extPersistentTopics.setServletContext(new MockServletContext());
+ extPersistentTopics.setServletContext(mock(ServletContext.class));
extPersistentTopics.setPulsar(pulsar);
doReturn(false).when(extPersistentTopics).isRequestHttps();
doReturn(null).when(extPersistentTopics).originalPrincipal();
@@ -169,7 +170,7 @@ public class PersistentTopicsTest extends MockedPulsarServiceBaseTest {
doReturn(mock(AuthenticationDataHttps.class)).when(extPersistentTopics).clientAuthData();
nonPersistentTopic = spy(NonPersistentTopics.class);
- nonPersistentTopic.setServletContext(new MockServletContext());
+ nonPersistentTopic.setServletContext(mock(ServletContext.class));
nonPersistentTopic.setPulsar(pulsar);
namespaceResources = mock(NamespaceResources.class);
doReturn(false).when(nonPersistentTopic).isRequestHttps();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/ResourceGroupsTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/ResourceGroupsTest.java
index b8b4f0ea160..c1fa24ae875 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/ResourceGroupsTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/ResourceGroupsTest.java
@@ -19,6 +19,7 @@
package org.apache.pulsar.broker.admin;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;
@@ -27,6 +28,7 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
+import javax.servlet.ServletContext;
import org.apache.pulsar.broker.admin.v2.ResourceGroups;
import org.apache.pulsar.broker.auth.MockedPulsarServiceBaseTest;
import org.apache.pulsar.broker.web.RestException;
@@ -51,7 +53,7 @@ public class ResourceGroupsTest extends MockedPulsarServiceBaseTest {
protected void setup() throws Exception {
super.internalSetup();
resourcegroups = spy(ResourceGroups.class);
- resourcegroups.setServletContext(new MockServletContext());
+ resourcegroups.setServletContext(mock(ServletContext.class));
resourcegroups.setPulsar(pulsar);
doReturn(false).when(resourcegroups).isRequestHttps();
doReturn("test").when(resourcegroups).clientAppId();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/TopicPoliciesTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/TopicPoliciesTest.java
index a5edcfa5830..c5537897e7b 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/TopicPoliciesTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/admin/TopicPoliciesTest.java
@@ -29,6 +29,9 @@ import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
+import com.fasterxml.jackson.databind.ObjectReader;
+import java.io.IOException;
+import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -45,6 +48,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
@@ -87,6 +91,7 @@ import org.apache.pulsar.common.policies.data.BacklogQuota;
import org.apache.pulsar.common.policies.data.ClusterData;
import org.apache.pulsar.common.policies.data.DelayedDeliveryPolicies;
import org.apache.pulsar.common.policies.data.DispatchRate;
+import org.apache.pulsar.common.policies.data.ErrorData;
import org.apache.pulsar.common.policies.data.HierarchyTopicPolicies;
import org.apache.pulsar.common.policies.data.InactiveTopicDeleteMode;
import org.apache.pulsar.common.policies.data.InactiveTopicPolicies;
@@ -104,6 +109,7 @@ import org.apache.pulsar.common.policies.data.TopicStats;
import org.apache.pulsar.common.policies.data.TopicType;
import org.apache.pulsar.common.policies.data.impl.DispatchRateImpl;
import org.apache.pulsar.common.util.FutureUtil;
+import org.apache.pulsar.common.util.ObjectMapperFactory;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import org.awaitility.reflect.WhiteboxImpl;
@@ -4043,18 +4049,37 @@ public class TopicPoliciesTest extends MockedPulsarServiceBaseTest {
.header("Content-Type", "application/json")
.post(Entity.json(backlogQuotaWithNonPolicy));
assertEquals(response1.getStatus(), 400);
- assertTrue(response1.getStatusInfo().getReasonPhrase().contains("policy cannot be null"));
+ assertTrue(resolveErrorReason(response1).contains("policy cannot be null"));
// Topic level.
Response response2 = httpClient.target(hostAndPort).path(topicPath).request()
.header("Content-Type", "application/json")
.post(Entity.json(backlogQuotaWithNonPolicy));
assertEquals(response2.getStatus(), 400);
- assertTrue(response2.getStatusInfo().getReasonPhrase().contains("policy cannot be null"));
+ assertTrue(resolveErrorReason(response2).contains("policy cannot be null"));
// cleanup.
httpClient.close();
}
+ String resolveErrorReason(Response response) {
+ ErrorData errorData = null;
+ if (response.hasEntity() && response.getMediaType().isCompatible(MediaType.APPLICATION_JSON_TYPE)) {
+ Object responseBody = response.getEntity();
+ ObjectReader reader =
+ ObjectMapperFactory.getMapper().getObjectMapper().reader();
+ try {
+ errorData = reader.readValue((InputStream) responseBody, ErrorData.class);
+ } catch (IOException e) {
+ // ignore
+ if (log.isDebugEnabled()) {
+ log.debug("Failed to parse error response: {}", response);
+ }
+ }
+ }
+ return errorData != null ? errorData.reason : response.getStatusInfo().getReasonPhrase();
+ }
+
+
@Test
public void testSetSubRateWithSub() throws Exception {
String topic = "persistent://" + myNamespace + "/testSetSubRateWithSub";
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/BrokerInterceptorTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/BrokerInterceptorTest.java
index 65fb7f398b3..72f1c4f5009 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/BrokerInterceptorTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/BrokerInterceptorTest.java
@@ -49,6 +49,7 @@ import org.apache.pulsar.common.nar.NarClassLoader;
import org.apache.pulsar.common.policies.data.TenantInfoImpl;
import org.awaitility.Awaitility;
import org.testng.Assert;
+import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -109,17 +110,18 @@ public class BrokerInterceptorTest extends ProducerConsumerBase {
@Override
protected void cleanup() throws Exception {
- teardown();
+ teardown(null);
}
@AfterMethod(alwaysRun = true)
- public void teardown() throws Exception {
+ public void teardown(ITestResult testResult) throws Exception {
this.listeners.close();
-
- verify(listener1, times(1)).close();
- verify(listener2, times(1)).close();
- verify(ncl1, times(1)).close();
- verify(ncl2, times(1)).close();
+ if (testResult != null && testResult.getStatus() == ITestResult.SUCCESS) {
+ verify(listener1, times(1)).close();
+ verify(listener2, times(1)).close();
+ verify(ncl1, times(1)).close();
+ verify(ncl2, times(1)).close();
+ }
super.internalCleanup();
}
@@ -212,13 +214,13 @@ public class BrokerInterceptorTest extends ProducerConsumerBase {
@Cleanup
Producer<String> producer = pulsarClient.newProducer(Schema.STRING)
- .topic("test-before-send-message")
- .create();
+ .topic("test-before-send-message")
+ .create();
Consumer<String> consumer = pulsarClient.newConsumer(Schema.STRING)
- .topic("test-before-send-message")
- .subscriptionName("test")
- .subscribe();
+ .topic("test-before-send-message")
+ .subscriptionName("test")
+ .subscribe();
assertEquals(counterBrokerInterceptor.getMessageProducedCount(), 0);
assertEquals(counterBrokerInterceptor.getMessageDispatchCount(), 0);
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/CounterBrokerInterceptor.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/CounterBrokerInterceptor.java
index a7302a907a0..a13a3671b2d 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/CounterBrokerInterceptor.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/intercept/CounterBrokerInterceptor.java
@@ -252,7 +252,11 @@ public class CounterBrokerInterceptor implements BrokerInterceptor {
}
if (response instanceof Response) {
Response res = (Response) response;
- responseList.add(new ResponseEvent(res.getHttpChannel().getRequest().getRequestURI(), res.getStatus()));
+ responseList.add(new ResponseEvent(res.getRequest().getHttpURI().getPath(), res.getStatus()));
+ } else if (response instanceof org.eclipse.jetty.ee8.nested.Response) {
+ org.eclipse.jetty.ee8.nested.Response res = (org.eclipse.jetty.ee8.nested.Response) response;
+ responseList.add(
+ new ResponseEvent(res.getHttpChannel().getRequest().getHttpURI().getPath(), res.getStatus()));
}
}
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/BrokerServiceTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/BrokerServiceTest.java
index 56a08eac209..0a8f6601ad5 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/BrokerServiceTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/BrokerServiceTest.java
@@ -853,7 +853,7 @@ public class BrokerServiceTest extends BrokerTestBase {
fail("should fail");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("Unauthorized"));
+ assertTrue(e.getMessage().contains("Authentication required"));
} finally {
pulsarClient.close();
}
@@ -916,7 +916,7 @@ public class BrokerServiceTest extends BrokerTestBase {
fail("should fail");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("Unauthorized"));
+ assertTrue(e.getMessage().contains("Authentication required"));
} finally {
pulsarClient.close();
}
@@ -934,7 +934,7 @@ public class BrokerServiceTest extends BrokerTestBase {
.subscribe();
fail("should fail");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("Unauthorized"));
+ assertTrue(e.getMessage().contains("Authentication required"));
} finally {
pulsarClient.close();
}
@@ -978,7 +978,7 @@ public class BrokerServiceTest extends BrokerTestBase {
.subscribe();
fail("should fail");
} catch (Exception e) {
- assertTrue(e.getMessage().contains("Unauthorized"));
+ assertTrue(e.getMessage().contains("Authentication required"));
} finally {
pulsarClient.close();
}
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/ExceptionHandlerTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/ExceptionHandlerTest.java
index 3c0c2f4e687..660f4bce28c 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/ExceptionHandlerTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/ExceptionHandlerTest.java
@@ -23,8 +23,6 @@ import static org.eclipse.jetty.http.HttpStatus.PRECONDITION_FAILED_412;
import javax.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import org.apache.pulsar.common.intercept.InterceptException;
-import org.eclipse.jetty.server.HttpChannel;
-import org.eclipse.jetty.server.Response;
import org.mockito.Mockito;
import org.testng.annotations.Test;
@@ -41,21 +39,18 @@ public class ExceptionHandlerTest {
String internal = "internal exception";
String illegal = "illegal argument exception ";
ExceptionHandler handler = new ExceptionHandler();
- HttpServletResponse response = Mockito.mock(HttpServletResponse.class);
+
+ HttpServletResponse response = Mockito.mock(HttpServletResponse.class, Mockito.RETURNS_DEEP_STUBS);
handler.handle(response, new InterceptException(PRECONDITION_FAILED_412, restriction));
- Mockito.verify(response).sendError(PRECONDITION_FAILED_412, restriction);
+ Mockito.verify(response).setStatus(PRECONDITION_FAILED_412);
+ Mockito.verify(response.getOutputStream()).write(("{\"reason\":\"" + restriction + "\"}").getBytes());
handler.handle(response, new InterceptException(INTERNAL_SERVER_ERROR_500, internal));
- Mockito.verify(response).sendError(INTERNAL_SERVER_ERROR_500, internal);
+ Mockito.verify(response).setStatus(INTERNAL_SERVER_ERROR_500);
+ Mockito.verify(response.getOutputStream()).write(("{\"reason\":\"" + internal + "\"}").getBytes());
handler.handle(response, new IllegalArgumentException(illegal));
Mockito.verify(response).sendError(INTERNAL_SERVER_ERROR_500, illegal);
-
- Response response2 = Mockito.mock(Response.class);
- HttpChannel httpChannel = Mockito.mock(HttpChannel.class);
- Mockito.when(response2.getHttpChannel()).thenReturn(httpChannel);
- handler.handle(response2, new InterceptException(PRECONDITION_FAILED_412, restriction));
- Mockito.verify(httpChannel).sendResponse(Mockito.any(), Mockito.any(), Mockito.anyBoolean());
}
}
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/WebServiceOriginalClientIPTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/WebServiceOriginalClientIPTest.java
index 42186e988a2..3fc4cdb9f18 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/WebServiceOriginalClientIPTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/WebServiceOriginalClientIPTest.java
@@ -27,9 +27,10 @@ import nl.altindag.console.ConsoleCaptor;
import org.apache.pulsar.broker.auth.MockedPulsarServiceBaseTest;
import org.assertj.core.api.ThrowingConsumer;
import org.awaitility.Awaitility;
+import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.ProxyProtocolClientConnectionFactory.V2;
-import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -45,7 +46,8 @@ public class WebServiceOriginalClientIPTest extends MockedPulsarServiceBaseTest
@Override
protected void setup() throws Exception {
super.internalSetup();
- httpClient = new HttpClient(new SslContextFactory(true));
+ httpClient = new HttpClient();
+ httpClient.setSslContextFactory(new SslContextFactory.Client(true));
httpClient.start();
}
@@ -81,7 +83,7 @@ public class WebServiceOriginalClientIPTest extends MockedPulsarServiceBaseTest
performLoggingTest(consoleCaptor -> {
// Send a GET request to the metrics URL
ContentResponse response = httpClient.newRequest(metricsUrl)
- .header("X-Forwarded-For", "11.22.33.44:12345")
+ .headers(hdrs -> hdrs.ensureField(new HttpField("X-Forwarded-For", "11.22.33.44:12345")))
.send();
// Validate the response
@@ -100,7 +102,7 @@ public class WebServiceOriginalClientIPTest extends MockedPulsarServiceBaseTest
performLoggingTest(consoleCaptor -> {
// Send a GET request to the metrics URL
ContentResponse response = httpClient.newRequest(metricsUrl)
- .header("Forwarded", "for=11.22.33.44:12345")
+ .headers(hdrs -> hdrs.ensureField(new HttpField("Forwarded", "for=11.22.33.44:12345")))
.send();
// Validate the response
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoaderTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoaderTest.java
index 48b1135f10c..6f7879bda4f 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoaderTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/AdditionalServletWithClassLoaderTest.java
@@ -25,10 +25,10 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
+import javax.servlet.Servlet;
import org.apache.pulsar.broker.ServiceConfiguration;
import org.apache.pulsar.common.configuration.PulsarConfiguration;
import org.apache.pulsar.common.nar.NarClassLoader;
-import org.eclipse.jetty.servlet.ServletHolder;
import org.testng.annotations.Test;
@@ -55,10 +55,11 @@ public class AdditionalServletWithClassLoaderTest {
// test getServlet
assertEquals(wrapper.getServlet(), servlet);
// test getServletHolder
- ServletHolder servletHolder = new ServletHolder();
- when(servlet.getServletHolder()).thenReturn(servletHolder);
- assertEquals(wrapper.getServletHolder(), servletHolder);
- verify(servlet, times(1)).getServletHolder();
+ Servlet servletInstance = mock(Servlet.class);
+ when(servlet.getServletInstance()).thenReturn(servletInstance);
+ when(servlet.getServletType()).thenReturn(AdditionalServlet.AdditionalServletType.JAVAX_SERVLET);
+ assertEquals(wrapper.getServletInstance(), servletInstance);
+ verify(servlet, times(1)).getServletInstance();
}
@Test
@@ -77,7 +78,7 @@ public class AdditionalServletWithClassLoaderTest {
}
@Override
- public ServletHolder getServletHolder() {
+ public Object getServletInstance() {
assertEquals(Thread.currentThread().getContextClassLoader(), narLoader);
return null;
}
@@ -101,7 +102,7 @@ public class AdditionalServletWithClassLoaderTest {
additionalServletWithClassLoader.loadConfig(conf);
assertEquals(Thread.currentThread().getContextClassLoader(), curClassLoader);
// test getServletHolder
- assertNull(additionalServletWithClassLoader.getServletHolder());
+ assertNull(additionalServletWithClassLoader.getServletInstance());
assertEquals(Thread.currentThread().getContextClassLoader(), curClassLoader);
// test getServlet
assertEquals(additionalServletWithClassLoader.getServlet(), servlet);
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServletWithClassLoader.java b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServletWithClassLoader.java
index f65a98fd3f1..cf186f9527b 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServletWithClassLoader.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/broker/web/plugin/servlet/MockAdditionalServletWithClassLoader.java
@@ -20,7 +20,6 @@ package org.apache.pulsar.broker.web.plugin.servlet;
import org.apache.pulsar.broker.PulsarService;
import org.apache.pulsar.common.configuration.PulsarConfiguration;
-import org.eclipse.jetty.servlet.ServletHolder;
public class MockAdditionalServletWithClassLoader implements AdditionalServletWithPulsarService{
@Override
@@ -34,7 +33,7 @@ public class MockAdditionalServletWithClassLoader implements AdditionalServletWi
}
@Override
- public ServletHolder getServletHolder() {
+ public Object getServletInstance() {
return null;
}
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/client/api/MockBrokerService.java b/pulsar-broker/src/test/java/org/apache/pulsar/client/api/MockBrokerService.java
index b7e0ee42903..6e2db9ade01 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/client/api/MockBrokerService.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/client/api/MockBrokerService.java
@@ -23,6 +23,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
@@ -69,9 +70,10 @@ import org.apache.pulsar.common.protocol.FrameDecoderUtil;
import org.apache.pulsar.common.protocol.PulsarDecoder;
import org.apache.pulsar.common.protocol.schema.SchemaVersion;
import org.apache.pulsar.common.util.netty.EventLoopUtil;
-import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.ee8.nested.AbstractHandler;
+import org.eclipse.jetty.ee8.nested.ContextHandler;
+import org.eclipse.jetty.ee8.nested.Request;
import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.handler.AbstractHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -93,7 +95,7 @@ public class MockBrokerService {
private final Pattern multiPartPattern = Pattern.compile(".*/multi-part-.*");
@Override
- public void handle(String s, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String responseString;
log.info("Received HTTP request {}", baseRequest.getRequestURI());
@@ -294,7 +296,7 @@ public class MockBrokerService {
public MockBrokerService() {
server = new Server(0);
- server.setHandler(new GenericResponseHandler());
+ server.setHandler(new ContextHandler("/", new GenericResponseHandler()));
}
public void start() {
@@ -337,7 +339,7 @@ public class MockBrokerService {
@Override
public void initChannel(SocketChannel ch) throws Exception {
FrameDecoderUtil.addFrameDecoder(ch.pipeline(), maxMessageSize);
- ch.pipeline().addLast("handler", new MockServerCnx());
+ ch.pipeline().addLast("handler", (ChannelHandler) new MockServerCnx());
}
});
// Bind and start to accept incoming connections.
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssConsumer.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssConsumer.java
index ab8372cbfb0..b16ec3a5f15 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssConsumer.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssConsumer.java
@@ -34,14 +34,17 @@ import org.apache.pulsar.client.impl.crypto.MessageCryptoBc;
import org.apache.pulsar.common.util.ObjectMapperFactory;
import org.apache.pulsar.websocket.data.ConsumerMessage;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
@Slf4j
-@WebSocket(maxTextMessageSize = 64 * 1024)
-public class ClientSideEncryptionWssConsumer extends WebSocketAdapter implements Closeable {
+@WebSocket
+public class ClientSideEncryptionWssConsumer implements Closeable {
private Session session;
private final CryptoKeyReader cryptoKeyReader;
@@ -69,8 +72,8 @@ public class ClientSideEncryptionWssConsumer extends WebSocketAdapter implements
public void start() throws Exception {
wssClient = new WebSocketClient();
wssClient.start();
- session = wssClient.connect(this, buildConnectURL(), new ClientUpgradeRequest()).get();
- assertTrue(session.isOpen());
+ Session ses = wssClient.connect(this, new ClientUpgradeRequest(buildConnectURL())).get();
+ assertTrue(ses.isOpen());
}
private URI buildConnectURL() throws PulsarClientException.CryptoException {
@@ -93,24 +96,24 @@ public class ClientSideEncryptionWssConsumer extends WebSocketAdapter implements
return msg;
}
- @Override
+ @OnWebSocketClose
public void onWebSocketClose(int statusCode, String reason) {
log.info("Connection closed: {} - {}", statusCode, reason);
this.session = null;
}
- @Override
+ @OnWebSocketOpen
public void onWebSocketConnect(Session session) {
log.info("Got connect: {}", session);
this.session = session;
}
- @Override
+ @OnWebSocketError
public void onWebSocketError(Throwable cause) {
log.error("Received an error", cause);
}
- @Override
+ @OnWebSocketMessage
public void onWebSocketText(String text) {
try {
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssProducer.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssProducer.java
index 9188bac7b37..b2a4e8a97ec 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssProducer.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ClientSideEncryptionWssProducer.java
@@ -42,15 +42,19 @@ import org.apache.pulsar.common.api.proto.CompressionType;
import org.apache.pulsar.common.api.proto.MessageIdData;
import org.apache.pulsar.common.util.ObjectMapperFactory;
import org.apache.pulsar.websocket.data.ProducerMessage;
+import org.eclipse.jetty.websocket.api.Callback;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.WebSocketAdapter;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
@Slf4j
-@WebSocket(maxTextMessageSize = 64 * 1024)
-public class ClientSideEncryptionWssProducer extends WebSocketAdapter implements Closeable {
+@WebSocket
+public class ClientSideEncryptionWssProducer implements Closeable {
private Session session;
private volatile CompletableFuture<MessageIdData> sendFuture;
@@ -80,8 +84,8 @@ public class ClientSideEncryptionWssProducer extends WebSocketAdapter implements
public void start() throws Exception {
wssClient = new WebSocketClient();
wssClient.start();
- session = wssClient.connect(this, buildConnectURL(), new ClientUpgradeRequest()).get();
- assertTrue(session.isOpen());
+ Session ses = wssClient.connect(this, new ClientUpgradeRequest(buildConnectURL())).get();
+ assertTrue(ses.isOpen());
}
private URI buildConnectURL() throws PulsarClientException.CryptoException {
@@ -130,7 +134,7 @@ public class ClientSideEncryptionWssProducer extends WebSocketAdapter implements
// Do send.
sendFuture = new CompletableFuture<>();
String jsonMsg = ObjectMapperFactory.getMapper().writer().writeValueAsString(msg);
- this.session.getRemote().sendString(jsonMsg);
+ this.session.sendText(jsonMsg, Callback.NOOP);
// Wait for response.
executor.schedule(() -> {
synchronized (ClientSideEncryptionWssProducer.this) {
@@ -142,7 +146,7 @@ public class ClientSideEncryptionWssProducer extends WebSocketAdapter implements
return sendFuture.get();
}
- @Override
+ @OnWebSocketClose
public void onWebSocketClose(int statusCode, String reason) {
log.info("Connection closed: {} - {}", statusCode, reason);
this.session = null;
@@ -151,18 +155,18 @@ public class ClientSideEncryptionWssProducer extends WebSocketAdapter implements
}
}
- @Override
+ @OnWebSocketOpen
public void onWebSocketConnect(Session session) {
log.info("Got connect: {}", session);
this.session = session;
}
- @Override
+ @OnWebSocketError
public void onWebSocketError(Throwable cause) {
log.error("Received an error", cause);
}
- @Override
+ @OnWebSocketMessage
public void onWebSocketText(String text) {
try {
ResponseOfSend responseOfSend =
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyAuthenticationTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyAuthenticationTest.java
index 77632fbd418..b7f45c8c196 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyAuthenticationTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyAuthenticationTest.java
@@ -127,13 +127,13 @@ public class ProxyAuthenticationTest extends ProducerConsumerBase {
SimpleProducerSocket produceSocket = new SimpleProducerSocket();
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
log.info("Connecting to : {}", consumeUri);
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
Assert.assertTrue(consumerFuture.get().isOpen());
Assert.assertTrue(producerFuture.get().isOpen());
@@ -188,13 +188,13 @@ public class ProxyAuthenticationTest extends ProducerConsumerBase {
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
Assert.assertTrue(consumerFuture.get().isOpen());
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
Assert.assertTrue(producerFuture.get().isOpen());
Awaitility.await().untilAsserted(() -> Assert.assertTrue(
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyEncryptionPublishConsumeTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyEncryptionPublishConsumeTest.java
index d694e9596af..e4aa3dec7ea 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyEncryptionPublishConsumeTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyEncryptionPublishConsumeTest.java
@@ -118,15 +118,15 @@ public class ProxyEncryptionPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
consumeClient2.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeUri, consumeRequest1);
- Future<Session> consumerFuture2 = consumeClient2.connect(consumeSocket2, consumeUri, consumeRequest2);
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(consumeUri);
+ ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeRequest1);
+ Future<Session> consumerFuture2 = consumeClient2.connect(consumeSocket2, consumeRequest2);
log.info("Connecting to : {}", consumeUri);
readClient.start();
- ClientUpgradeRequest readRequest = new ClientUpgradeRequest();
- Future<Session> readerFuture = readClient.connect(readSocket, readUri, readRequest);
+ ClientUpgradeRequest readRequest = new ClientUpgradeRequest(readUri);
+ Future<Session> readerFuture = readClient.connect(readSocket, readRequest);
log.info("Connecting to : {}", readUri);
// let it connect
@@ -137,9 +137,9 @@ public class ProxyEncryptionPublishConsumeTest extends ProducerConsumerBase {
// Also make sure subscriptions and reader are already created
Thread.sleep(500);
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertTrue(producerFuture.get().isOpen());
Awaitility.await().untilAsserted(() -> {
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyIdleTimeoutTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyIdleTimeoutTest.java
index ed05a99a22c..b6926ae7bbb 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyIdleTimeoutTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyIdleTimeoutTest.java
@@ -95,8 +95,8 @@ public class ProxyIdleTimeoutTest extends ProducerConsumerBase {
try {
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertThat(producerFuture).succeedsWithin(2, SECONDS);
Session session = producerFuture.get();
Awaitility.await().during(5, SECONDS).untilAsserted(() -> assertThat(session.isOpen()).isFalse());
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPingTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPingTest.java
index 813d978f52a..632128b37a8 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPingTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPingTest.java
@@ -97,8 +97,8 @@ public class ProxyPingTest extends ProducerConsumerBase {
try {
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertThat(producerFuture).succeedsWithin(2, SECONDS);
Session session = producerFuture.get();
Awaitility.await().during(5, SECONDS).untilAsserted(() -> assertThat(session.isOpen()).isTrue());
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTest.java
index 30db9ee43be..f1255a8c97a 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTest.java
@@ -35,6 +35,7 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.reflect.TypeToken;
import java.net.URI;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -68,7 +69,7 @@ import org.apache.pulsar.websocket.stats.ProxyTopicStat.ConsumerStats;
import org.apache.pulsar.websocket.stats.ProxyTopicStat.ProducerStats;
import org.awaitility.Awaitility;
import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.UpgradeException;
+import org.eclipse.jetty.websocket.api.exceptions.UpgradeException;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.glassfish.jersey.client.ClientConfig;
@@ -144,15 +145,15 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
consumeClient2.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeUri, consumeRequest1);
- Future<Session> consumerFuture2 = consumeClient2.connect(consumeSocket2, consumeUri, consumeRequest2);
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(consumeUri);
+ ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeRequest1);
+ Future<Session> consumerFuture2 = consumeClient2.connect(consumeSocket2, consumeRequest2);
log.info("Connecting to : {}", consumeUri);
readClient.start();
- ClientUpgradeRequest readRequest = new ClientUpgradeRequest();
- Future<Session> readerFuture = readClient.connect(readSocket, readUri, readRequest);
+ ClientUpgradeRequest readRequest = new ClientUpgradeRequest(readUri);
+ Future<Session> readerFuture = readClient.connect(readSocket, readRequest);
log.info("Connecting to : {}", readUri);
// let it connect
@@ -163,9 +164,9 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
// Also make sure subscriptions and reader are already created
Thread.sleep(500);
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertTrue(producerFuture.get().isOpen());
int retry = 0;
@@ -214,16 +215,16 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
log.info("Connecting to : {}", consumeUri);
// let it connect
assertTrue(consumerFuture.get().isOpen());
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertTrue(producerFuture.get().isOpen());
// Send 30 message in total.
produceSocket.sendMessage(20);
@@ -284,8 +285,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
// setup a consumer
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
consumerFuture.get();
List<String> subs = admin.topics().getSubscriptions(topic);
assertEquals(subs.size(), 1);
@@ -314,8 +315,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeUri, consumeRequest1);
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeRequest1);
consumerFuture1.get();
fail("should fail: empty subscription");
} catch (Exception e) {
@@ -341,15 +342,15 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeUri, consumeRequest1);
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeRequest1);
consumerFuture1.get();
try {
consumeClient2.start();
- ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest();
+ ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest(consumeUri);
Future<Session> consumerFuture2 =
- consumeClient2.connect(consumeSocket2, consumeUri, consumeRequest2);
+ consumeClient2.connect(consumeSocket2, consumeRequest2);
consumerFuture2.get();
fail("should fail: conflicting subscription name");
} catch (Exception e) {
@@ -378,15 +379,15 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
produceClient1.start();
- ClientUpgradeRequest produceRequest1 = new ClientUpgradeRequest();
- Future<Session> producerFuture1 = produceClient1.connect(produceSocket1, produceUri, produceRequest1);
+ ClientUpgradeRequest produceRequest1 = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture1 = produceClient1.connect(produceSocket1, produceRequest1);
producerFuture1.get();
try {
produceClient2.start();
- ClientUpgradeRequest produceRequest2 = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest2 = new ClientUpgradeRequest(produceUri);
Future<Session> producerFuture2 =
- produceClient2.connect(produceSocket2, produceUri, produceRequest2);
+ produceClient2.connect(produceSocket2, produceRequest2);
producerFuture2.get();
fail("should fail: conflicting producer name");
} catch (Exception e) {
@@ -434,8 +435,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
// Create subscription
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
consumerFuture.get();
} finally {
stopWebSocketClient(consumeClient);
@@ -444,8 +445,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
// Fill the backlog
try {
produceClient1.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient1.connect(produceSocket1, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient1.connect(produceSocket1, produceRequest);
producerFuture.get();
produceSocket1.sendMessage(100);
} finally {
@@ -456,16 +457,15 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
// New producer fails to connect
try {
+ produceClient2.setIdleTimeout(Duration.ofSeconds(5));
produceClient2.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient2.connect(produceSocket2, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient2.connect(produceSocket2, produceRequest);
producerFuture.get();
fail("should fail: backlog quota exceeded");
} catch (Exception e) {
// Expected
assertTrue(e.getCause() instanceof UpgradeException);
- assertEquals(((UpgradeException) e.getCause()).getResponseStatusCode(),
- HttpServletResponse.SC_SERVICE_UNAVAILABLE);
} finally {
stopWebSocketClient(produceClient2);
admin.topics().skipAllMessages("persistent://" + topic, subscription);
@@ -504,8 +504,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
producerFuture.get();
fail("should fail: topic does not exist");
} catch (Exception e) {
@@ -518,8 +518,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
consumerFuture.get();
fail("should fail: topic does not exist");
} catch (Exception e) {
@@ -548,8 +548,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
producerFuture.get();
fail("should fail: producer fenced");
} catch (Exception e) {
@@ -577,8 +577,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
producerFuture.get();
fail("should fail: topic terminated");
} catch (Exception e) {
@@ -621,13 +621,13 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeUri, consumeRequest1);
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeRequest1);
log.info("Connecting to : {}", consumeUri);
readClient.start();
- ClientUpgradeRequest readRequest = new ClientUpgradeRequest();
- Future<Session> readerFuture = readClient.connect(readSocket, readUri, readRequest);
+ ClientUpgradeRequest readRequest = new ClientUpgradeRequest(readUri);
+ Future<Session> readerFuture = readClient.connect(readSocket, readRequest);
log.info("Connecting to : {}", readUri);
assertTrue(consumerFuture1.get().isOpen());
@@ -635,9 +635,9 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
Thread.sleep(500);
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
// let it connect
assertTrue(producerFuture.get().isOpen());
@@ -691,8 +691,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
producerFuture.get();
produceSocket.sendMessage(100);
} finally {
@@ -703,8 +703,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
consumerFuture.get();
} finally {
stopWebSocketClient(consumeClient);
@@ -735,19 +735,19 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
consumeClient2.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeUri, consumeRequest1);
- Future<Session> consumerFuture2 = consumeClient2.connect(consumeSocket2, consumeUri, consumeRequest2);
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(consumeUri);
+ ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture1 = consumeClient1.connect(consumeSocket1, consumeRequest1);
+ Future<Session> consumerFuture2 = consumeClient2.connect(consumeSocket2, consumeRequest2);
log.info("Connecting to : {}", consumeUri);
// let it connect
assertTrue(consumerFuture1.get().isOpen());
assertTrue(consumerFuture2.get().isOpen());
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertTrue(producerFuture.get().isOpen());
produceSocket.sendMessage(100);
@@ -808,20 +808,20 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient1.start();
consumeClient2.start();
- ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest();
- ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest();
+ ClientUpgradeRequest consumeRequest1 = new ClientUpgradeRequest(URI.create(consumerUri));
+ ClientUpgradeRequest consumeRequest2 = new ClientUpgradeRequest(URI.create(dlqUri));
Future<Session> consumerFuture1 =
- consumeClient1.connect(consumeSocket1, URI.create(consumerUri), consumeRequest1);
+ consumeClient1.connect(consumeSocket1, consumeRequest1);
Future<Session> consumerFuture2 =
- consumeClient2.connect(consumeSocket2, URI.create(dlqUri), consumeRequest2);
+ consumeClient2.connect(consumeSocket2, consumeRequest2);
assertTrue(consumerFuture1.get().isOpen());
assertTrue(consumerFuture2.get().isOpen());
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(URI.create(producerUri));
produceClient.start();
Future<Session> producerFuture =
- produceClient.connect(produceSocket, URI.create(producerUri), produceRequest);
+ produceClient.connect(produceSocket, produceRequest);
assertTrue(producerFuture.get().isOpen());
assertEquals(consumeSocket1.getReceivedMessagesCount(), 0);
@@ -868,15 +868,13 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumeClient.start();
- final ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- final Future<Session> consumerFuture = consumeClient.connect(consumeSocket, URI.create(consumerUri),
- consumeRequest);
+ final ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(URI.create(consumerUri));
+ final Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
assertTrue(consumerFuture.get().isOpen());
produceClient.start();
- final ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
- final Future<Session> producerFuture = produceClient.connect(produceSocket, URI.create(producerUri),
- produceRequest);
+ final ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(URI.create(producerUri));
+ final Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
assertTrue(producerFuture.get().isOpen());
assertEquals(consumeSocket.getReceivedMessagesCount(), 0);
@@ -914,9 +912,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumerClient.start();
- ClientUpgradeRequest consumerRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture =
- consumerClient.connect(consumeSocket, URI.create(consumerUri), consumerRequest);
+ ClientUpgradeRequest consumerRequest = new ClientUpgradeRequest(URI.create(consumerUri));
+ Future<Session> consumerFuture = consumerClient.connect(consumeSocket, consumerRequest);
assertTrue(consumerFuture.get().isOpen());
assertEquals(consumeSocket.getReceivedMessagesCount(), 0);
@@ -995,9 +992,8 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumerClient.start();
- ClientUpgradeRequest consumerRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture =
- consumerClient.connect(consumeSocket, URI.create(consumerUri), consumerRequest);
+ ClientUpgradeRequest consumerRequest = new ClientUpgradeRequest(URI.create(consumerUri));
+ Future<Session> consumerFuture = consumerClient.connect(consumeSocket, consumerRequest);
assertTrue(consumerFuture.get().isOpen());
assertEquals(consumeSocket.getReceivedMessagesCount(), 0);
@@ -1136,12 +1132,10 @@ public class ProxyPublishConsumeTest extends ProducerConsumerBase {
try {
consumerClient1.start();
consumerClient2.start();
- ClientUpgradeRequest consumerRequest1 = new ClientUpgradeRequest();
- ClientUpgradeRequest consumerRequest2 = new ClientUpgradeRequest();
- Future<Session> consumerFuture1 =
- consumerClient1.connect(consumeSocket1, URI.create(consumerUri1), consumerRequest1);
- Future<Session> consumerFuture2 =
- consumerClient2.connect(consumeSocket2, URI.create(consumerUri2), consumerRequest2);
+ ClientUpgradeRequest consumerRequest1 = new ClientUpgradeRequest(URI.create(consumerUri1));
+ ClientUpgradeRequest consumerRequest2 = new ClientUpgradeRequest(URI.create(consumerUri2));
+ Future<Session> consumerFuture1 = consumerClient1.connect(consumeSocket1, consumerRequest1);
+ Future<Session> consumerFuture2 = consumerClient2.connect(consumeSocket2, consumerRequest2);
assertTrue(consumerFuture1.get().isOpen());
assertTrue(consumerFuture2.get().isOpen());
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTlsTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTlsTest.java
index 8ffa09de64e..1c5439d4b9e 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTlsTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeTlsTest.java
@@ -24,10 +24,10 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import java.net.URI;
-import java.security.GeneralSecurityException;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
+import lombok.Cleanup;
import org.apache.pulsar.client.api.TlsProducerConsumerBase;
import org.apache.pulsar.client.impl.auth.AuthenticationTls;
import org.apache.pulsar.common.util.SecurityUtility;
@@ -37,6 +37,7 @@ import org.apache.pulsar.websocket.service.ProxyServer;
import org.apache.pulsar.websocket.service.WebSocketProxyConfiguration;
import org.apache.pulsar.websocket.service.WebSocketServiceStarter;
import org.awaitility.Awaitility;
+import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
@@ -95,7 +96,7 @@ public class ProxyPublishConsumeTlsTest extends TlsProducerConsumerBase {
}
@Test(timeOut = 30000)
- public void socketTest() throws GeneralSecurityException {
+ public void socketTest() throws Exception {
String consumerUri =
"wss://localhost:" + proxyServer.getListenPortHTTPS().get()
+ "/ws/consumer/persistent/my-property/use/my-ns/my-topic/my-sub";
@@ -104,25 +105,30 @@ public class ProxyPublishConsumeTlsTest extends TlsProducerConsumerBase {
URI consumeUri = URI.create(consumerUri);
URI produceUri = URI.create(producerUri);
- SslContextFactory sslContextFactory = new SslContextFactory();
+ SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
sslContextFactory.setSslContext(SecurityUtility
.createSslContext(false, SecurityUtility.loadCertificatesFromPemFile(CA_CERT_FILE_PATH), null));
+ @Cleanup("stop")
+ HttpClient httpClient = new HttpClient();
+ httpClient.setSslContextFactory(sslContextFactory);
- WebSocketClient consumeClient = new WebSocketClient(sslContextFactory);
+ @Cleanup("stop")
+ WebSocketClient consumeClient = new WebSocketClient(httpClient);
SimpleConsumerSocket consumeSocket = new SimpleConsumerSocket();
- WebSocketClient produceClient = new WebSocketClient(sslContextFactory);
+ @Cleanup("stop")
+ WebSocketClient produceClient = new WebSocketClient(httpClient);
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
log.info("Connecting to : {}", consumeUri);
Assert.assertTrue(consumerFuture.get().isOpen());
SimpleProducerSocket produceSocket = new SimpleProducerSocket();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
Assert.assertTrue(producerFuture.get().isOpen());
@@ -132,9 +138,6 @@ public class ProxyPublishConsumeTlsTest extends TlsProducerConsumerBase {
});
consumeSocket.awaitClose(1, TimeUnit.SECONDS);
produceSocket.awaitClose(1, TimeUnit.SECONDS);
- } catch (Throwable t) {
- log.error(t.getMessage());
- Assert.fail(t.getMessage());
} finally {
try {
consumeClient.stop();
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeWithoutZKTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeWithoutZKTest.java
index e923fd85613..983aa1bff77 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeWithoutZKTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyPublishConsumeWithoutZKTest.java
@@ -96,13 +96,13 @@ public class ProxyPublishConsumeWithoutZKTest extends ProducerConsumerBase {
try {
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
log.info("Connecting to : {}", consumeUri);
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
produceClient.start();
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
// let it connect
Assert.assertTrue(consumerFuture.get().isOpen());
Assert.assertTrue(producerFuture.get().isOpen());
diff --git a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyRoleAuthTest.java b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyRoleAuthTest.java
index 0eb90b9fe36..c97dbb3812b 100644
--- a/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyRoleAuthTest.java
+++ b/pulsar-broker/src/test/java/org/apache/pulsar/websocket/proxy/ProxyRoleAuthTest.java
@@ -231,18 +231,18 @@ public class ProxyRoleAuthTest extends ProducerConsumerBase {
try {
// Connect consumer with CLIENT_TOKEN in Authorization header
consumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
// Add JWT token authentication for WebSocket client
consumeRequest.setHeader("Authorization", "Bearer " + CLIENT_TOKEN);
- Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeUri, consumeRequest);
+ Future<Session> consumerFuture = consumeClient.connect(consumeSocket, consumeRequest);
log.info("Connecting consumer to: {} with CLIENT_TOKEN", consumeUri);
// Connect producer with CLIENT_TOKEN in Authorization header
produceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
// Add JWT token authentication for WebSocket client
produceRequest.setHeader("Authorization", "Bearer " + CLIENT_TOKEN);
- Future<Session> producerFuture = produceClient.connect(produceSocket, produceUri, produceRequest);
+ Future<Session> producerFuture = produceClient.connect(produceSocket, produceRequest);
log.info("Connecting producer to: {} with CLIENT_TOKEN", produceUri);
// Verify connections are established
@@ -316,11 +316,10 @@ public class ProxyRoleAuthTest extends ProducerConsumerBase {
SimpleConsumerSocket consumeSocket = new SimpleConsumerSocket();
unauthorizedConsumeClient.start();
- ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest consumeRequest = new ClientUpgradeRequest(consumeUri);
// Use UNAUTHORIZED_TOKEN which doesn't have permissions
consumeRequest.setHeader("Authorization", "Bearer " + UNAUTHORIZED_TOKEN);
- Future<Session> consumerFuture = unauthorizedConsumeClient.connect(consumeSocket, consumeUri,
- consumeRequest);
+ Future<Session> consumerFuture = unauthorizedConsumeClient.connect(consumeSocket, consumeRequest);
log.info("Attempting to connect consumer with unauthorized token to: {}", consumeUri);
@@ -340,11 +339,10 @@ public class ProxyRoleAuthTest extends ProducerConsumerBase {
SimpleProducerSocket produceSocket = new SimpleProducerSocket();
unauthorizedProduceClient.start();
- ClientUpgradeRequest produceRequest = new ClientUpgradeRequest();
+ ClientUpgradeRequest produceRequest = new ClientUpgradeRequest(produceUri);
// Use UNAUTHORIZED_TOKEN which doesn't have permissions
produceRequest.setHeader("Authorization", "Bearer " + UNAUTHORIZED_TOKEN);
- Future<Session> producerFuture = unauthorizedProduceClient.connect(produceSocket, produceUri,
... 4333 lines suppressed ...