Allow to attach labels to metrics (#2650)

* Allow to attach labels to metrics

* Removed testCache since it's now invalid

* Fixed checkstyle

* Unused imports

* Fixed test
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeperClientStats.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeperClientStats.java
index ddc757e..7d58e5f 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeperClientStats.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BookKeeperClientStats.java
@@ -103,6 +103,8 @@
     String WRITE_TIMED_OUT_DUE_TO_NOT_ENOUGH_FAULT_DOMAINS = "WRITE_TIME_OUT_DUE_TO_NOT_ENOUGH_FAULT_DOMAINS";
     String NUM_WRITABLE_BOOKIES_IN_DEFAULT_FAULTDOMAIN = "NUM_WRITABLE_BOOKIES_IN_DEFAULT_FAULTDOMAIN";
 
+    String BOOKIE_LABEL = "bookie";
+
     OpStatsLogger getCreateOpLogger();
     OpStatsLogger getOpenOpLogger();
     OpStatsLogger getDeleteOpLogger();
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/BookKeeperClientStatsImpl.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/BookKeeperClientStatsImpl.java
index 811a4b8..db1b448 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/BookKeeperClientStatsImpl.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/impl/BookKeeperClientStatsImpl.java
@@ -274,7 +274,7 @@
     }
     @Override
     public Counter getEnsembleBookieDistributionCounter(String bookie) {
-        return stats.getCounter(LEDGER_ENSEMBLE_BOOKIE_DISTRIBUTION + "-" + bookie);
+        return stats.scopeLabel(BOOKIE_LABEL, bookie).getCounter(LEDGER_ENSEMBLE_BOOKIE_DISTRIBUTION);
     }
     @Override
     public OpStatsLogger getWriteDelayedDueToNotEnoughFaultDomainsLatency() {
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PerChannelBookieClient.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PerChannelBookieClient.java
index 89863a3..279497e 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PerChannelBookieClient.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PerChannelBookieClient.java
@@ -404,7 +404,7 @@
         }
 
         this.statsLogger = parentStatsLogger.scope(BookKeeperClientStats.CHANNEL_SCOPE)
-            .scope(buildStatsLoggerScopeName(bookieId));
+            .scopeLabel(BookKeeperClientStats.BOOKIE_LABEL, bookieId.toString());
 
         readEntryOpLogger = statsLogger.getOpStatsLogger(BookKeeperClientStats.CHANNEL_READ_OP);
         addEntryOpLogger = statsLogger.getOpStatsLogger(BookKeeperClientStats.CHANNEL_ADD_OP);
@@ -504,12 +504,6 @@
         };
     }
 
