Initial implementation of cloudwatch reporter

Report gerrit metrics to AWS cloudwatch

Feature: Issue 13125
Change-Id: I62a96fff0999e301e4e3dc7e0d9fb8af33efbcae
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..1be89ee
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,58 @@
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+gerrit_plugin(
+    name = "metrics-reporter-cloudwatch",
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: metrics-reporter-cloudwatch",
+        "Gerrit-Module: com.googlesource.gerrit.plugins.metricsreportercloudwatch.GerritCloudwatchModule",
+    ],
+    resources = glob(["src/main/resources/**/*"]),
+    deps = [
+        "@dropwizard_metrics_cloudwatch//jar",
+        "@dropwizard_metrics_jvm//jar",
+        "@awssdk_cloudwatch//jar",
+        "@awssdk_aws_core//jar",
+        "@awssdk_regions//jar",
+        "@aws_java_sdk_core//jar",
+        "@awssdk_auth//jar",
+        "@awssdk_sdk_core//jar",
+        "@awssdk_utils//jar",
+        "@awssdk_http_client_spi//jar",
+        "@awssdk_profiles//jar",
+        "@awssdk_query_protocol//jar",
+        "@awssdk_protocol_core//jar",
+        "@io_netty_all//jar",
+        "@awssdk_netty_nio_client//jar",
+        "@awssdk_metrics_spi//jar",
+        "@reactivestreams//jar",
+        "@jackson_core//jar",
+        "@jackson_annotations//jar",
+        "@jackson_databind//jar",
+    ],
+)
+
+junit_tests(
+    name = "metrics-reporter-cloudwatch_tests",
+    srcs = glob(["src/test/java/**/*.java"]),
+    resources = glob(["src/test/resources/**/*"]),
+    deps = [
+        ":metrics-reporter-cloudwatch__plugin_test_deps",
+    ],
+)
+
+java_library(
+    name = "metrics-reporter-cloudwatch__plugin_test_deps",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":metrics-reporter-cloudwatch__plugin",
+    ],
+)
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
new file mode 100644
index 0000000..b20ee27
--- /dev/null
+++ b/external_plugin_deps.bzl
@@ -0,0 +1,123 @@
+load("//tools/bzl:maven_jar.bzl", "maven_jar")
+
+def external_plugin_deps():
+  maven_jar(
+    name = 'dropwizard_metrics_cloudwatch',
+    artifact = 'io.github.azagniotov:dropwizard-metrics-cloudwatch:2.0.5',
+    sha1 = '7fed805d8fb31e54d75597a2b4f5b958eecad0ab',
+  )
+
+  maven_jar(
+    name = 'dropwizard_metrics_jvm',
+    artifact = 'io.dropwizard.metrics:metrics-jvm:4.1.10.1',
+    sha1 = '88d9e476c2944a9f0158474dc2b7064c96e26317',
+  )
+
+  maven_jar(
+    name = 'reactivestreams',
+    artifact = 'org.reactivestreams:reactive-streams:1.0.3',
+    sha1 = 'd9fb7a7926ffa635b3dcaa5049fb2bfa25b3e7d0',
+  )
+
+  maven_jar(
+    name = 'awssdk_cloudwatch',
+    artifact = 'software.amazon.awssdk:cloudwatch:2.13.54',
+    sha1 = '46e4f3dd21f1b6a61f08ae7195f0f025e207af5f',
+  )
+
+  maven_jar(
+    name = 'awssdk_auth',
+    artifact = 'software.amazon.awssdk:auth:2.13.54',
+    sha1 = '68b522302874b580ecd5563fe58e492136a24c81',
+  )
+
+  maven_jar(
+    name = 'awssdk_sdk_core',
+    artifact = 'software.amazon.awssdk:sdk-core:2.13.54',
+    sha1 = '10163b0cbe76600891e74516b958ee7628e70e2a',
+  )
+
+  maven_jar(
+    name = 'awssdk_aws_core',
+    artifact = 'software.amazon.awssdk:aws-core:2.13.54',
+    sha1 = '356c0c26afa7fb2a1edb921fc16e9de6a533f559',
+  )
+
+  maven_jar(
+    name = 'awssdk_profiles',
+    artifact = 'software.amazon.awssdk:profiles:2.13.54',
+    sha1 = '705146ffaa1aab5442791d57238b4ba304787e16',
+  )
+
+  maven_jar(
+    name = 'awssdk_regions',
+    artifact = 'software.amazon.awssdk:regions:2.13.54',
+    sha1 = '0de5b12c51d5332720fcb32581af3bd3ff88b21c',
+  )
+
+  maven_jar(
+    name = 'awssdk_metrics_spi',
+    artifact = 'software.amazon.awssdk:metrics-spi:2.13.54',
+    sha1 = '0610a43ba773be1a05fdf05416bb671d0eaa9916',
+  )
+
+  maven_jar(
+    name = 'awssdk_utils',
+    artifact = 'software.amazon.awssdk:utils:2.13.54',
+    sha1 = 'dae698acd98027117a13550f106b1ea6539d076a',
+  )
+
+  maven_jar(
+    name = 'awssdk_http_client_spi',
+    artifact = 'software.amazon.awssdk:http-client-spi:2.13.54',
+    sha1 = 'bdecd06aa2793f1366b1eeaa385314f22336fddb',
+  )
+
+  maven_jar(
+    name = 'awssdk_query_protocol',
+    artifact = 'software.amazon.awssdk:aws-query-protocol:2.13.54',
+    sha1 = '2145252d0352b89ee59d3a4fab6b6ea03b032eb1',
+  )
+
+  maven_jar(
+    name = 'awssdk_protocol_core',
+    artifact = 'software.amazon.awssdk:protocol-core:2.13.54',
+    sha1 = '28bc71e1450dd1515a251d74511652ccf0f2af01',
+  )
+
+
+  maven_jar(
+    name = 'awssdk_netty_nio_client',
+    artifact = 'software.amazon.awssdk:netty-nio-client:2.13.54',
+    sha1 = '03de0e583e6b244743915640fa9d00d68c5fd077',
+  )
+
+  maven_jar(
+    name = 'io_netty_all',
+    artifact = 'io.netty:netty-all:4.1.51.Final',
+    sha1 = '5e5f741acc4c211ac4572c31c7e5277ec465e4e4',
+  )
+
+  maven_jar(
+      name = 'aws_java_sdk_core',
+      artifact = 'com.amazonaws:aws-java-sdk-core:1.11.820',
+      sha1 = '8902eefbbcd087a89e57a3e88c8e383ed0d7bab9',
+    )
+
+  maven_jar(
+      name = 'jackson_core',
+      artifact = 'com.fasterxml.jackson.core:jackson-core:2.11.1',
+      sha1 = '8b02908d53183fdf9758e7e20f2fdee87613a962',
+    )
+
+  maven_jar(
+      name = 'jackson_annotations',
+      artifact = 'com.fasterxml.jackson.core:jackson-annotations:2.11.1',
+      sha1 = 'f083c4ac0fb8b3c6b8d5b62cd54122228ef62cee',
+    )
+
+  maven_jar(
+      name = 'jackson_databind',
+      artifact = 'com.fasterxml.jackson.core:jackson-databind:2.11.1',
+      sha1 = 'f5d24a1dcf46000316d40c8c61196c48dd5677c5',
+    )
diff --git a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchModule.java b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchModule.java
new file mode 100644
index 0000000..50ae062
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// 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 com.googlesource.gerrit.plugins.metricsreportercloudwatch;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.inject.Scopes;
+
+public class GerritCloudwatchModule extends LifecycleModule {
+
+  @Override
+  protected void configure() {
+    bind(GerritCloudwatchReporterConfig.class).in(Scopes.SINGLETON);
+    bind(GerritCloudwatchReporter.class).in(Scopes.SINGLETON);
+    listener().to(GerritCloudwatchReporter.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
new file mode 100644
index 0000000..1e218e4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporter.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// 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 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;
+import com.google.inject.Inject;
+import io.github.azagniotov.metrics.reporter.cloudwatch.CloudWatchReporter;
+import java.util.concurrent.TimeUnit;
+import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
+import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClientBuilder;
+
+public class GerritCloudwatchReporter implements LifecycleListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final CloudWatchReporter cloudWatchReporter;
+  private final GerritCloudwatchReporterConfig config;
+
+  @Inject
+  public GerritCloudwatchReporter(GerritCloudwatchReporterConfig config, MetricRegistry registry)
+      throws IllegalStateException {
+    this.config = config;
+
+    CloudWatchAsyncClientBuilder cloudWatchAsyncClientBuilder = CloudWatchAsyncClient.builder();
+
+    CloudWatchReporter.Builder cloudWatchReporterBuilder =
+        CloudWatchReporter.forRegistry(
+                registry, cloudWatchAsyncClientBuilder.build(), config.getNamespace())
+            .convertRatesTo(TimeUnit.SECONDS)
+            .convertDurationsTo(TimeUnit.MILLISECONDS)
+            .filter(MetricFilter.ALL)
+            .withZeroValuesSubmission()
+            .withReportRawCountValue()
+            .withHighResolution();
+
+    if (config.getDryRun()) {
+      cloudWatchReporterBuilder = cloudWatchReporterBuilder.withDryRun();
+    }
+
+    cloudWatchReporter = cloudWatchReporterBuilder.build();
+  }
+
+  @Override
+  public void start() {
+    logger.atInfo().log(
+        String.format(
+            "Reporting to CloudWatch [namespace:'%s'] at rate of '%d' seconds, after initial delay of %d seconds",
+            config.getNamespace(), config.getRate(), config.getInitialDelay()));
+    cloudWatchReporter.start(config.getInitialDelay(), config.getRate(), TimeUnit.SECONDS);
+  }
+
+  @Override
+  public void stop() {
+    cloudWatchReporter.stop();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
new file mode 100644
index 0000000..858d5d2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfig.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// 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 com.googlesource.gerrit.plugins.metricsreportercloudwatch;
+
+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.concurrent.TimeUnit;
+
+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 DEFAULT_NAMESPACE = "gerrit";
+  protected static final String DEFAULT_EMPTY_STRING = "";
+  protected static final Boolean DEFAULT_DRY_RUN = false;
+  protected static final Long DEFAULT_RATE_SECS = 60L;
+  protected static final Integer DEFAULT_INITIAL_DELAY_SECS = 0;
+
+  private final int rate;
+  private final String namespace;
+  private final int initialDelay;
+  private final Boolean dryRun;
+
+  @Inject
+  public GerritCloudwatchReporterConfig(
+      PluginConfigFactory configFactory, @PluginName String pluginName) {
+    PluginConfig pluginConfig = configFactory.getFromGerritConfig(pluginName);
+
+    this.namespace = pluginConfig.getString(KEY_NAMESPACE, DEFAULT_NAMESPACE);
+
+    this.dryRun = pluginConfig.getBoolean(KEY_DRYRUN, DEFAULT_DRY_RUN);
+
+    this.rate =
+        (int)
+            ConfigUtil.getTimeUnit(
+                pluginConfig.getString(KEY_RATE, DEFAULT_EMPTY_STRING),
+                DEFAULT_RATE_SECS,
+                TimeUnit.SECONDS);
+
+    this.initialDelay =
+        (int)
+            ConfigUtil.getTimeUnit(
+                pluginConfig.getString(KEY_INITIAL_DELAY, DEFAULT_EMPTY_STRING),
+                DEFAULT_INITIAL_DELAY_SECS,
+                TimeUnit.SECONDS);
+  }
+
+  public int getRate() {
+    return rate;
+  }
+
+  public String getNamespace() {
+    return namespace;
+  }
+
+  public int getInitialDelay() {
+    return initialDelay;
+  }
+
+  public Boolean getDryRun() {
+    return dryRun;
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..f40b8bb
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,4 @@
+# About
+
+This plugin sends Gerrit metrics to Cloud Watch in AWS.
+You can find more information on CloudWatch [here](https://aws.amazon.com/cloudwatch/)
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
new file mode 100644
index 0000000..a634655
--- /dev/null
+++ b/src/main/resources/Documentation/build.md
@@ -0,0 +1,35 @@
+# Build
+
+This plugin can be built with Bazel.
+
+Clone (or link) this plugin to the `plugins` directory of Gerrit's source tree.
+Put the external dependency Bazel build file into the Gerrit /plugins directory,
+replacing the existing empty one.
+
+```
+  cd gerrit/plugins
+  ln -fs metrics-reporter-cloudwatch/external_plugin_deps.bzl .
+```
+
+Then run:
+
+```bash
+  bazelisk build plugins/metrics-reporter-cloudwatch
+```
+
+in the root of Gerrit's source tree to build
+The output is created in:
+
+```
+  bazel-bin/plugins/metrics-reporter-cloudwatch/metrics-reporter-cloudwatch.jar
+```
+
+# Test
+
+
+You can run tests with bazelisk, as such:
+
+```bash
+bazelisk plugins/metrics-reporter-cloudwatch:metrics-reporter-cloudwatch_tests
+```
+
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..36d1271
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,53 @@
+# Configuration
+
+The configuration of the @PLUGIN@ plugin is done in the `[plugin "@PLUGIN@"]`
+section of the `gerrit.config` file.
+
+## Authentication
+
+To make requests to AWS, this plugin uses the default AWS credential provider
+chain. This means that the java SDK will try to find the relevant AWS
+credentials (and region) by looking, in order to environment variables
+(`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`), then system properties
+`aws.accessKeyId` and `aws.secretKey`, then `Web Identity Token`, your
+`~/.aws/credentials`, and so on.
+
+Find the all the details about the default provider chain
+[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)
+
+## Metrics Reporter
+
+* `plugin.@PLUGIN@.dryRun` (Optional): the reporter will log.DEBUG the metrics,
+instead of doing a real POST to CloudWatch.
+    * Type: Boolean
+    * Default: false
+    * Example: true
+
+There will also be a log entry at WARN level to inform the plugin is running in
+dry-run mode:
+
+```** Reporter is running in 'DRY RUN' mode **```
+
+To observe the metrics increase the log level, as such:
+
+```bash
+ssh -p <port> admin@<server> gerrit logging set-level debug io.github.azagniotov.metrics.reporter.cloudwatch.CloudWatchReporter
+```
+
+* `plugin.@PLUGIN@.namespace` (Optional): The CloudWatch namespace for Gerrit metrics.
+    * Type: String
+    * Default: "gerrit"
+    * AWS Docs: [Namespaces](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Namespace)
+    * Example: "my-gerrit-metrics"
+
+* `plugin.@PLUGIN@.rate` (Optional): The rate at which metrics should be fired to AWS.
+    * Type: Time
+    * Default: "60s"
+    * Example: 5m
+
+* `plugin.@PLUGIN@.initialDelay` (Optional): The time to delay the first reporting
+execution.
+    * Type: Time
+    * Default: "0"
+    * Example: 60 seconds
\ 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
new file mode 100644
index 0000000..0a6be81
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/GerritCloudwatchReporterConfigTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// 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 com.googlesource.gerrit.plugins.metricsreportercloudwatch;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.when;
+
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class GerritCloudwatchReporterConfigTest {
+  private static final String PLUGIN_NAME = "foo";
+  private final PluginConfig emptyGlobalPluginConfig = new PluginConfig(PLUGIN_NAME, new Config());
+
+  GerritCloudwatchReporterConfig reporterConfig;
+
+  @Mock PluginConfigFactory configFactory;
+
+  @Test
+  public void shouldGetAllDefaultsWhenConfigurationIsEmpty() {
+    when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(emptyGlobalPluginConfig);
+    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+
+    assertThat(reporterConfig.getInitialDelay())
+        .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_INITIAL_DELAY_SECS);
+    assertThat(reporterConfig.getNamespace())
+        .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_NAMESPACE);
+    assertThat(reporterConfig.getRate())
+        .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_RATE_SECS);
+    assertThat(reporterConfig.getDryRun())
+        .isEqualTo(GerritCloudwatchReporterConfig.DEFAULT_DRY_RUN);
+  }
+
+  @Test
+  public void shouldReadMetricValuesFromConfiguration() {
+    PluginConfig globalPluginConfig = emptyGlobalPluginConfig;
+    globalPluginConfig.setString(GerritCloudwatchReporterConfig.KEY_NAMESPACE, "foobar");
+    globalPluginConfig.setString(GerritCloudwatchReporterConfig.KEY_RATE, "3m");
+    globalPluginConfig.setString(GerritCloudwatchReporterConfig.KEY_INITIAL_DELAY, "20s");
+    globalPluginConfig.setBoolean(GerritCloudwatchReporterConfig.KEY_DRYRUN, true);
+
+    when(configFactory.getFromGerritConfig(PLUGIN_NAME)).thenReturn(globalPluginConfig);
+    reporterConfig = new GerritCloudwatchReporterConfig(configFactory, PLUGIN_NAME);
+
+    assertThat(reporterConfig.getInitialDelay()).isEqualTo(20);
+    assertThat(reporterConfig.getNamespace()).isEqualTo("foobar");
+    assertThat(reporterConfig.getRate()).isEqualTo(180);
+    assertThat(reporterConfig.getDryRun()).isTrue();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
new file mode 100644
index 0000000..9e79351
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/metricsreportercloudwatch/MetricsReporterCloudwatchIT.java
@@ -0,0 +1,133 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed 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
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// 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 com.googlesource.gerrit.plugins.metricsreportercloudwatch;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Stopwatch;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.spi.LoggingEvent;
+import org.junit.Test;
+
+@UseLocalDisk
+@TestPlugin(
+    name = "metrics-reporter-cloudwatch",
+    sysModule = "com.googlesource.gerrit.plugins.metricsreportercloudwatch.GerritCloudwatchModule")
+public class MetricsReporterCloudwatchIT extends LightweightPluginDaemonTest {
+  private static final String TEST_METRIC_NAME = "test/metric/name";
+  private static final long TEST_METRIC_INCREMENT = 1234567L;
+  private static final String TEST_TIMEOUT = "10";
+  private static final Duration TEST_TIMEOUT_DURATION =
+      Duration.ofSeconds(Integer.valueOf(TEST_TIMEOUT));
+
+  @Inject private MetricMaker metricMaker;
+  private Counter0 testCounterMetric;
+
+  @Override
+  public void setUpTestPlugin() throws Exception {
+    System.setProperty("aws.region", "us-west-1");
+
+    testCounterMetric = metricMaker.newCounter(TEST_METRIC_NAME, new Description("test metric"));
+
+    super.setUpTestPlugin();
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.dryrun", value = "true")
+  public void shouldCloudwatchReporterBeStartedInDryRun() throws Exception {
+    InMemoryLoggerAppender dryRunMetricsOutput = newInMemoryLogger();
+
+    waitUntil(() -> dryRunMetricsOutput.metricsStream().anyMatch(l -> l.contains("DRY RUN")));
+  }
+
+  @Test
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.dryrun", value = "true")
+  @GerritConfig(name = "plugin.metrics-reporter-cloudwatch.rate", value = TEST_TIMEOUT)
+  public void shouldReportMetricValueToCloudwatch() throws Exception {
+    InMemoryLoggerAppender dryRunMetricsOutput = newInMemoryLogger();
+
+    testCounterMetric.incrementBy(TEST_METRIC_INCREMENT);
+
+    waitUntil(
+        () ->
+            dryRunMetricsOutput
+                .metricsStream()
+                .filter(l -> l.contains("MetricName=" + TEST_METRIC_NAME))
+                .anyMatch(l -> l.contains("Value=" + TEST_METRIC_INCREMENT)));
+  }
+
+  private static InMemoryLoggerAppender newInMemoryLogger() {
+    InMemoryLoggerAppender dryRunMetricsOutput = new InMemoryLoggerAppender();
+    for (Enumeration<?> logger = LogManager.getCurrentLoggers(); logger.hasMoreElements(); ) {
+      Logger log = (Logger) logger.nextElement();
+      if (log.getName().contains("CloudWatchReporter")) {
+        log.addAppender(dryRunMetricsOutput);
+        log.setLevel(Level.DEBUG);
+      }
+    }
+    return dryRunMetricsOutput;
+  }
+
+  private static void waitUntil(Supplier<Boolean> waitCondition) throws InterruptedException {
+    Stopwatch stopwatch = Stopwatch.createStarted();
+    while (!waitCondition.get()) {
+      if (stopwatch.elapsed().compareTo(TEST_TIMEOUT_DURATION) > 0) {
+        throw new InterruptedException();
+      }
+      MILLISECONDS.sleep(50);
+    }
+  }
+
+  static class InMemoryLoggerAppender extends AppenderSkeleton {
+    private final Splitter metricsDatumSplitter = Splitter.on("MetricDatum");
+
+    private ArrayList<String> logLines = new ArrayList<>();
+
+    @Override
+    public void close() {}
+
+    @Override
+    public boolean requiresLayout() {
+      return false;
+    }
+
+    @Override
+    protected void append(LoggingEvent event) {
+      String logMessage = event.getMessage().toString();
+      logLines.add(logMessage);
+    }
+
+    public Stream<String> metricsStream() {
+      return logLines.stream().flatMap(metricsDatumSplitter::splitToStream);
+    }
+  }
+}