From dfcde19503b9a7689469f5c3d443216f1edb6a5a Mon Sep 17 00:00:00 2001 From: Owen Corrigan <76431349+OwenCorrigan76@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:47:19 +0100 Subject: [PATCH] Move metric filtering to KafkaPrometheusReporter (#38) Signed-off-by: Owen --- .../kafka/metrics/KafkaMetricsCollector.java | 80 +++++-------------- .../KafkaPrometheusMetricsReporter.java | 29 ++++--- .../strimzi/kafka/metrics/MetricWrapper.java | 7 +- .../metrics/KafkaMetricsCollectorTest.java | 74 +++++++---------- .../KafkaPrometheusMetricsReporterTest.java | 37 +++++---- .../kafka/metrics/MetricWrapperTest.java | 41 ++++++---- 6 files changed, 116 insertions(+), 152 deletions(-) diff --git a/src/main/java/io/strimzi/kafka/metrics/KafkaMetricsCollector.java b/src/main/java/io/strimzi/kafka/metrics/KafkaMetricsCollector.java index ec20875..41d7f74 100644 --- a/src/main/java/io/strimzi/kafka/metrics/KafkaMetricsCollector.java +++ b/src/main/java/io/strimzi/kafka/metrics/KafkaMetricsCollector.java @@ -4,25 +4,20 @@ */ package io.strimzi.kafka.metrics; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.prometheus.metrics.model.registry.MultiCollector; import io.prometheus.metrics.model.snapshots.GaugeSnapshot; import io.prometheus.metrics.model.snapshots.InfoSnapshot; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.PrometheusNaming; import org.apache.kafka.common.MetricName; -import org.apache.kafka.common.metrics.KafkaMetric; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** @@ -31,78 +26,59 @@ public class KafkaMetricsCollector implements MultiCollector { private static final Logger LOG = LoggerFactory.getLogger(KafkaMetricsCollector.class); - - private final Map metrics; - private final PrometheusMetricsReporterConfig config; - @SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the setPrefix method - private String prefix; + private final Map metrics; /** * Constructs a new KafkaMetricsCollector with provided configuration. - * - * @param config The configuration for the PrometheusMetricsReporter. */ - public KafkaMetricsCollector(PrometheusMetricsReporterConfig config) { - this.config = config; + public KafkaMetricsCollector() { this.metrics = new ConcurrentHashMap<>(); } /** - * Sets the prefix to be used for metric names. This is always called before addMetric/removeMetric - * - * @param prefix The prefix to set. - */ - public void setPrefix(String prefix) { - this.prefix = PrometheusNaming.prometheusName(prefix); - } - - /** - * Adds a Kafka metric to be collected. + * This method is used to add a Kafka metric to the collection for reporting. + * The metric is wrapped in a MetricWrapper object which contains additional information + * such as the prometheus name of the metric. * - * @param metric The Kafka metric to add. + * @param name The name of the metric in the Kafka system. This is used as the key in the metrics map. + * @param metric The Kafka metric to add. This is wrapped in a MetricWrapper object. */ - public void addMetric(KafkaMetric metric) { - metrics.put(metric.metricName(), metric); + public void addMetric(MetricName name, MetricWrapper metric) { + metrics.put(name, metric); } /** * Removes a Kafka metric from collection. * - * @param metric The Kafka metric to remove. + * @param name The Kafka metric to remove. */ - public void removeMetric(KafkaMetric metric) { - metrics.remove(metric.metricName()); + public void removeMetric(MetricName name) { + metrics.remove(name); } /** * Called when the Prometheus server scrapes metrics. - * @return metrics that match the configured allowlist + * @return MetricSnapshots object that contains snapshots of metrics */ @Override public MetricSnapshots collect() { Map gaugeBuilders = new HashMap<>(); Map infoBuilders = new HashMap<>(); - for (Map.Entry entry : metrics.entrySet()) { - MetricName metricName = entry.getKey(); - KafkaMetric kafkaMetric = entry.getValue(); - - String prometheusMetricName = MetricWrapper.prometheusName(prefix, metricName); - if (!config.isAllowed(prometheusMetricName)) { - LOG.trace("Ignoring metric {} as it does not match the allowlist", prometheusMetricName); - continue; - } - Labels labels = labelsFromTags(metricName.tags(), metricName.name()); + for (Map.Entry entry : metrics.entrySet()) { + MetricWrapper metricWrapper = entry.getValue(); + String prometheusMetricName = metricWrapper.prometheusName(); + Object metric = metricWrapper.value(); + Labels labels = metricWrapper.labels(); LOG.debug("Collecting metric {} with the following labels: {}", prometheusMetricName, labels); - Object valueObj = kafkaMetric.metricValue(); - if (valueObj instanceof Number) { - double value = ((Number) valueObj).doubleValue(); + if (metric instanceof Number) { + double value = ((Number) metric).doubleValue(); GaugeSnapshot.Builder builder = gaugeBuilders.computeIfAbsent(prometheusMetricName, k -> GaugeSnapshot.builder().name(prometheusMetricName)); builder.dataPoint(DataPointSnapshotBuilder.gaugeDataPoint(labels, value)); } else { InfoSnapshot.Builder builder = infoBuilders.computeIfAbsent(prometheusMetricName, k -> InfoSnapshot.builder().name(prometheusMetricName)); - builder.dataPoint(DataPointSnapshotBuilder.infoDataPoint(labels, valueObj, metricName.name())); + builder.dataPoint(DataPointSnapshotBuilder.infoDataPoint(labels, metric, metricWrapper.attribute())); } } List snapshots = new ArrayList<>(); @@ -114,18 +90,4 @@ public MetricSnapshots collect() { } return new MetricSnapshots(snapshots); } - - static Labels labelsFromTags(Map tags, String metricName) { - Labels.Builder builder = Labels.builder(); - Set labelNames = new HashSet<>(); - for (Map.Entry label : tags.entrySet()) { - String newLabelName = PrometheusNaming.sanitizeLabelName(label.getKey()); - if (labelNames.add(newLabelName)) { - builder.label(newLabelName, label.getValue()); - } else { - LOG.warn("Ignoring duplicate label key: {} with value: {} from metric: {} ", newLabelName, label.getValue(), metricName); - } - } - return builder.build(); - } } diff --git a/src/main/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporter.java b/src/main/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporter.java index f33846e..1879717 100644 --- a/src/main/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporter.java +++ b/src/main/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporter.java @@ -8,6 +8,7 @@ import io.prometheus.metrics.exporter.httpserver.HTTPServer; import io.prometheus.metrics.instrumentation.jvm.JvmMetrics; import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.PrometheusNaming; import org.apache.kafka.common.config.ConfigException; import org.apache.kafka.common.metrics.KafkaMetric; import org.apache.kafka.common.metrics.MetricsContext; @@ -29,12 +30,15 @@ public class KafkaPrometheusMetricsReporter implements MetricsReporter { private static final Logger LOG = LoggerFactory.getLogger(KafkaPrometheusMetricsReporter.class); - private final PrometheusRegistry registry; @SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the configure method - private KafkaMetricsCollector kafkaMetricsCollector; + private KafkaMetricsCollector collector; + @SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the configure method + private PrometheusMetricsReporterConfig config; @SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the configure method private Optional httpServer; + @SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the contextChange method + private String prefix; /** * Constructor @@ -50,8 +54,8 @@ public KafkaPrometheusMetricsReporter() { @Override public void configure(Map map) { - PrometheusMetricsReporterConfig config = new PrometheusMetricsReporterConfig(map, registry); - kafkaMetricsCollector = new KafkaMetricsCollector(config); + config = new PrometheusMetricsReporterConfig(map, registry); + collector = new KafkaMetricsCollector(); // Add JVM metrics JvmMetrics.builder().register(registry); httpServer = config.startHttpServer(); @@ -60,25 +64,30 @@ public void configure(Map map) { @Override public void init(List metrics) { - registry.register(kafkaMetricsCollector); + registry.register(collector); for (KafkaMetric metric : metrics) { metricChange(metric); } } - @Override public void metricChange(KafkaMetric metric) { - kafkaMetricsCollector.addMetric(metric); + String prometheusName = MetricWrapper.prometheusName(prefix, metric.metricName()); + if (!config.isAllowed(prometheusName)) { + LOG.trace("Ignoring metric {} as it does not match the allowlist", prometheusName); + } else { + MetricWrapper metricWrapper = new MetricWrapper(prometheusName, metric, metric.metricName().name()); + collector.addMetric(metric.metricName(), metricWrapper); + } } @Override public void metricRemoval(KafkaMetric metric) { - kafkaMetricsCollector.removeMetric(metric); + collector.removeMetric(metric.metricName()); } @Override public void close() { - registry.unregister(kafkaMetricsCollector); + registry.unregister(collector); } @Override @@ -97,7 +106,7 @@ public Set reconfigurableConfigs() { @Override public void contextChange(MetricsContext metricsContext) { String prefix = metricsContext.contextLabels().get(MetricsContext.NAMESPACE); - kafkaMetricsCollector.setPrefix(prefix); + this.prefix = PrometheusNaming.prometheusName(prefix); } // for testing diff --git a/src/main/java/io/strimzi/kafka/metrics/MetricWrapper.java b/src/main/java/io/strimzi/kafka/metrics/MetricWrapper.java index 8c04767..967aeee 100644 --- a/src/main/java/io/strimzi/kafka/metrics/MetricWrapper.java +++ b/src/main/java/io/strimzi/kafka/metrics/MetricWrapper.java @@ -29,7 +29,6 @@ public class MetricWrapper { private final Object value; private final String attribute; - // Will be used when implementing https://github.com/strimzi/metrics-reporter/issues/9 /** * Constructor from Kafka Metrics * @param prometheusName The name of the metric in the prometheus format @@ -89,7 +88,7 @@ public String attribute() { return attribute; } - private static Labels labelsFromScope(String scope, String metricName) { + static Labels labelsFromScope(String scope, String metricName) { Labels.Builder builder = Labels.builder(); Set labelNames = new HashSet<>(); if (scope != null) { @@ -108,8 +107,7 @@ private static Labels labelsFromScope(String scope, String metricName) { return builder.build(); } - // Will be used when implementing https://github.com/strimzi/metrics-reporter/issues/9 - private static Labels labelsFromTags(Map tags, String metricName) { + static Labels labelsFromTags(Map tags, String metricName) { Labels.Builder builder = Labels.builder(); Set labelNames = new HashSet<>(); for (Map.Entry label : tags.entrySet()) { @@ -137,7 +135,6 @@ public static String prometheusName(MetricName metricName) { metricName.getName()).toLowerCase(Locale.ROOT)); } - // Will be used when implementing https://github.com/strimzi/metrics-reporter/issues/9 /** * Compute the Prometheus name from a Kafka MetricName * @param prefix The prefix to add to the metric name diff --git a/src/test/java/io/strimzi/kafka/metrics/KafkaMetricsCollectorTest.java b/src/test/java/io/strimzi/kafka/metrics/KafkaMetricsCollectorTest.java index 45cb29d..6844d0c 100644 --- a/src/test/java/io/strimzi/kafka/metrics/KafkaMetricsCollectorTest.java +++ b/src/test/java/io/strimzi/kafka/metrics/KafkaMetricsCollectorTest.java @@ -4,13 +4,11 @@ */ package io.strimzi.kafka.metrics; -import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.model.snapshots.GaugeSnapshot; import io.prometheus.metrics.model.snapshots.InfoSnapshot; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.PrometheusNaming; import org.apache.kafka.common.MetricName; import org.apache.kafka.common.metrics.Gauge; import org.apache.kafka.common.metrics.KafkaMetric; @@ -20,7 +18,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -29,6 +26,7 @@ public class KafkaMetricsCollectorTest { + private static final String METRIC_PREFIX = "kafka.server"; private final MetricConfig metricConfig = new MetricConfig(); private final Time time = Time.SYSTEM; private Map tagsMap; @@ -47,58 +45,49 @@ public void setup() { } @Test - public void testMetricLifecycle() { - Map props = new HashMap<>(); - props.put(PrometheusMetricsReporterConfig.ALLOWLIST_CONFIG, "kafka_server_group_name.*"); - PrometheusMetricsReporterConfig config = new PrometheusMetricsReporterConfig(props, new PrometheusRegistry()); - KafkaMetricsCollector collector = new KafkaMetricsCollector(config); - collector.setPrefix("kafka.server"); + public void testCollect() { + KafkaMetricsCollector collector = new KafkaMetricsCollector(); MetricSnapshots metrics = collector.collect(); assertEquals(0, metrics.size()); - // Adding a metric not matching the allowlist does nothing - collector.addMetric(buildMetric("name", "other", 2.0)); - metrics = collector.collect(); - assertEquals(0, metrics.size()); + // Adding a metric + MetricName metricName = new MetricName("name", "group", "description", tagsMap); + MetricWrapper metricWrapper = newMetric(metricName, 1.0); - // Adding a metric that matches the allowlist - collector.addMetric(buildMetric("name", "group", 1.0)); + collector.addMetric(metricName, metricWrapper); metrics = collector.collect(); assertEquals(1, metrics.size()); MetricSnapshot snapshot = metrics.get(0); assertGaugeSnapshot(snapshot, 1.0, labels); - // Adding the same metric updates its value - collector.addMetric(buildMetric("name", "group", 3.0)); + // Updating the value of the metric + collector.addMetric(metricName, newMetric(metricName, 3.0)); metrics = collector.collect(); assertEquals(1, metrics.size()); MetricSnapshot updatedSnapshot = metrics.get(0); assertGaugeSnapshot(updatedSnapshot, 3.0, labels); - // Removing the metric - collector.removeMetric(buildMetric("name", "group", 4.0)); + // Removing a metric + collector.removeMetric(metricName); metrics = collector.collect(); assertEquals(0, metrics.size()); } @Test public void testCollectNonNumericMetric() { - Map props = new HashMap<>(); - props.put(PrometheusMetricsReporterConfig.ALLOWLIST_CONFIG, "kafka_server_group_name.*"); - PrometheusMetricsReporterConfig config = new PrometheusMetricsReporterConfig(props, new PrometheusRegistry()); - KafkaMetricsCollector collector = new KafkaMetricsCollector(config); - collector.setPrefix("kafka.server"); + KafkaMetricsCollector collector = new KafkaMetricsCollector(); MetricSnapshots metrics = collector.collect(); assertEquals(0, metrics.size()); // Adding a non-numeric metric converted String nonNumericValue = "myValue"; - KafkaMetric nonNumericMetric = buildNonNumericMetric("name", "group", nonNumericValue); - collector.addMetric(nonNumericMetric); + MetricName metricName = new MetricName("name", "group", "description", tagsMap); + MetricWrapper metricWrapper = newNonNumericMetric(metricName, nonNumericValue); + collector.addMetric(metricName, metricWrapper); metrics = collector.collect(); assertEquals(1, metrics.size()); @@ -109,19 +98,6 @@ public void testCollectNonNumericMetric() { assertEquals(expectedLabels, snapshot.getDataPoints().get(0).getLabels()); } - @Test - public void testLabelsFromTags() { - Map tags = new LinkedHashMap<>(); - tags.put("k-1", "v1"); - tags.put("k_1", "v2"); - - Labels labels = KafkaMetricsCollector.labelsFromTags(tags, "name"); - - assertEquals("k_1", PrometheusNaming.sanitizeLabelName("k-1")); - assertEquals("v1", labels.get("k_1")); - assertEquals(1, labels.size()); - } - private void assertGaugeSnapshot(MetricSnapshot snapshot, double expectedValue, Labels expectedLabels) { assertInstanceOf(GaugeSnapshot.class, snapshot); GaugeSnapshot gaugeSnapshot = (GaugeSnapshot) snapshot; @@ -131,24 +107,28 @@ private void assertGaugeSnapshot(MetricSnapshot snapshot, double expectedValue, assertEquals(expectedLabels, datapoint.getLabels()); } - private KafkaMetric buildMetric(String name, String group, double value) { + private MetricWrapper newMetric(MetricName metricName, double value) { Measurable measurable = (config, now) -> value; - return new KafkaMetric( + KafkaMetric kafkaMetric = new KafkaMetric( new Object(), - new MetricName(name, group, "", tagsMap), + metricName, measurable, metricConfig, time); + String prometheusName = MetricWrapper.prometheusName(METRIC_PREFIX, metricName); + return new MetricWrapper(prometheusName, kafkaMetric, metricName.name()); } - private KafkaMetric buildNonNumericMetric(String name, String group, String value) { - Gauge measurable = (config, now) -> value; - return new KafkaMetric( + private MetricWrapper newNonNumericMetric(MetricName metricName, String value) { + Gauge gauge = (config, now) -> value; + KafkaMetric kafkaMetric = new KafkaMetric( new Object(), - new MetricName(name, group, "", tagsMap), - measurable, + metricName, + gauge, metricConfig, time); + String prometheusName = MetricWrapper.prometheusName(METRIC_PREFIX, metricName); + return new MetricWrapper(prometheusName, kafkaMetric, metricName.name()); } } diff --git a/src/test/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporterTest.java b/src/test/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporterTest.java index 4dd32fe..bf0e487 100644 --- a/src/test/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporterTest.java +++ b/src/test/java/io/strimzi/kafka/metrics/KafkaPrometheusMetricsReporterTest.java @@ -38,32 +38,37 @@ public void testLifeCycle() throws Exception { KafkaPrometheusMetricsReporter reporter = new KafkaPrometheusMetricsReporter(new PrometheusRegistry()); Map configs = new HashMap<>(); configs.put(PrometheusMetricsReporterConfig.LISTENER_CONFIG, "http://:0"); + configs.put(PrometheusMetricsReporterConfig.ALLOWLIST_CONFIG, "kafka_server_group_name.*"); reporter.configure(configs); reporter.contextChange(new KafkaMetricsContext("kafka.server")); - Optional port = reporter.getPort(); - assertTrue(port.isPresent()); - int initialMetrics = getMetrics(port.get()).size(); - KafkaMetric metric1 = buildMetric("name1", "group", 0); - reporter.init(Collections.singletonList(metric1)); + int port = reporter.getPort().orElseThrow(); + // initialMetrics is used because JVM metrics are added to the registry + int initialMetrics = getMetrics(port).size(); - List metrics = getMetrics(port.get()); - assertEquals(initialMetrics + 1, metrics.size()); + // Adding a metric not matching the allowlist does nothing + KafkaMetric metric1 = buildMetric("other", "group", 0); + reporter.init(Collections.singletonList(metric1)); + List metrics = getMetrics(port); + assertEquals(initialMetrics, metrics.size()); - KafkaMetric metric2 = buildMetric("name2", "group", 0); + // Adding a metric that matches the allowlist + KafkaMetric metric2 = buildMetric("name", "group", 0); reporter.metricChange(metric2); - metrics = getMetrics(port.get()); - assertEquals(initialMetrics + 2, metrics.size()); + metrics = getMetrics(port); + assertEquals(initialMetrics + 1, metrics.size()); - KafkaMetric metric3 = buildNonNumericMetric("name3", "group"); + // Adding a non-numeric metric + KafkaMetric metric3 = buildNonNumericMetric("name1", "group"); reporter.metricChange(metric3); - metrics = getMetrics(port.get()); - assertEquals(initialMetrics + 3, metrics.size()); - - reporter.metricRemoval(metric1); - metrics = getMetrics(port.get()); + metrics = getMetrics(port); assertEquals(initialMetrics + 2, metrics.size()); + // Removing a metric + reporter.metricRemoval(metric3); + metrics = getMetrics(port); + assertEquals(initialMetrics + 1, metrics.size()); + reporter.close(); } diff --git a/src/test/java/io/strimzi/kafka/metrics/MetricWrapperTest.java b/src/test/java/io/strimzi/kafka/metrics/MetricWrapperTest.java index f4b4ee4..518bf9c 100644 --- a/src/test/java/io/strimzi/kafka/metrics/MetricWrapperTest.java +++ b/src/test/java/io/strimzi/kafka/metrics/MetricWrapperTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.api.Test; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -17,21 +19,30 @@ public class MetricWrapperTest { @Test public void testLabelsFromScope() { - MetricWrapper mw = new MetricWrapper("", "k1.v1.k2.v2", null, ""); - assertEquals(Labels.of("k1", "v1", "k2", "v2"), mw.labels()); - mw = new MetricWrapper("", "k1.v1.k2.v2.", null, ""); - assertEquals(Labels.of("k1", "v1", "k2", "v2"), mw.labels()); - mw = new MetricWrapper("", null, null, ""); - assertEquals(Labels.EMPTY, mw.labels()); - mw = new MetricWrapper("", "k1", null, ""); - assertEquals(Labels.EMPTY, mw.labels()); - mw = new MetricWrapper("", "k1.", null, ""); - assertEquals(Labels.EMPTY, mw.labels()); - mw = new MetricWrapper("", "k1.v1.k", null, ""); - assertEquals(Labels.EMPTY, mw.labels()); - - mw = new MetricWrapper("", "k-1.v1.k_1.v2", null, ""); - Labels labels = mw.labels(); + assertEquals(Labels.of("k1", "v1", "k2", "v2"), MetricWrapper.labelsFromScope("k1.v1.k2.v2", "name")); + assertEquals(Labels.EMPTY, MetricWrapper.labelsFromScope(null, "name")); + assertEquals(Labels.EMPTY, MetricWrapper.labelsFromScope("k1", "name")); + assertEquals(Labels.EMPTY, MetricWrapper.labelsFromScope("k1.", "name")); + assertEquals(Labels.EMPTY, MetricWrapper.labelsFromScope("k1.v1.k", "name")); + + Labels labels = MetricWrapper.labelsFromScope("k-1.v1.k_1.v2", "name"); + assertEquals("k_1", PrometheusNaming.sanitizeLabelName("k-1")); + assertEquals("v1", labels.get("k_1")); + assertEquals(1, labels.size()); + } + + @Test + public void testLabelsFromTags() { + Map tags = new LinkedHashMap<>(); + tags.put("k1", "v1"); + tags.put("k2", "v2"); + Labels labels = MetricWrapper.labelsFromTags(tags, ""); + assertEquals(Labels.of("k1", "v1", "k2", "v2"), labels); + + tags = new LinkedHashMap<>(); + tags.put("k-1", "v1"); + tags.put("k_1", "v2"); + labels = MetricWrapper.labelsFromTags(tags, ""); assertEquals("k_1", PrometheusNaming.sanitizeLabelName("k-1")); assertEquals("v1", labels.get("k_1")); assertEquals(1, labels.size());