Add ability to exclude metrics

Handle exclusion rules so that the matching metrics are not reported to
CloudWatch.
This is useful to contain the metrics to only the wanted ones,
minimizing network traffic and, crucially, AWS bills.

This change is an adaptation of the approach used by [1] and by [2].

[1] https://gerrit-review.googlesource.com/c/plugins/metrics-reporter-prometheus/+/253638
[2] https://gerrit-review.googlesource.com/c/plugins/metrics-reporter-jmx/+/253634

Feature: Issue 13148
Change-Id: I82635cd152582e7f10eff8ebbe0eeaf29d868e7e
diff --git a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
index 1e218e4..91b7f63 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
@@ -13,7 +13,6 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.metricsreportercloudwatch;
 
-import com.codahale.metrics.MetricFilter;
 import com.codahale.metrics.MetricRegistry;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -41,7 +40,7 @@
                 registry, cloudWatchAsyncClientBuilder.build(), config.getNamespace())
             .convertRatesTo(TimeUnit.SECONDS)
             .convertDurationsTo(TimeUnit.MILLISECONDS)
-            .filter(MetricFilter.ALL)
+            .filter(config.getExclusionFilter())
             .withZeroValuesSubmission()
             .withReportRawCountValue()
             .withHighResolution();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
index 858d5d2..b5960f7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
@@ -13,18 +13,26 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.metricsreportercloudwatch;
 
+import static java.util.stream.Collectors.toList;
+
+import com.codahale.metrics.MetricFilter;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
 
 class GerritCloudwatchReporterConfig {
   protected static final String KEY_NAMESPACE = "namespace";
   protected static final String KEY_RATE = "rate";
   protected static final String KEY_DRYRUN = "dryRun";
   protected static final String KEY_INITIAL_DELAY = "initialDelay";
+  protected static final String KEY_EXCLUDE_METRICS = "excludeMetrics";
 
   protected static final String DEFAULT_NAMESPACE = "gerrit";
   protected static final String DEFAULT_EMPTY_STRING = "";
@@ -36,6 +44,7 @@
   private final String namespace;
   private final int initialDelay;
   private final Boolean dryRun;
+  private final MetricFilter exclusionFilter;
 
   @Inject
   public GerritCloudwatchReporterConfig(
@@ -59,6 +68,8 @@
                 pluginConfig.getString(KEY_INITIAL_DELAY, DEFAULT_EMPTY_STRING),
                 DEFAULT_INITIAL_DELAY_SECS,
                 TimeUnit.SECONDS);
+
+    this.exclusionFilter = buildExclusionFilter(pluginConfig.getStringList(KEY_EXCLUDE_METRICS));
   }
 
   public int getRate() {
@@ -76,4 +87,17 @@
   public Boolean getDryRun() {
     return dryRun;
   }
+
+  public MetricFilter getExclusionFilter() {
+    return exclusionFilter;
+  }
+
+  private MetricFilter buildExclusionFilter(String[] exclusionList) {
+    final List<Pattern> excludedMetricPatterns =
+        Arrays.stream(exclusionList).map(Pattern::compile).collect(toList());
+
+    Predicate<String> filter =
+        s -> excludedMetricPatterns.stream().anyMatch(e -> e.matcher(s).matches());
+    return (s, metric) -> !filter.test(s);
+  }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 36d1271..2d6da8c 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -50,4 +50,13 @@
 execution.
     * Type: Time
     * Default: "0"
-    * Example: 60 seconds
\ No newline at end of file
+    * Example: 60 seconds
+
+* `plugin.@PLUGIN@.excludeMetrics` (Optional): Regex pattern used to exclude
+metrics from the report. It can be specified multiple times.
+Note that pattern matching is done on the whole metric name, not only on a part of it.
+    * Type: String
+    * Example: "plugins.*"
+
+In case of invalid pattern, the plugin will fail to load and the relevant error will
+be logged in the _error_log_ file.
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java
index 0a6be81..dbaf40d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java
@@ -14,10 +14,15 @@
 package com.googlesource.gerrit.plugins.metricsreportercloudwatch;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static org.mockito.Mockito.when;
 
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.MetricFilter;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
+import java.util.Arrays;
+import java.util.regex.PatternSyntaxException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -64,4 +69,35 @@
     assertThat(reporterConfig.getRate()).isEqualTo(180);
     assertThat(reporterConfig.getDryRun()).isTrue();
   }
+
+  @Test
+  public void shouldReadCorrectExclusionFilter() {
+    PluginConfig globalPluginConfig = emptyGlobalPluginConfig;
+    globalPluginConfig.setStringList(
+        GerritCloudwatchReporterConfig.KEY_EXCLUDE_METRICS, Arrays.asList("foo.*", ".*bar"));
+
+    when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(globalPluginConfig);
+    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+
+    MetricFilter exclusionFilter = reporterConfig.getExclusionFilter();
+    assertThat(exclusionFilter.matches("foo/metrics/for/testing", new Counter())).isFalse();
+    assertThat(exclusionFilter.matches("some/metrics/for/bar", new Counter())).isFalse();
+    assertThat(exclusionFilter.matches("any/other/metric", new Counter())).isTrue();
+  }
+
+  @Test
+  public void shouldThrowAnExceptionWhenExcludeMetricsRegexIsNotValid() {
+    final String INVALID_REGEXP = "[[?";
+    PluginConfig globalPluginConfig = emptyGlobalPluginConfig;
+    globalPluginConfig.setString(
+        GerritCloudwatchReporterConfig.KEY_EXCLUDE_METRICS, INVALID_REGEXP);
+
+    when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(globalPluginConfig);
+
+    assertThrows(
+        PatternSyntaxException.class,
+        () -> {
+          new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+        });
+  }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
index 61066cf..463d05f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
@@ -14,6 +14,7 @@
 
 package com.googlesource.gerrit.plugins.metricsreportercloudwatch;
 
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import com.google.common.base.Splitter;
@@ -85,6 +86,26 @@
                 .anyMatch(l -> l.contains("Value=" + TEST_METRIC_INCREMENT)));
   }
 
+  @Test
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.dryrun", value = "true")
+  @GerritConfig(
+      name = "plugin.metrics-reporter-cloudwatch.excludeMetrics",
+      value = TEST_METRIC_NAME)
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.rate", value = TEST_TIMEOUT)
+  public void shouldExcludeMetrics() {
+    InMemoryLoggerAppender dryRunMetricsOutput = newInMemoryLogger();
+
+    assertThrows(
+        InterruptedException.class,
+        () -> {
+          waitUntil(
+              () ->
+                  dryRunMetricsOutput
+                      .metricsStream()
+                      .anyMatch(l -> l.contains("MetricName=" + TEST_METRIC_NAME)));
+        });
+  }
+
   private static InMemoryLoggerAppender newInMemoryLogger() {
     InMemoryLoggerAppender dryRunMetricsOutput = new InMemoryLoggerAppender();
     for (Enumeration<?> logger = LogManager.getCurrentLoggers(); logger.hasMoreElements(); ) {