3131import java .util .Properties ;
3232import java .util .concurrent .ConcurrentHashMap ;
3333import java .util .concurrent .ConcurrentMap ;
34+ import java .util .concurrent .ScheduledExecutorService ;
35+ import java .util .concurrent .ScheduledThreadPoolExecutor ;
36+ import java .util .concurrent .ThreadFactory ;
37+ import java .util .concurrent .TimeUnit ;
38+ import java .util .concurrent .atomic .AtomicInteger ;
3439import java .util .function .BiConsumer ;
3540import javax .servlet .ServletException ;
3641import javax .servlet .http .HttpServletRequest ;
@@ -81,6 +86,9 @@ public class PrometheusMetricsProvider implements MetricsProvider {
8186
8287 private Server server ;
8388 private int numWorkerThreads ;
89+ private long workerShutdownTimeoutMs = 1000 ;
90+ private int summaryRotateSeconds = 60 ;
91+ private ScheduledExecutorService summaryRotateExecutor ;
8492 private String host ;
8593
8694 // SSL Configuration fields
@@ -101,7 +109,27 @@ public class PrometheusMetricsProvider implements MetricsProvider {
101109 public static final String HTTP_PORT = "httpPort" ;
102110 public static final String EXPORT_JVM_INFO = "exportJvmInfo" ;
103111 public static final String HTTPS_PORT = "httpsPort" ;
112+ /**
113+ * @deprecated DataSketches-based summaries are lock-free per-thread and no longer require
114+ * worker threads. This property is ignored. See ZOOKEEPER-4741.
115+ */
116+ @ Deprecated
104117 public static final String NUM_WORKER_THREADS = "numWorkerThreads" ;
118+ /**
119+ * @deprecated DataSketches-based summaries no longer use a bounded worker queue. This property
120+ * is ignored. See ZOOKEEPER-4741.
121+ */
122+ @ Deprecated
123+ public static final String MAX_QUEUE_SIZE = "maxQueueSize" ;
124+ /** Timeout in ms for shutting down the summary rotation executor. */
125+ public static final String WORKER_SHUTDOWN_TIMEOUT_MS = "workerShutdownTimeoutMs" ;
126+ /**
127+ * Interval in seconds for rotating per-thread DataSketches into the aggregated result that is
128+ * exposed via {@code /metrics}. Quantiles from observations in the current interval become
129+ * visible after the next rotation. Default is 60 seconds.
130+ */
131+ public static final String PROMETHEUS_SUMMARY_ROTATE_INTERVAL_SECONDS =
132+ "prometheusMetricsSummaryRotateIntervalSeconds" ;
105133 public static final String SSL_KEYSTORE_LOCATION = "ssl.keyStore.location" ;
106134 public static final String SSL_KEYSTORE_PASSWORD = "ssl.keyStore.password" ;
107135 public static final String SSL_KEYSTORE_TYPE = "ssl.keyStore.type" ;
@@ -144,6 +172,14 @@ public void configure(Properties configuration) throws MetricsProviderLifeCycleE
144172 this .httpsPort = Integer .parseInt (configuration .getProperty (HTTPS_PORT , "-1" ));
145173 this .exportJvmInfo = Boolean .parseBoolean (configuration .getProperty (EXPORT_JVM_INFO , "true" ));
146174 this .numWorkerThreads = Integer .parseInt (configuration .getProperty (NUM_WORKER_THREADS , "10" ));
175+ if (configuration .containsKey (NUM_WORKER_THREADS ) || configuration .containsKey (MAX_QUEUE_SIZE )) {
176+ LOG .warn ("The configuration {} and {} are deprecated and ignored. See ZOOKEEPER-4741." ,
177+ NUM_WORKER_THREADS , MAX_QUEUE_SIZE );
178+ }
179+ this .workerShutdownTimeoutMs =
180+ Long .parseLong (configuration .getProperty (WORKER_SHUTDOWN_TIMEOUT_MS , "1000" ));
181+ this .summaryRotateSeconds =
182+ Integer .parseInt (configuration .getProperty (PROMETHEUS_SUMMARY_ROTATE_INTERVAL_SECONDS , "60" ));
147183
148184 // If httpsPort is specified, parse all SSL properties
149185 if (this .httpsPort != -1 ) {
@@ -244,6 +280,7 @@ public void start() throws MetricsProviderLifeCycleException {
244280 LOG .info ("Prometheus metrics provider with Jetty started. HTTP port: {}, HTTPS port: {}" ,
245281 httpPort != -1 ? httpPort : "disabled" , httpsPort != -1 ? httpsPort : "disabled" );
246282
283+ startSummaryRotateTask ();
247284 } catch (Exception e ) {
248285 LOG .error ("Failed to start Prometheus Jetty server" , e );
249286 // Ensure server is stopped on startup failure
@@ -252,6 +289,48 @@ public void start() throws MetricsProviderLifeCycleException {
252289 }
253290 }
254291
292+ private void startSummaryRotateTask () {
293+ summaryRotateExecutor = new ScheduledThreadPoolExecutor (1 , new SummaryRotateThreadFactory ());
294+ summaryRotateExecutor .scheduleAtFixedRate (() -> {
295+ try {
296+ rootContext .rotateAllSummaries ();
297+ } catch (Exception err ) {
298+ LOG .error ("Cannot rotate Prometheus summaries" , err );
299+ }
300+ }, summaryRotateSeconds , summaryRotateSeconds , TimeUnit .SECONDS );
301+ }
302+
303+ private void shutdownSummaryRotateExecutor () {
304+ if (summaryRotateExecutor == null ) {
305+ return ;
306+ }
307+ LOG .info ("Shutting down Prometheus summary rotate executor with timeout {}ms" , workerShutdownTimeoutMs );
308+ summaryRotateExecutor .shutdown ();
309+ try {
310+ if (!summaryRotateExecutor .awaitTermination (workerShutdownTimeoutMs , TimeUnit .MILLISECONDS )) {
311+ LOG .warn ("Summary rotate executor did not terminate in {}ms; forcing shutdown" ,
312+ workerShutdownTimeoutMs );
313+ summaryRotateExecutor .shutdownNow ();
314+ }
315+ } catch (InterruptedException e ) {
316+ Thread .currentThread ().interrupt ();
317+ summaryRotateExecutor .shutdownNow ();
318+ } finally {
319+ summaryRotateExecutor = null ;
320+ }
321+ }
322+
323+ private static class SummaryRotateThreadFactory implements ThreadFactory {
324+ private static final AtomicInteger counter = new AtomicInteger (1 );
325+
326+ @ Override
327+ public Thread newThread (Runnable runnable ) {
328+ Thread thread = new Thread (runnable , "PrometheusSummaryRotate-" + counter .getAndIncrement ());
329+ thread .setDaemon (true );
330+ return thread ;
331+ }
332+ }
333+
255334 private void setKeyStoreScanner (SslContextFactory .Server sslContextFactory ) {
256335 KeyStoreScanner keystoreScanner = new KeyStoreScanner (sslContextFactory );
257336 keystoreScanner .setScanInterval (SCAN_INTERVAL );
@@ -335,6 +414,7 @@ private ServerConnector createSslConnector(Server server, int acceptors, int sel
335414
336415 @ Override
337416 public void stop () {
417+ shutdownSummaryRotateExecutor ();
338418 if (server != null ) {
339419 try {
340420 LOG .info ("Stopping Prometheus Jetty server." );
@@ -482,16 +562,12 @@ public void unregisterGaugeSet(final String name) {
482562 unregisterGauge (name );
483563 }
484564
485- private io .prometheus .metrics .core .metrics .Summary createPrometheusSummary (String name , DetailLevel detailLevel ,
486- String ... labelNames ) {
487- io .prometheus .metrics .core .metrics .Summary .Builder builder = io .prometheus .metrics .core .metrics .Summary
488- .builder ().name (name ).help (name + " summary" ).quantile (0.5 , 0.05 ); // Median
489-
565+ private SketchesSummary createSketchesSummary (String name , DetailLevel detailLevel , String ... labelNames ) {
566+ SketchesSummary .Builder builder = SketchesSummary .build (name , name + " summary" )
567+ .quantile (0.5 ); // Median
490568 if (detailLevel == DetailLevel .ADVANCED ) {
491- builder .quantile (0.95 , 0.05 ) // 95th percentile
492- .quantile (0.99 , 0.05 ); // 99th percentile
569+ builder .quantile (0.95 ).quantile (0.99 ); // 95th and 99th percentile
493570 }
494-
495571 if (labelNames .length > 0 ) {
496572 builder .labelNames (labelNames );
497573 }
@@ -508,9 +584,7 @@ public Summary getSummary(String name, DetailLevel detailLevel) {
508584 throw new IllegalArgumentException (
509585 "Already registered a summary as " + key + " with a different detail level" );
510586 }
511- io .prometheus .metrics .core .metrics .Summary prometheusSummary = createPrometheusSummary (key ,
512- detailLevel );
513- return new PrometheusSummaryWrapper (prometheusSummary );
587+ return new PrometheusSummaryWrapper (createSketchesSummary (key , detailLevel ), key );
514588 });
515589 }
516590
@@ -524,11 +598,16 @@ public SummarySet getSummarySet(String name, DetailLevel detailLevel) {
524598 throw new IllegalArgumentException (
525599 "Already registered a summary set as " + key + " with a different detail level" );
526600 }
527- io .prometheus .metrics .core .metrics .Summary prometheusSummary = createPrometheusSummary (key , detailLevel ,
528- LABEL );
529- return new PrometheusLabelledSummaryWrapper (prometheusSummary );
601+ return new PrometheusLabelledSummaryWrapper (createSketchesSummary (key , detailLevel , LABEL ), key );
530602 });
531603 }
604+
605+ void rotateAllSummaries () {
606+ basicSummaries .values ().forEach (s -> s .inner .rotate ());
607+ advancedSummaries .values ().forEach (s -> s .inner .rotate ());
608+ basicSummarySets .values ().forEach (s -> s .inner .rotate ());
609+ advancedSummarySets .values ().forEach (s -> s .inner .rotate ());
610+ }
532611 }
533612
534613 // --- Wrapper classes to adapt Prometheus metrics to ZooKeeper's metric interfaces ---
@@ -578,29 +657,43 @@ public void inc(String key) {
578657 }
579658 }
580659
581- private static class PrometheusSummaryWrapper implements Summary {
582- private final io .prometheus .metrics .core .metrics .Summary prometheusSummary ;
660+ static class PrometheusSummaryWrapper implements Summary {
661+ // VisibleForTesting
662+ final SketchesSummary inner ;
663+ private final String name ;
583664
584- public PrometheusSummaryWrapper (io .prometheus .metrics .core .metrics .Summary prometheusSummary ) {
585- this .prometheusSummary = prometheusSummary ;
665+ PrometheusSummaryWrapper (SketchesSummary inner , String name ) {
666+ this .inner = inner ;
667+ this .name = name ;
586668 }
587669
588670 @ Override
589671 public void add (long value ) {
590- this .prometheusSummary .observe (value );
672+ try {
673+ inner .observe (value );
674+ } catch (IllegalArgumentException err ) {
675+ LOG .error ("invalid delta {} for metric {}" , value , name , err );
676+ }
591677 }
592678 }
593679
594- private static class PrometheusLabelledSummaryWrapper implements SummarySet {
595- private final io .prometheus .metrics .core .metrics .Summary prometheusSummary ;
680+ static class PrometheusLabelledSummaryWrapper implements SummarySet {
681+ // VisibleForTesting
682+ final SketchesSummary inner ;
683+ private final String name ;
596684
597- public PrometheusLabelledSummaryWrapper (io .prometheus .metrics .core .metrics .Summary prometheusSummary ) {
598- this .prometheusSummary = prometheusSummary ;
685+ PrometheusLabelledSummaryWrapper (SketchesSummary inner , String name ) {
686+ this .inner = inner ;
687+ this .name = name ;
599688 }
600689
601690 @ Override
602691 public void add (String key , long value ) {
603- this .prometheusSummary .labelValues (key ).observe (value );
692+ try {
693+ inner .labels (key ).observe (value );
694+ } catch (IllegalArgumentException err ) {
695+ LOG .error ("invalid value {} for metric {} with key {}" , value , name , key , err );
696+ }
604697 }
605698 }
606699}
0 commit comments