diff --git a/core/pom.xml b/core/pom.xml index 028db73ef3d..481f309800d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -299,6 +299,7 @@ io[.]micrometer[.]core[.]instrument[.]MeterRegistry + io[.]micrometer[.]core[.]instrument[.]config[.]MeterFilter io[.]opentelemetry[.]api[.]OpenTelemetry org[.]apache[.]hadoop[.]io[.]Text org[.]apache[.]accumulo[.]core[.]client[.].* diff --git a/core/src/main/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactory.java b/core/src/main/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactory.java index 2e24726e869..5d73d5a5563 100644 --- a/core/src/main/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactory.java +++ b/core/src/main/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactory.java @@ -18,14 +18,19 @@ */ package org.apache.accumulo.core.spi.metrics; +import static java.util.Objects.requireNonNull; + import java.util.HashMap; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; import io.micrometer.core.instrument.logging.LoggingRegistryConfig; @@ -49,6 +54,21 @@ *
  *     general.custom.metrics.opts.logging.step = 10s
  * 
+ * + * To filter out meters that start with any particular prefix, set + * {@code general.custom.metrics.opts.meter.id.filters} in the Accumulo configuration. + * + *
+ *      general.custom.metrics.opts.meter.id.filters = prefix1,prefix2,prefix3
+ *
+ * 
+ * + * To assign a custom delimiter to filters, set + * {@code general.custom.metrics.opts.meter.id.delimiter} in the Accumulo configuration. + * + *
+ * general.custom.metrics.opts.meter.id.delimiter = delimiter
+ * 
*/ public class LoggingMeterRegistryFactory implements MeterRegistryFactory { @@ -76,6 +96,49 @@ public MeterRegistry create(final InitParameters params) { LOG.info("Creating logging metrics registry with params: {}", params); metricsProps.putAll(params.getOptions()); - return LoggingMeterRegistry.builder(lconf).loggingSink(metricConsumer).build(); + + MeterRegistry registry = + LoggingMeterRegistry.builder(lconf).loggingSink(metricConsumer).build(); + String filters = metricsProps.get("meter.id.filters"); + // Sets the default delimiter if none is specified + String delimiter = metricsProps.getOrDefault("meter.id.delimiter", ","); + + if (filters != null && delimiter != null) { + registry.config().meterFilter(getMeterFilter(filters, delimiter)); + } + return registry; + } + + /** + * This function uses terms specified in the patternList parameter to filter out specific metrics + * that the user doesn't want. + * + * @param patternList a delimited set of terms that will filter out meters that start with any one + * of those terms. + * @param delimiter that contains either a user specified delimiter, or the default comma + * delimiter to split the patternList. + * @return a predicate with the type of MeterFilter, that describes which metrics to deny and + * subsequently filter out. + */ + public static MeterFilter getMeterFilter(String patternList, String delimiter) { + requireNonNull(patternList, "patternList must not be null"); + + // Trims whitespace and all other non-visible characters. + patternList = patternList.replaceAll("\\s+", ""); + + // Gets the default delimiter or the delimiter the user supplied + String[] patterns = patternList.split(delimiter); + Predicate finalPredicate = id -> false; + + if (patternList.isEmpty()) { + return MeterFilter.deny(finalPredicate); + } + + for (String prefix : patterns) { + Predicate predicate = id -> id.getName().startsWith(prefix); + finalPredicate = finalPredicate.or(predicate); + + } + return MeterFilter.deny(finalPredicate); } } diff --git a/core/src/test/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactoryTest.java b/core/src/test/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactoryTest.java index 9d628d27752..f57b5abe7cd 100644 --- a/core/src/test/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactoryTest.java +++ b/core/src/test/java/org/apache/accumulo/core/spi/metrics/LoggingMeterRegistryFactoryTest.java @@ -18,13 +18,17 @@ */ package org.apache.accumulo.core.spi.metrics; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import java.util.List; import java.util.Map; import org.apache.accumulo.core.spi.common.ServiceEnvironment; import org.junit.jupiter.api.Test; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.logging.LoggingMeterRegistry; class LoggingMeterRegistryFactoryTest { @@ -32,16 +36,24 @@ class LoggingMeterRegistryFactoryTest { @Test public void createTest() { LoggingMeterRegistryFactory factory = new LoggingMeterRegistryFactory(); - var reg = factory.create(new LoggingMetricsParams()); + var reg = + factory.create(new LoggingMetricsParams(Map.of("prop1", "abc", "logging.step", "1s"))); assertInstanceOf(LoggingMeterRegistry.class, reg); } private static class LoggingMetricsParams implements MeterRegistryFactory.InitParameters { + private final Map options; + + public LoggingMetricsParams(Map options) { + this.options = options; + } + @Override public Map getOptions() { + return options; // note: general.custom.metrics.opts. is expected to be stripped before passing the options. - return Map.of("prop1", "abc", "logging.step", "1s"); + // return Map.of("prop1", "abc", "logging.step", "1s", "meter.id.filters", "Foo,Bar,Hat"); } @Override @@ -49,4 +61,107 @@ public ServiceEnvironment getServiceEnv() { return null; } } + + /** + * Create a factory and test that the given filters are applied to all ID's that match the given + * meter.id.filters. + */ + @Test + public void createWithFilters() { + LoggingMeterRegistryFactory factory = new LoggingMeterRegistryFactory(); + var reg = factory.create(new LoggingMetricsParams( + Map.of("prop1", "abc", "logging.step", "1s", "meter.id.filters", "Foo,Bar,Hat"))); + reg.timer("FooTimer", "tag1", "tag2"); // Yes filters this out + reg.timer("ProperTimer", "tag1", "tag2"); + + List meterReg = reg.getMeters(); // Does this only contain ProperTimer? Yes + assertEquals("ProperTimer", meterReg.get(0).getId().getName()); + } + + /** + * Verify that the filters can be split by either the default or user-specified delimiter. + */ + @Test + public void testNonCommaDelimiter() { + LoggingMeterRegistryFactory factory = new LoggingMeterRegistryFactory(); + var reg = factory.create(new LoggingMetricsParams(Map.of("prop1", "abc", "logging.step", "1s", + "meter.id.filters", "Foo:Bar:Hat", "meter.id.delimiter", ":"))); + reg.timer("FooTimer", "tag1", "tag2"); + reg.timer("ProperTimer", "tag1", "tag2"); + reg.timer("BarTimer", "tag1", "tag2"); + reg.timer("HatTimer", "tag1", "tag2"); + reg.gauge("BarGauge", 10); + reg.gauge("ProperGauge", 10); + reg.counter("HatCounter", "tag1", "tag2"); + reg.counter("ProperCounter", "tag1", "tag2"); + + List meterReg = reg.getMeters(); + StringBuilder allMeters = new StringBuilder(); + for (Meter m : meterReg) { + allMeters.append(m.getId().getName()); + } + + assertFalse(allMeters.toString().contains("Foo")); + assertFalse(allMeters.toString().contains("Bar")); + assertFalse(allMeters.toString().contains("Hat")); + } + + /** + * Verify that {@link LoggingMeterRegistryFactory#getMeterFilter(String, String)} only filters out + * meters where the filter terms are prefixes for the ID. + */ + @Test + public void testIfPatternIsNotFirst() { + LoggingMeterRegistryFactory factory = new LoggingMeterRegistryFactory(); + var reg = factory.create(new LoggingMetricsParams( + Map.of("prop1", "abc", "logging.step", "1s", "meter.id.filters", "Foo,Bar,Hat"))); + reg.timer("FooTimer", "tag1", "tag2"); + reg.timer("TimerFoo", "tag1", "tag2"); + + List meterReg = reg.getMeters(); + StringBuilder allMeters = new StringBuilder(); + for (Meter m : meterReg) { + allMeters.append(m.getId().getName()); + } + assertEquals("TimerFoo", allMeters.toString()); + } + + /** + * Test that {@link LoggingMeterRegistryFactory#getMeterFilter(String, String)} can filter out + * meters correctly even when given duplicates. + */ + @Test + public void testIfDuplicatePatterns() { + LoggingMeterRegistryFactory factory = new LoggingMeterRegistryFactory(); + var reg = factory.create(new LoggingMetricsParams( + Map.of("prop1", "abc", "logging.step", "1s", "meter.id.filters", "Foo,Foo,Foo"))); + reg.timer("FooTimer", "tag1", "tag2"); + + List meterReg = reg.getMeters(); + StringBuilder allMeters = new StringBuilder(); + for (Meter m : meterReg) { + allMeters.append(m.getId().getName()); + } + + assertEquals("", allMeters.toString()); + } + + /** + * Test that when {@link LoggingMeterRegistryFactory#getMeterFilter(String, String)} is given an + * empty string of filters, all IDs are still passed through. + */ + @Test + public void testEmptyPatternList() { + LoggingMeterRegistryFactory factory = new LoggingMeterRegistryFactory(); + var reg = factory.create(new LoggingMetricsParams( + Map.of("prop1", "abc", "logging.step", "1s", "meter.id.filters", ""))); + reg.timer("FooTimer", "tag1", "tag2"); + List meterReg = reg.getMeters(); + StringBuilder allMeters = new StringBuilder(); + for (Meter m : meterReg) { + allMeters.append(m.getId().getName()); + } + assertEquals("FooTimer", allMeters.toString()); + } + }