-    public static String buildStatsLoggerScopeName(BookieId addr) {
-        StringBuilder nameBuilder = new StringBuilder();
-        nameBuilder.append(addr.toString().replace('.', '_').replace('-', '_').replace(":", "_"));
-        return nameBuilder.toString();
-    }
-
     private void completeOperation(GenericCallback<PerChannelBookieClient> op, int rc) {
         //Thread.dumpStack();
         closeLock.readLock().lock();
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestDelayEnsembleChange.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestDelayEnsembleChange.java
index 7b73f8b..996c040 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestDelayEnsembleChange.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestDelayEnsembleChange.java
@@ -197,7 +197,8 @@
             assertTrue(
                     LEDGER_ENSEMBLE_BOOKIE_DISTRIBUTION + " should be > 0 for " + addr,
                     bkc.getTestStatsProvider().getCounter(
-                            CLIENT_SCOPE + "." + LEDGER_ENSEMBLE_BOOKIE_DISTRIBUTION + "-" + addr)
+                            CLIENT_SCOPE + ".bookie_" + addr.toString().replace('-', '_')
+                                    + "." + LEDGER_ENSEMBLE_BOOKIE_DISTRIBUTION)
                             .get() > 0);
         }
         assertTrue(
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/BookieClientTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/BookieClientTest.java
index ff838f3..944203c 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/BookieClientTest.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/test/BookieClientTest.java
@@ -57,7 +57,6 @@
 import org.apache.bookkeeper.proto.BookkeeperInternalCallbacks.ReadEntryCallback;
 import org.apache.bookkeeper.proto.BookkeeperInternalCallbacks.WriteCallback;
 import org.apache.bookkeeper.proto.BookkeeperProtocol;
-import org.apache.bookkeeper.proto.PerChannelBookieClient;
 import org.apache.bookkeeper.stats.NullStatsLogger;
 import org.apache.bookkeeper.test.TestStatsProvider.TestOpStatsLogger;
 import org.apache.bookkeeper.test.TestStatsProvider.TestStatsLogger;
@@ -338,7 +337,7 @@
 
         TestOpStatsLogger perChannelBookieClientScopeOfThisAddr = (TestOpStatsLogger) statsLogger
                 .scope(BookKeeperClientStats.CHANNEL_SCOPE)
-                .scope(PerChannelBookieClient.buildStatsLoggerScopeName(addr.toBookieId()))
+                .scopeLabel(BookKeeperClientStats.BOOKIE_LABEL, addr.toBookieId().toString())
                 .getOpStatsLogger(BookKeeperClientStats.GET_BOOKIE_INFO_OP);
         int expectedBookieInfoSuccessCount = (limitStatsLogging) ? 0 : 1;
         assertEquals("BookieInfoSuccessCount", expectedBookieInfoSuccessCount,
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/tls/TestTLS.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/tls/TestTLS.java
index 3123b8b..d235073 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/tls/TestTLS.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/tls/TestTLS.java
@@ -27,7 +27,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.net.InetSocketAddress;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
@@ -60,7 +59,6 @@
 import org.apache.bookkeeper.proto.BookieConnectionPeer;
 import org.apache.bookkeeper.proto.BookieServer;
 import org.apache.bookkeeper.proto.ClientConnectionPeer;
-import org.apache.bookkeeper.proto.PerChannelBookieClient;
 import org.apache.bookkeeper.proto.TestPerChannelBookieClient;
 import org.apache.bookkeeper.test.BookKeeperClusterTestCase;
 import org.apache.bookkeeper.test.TestStatsProvider;
@@ -884,10 +882,12 @@
         // verify stats
         for (int i = 0; i < numBookies; i++) {
             BookieServer bookie = bs.get(i);
-            InetSocketAddress addr = bookie.getLocalAddress().getSocketAddress();
             StringBuilder nameBuilder = new StringBuilder(BookKeeperClientStats.CHANNEL_SCOPE)
                     .append(".")
-                    .append(PerChannelBookieClient.buildStatsLoggerScopeName(bookie.getBookieId()))
+                    .append("bookie_")
+                    .append(bookie.getBookieId().toString()
+                    .replace('.', '_')
+                    .replace('-', '_'))
                     .append(".");
 
             // check stats on TLS enabled client
@@ -983,10 +983,12 @@
         }
 
         // check failed handshake counter
-        InetSocketAddress addr = bookie.getLocalAddress().getSocketAddress();
         StringBuilder nameBuilder = new StringBuilder(BookKeeperClientStats.CHANNEL_SCOPE)
                 .append(".")
-                .append(PerChannelBookieClient.buildStatsLoggerScopeName(bookie.getBookieId()))
+                .append("bookie_")
+                .append(bookie.getBookieId().toString()
+                        .replace('.', '_')
+                        .replace('-', '_'))
                 .append(".");
 
         assertEquals("TLS handshake failure expected", 1,
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/DataSketchesOpStatsLogger.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/DataSketchesOpStatsLogger.java
index ad0e7d4..0c72d580 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/DataSketchesOpStatsLogger.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/DataSketchesOpStatsLogger.java
@@ -55,9 +55,12 @@
     private final LongAdder successSumAdder = new LongAdder();
     private final LongAdder failSumAdder = new LongAdder();
 
-    DataSketchesOpStatsLogger() {
+    private final Map<String, String> labels;
+
+    public DataSketchesOpStatsLogger(Map<String, String> labels) {
         this.current = new ThreadLocalAccessor();
         this.replacement = new ThreadLocalAccessor();
+        this.labels = labels;
     }
 
     @Override
@@ -173,6 +176,10 @@
         return s != null ? s.getQuantile(quantile) : Double.NaN;
     }
 
+    public Map<String, String> getLabels() {
+        return labels;
+    }
+
     private static class LocalData {
         private final DoublesSketch successSketch = new DoublesSketchBuilder().build();
         private final DoublesSketch failSketch = new DoublesSketchBuilder().build();
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/LongAdderCounter.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/LongAdderCounter.java
index 4b67703..ddd278e 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/LongAdderCounter.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/LongAdderCounter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.bookkeeper.stats.prometheus;
 
+import java.util.Map;
 import java.util.concurrent.atomic.LongAdder;
 
 import org.apache.bookkeeper.stats.Counter;
@@ -29,6 +30,12 @@
 public class LongAdderCounter implements Counter {
     private final LongAdder counter = new LongAdder();
 
+    private final Map<String, String> labels;
+
+    public LongAdderCounter(Map<String, String> labels) {
+        this.labels = labels;
+    }
+
     @Override
     public void clear() {
         counter.reset();
@@ -53,4 +60,8 @@
     public Long get() {
         return counter.sum();
     }
+
+    public Map<String, String> getLabels() {
+        return labels;
+    }
 }
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusMetricsProvider.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusMetricsProvider.java
index f999068..573df79 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusMetricsProvider.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusMetricsProvider.java
@@ -35,14 +35,14 @@
 import java.io.Writer;
 import java.lang.reflect.Field;
 import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 
-import org.apache.bookkeeper.stats.CachingStatsProvider;
 import org.apache.bookkeeper.stats.StatsLogger;
 import org.apache.bookkeeper.stats.StatsProvider;
 import org.apache.commons.configuration.Configuration;
@@ -75,14 +75,13 @@
     final CollectorRegistry registry;
 
     Server server;
-    private final CachingStatsProvider cachingStatsProvider;
 
     /*
      * These acts a registry of the metrics defined in this provider
      */
-    final ConcurrentMap<String, LongAdderCounter> counters = new ConcurrentSkipListMap<>();
-    final ConcurrentMap<String, SimpleGauge<? extends Number>> gauges = new ConcurrentSkipListMap<>();
-    final ConcurrentMap<String, DataSketchesOpStatsLogger> opStats = new ConcurrentSkipListMap<>();
+    final ConcurrentMap<ScopeContext, LongAdderCounter> counters = new ConcurrentHashMap<>();
+    final ConcurrentMap<ScopeContext, SimpleGauge<? extends Number>> gauges = new ConcurrentHashMap<>();
+    final ConcurrentMap<ScopeContext, DataSketchesOpStatsLogger> opStats = new ConcurrentHashMap<>();
 
     public PrometheusMetricsProvider() {
         this(CollectorRegistry.defaultRegistry);
@@ -90,35 +89,6 @@
 
     public PrometheusMetricsProvider(CollectorRegistry registry) {
         this.registry = registry;
-        this.cachingStatsProvider = new CachingStatsProvider(new StatsProvider() {
-            @Override
-            public void start(Configuration conf) {
-                // nop
-            }
-
-            @Override
-            public void stop() {
-                // nop
-            }
-
-            @Override
-            public StatsLogger getStatsLogger(String scope) {
-                return new PrometheusStatsLogger(PrometheusMetricsProvider.this, scope);
-            }
-
-            @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);
-            }
-        });
     }
 
     @Override
@@ -190,21 +160,30 @@
 
     @Override
     public StatsLogger getStatsLogger(String scope) {
-        return this.cachingStatsProvider.getStatsLogger(scope);
+        return new PrometheusStatsLogger(PrometheusMetricsProvider.this, scope, Collections.emptyMap());
     }
 
     @Override
     public void writeAllMetrics(Writer writer) throws IOException {
         PrometheusTextFormatUtil.writeMetricsCollectedByPrometheusClient(writer, registry);
 
-        gauges.forEach((name, gauge) -> PrometheusTextFormatUtil.writeGauge(writer, name, gauge));
-        counters.forEach((name, counter) -> PrometheusTextFormatUtil.writeCounter(writer, name, counter));
-        opStats.forEach((name, opStatLogger) -> PrometheusTextFormatUtil.writeOpStat(writer, name, opStatLogger));
+        gauges.forEach((sc, gauge) -> PrometheusTextFormatUtil.writeGauge(writer, sc.getScope(), gauge));
+        counters.forEach((sc, counter) -> PrometheusTextFormatUtil.writeCounter(writer, sc.getScope(), counter));
+        opStats.forEach((sc, opStatLogger) ->
+                PrometheusTextFormatUtil.writeOpStat(writer, sc.getScope(), opStatLogger));
     }
 
     @Override
     public String getStatsName(String... statsComponents) {
-        return cachingStatsProvider.getStatsName(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
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusStatsLogger.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusStatsLogger.java
index 472a3fb..dcc7527 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusStatsLogger.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusStatsLogger.java
@@ -19,7 +19,8 @@
 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;
@@ -32,25 +33,27 @@
 
     private final PrometheusMetricsProvider provider;
     private final String scope;
+    private final Map<String, String> labels;
 
-    PrometheusStatsLogger(PrometheusMetricsProvider provider, String scope) {
+    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(completeName(name), x -> new DataSketchesOpStatsLogger());
+        return provider.opStats.computeIfAbsent(scopeContext(name), x -> new DataSketchesOpStatsLogger(labels));
     }
 
     @Override
     public Counter getCounter(String name) {
-        return provider.counters.computeIfAbsent(completeName(name), x -> new LongAdderCounter());
+        return provider.counters.computeIfAbsent(scopeContext(name), x -> new LongAdderCounter(labels));
     }
 
     @Override
     public <T extends Number> void registerGauge(String name, Gauge<T> gauge) {
-        provider.gauges.computeIfAbsent(completeName(name), x -> new SimpleGauge<T>(gauge));
+        provider.gauges.computeIfAbsent(scopeContext(name), x -> new SimpleGauge<T>(gauge, labels));
     }
 
     @Override
@@ -65,11 +68,21 @@
 
     @Override
     public StatsLogger scope(String name) {
-        return new PrometheusStatsLogger(provider, completeName(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) {
-        String completeName = scope.isEmpty() ? name : Joiner.on('_').join(scope, name);
-        return Collector.sanitizeMetricName(completeName);
+        return Collector.sanitizeMetricName(scope.isEmpty() ? name : Joiner.on('_').join(scope, name));
     }
 }
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusTextFormatUtil.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusTextFormatUtil.java
index d2fae28..330bcd6 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusTextFormatUtil.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/PrometheusTextFormatUtil.java
@@ -24,8 +24,7 @@
 import java.io.IOException;
 import java.io.Writer;
 import java.util.Enumeration;
-
-import org.apache.bookkeeper.stats.Counter;
+import java.util.Map;
 
 /**
  * Logic to write metrics in Prometheus text format.
@@ -37,19 +36,23 @@
         // bookie_storage_entries_count 519
         try {
             w.append("# TYPE ").append(name).append(" gauge\n");
-            w.append(name).append(' ').append(gauge.getSample().toString()).append('\n');
+            w.append(name);
+            writeLabels(w, gauge.getLabels());
+            w.append(' ').append(gauge.getSample().toString()).append('\n');
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
     }
 
-    static void writeCounter(Writer w, String name, Counter counter) {
+    static void writeCounter(Writer w, String name, LongAdderCounter counter) {
         // Example:
         // # TYPE jvm_threads_started_total counter
         // jvm_threads_started_total 59
         try {
             w.append("# TYPE ").append(name).append(" counter\n");
-            w.append(name).append(' ').append(counter.get().toString()).append('\n');
+            w.append(name);
+            writeLabels(w, counter.getLabels());
+            w.append(' ').append(counter.get().toString()).append('\n');
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -103,22 +106,58 @@
         }
     }
 
+    private static void writeLabels(Writer w, Map<String, String> labels) throws IOException {
+        if (labels.isEmpty()) {
+            return;
+        }
+
+        w.append('{');
+        writeLabelsNoBraces(w, labels);
+        w.append('}');
+    }
+
+    private static 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 static 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("\"} ")
+        w.append(name)
+                .append("{success=\"").append(success.toString())
+                .append("\",quantile=\"").append(Double.toString(quantile))
+                .append("\", ");
+        writeLabelsNoBraces(w, opStat.getLabels());
+        w.append("} ")
                 .append(Double.toString(opStat.getQuantileValue(success, quantile))).append('\n');
     }
 
     private static void writeCount(Writer w, DataSketchesOpStatsLogger opStat, String name, Boolean success)
             throws IOException {
-        w.append(name).append("_count{success=\"").append(success.toString()).append("\"} ")
+        w.append(name).append("_count{success=\"").append(success.toString()).append("\", ");
+        writeLabelsNoBraces(w, opStat.getLabels());
+        w.append("\"} ")
                 .append(Long.toString(opStat.getCount(success))).append('\n');
     }
 
     private static void writeSum(Writer w, DataSketchesOpStatsLogger opStat, String name, Boolean success)
             throws IOException {
-        w.append(name).append("_sum{success=\"").append(success.toString()).append("\"} ")
+        w.append(name).append("_sum{success=\"").append(success.toString()).append("\", ");
+        writeLabelsNoBraces(w, opStat.getLabels());
+        w.append("\"} ")
                 .append(Double.toString(opStat.getSum(success))).append('\n');
     }
 
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/ScopeContext.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/ScopeContext.java
new file mode 100644
index 0000000..48a8d63
--- /dev/null
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/ScopeContext.java
@@ -0,0 +1,55 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.bookkeeper.stats.prometheus;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 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;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    @Override
+    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 int hashCode() {
+        return Objects.hash(scope, labels);
+    }
+}
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/SimpleGauge.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/SimpleGauge.java
index 1f30872..1d831cc 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/SimpleGauge.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/main/java/org/apache/bookkeeper/stats/prometheus/SimpleGauge.java
@@ -16,6 +16,7 @@
  */
 package org.apache.bookkeeper.stats.prometheus;
 
+import java.util.Map;
 import org.apache.bookkeeper.stats.Gauge;
 
 /**
@@ -23,18 +24,19 @@
  */
 public class SimpleGauge<T extends Number> {
 
-    // public SimpleGauge(CollectorRegistry registry, String name) {
-    // this.gauge = PrometheusUtil.safeRegister(registry,
-    // Gauge.build().name(Collector.sanitizeMetricName(name)).help("-").create());
-    // }
-
+    private final Map<String, String> labels;
     private final Gauge<T> gauge;
 
-    public SimpleGauge(final Gauge<T> gauge) {
+    public SimpleGauge(final Gauge<T> gauge, Map<String, String> labels) {
         this.gauge = gauge;
+        this.labels = labels;
     }
 
     Number getSample() {
         return gauge.getSample();
     }
+
+    public Map<String, String> getLabels() {
+        return labels;
+    }
 }
diff --git a/bookkeeper-stats-providers/prometheus-metrics-provider/src/test/java/org/apache/bookkeeper/stats/prometheus/TestPrometheusMetricsProvider.java b/bookkeeper-stats-providers/prometheus-metrics-provider/src/test/java/org/apache/bookkeeper/stats/prometheus/TestPrometheusMetricsProvider.java
index 5fd34a3..df954e6 100644
--- a/bookkeeper-stats-providers/prometheus-metrics-provider/src/test/java/org/apache/bookkeeper/stats/prometheus/TestPrometheusMetricsProvider.java
+++ b/bookkeeper-stats-providers/prometheus-metrics-provider/src/test/java/org/apache/bookkeeper/stats/prometheus/TestPrometheusMetricsProvider.java
@@ -21,9 +21,9 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 
+import java.util.Collections;
 import lombok.Cleanup;
 import org.apache.bookkeeper.stats.Counter;
-import org.apache.bookkeeper.stats.OpStatsLogger;
 import org.apache.bookkeeper.stats.StatsLogger;
 import org.apache.commons.configuration.PropertiesConfiguration;
 import org.junit.Test;
@@ -34,25 +34,6 @@
 public class TestPrometheusMetricsProvider {
 
     @Test
-    public void testCache() {
-        PrometheusMetricsProvider provider = new PrometheusMetricsProvider();
-
-        StatsLogger statsLogger =  provider.getStatsLogger("test");
-
-        OpStatsLogger opStatsLogger1 = statsLogger.getOpStatsLogger("optest");
-        OpStatsLogger opStatsLogger2 = statsLogger.getOpStatsLogger("optest");
-        assertSame(opStatsLogger1, opStatsLogger2);
-
-        Counter counter1 = statsLogger.getCounter("countertest");
-        Counter counter2 = statsLogger.getCounter("countertest");
-        assertSame(counter1, counter2);
-
-        StatsLogger scope1 = statsLogger.scope("scopetest");
-        StatsLogger scope2 = statsLogger.scope("scopetest");
-        assertSame(scope1, scope2);
-    }
-
-    @Test
     public void testStartNoHttp() {
         PropertiesConfiguration config = new PropertiesConfiguration();
         config.setProperty(PrometheusMetricsProvider.PROMETHEUS_STATS_HTTP_ENABLE, false);
@@ -106,7 +87,7 @@
 
     @Test
     public void testCounter() {
-        LongAdderCounter counter = new LongAdderCounter();
+        LongAdderCounter counter = new LongAdderCounter(Collections.emptyMap());
         long value = counter.get();
         assertEquals(0L, value);
         counter.inc();
diff --git a/bookkeeper-stats/src/main/java/org/apache/bookkeeper/stats/StatsLogger.java b/bookkeeper-stats/src/main/java/org/apache/bookkeeper/stats/StatsLogger.java
index f750685..3023d36 100644
--- a/bookkeeper-stats/src/main/java/org/apache/bookkeeper/stats/StatsLogger.java
+++ b/bookkeeper-stats/src/main/java/org/apache/bookkeeper/stats/StatsLogger.java
@@ -65,6 +65,27 @@
     StatsLogger scope(String name);
 
     /**
+     * Provide the stats logger with an attached label.
+     *
+     * @param labelName
+     *          the name of the label.
+     * @param labelValue
+     *          the value of the label.
+     *
+     * @return stats logger under scope <i>name</i>.
+     */
+    default StatsLogger scopeLabel(String labelName, String labelValue) {
+        // Provide default implementation for backward compatibility
+        return scope(new StringBuilder()
+                .append(labelName)
+                .append('_')
+                .append(labelValue.replace('.', '_')
+                        .replace('-', '_')
+                        .replace(':', '_'))
+                .toString());
+    }
+
+    /**
      * Remove the given <i>statsLogger</i> for scope <i>name</i>.
      * It can be no-op if the underlying stats provider doesn't have the ability to remove scope.
      *