Add InstanceId dimension, when available

When available in the gerrit configuration, metrics are hydrated with
the InstanceId, so that the behaviour of a specific instance can be
observed within a cluster of gerrit instances.

Feature: Issue 13217
Change-Id: I963f52b6f491b7fd84ff227d68fca19ec85138db
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 8cafca5..4803e14 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
@@ -46,6 +46,13 @@
             .withReportRawCountValue()
             .withHighResolution();
 
+    config
+        .getMaybeInstanceId()
+        .ifPresent(
+            instanceId ->
+                cloudWatchReporterBuilder.withGlobalDimensions(
+                    String.format("InstanceId=%s", instanceId)));
+
     if (config.getDryRun()) {
       cloudWatchReporterBuilder.withDryRun();
     }
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 951f526..62535dd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
@@ -16,13 +16,16 @@
 import static java.util.stream.Collectors.toList;
 
 import com.codahale.metrics.MetricFilter;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritInstanceId;
 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.Optional;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
@@ -48,12 +51,17 @@
   private final Boolean dryRun;
   private final MetricFilter exclusionFilter;
   private final Boolean jvmMetrics;
+  private final Optional<String> maybeInstanceId;
 
   @Inject
   public GerritCloudwatchReporterConfig(
-      PluginConfigFactory configFactory, @PluginName String pluginName) {
+      PluginConfigFactory configFactory,
+      @PluginName String pluginName,
+      @Nullable @GerritInstanceId String instanceId) {
     PluginConfig pluginConfig = configFactory.getFromGerritConfig(pluginName);
 
+    this.maybeInstanceId = Optional.ofNullable(instanceId);
+
     this.namespace = pluginConfig.getString(KEY_NAMESPACE, DEFAULT_NAMESPACE);
 
     this.dryRun = pluginConfig.getBoolean(KEY_DRYRUN, DEFAULT_DRY_RUN);
@@ -109,4 +117,8 @@
         s -> excludedMetricPatterns.stream().anyMatch(e -> e.matcher(s).matches());
     return (s, metric) -> !filter.test(s);
   }
+
+  public Optional<String> getMaybeInstanceId() {
+    return maybeInstanceId;
+  }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index cb645c5..b29d4d2 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -16,6 +16,19 @@
 [here](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html) and
 [here](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html)
 
+## InstanceId Dimension
+
+Gerrit can be optionally configured to have a unique identifier, the
+[instanceId](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#gerrit.instanceId),
+which represents a specific instance within a group of Gerrit instances.
+
+When the instanceId is set this plugin will hydrate all the metrics sent to CloudWatch with
+an additional dimension named `InstanceId`, populated with the value of the `gerrit.instanceId`
+configuration.
+
+This is useful as it allows to correlate cloudwatch metrics to specific instances
+they originated from.
+
 ## Metrics Reporter
 
 * `plugin.@PLUGIN@.dryRun` (Optional): the reporter will log.DEBUG the metrics,
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 30c769e..f00ee2d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.regex.PatternSyntaxException;
 import org.eclipse.jgit.lib.Config;
 import org.junit.Test;
@@ -31,6 +32,7 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class GerritCloudwatchReporterConfigTest {
+  private static final String gerritInstanceId = "testInstanceId";
   private static final String PLUGIN_NAME = "foo";
   private final PluginConfig emptyGlobalPluginConfig = new PluginConfig(PLUGIN_NAME, new Config());
 
@@ -41,7 +43,7 @@
   @Test
   public void shouldGetAllDefaultsWhenConfigurationIsEmpty() {
     when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(emptyGlobalPluginConfig);
-    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME, null);
 
     assertThat(reporterConfig.getInitialDelay())
         .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_INITIAL_DELAY_SECS);
@@ -53,6 +55,7 @@
         .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_DRY_RUN);
     assertThat(reporterConfig.getJvmMetrics())
         .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_JVM_METRICS);
+    assertThat(reporterConfig.getMaybeInstanceId()).isEqualTo(Optional.empty());
   }
 
   @Test
@@ -65,13 +68,15 @@
     globalPluginConfig.setBoolean(GerritCloudwatchReporterConfig.KEY_JVM_METRICS, true);
 
     when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(globalPluginConfig);
-    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+    reporterConfig =
+        new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME, gerritInstanceId);
 
     assertThat(reporterConfig.getInitialDelay()).isEqualTo(20);
     assertThat(reporterConfig.getNamespace()).isEqualTo("foobar");
     assertThat(reporterConfig.getRate()).isEqualTo(180);
     assertThat(reporterConfig.getDryRun()).isTrue();
     assertThat(reporterConfig.getJvmMetrics()).isTrue();
+    assertThat(reporterConfig.getMaybeInstanceId()).isEqualTo(Optional.of(gerritInstanceId));
   }
 
   @Test
@@ -81,7 +86,8 @@
         GerritCloudwatchReporterConfig.KEY_EXCLUDE_METRICS, Arrays.asList("foo.*", ".*bar"));
 
     when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(globalPluginConfig);
-    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+    reporterConfig =
+        new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME, gerritInstanceId);
 
     MetricFilter exclusionFilter = reporterConfig.getExclusionFilter();
     assertThat(exclusionFilter.matches("foo/metrics/for/testing", new Counter())).isFalse();
@@ -101,7 +107,7 @@
     assertThrows(
         PatternSyntaxException.class,
         () -> {
-          new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+          new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME, gerritInstanceId);
         });
   }
 }
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 43b448f..94aaf21 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
@@ -44,6 +44,7 @@
     name = "metrics-reporter-cloudwatch",
     sysModule = "com.googlesource.gerrit.plugins.metricsreportercloudwatch.GerritCloudwatchModule")
 public class MetricsReporterCloudwatchIT extends LightweightPluginDaemonTest {
+  private static final String GERRIT_INSTANCE_ID = "testInstanceId";
   private static final String TEST_METRIC_NAME = "test/metric/name";
   private static final long TEST_METRIC_INCREMENT = 1234567L;
   private static final String TEST_JVM_METRIC_NAME = "jvm.uptime";
@@ -137,6 +138,23 @@
         });
   }
 
+  @Test
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.dryrun", value = "true")
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.rate", value = TEST_TIMEOUT)
+  @GerritConfig(name = "gerrit.instanceId", value = GERRIT_INSTANCE_ID)
+  public void shouldAddInstanceIdDimensionWhenAvailable() throws Exception {
+    InMemoryLoggerAppender dryRunMetricsOutput = newInMemoryLogger();
+
+    waitUntil(
+        () ->
+            dryRunMetricsOutput
+                .metricsStream()
+                .anyMatch(
+                    l ->
+                        l.contains(
+                            String.format("Name=InstanceId, Value=%s", GERRIT_INSTANCE_ID))));
+  }
+
   private static InMemoryLoggerAppender newInMemoryLogger() {
     InMemoryLoggerAppender dryRunMetricsOutput = new InMemoryLoggerAppender();
     for (Enumeration<?> logger = LogManager.getCurrentLoggers(); logger.hasMoreElements(); ) {