diff --git a/.github/workflows/maven.yml b/.github/workflows/build.yml similarity index 97% rename from .github/workflows/maven.yml rename to .github/workflows/build.yml index 7960bae8..1b7391df 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/build.yml @@ -30,9 +30,9 @@ jobs: distribution: temurin cache: maven - name: Check formatting - run: ./mvnw --batch-mode fmt:check - - name: Build - run: ./mvnw --batch-mode package + run: ./mvnw -B fmt:check + - name: Build and verify + run: ./mvnw -B verify - name: Update dependency graph if: github.event_name == 'push' && github.ref == 'refs/heads/master' uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 # v5.0.0 diff --git a/RELEASING.md b/RELEASING.md index dc01527c..d8e2ddc1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -91,7 +91,7 @@ It usually appears immediately after the release process is done, but can take a ## Docker images As part of the release process, `./mvnw` will create the git tag. -This tag is picked up by [GitHub Actions](https://github.com/prometheus/cloudwatch_exporter/actions/workflows/maven.yml), which builds and pushes the [Docker images](README.md#docker-images). +This tag is picked up by [GitHub Actions](https://github.com/prometheus/cloudwatch_exporter/actions/workflows/build.yml), which builds and pushes the [Docker images](README.md#docker-images). ## GitHub Release diff --git a/pom.xml b/pom.xml index 60d611c5..faf34a4b 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt repo @@ -50,11 +50,13 @@ ${maven.build.timestamp} 17 + 3.27.7 3.2.4 1.22.0 3.0 0.16.0 - 4.13.2 + 12.1.10 + 6.1.0 5.23.0 2.0.18 2.6 @@ -72,6 +74,8 @@ 3.4.0 3.5.6 2.21.0 + 0.8.14 + @@ -123,12 +127,12 @@ org.eclipse.jetty jetty-server - 12.0.36 + ${jetty.version} org.eclipse.jetty.ee10 jetty-ee10-servlet - 12.0.36 + ${jetty.version} com.github.ben-manes.caffeine @@ -137,9 +141,15 @@ - junit - junit - ${junit.version} + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.version} test @@ -246,9 +256,31 @@ maven-surefire-plugin ${maven-surefire-plugin.version} - -javaagent:${settings.localRepository}/org/mockito/mockito-core/5.23.0/mockito-core-5.23.0.jar -XX:+EnableDynamicAgentLoading -Djdk.net.URLClassPath.disableClassPathURLCheck=true + false + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar @{argLine} -XX:+EnableDynamicAgentLoading -Djdk.net.URLClassPath.disableClassPathURLCheck=true + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + jacoco-prepare-agent + process-test-classes + + prepare-agent + + + + jacoco-report + verify + + report + + + + org.apache.maven.plugins maven-javadoc-plugin @@ -367,4 +399,4 @@ - + \ No newline at end of file diff --git a/scripts/stress-test.sh b/scripts/stress-test.sh new file mode 100755 index 00000000..8c32719e --- /dev/null +++ b/scripts/stress-test.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# Copyright (c) The Prometheus Authors +# +# 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. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +readonly PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +readonly MVNW="${PROJECT_DIR}/mvnw" + +readonly LOG_FILE="stress-test.log" + +usage() { + cat <<'EOF' +Usage: ./scripts/stress-test.sh + +Run CloudWatch Exporter tests multiple times to check for flaky tests. + +Arguments: + Number of times to run the test suite. + +Examples: + ./scripts/stress-test.sh 5 + ./scripts/stress-test.sh 10 +EOF +} + +log() { + echo "[INFO] $*" | tee -a "${LOG_FILE}" +} + +fail() { + echo "[ERROR] $*" | tee -a "${LOG_FILE}" >&2 + exit 1 +} + +main() { + local iterations iteration start_time end_time elapsed_time total_elapsed + + if [[ $# -ne 1 ]]; then + usage + exit 1 + fi + + iterations="$1" + + [[ -x "${MVNW}" ]] || fail "mvnw not found or not executable: ${MVNW}" + + if [[ ! "${iterations}" =~ ^[1-9][0-9]*$ ]]; then + fail "Invalid iterations '${iterations}'. Must be a positive integer." + fi + + rm -f "${LOG_FILE}" + + log "Starting stress test with ${iterations} iteration(s)" + log "" + + log "Building project (initial build)..." + local build_start + build_start="${SECONDS}" + if ! (cd "${PROJECT_DIR}" && ./mvnw -B compile -DskipTests) |& tee -a "${LOG_FILE}"; then + fail "Initial build failed. See ${LOG_FILE} for details." + fi + local build_elapsed + build_elapsed=$((SECONDS - build_start)) + log "Initial build completed in ${build_elapsed}s" + log "" + + log "Running ${iterations} test iteration(s)..." + log "" + + for ((iteration = 1; iteration <= iterations; iteration++)); do + start_time="${SECONDS}" + log "Iteration ${iteration}/${iterations} started" + + if ! (cd "${PROJECT_DIR}" && ./mvnw -B test) |& tee -a "${LOG_FILE}"; then + end_time="${SECONDS}" + elapsed_time=$((end_time - start_time)) + fail "Iteration ${iteration}/${iterations} failed after ${elapsed_time}s. See ${LOG_FILE} for details." + fi + + end_time="${SECONDS}" + elapsed_time=$((end_time - start_time)) + log "Iteration ${iteration}/${iterations} passed (${elapsed_time}s)" + log "" + done + + total_elapsed="${SECONDS}" + + log "All ${iterations} iteration(s) completed successfully in ${total_elapsed}s" +} + +main "$@" diff --git a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java index 71ff755c..d5c960ca 100644 --- a/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java +++ b/src/main/java/io/prometheus/cloudwatch/CloudWatchCollector.java @@ -112,6 +112,7 @@ public CloudWatchCollector(String yamlConfig) { } /* For unittests. */ + @SuppressWarnings("unchecked") protected CloudWatchCollector( String jsonConfig, CloudWatchClient cloudWatchClient, @@ -141,6 +142,7 @@ protected void reloadConfig() throws IOException { } } + @SuppressWarnings("unchecked") protected void loadConfig( Reader in, CloudWatchClient cloudWatchClient, ResourceGroupsTaggingApiClient taggingClient) { loadConfig( @@ -149,6 +151,7 @@ protected void loadConfig( taggingClient); } + @SuppressWarnings("unchecked") private void loadConfig( Map config, CloudWatchClient cloudWatchClient, diff --git a/src/test/java/io/prometheus/cloudwatch/CachingDimensionExpiryTest.java b/src/test/java/io/prometheus/cloudwatch/CachingDimensionExpiryTest.java index 4b77db8a..41efe0c9 100644 --- a/src/test/java/io/prometheus/cloudwatch/CachingDimensionExpiryTest.java +++ b/src/test/java/io/prometheus/cloudwatch/CachingDimensionExpiryTest.java @@ -1,6 +1,6 @@ package io.prometheus.cloudwatch; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheKey; import io.prometheus.cloudwatch.CachingDimensionSource.DimensionExpiry; @@ -8,7 +8,7 @@ import java.time.Instant; import java.util.Collections; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class CachingDimensionExpiryTest { @@ -27,7 +27,7 @@ public void expireAfterCreateUsesDefaultWithEmptyOverrides() { emptyData, Instant.now().toEpochMilli()); - assertEquals(35, Duration.ofNanos(afterCreate).toSeconds()); + assertThat(Duration.ofNanos(afterCreate).toSeconds()).isEqualTo(35); } @Test @@ -42,7 +42,7 @@ public void expireAfterCreateUsesMetricLevelOverride() { emptyData, Instant.now().toEpochMilli()); - assertEquals(100, Duration.ofNanos(afterCreate).toSeconds()); + assertThat(Duration.ofNanos(afterCreate).toSeconds()).isEqualTo(100); } @Test @@ -57,7 +57,7 @@ public void expireAfterCreateUsesDefaultIfNoMatchedOverride() { emptyData, Instant.now().toEpochMilli()); - assertEquals(35, Duration.ofNanos(afterCreate).toSeconds()); + assertThat(Duration.ofNanos(afterCreate).toSeconds()).isEqualTo(35); } @Test @@ -73,7 +73,7 @@ public void expireAfterUpdateUsesCurrentDuration() { Instant.now().toEpochMilli(), 10_000_000); - assertEquals(10_000_000, afterUpdate); + assertThat(afterUpdate).isEqualTo(10_000_000); } @Test @@ -88,7 +88,30 @@ public void expireAfterReadUsesCurrentDuration() { emptyData, Instant.now().toEpochMilli(), 20_000_000); - assertEquals(20_000_000, afterRead); + assertThat(afterRead).isEqualTo(20_000_000); + } + + @Test + public void dimensionCacheKeyEqualsHandlesIdentityNullDifferentTypesAndFields() { + DimensionCacheKey key = createDimensionCacheKey("AWS/S3", "BucketSizeBytes", 100); + DimensionCacheKey same = createDimensionCacheKey("AWS/S3", "BucketSizeBytes", 100); + DimensionCacheKey differentRule = createDimensionCacheKey("AWS/EC2", "CPUUtilization", 100); + DimensionCacheKey differentTags = + new DimensionCacheKey( + createMetricRule("AWS/S3", "BucketSizeBytes", 100), List.of("bucket-a")); + + assertThat(key).isEqualTo(key); + assertThat(key).isEqualTo(same); + assertThat(key).isNotEqualTo(null); + assertThat(key).isNotEqualTo("not a key"); + assertThat(key).isNotEqualTo(differentRule); + assertThat(key).isNotEqualTo(differentTags); + assertThat(key.hashCode()).isEqualTo(same.hashCode()); + } + + @Test + public void dimensionCacheKeyHashCodeHandlesNullFields() { + assertThat(new DimensionCacheKey(null, null).hashCode()).isZero(); } private DimensionCacheKey createDimensionCacheKey( diff --git a/src/test/java/io/prometheus/cloudwatch/CachingDimensionSourceTest.java b/src/test/java/io/prometheus/cloudwatch/CachingDimensionSourceTest.java index ea24bd99..734d1736 100644 --- a/src/test/java/io/prometheus/cloudwatch/CachingDimensionSourceTest.java +++ b/src/test/java/io/prometheus/cloudwatch/CachingDimensionSourceTest.java @@ -1,13 +1,13 @@ package io.prometheus.cloudwatch; import static io.prometheus.cloudwatch.DimensionSource.DimensionData; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; import io.prometheus.cloudwatch.CachingDimensionSource.DimensionCacheConfig; import java.time.Duration; import java.util.Collections; import java.util.List; -import org.junit.Test; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.services.cloudwatch.model.Dimension; public class CachingDimensionSourceTest { @@ -24,8 +24,8 @@ public void cachedFromDelegate() { sut.getDimensions(createMetricRule("AWS/Redshift", "WriteIOPS"), Collections.emptyList()); Dimension dimension = Dimension.builder().name("AWS/Redshift").value("WriteIOPS").build(); - assertEquals(1, source.called); - assertEquals(dimension, expected.getDimensions().get(0).get(0)); + assertThat(source.called).isEqualTo(1); + assertThat(expected.getDimensions().get(0).get(0)).isEqualTo(dimension); } private MetricRule createMetricRule(String namespace, String name) { diff --git a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java index 796e32e6..7433af9b 100644 --- a/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java +++ b/src/test/java/io/prometheus/cloudwatch/CloudWatchCollectorTest.java @@ -1,8 +1,9 @@ package io.prometheus.cloudwatch; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.within; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.never; @@ -12,6 +13,10 @@ import io.prometheus.client.Collector; import io.prometheus.client.CollectorRegistry; import io.prometheus.cloudwatch.RequestsMatchers.*; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Date; @@ -21,8 +26,8 @@ import java.util.List; import java.util.Properties; import java.util.Set; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; import software.amazon.awssdk.services.cloudwatch.model.*; @@ -37,7 +42,7 @@ public class CloudWatchCollectorTest { ResourceGroupsTaggingApiClient taggingClient; CollectorRegistry registry; - @Before + @BeforeEach public void setUp() { cloudWatchClient = Mockito.mock(CloudWatchClient.class); taggingClient = Mockito.mock(ResourceGroupsTaggingApiClient.class); @@ -153,41 +158,36 @@ public void testAllStatistics() throws Exception { .build()) .build()); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_maximum", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_elb_request_count_minimum", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 4.0, - registry.getSampleValue( - "aws_elb_request_count_sample_count", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 5.0, - registry.getSampleValue( - "aws_elb_request_count_sum", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_maximum", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_minimum", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_sample_count", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(4.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_sum", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(5.0, within(.01)); } @Test @@ -262,41 +262,36 @@ public void testAllStatisticsUsingGetMetricData() throws Exception { .build())) .build()); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_maximum", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_elb_request_count_minimum", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 4.0, - registry.getSampleValue( - "aws_elb_request_count_sample_count", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); - assertEquals( - 5.0, - registry.getSampleValue( - "aws_elb_request_count_sum", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_maximum", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_minimum", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_sample_count", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(4.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_sum", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(5.0, within(.01)); } @Test @@ -346,7 +341,7 @@ void assertMetricTimestampEquals( for (Collector.MetricFamilySamples.Sample s : samples.samples) { metricNames.add(s.name); if (s.name.equals(name)) { - assertEquals(expectedTimestamp, (Long) s.timestampMs); + assertThat((Long) s.timestampMs).isEqualTo(expectedTimestamp); return; } } @@ -376,13 +371,12 @@ public void testUsesNewestDatapoint() throws Exception { Datapoint.builder().timestamp(new Date(2).toInstant()).average(2.0).build()) .build()); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(3.0, within(.01)); } @Test @@ -454,31 +448,30 @@ public void testDimensions() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(3.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "b", "myOtherLB"}), - .01); - assertNull( - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] { - "job", - "instance", - "availability_zone", - "load_balancer_name", - "this_extra_dimension_is_ignored" - }, - new String[] {"aws_elb", "", "a", "myLB", "dummy"})); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "b", "myOtherLB"})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] { + "job", + "instance", + "availability_zone", + "load_balancer_name", + "this_extra_dimension_is_ignored" + }, + new String[] {"aws_elb", "", "a", "myLB", "dummy"})) + .isNull(); } @Test @@ -546,25 +539,24 @@ public void testDimensionSelect() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "b", "myLB"}), - .01); - assertNull( - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myOtherLB"})); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "b", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myOtherLB"})) + .isNull(); } @Test @@ -604,25 +596,24 @@ public void testAllSelectDimensionsKnown() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "b", "myLB"}), - .01); - assertNull( - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myOtherLB"})); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "b", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myOtherLB"})) + .isNull(); } @Test @@ -678,25 +669,24 @@ public void testAllSelectDimensionsKnownUsingGetMetricData() throws Exception { .build())) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "b", "myLB"}), - .01); - assertNull( - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myOtherLB"})); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "b", "myLB"})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myOtherLB"})) + .isNull(); } @Test @@ -765,25 +755,24 @@ public void testDimensionSelectRegex() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB1"}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "b", "myLB2"}), - .01); - assertNull( - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myOtherLB"})); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB1"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "b", "myLB2"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myOtherLB"})) + .isNull(); } @Test @@ -837,13 +826,12 @@ public void testGetDimensionsUsesNextToken() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); } @Test @@ -873,18 +861,18 @@ public void testExtendedStatistics() throws Exception { .build()) .build()); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_elb_latency_p95", new String[] {"job", "instance"}, new String[] {"aws_elb", ""}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_latency_p99_99", - new String[] {"job", "instance"}, - new String[] {"aws_elb", ""}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_latency_p95", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_latency_p99_99", + new String[] {"job", "instance"}, + new String[] {"aws_elb", ""})) + .isCloseTo(2.0, within(.01)); } @Test @@ -989,27 +977,24 @@ public void testDynamoIndexDimensions() throws Exception { .datapoints(Datapoint.builder().timestamp(new Date().toInstant()).sum(3.0).build()) .build()); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_dynamodb_consumed_read_capacity_units_index_sum", - new String[] {"job", "instance", "table_name", "global_secondary_index_name"}, - new String[] {"aws_dynamodb", "", "myTable", "myIndex"}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_dynamodb_online_index_consumed_write_capacity_sum", - new String[] {"job", "instance", "table_name", "global_secondary_index_name"}, - new String[] {"aws_dynamodb", "", "myTable", "myIndex"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_dynamodb_consumed_read_capacity_units_sum", - new String[] {"job", "instance", "table_name"}, - new String[] {"aws_dynamodb", "", "myTable"}), - .01); + assertThat( + registry.getSampleValue( + "aws_dynamodb_consumed_read_capacity_units_index_sum", + new String[] {"job", "instance", "table_name", "global_secondary_index_name"}, + new String[] {"aws_dynamodb", "", "myTable", "myIndex"})) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_dynamodb_online_index_consumed_write_capacity_sum", + new String[] {"job", "instance", "table_name", "global_secondary_index_name"}, + new String[] {"aws_dynamodb", "", "myTable", "myIndex"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_dynamodb_consumed_read_capacity_units_sum", + new String[] {"job", "instance", "table_name"}, + new String[] {"aws_dynamodb", "", "myTable"})) + .isCloseTo(3.0, within(.01)); } @Test @@ -1032,13 +1017,12 @@ public void testDynamoNoDimensions() throws Exception { .datapoints(Datapoint.builder().timestamp(new Date().toInstant()).sum(1.0).build()) .build()); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_dynamodb_account_provisioned_read_capacity_utilization_sum", - new String[] {"job", "instance"}, - new String[] {"aws_dynamodb", ""}), - .01); + assertThat( + registry.getSampleValue( + "aws_dynamodb_account_provisioned_read_capacity_utilization_sum", + new String[] {"job", "instance"}, + new String[] {"aws_dynamodb", ""})) + .isCloseTo(1.0, within(.01)); } @Test @@ -1113,27 +1097,26 @@ public void testTagSelectEC2() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-1"}), - .01); - assertNull( - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-2"})); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" - }), - .01); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-1"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-2"})) + .isNull(); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" + })) + .isCloseTo(1.0, within(.01)); } @Test @@ -1197,26 +1180,24 @@ public void testTagSelectWebACL() { Datapoint.builder().timestamp(new Date().toInstant()).sum(200.0).build()) .build()); - assertEquals( - 200.0, - registry.getSampleValue( - "aws_wafv2_counted_requests_sum", - new String[] {"job", "instance", "region", "rule", "web_acl"}, - new String[] {"aws_wafv2", "", "eu-west-1", "WebAclLog", "svc-integration-xxxx"}), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "web_acl", "tag_Monitoring"}, - new String[] { - "aws_wafv2", - "", - "arn:aws:wafv2:eu-west-1:123456789:regional/webacl/svc-integration-xxxx/d177aaf1-b18f-4f84-aa8e-f1c5c40fc426", - "svc-integration-xxxx", - "enabled" - }), - .01); + assertThat( + registry.getSampleValue( + "aws_wafv2_counted_requests_sum", + new String[] {"job", "instance", "region", "rule", "web_acl"}, + new String[] {"aws_wafv2", "", "eu-west-1", "WebAclLog", "svc-integration-xxxx"})) + .isCloseTo(200.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "web_acl", "tag_Monitoring"}, + new String[] { + "aws_wafv2", + "", + "arn:aws:wafv2:eu-west-1:123456789:regional/webacl/svc-integration-xxxx/d177aaf1-b18f-4f84-aa8e-f1c5c40fc426", + "svc-integration-xxxx", + "enabled" + })) + .isCloseTo(1.0, within(.01)); } @Test @@ -1307,33 +1288,30 @@ public void testTagSelectTargetGroup() { Datapoint.builder().timestamp(new Date().toInstant()).average(3.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_applicationelb_un_healthy_host_count_average", - new String[] {"job", "instance", "target_group", "load_balancer"}, - new String[] {"aws_applicationelb", "", "targetgroup/abc-123", "app/myLB/123"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_applicationelb_un_healthy_host_count_average", - new String[] {"job", "instance", "target_group", "load_balancer"}, - new String[] {"aws_applicationelb", "", "targetgroup/abc-234", "app/myLB/123"}), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "target_group", "tag_Monitoring"}, - new String[] { - "aws_applicationelb", - "", - "arn:aws:elasticloadbalancing:us-east-1:121212121212:targetgroup/abc-123", - "targetgroup/abc-123", - "enabled" - }), - .01); + assertThat( + registry.getSampleValue( + "aws_applicationelb_un_healthy_host_count_average", + new String[] {"job", "instance", "target_group", "load_balancer"}, + new String[] {"aws_applicationelb", "", "targetgroup/abc-123", "app/myLB/123"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_applicationelb_un_healthy_host_count_average", + new String[] {"job", "instance", "target_group", "load_balancer"}, + new String[] {"aws_applicationelb", "", "targetgroup/abc-234", "app/myLB/123"})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "target_group", "tag_Monitoring"}, + new String[] { + "aws_applicationelb", + "", + "arn:aws:elasticloadbalancing:us-east-1:121212121212:targetgroup/abc-123", + "targetgroup/abc-123", + "enabled" + })) + .isCloseTo(1.0, within(.01)); } @Test @@ -1438,38 +1416,36 @@ public void testTagSelectALB() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(4.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_applicationelb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer"}, - new String[] {"aws_applicationelb", "", "a", "app/myLB/123"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_applicationelb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer"}, - new String[] {"aws_applicationelb", "", "b", "app/myLB/123"}), - .01); - assertNull( - registry.getSampleValue( - "aws_applicationelb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer"}, - new String[] {"aws_applicationelb", "", "a", "app/myOtherLB/456"})); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "load_balancer", "tag_Monitoring"}, - new String[] { - "aws_applicationelb", - "", - "arn:aws:elasticloadbalancing:us-east-1:121212121212:loadbalancer/app/myLB/123", - "app/myLB/123", - "enabled" - }), - .01); + assertThat( + registry.getSampleValue( + "aws_applicationelb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer"}, + new String[] {"aws_applicationelb", "", "a", "app/myLB/123"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_applicationelb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer"}, + new String[] {"aws_applicationelb", "", "b", "app/myLB/123"})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_applicationelb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer"}, + new String[] {"aws_applicationelb", "", "a", "app/myOtherLB/456"})) + .isNull(); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "load_balancer", "tag_Monitoring"}, + new String[] { + "aws_applicationelb", + "", + "arn:aws:elasticloadbalancing:us-east-1:121212121212:loadbalancer/app/myLB/123", + "app/myLB/123", + "enabled" + })) + .isCloseTo(1.0, within(.01)); } @Test @@ -1562,38 +1538,34 @@ public void testTagSelectUsesPaginationToken() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(3.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-1"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-2"}), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" - }), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-2", "i-2", "enabled" - }), - .01); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-1"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-2"})) + .isCloseTo(3.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" + })) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-2", "i-2", "enabled" + })) + .isCloseTo(1.0, within(.01)); } @Test @@ -1652,20 +1624,18 @@ public void testNoSelection() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(3.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-1"}), - .01); - assertEquals( - 3.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-2"}), - .01); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-1"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-2"})) + .isCloseTo(3.0, within(.01)); } @Test @@ -1744,36 +1714,34 @@ public void testMultipleSelection() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(3.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-1"}), - .01); - assertNull( - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-2"})); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" - }), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-2", "i-2", "enabled" - }), - .01); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-1"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-2"})) + .isNull(); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" + })) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-2", "i-2", "enabled" + })) + .isCloseTo(1.0, within(.01)); } @Test @@ -1868,54 +1836,52 @@ public void testOptionalTagSelection() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(4.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-1"}), - .01); - assertNull( - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-2"})); - assertEquals( - 4.0, - registry.getSampleValue( - "aws_ec2_cpuutilization_average", - new String[] {"job", "instance", "instance_id"}, - new String[] {"aws_ec2", "", "i-no-tag"}), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" - }), - .01); - assertEquals( - 1.0, - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-2", "i-2", "enabled" - }), - .01); - assertNull( - registry.getSampleValue( - "aws_resource_info", - new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, - new String[] { - "aws_ec2", - "", - "arn:aws:ec2:us-east-1:121212121212:instance/i-no-tag", - "i-no-tag", - "enabled" - })); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-1"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-2"})) + .isNull(); + assertThat( + registry.getSampleValue( + "aws_ec2_cpuutilization_average", + new String[] {"job", "instance", "instance_id"}, + new String[] {"aws_ec2", "", "i-no-tag"})) + .isCloseTo(4.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-1", "i-1", "enabled" + })) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", "", "arn:aws:ec2:us-east-1:121212121212:instance/i-2", "i-2", "enabled" + })) + .isCloseTo(1.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_resource_info", + new String[] {"job", "instance", "arn", "instance_id", "tag_Monitoring"}, + new String[] { + "aws_ec2", + "", + "arn:aws:ec2:us-east-1:121212121212:instance/i-no-tag", + "i-no-tag", + "enabled" + })) + .isNull(); } @Test @@ -2012,16 +1978,15 @@ public void testBuildInfo() throws Exception { String buildVersion = properties.getProperty("BuildVersion"); String releaseDate = properties.getProperty("ReleaseDate"); - assertEquals( - 1L, - registry.getSampleValue( - "cloudwatch_exporter_build_info", - new String[] {"build_version", "release_date"}, - new String[] { - buildVersion != null ? buildVersion : "unknown", - releaseDate != null ? releaseDate : "unknown" - }), - .00001); + assertThat( + registry.getSampleValue( + "cloudwatch_exporter_build_info", + new String[] {"build_version", "release_date"}, + new String[] { + buildVersion != null ? buildVersion : "unknown", + releaseDate != null ? releaseDate : "unknown" + })) + .isCloseTo(1L, within(.00001)); } @Test @@ -2065,20 +2030,18 @@ public void testDimensionsWithDefaultCache() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); Mockito.verify(cloudWatchClient).listMetrics(any(ListMetricsRequest.class)); Mockito.verify(cloudWatchClient, times(2)) @@ -2126,23 +2089,306 @@ public void testDimensionsWithMetricLevelCache() throws Exception { Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) .build()); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); - assertEquals( - 2.0, - registry.getSampleValue( - "aws_elb_request_count_average", - new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, - new String[] {"aws_elb", "", "a", "myLB"}), - .01); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); + assertThat( + registry.getSampleValue( + "aws_elb_request_count_average", + new String[] {"job", "instance", "availability_zone", "load_balancer_name"}, + new String[] {"aws_elb", "", "a", "myLB"})) + .isCloseTo(2.0, within(.01)); Mockito.verify(cloudWatchClient).listMetrics(any(ListMetricsRequest.class)); Mockito.verify(cloudWatchClient, times(2)) .getMetricStatistics(any(GetMetricStatisticsRequest.class)); } + + @Test + public void loadConfigFromReaderRejectsEmptyYaml() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n", + cloudWatchClient, + taggingClient); + + assertThatThrownBy( + () -> collector.loadConfig(new StringReader(""), cloudWatchClient, taggingClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must provide metrics"); + } + + @Test + public void rejectsConfigWithoutMetrics() { + assertThatThrownBy( + () -> new CloudWatchCollector("---\nregion: reg\n", cloudWatchClient, taggingClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must provide metrics"); + } + + @Test + public void rejectsMetricWithoutRequiredCloudWatchNames() { + assertThatThrownBy( + () -> + new CloudWatchCollector( + "---\nmetrics:\n- aws_namespace: AWS/ELB\n", cloudWatchClient, taggingClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must provide aws_namespace and aws_metric_name"); + } + + @Test + public void parsesGlobalDefaultsAndMetricHelp() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\n" + + "period_seconds: 15\n" + + "range_seconds: 30\n" + + "delay_seconds: 45\n" + + "set_timestamp: false\n" + + "use_get_metric_data: true\n" + + "warn_on_empty_list_dimensions: true\n" + + "list_metrics_cache_ttl: 120\n" + + "metrics:\n" + + "- aws_namespace: AWS/ELB\n" + + " aws_metric_name: RequestCount\n" + + " help: Custom help\n" + + " aws_dimensions: [LoadBalancerName]\n" + + " aws_extended_statistics: [p99]\n", + cloudWatchClient, + taggingClient); + + MetricRule rule = collector.activeConfig.rules.get(0); + + assertThat(rule.periodSeconds).isEqualTo(15); + assertThat(rule.rangeSeconds).isEqualTo(30); + assertThat(rule.delaySeconds).isEqualTo(45); + assertThat(rule.cloudwatchTimestamp).isFalse(); + assertThat(rule.useGetMetricData).isTrue(); + assertThat(rule.warnOnEmptyListDimensions).isTrue(); + assertThat(rule.listMetricsCacheTtl).isEqualTo(Duration.ofSeconds(120)); + assertThat(rule.help).isEqualTo("Custom help"); + assertThat(rule.awsDimensions).containsExactly("LoadBalancerName"); + assertThat(rule.awsExtendedStatistics).containsExactly("p99"); + assertThat(rule.awsStatistics).isNull(); + } + + @Test + public void stringConstructorParsesConfigWithProvidedClients() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n", + cloudWatchClient, + taggingClient); + + assertThat(collector.activeConfig.rules).hasSize(1); + } + + @Test + public void readerConstructorParsesConfig() { + CloudWatchCollector collector = + new CloudWatchCollector( + new StringReader( + "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n")); + + assertThat(collector.activeConfig.rules).hasSize(1); + assertThat(collector.activeConfig.cloudWatchClient).isNotNull(); + assertThat(collector.activeConfig.taggingClient).isNotNull(); + } + + @Test + public void rejectsCombinedDimensionSelectAndRegex() { + assertThatThrownBy( + () -> + new CloudWatchCollector( + "---\n" + + "metrics:\n" + + "- aws_namespace: AWS/ELB\n" + + " aws_metric_name: RequestCount\n" + + " aws_dimension_select:\n" + + " LoadBalancerName: [lb]\n" + + " aws_dimension_select_regex:\n" + + " LoadBalancerName: [lb.*]\n", + cloudWatchClient, + taggingClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Must not provide aws_dimension_select and aws_dimension_select_regex at the same time"); + } + + @Test + public void rejectsIncompleteTagSelect() { + assertThatThrownBy( + () -> + new CloudWatchCollector( + "---\n" + + "metrics:\n" + + "- aws_namespace: AWS/EC2\n" + + " aws_metric_name: CPUUtilization\n" + + " aws_tag_select:\n" + + " resource_type_selection: ec2:instance\n", + cloudWatchClient, + taggingClient)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must provide resource_type_selection and resource_id_dimension"); + } + + @Test + public void collectReportsScrapeErrorWhenCloudWatchRequestFails() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n", + cloudWatchClient, + taggingClient); + Mockito.when(cloudWatchClient.getMetricStatistics(any(GetMetricStatisticsRequest.class))) + .thenThrow(new RuntimeException("boom")); + + List samples = collector.collect(); + + assertThat(errorSample(samples)).isEqualTo(1.0); + } + + @Test + public void stringConstructorBuildsClientsWithoutAwsCalls() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\nregion: reg\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n"); + + assertThat(collector.activeConfig.rules).hasSize(1); + assertThat(collector.activeConfig.cloudWatchClient).isNotNull(); + assertThat(collector.activeConfig.taggingClient).isNotNull(); + } + + @Test + public void parsesExplicitStatisticsAndMetricWarnOverride() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\n" + + "warn_on_empty_list_dimensions: false\n" + + "metrics:\n" + + "- aws_namespace: AWS/ELB\n" + + " aws_metric_name: RequestCount\n" + + " aws_statistics: [Sum, Average]\n" + + " warn_on_empty_list_dimensions: true\n", + cloudWatchClient, + taggingClient); + + MetricRule rule = collector.activeConfig.rules.get(0); + + assertThat(rule.awsStatistics).containsExactly(Statistic.SUM, Statistic.AVERAGE); + assertThat(rule.warnOnEmptyListDimensions).isTrue(); + } + + @Test + public void reloadConfigUsesExistingClientsAndUpdatedFile() throws Exception { + Path config = + Files.writeString( + Files.createTempFile("cloudwatch-exporter-reload", ".yml"), + "---\nmetrics:\n- aws_namespace: AWS/ELB\n aws_metric_name: RequestCount\n"); + String previousConfigFilePath = WebServer.configFilePath; + WebServer.configFilePath = config.toString(); + try { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\nmetrics:\n- aws_namespace: AWS/EC2\n aws_metric_name: CPUUtilization\n", + cloudWatchClient, + taggingClient); + + collector.reloadConfig(); + + assertThat(collector.activeConfig.rules).hasSize(1); + assertThat(collector.activeConfig.rules.get(0).awsNamespace).isEqualTo("AWS/ELB"); + assertThat(collector.activeConfig.cloudWatchClient).isSameAs(cloudWatchClient); + assertThat(collector.activeConfig.taggingClient).isSameAs(taggingClient); + } finally { + WebServer.configFilePath = previousConfigFilePath; + Files.deleteIfExists(config); + } + } + + @Test + public void customHelpIsUsedForMetricFamily() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\n" + + "metrics:\n" + + "- aws_namespace: AWS/ELB\n" + + " aws_metric_name: RequestCount\n" + + " help: Custom metric help\n", + cloudWatchClient, + taggingClient); + Mockito.when(cloudWatchClient.getMetricStatistics(any(GetMetricStatisticsRequest.class))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) + .build()); + + Collector.MetricFamilySamples average = + metricFamily(collector.collect(), "aws_elb_request_count_average"); + + assertThat(average.help).isEqualTo("Custom metric help"); + } + + @Test + public void resourceInfoIsEmittedOnceForDuplicateTagMappings() { + CloudWatchCollector collector = + new CloudWatchCollector( + "---\n" + + "region: reg\n" + + "metrics:\n" + + "- aws_namespace: AWS/EC2\n" + + " aws_metric_name: CPUUtilization\n" + + " aws_dimensions: [InstanceId]\n" + + " aws_tag_select:\n" + + " resource_type_selection: ec2:instance\n" + + " resource_id_dimension: InstanceId\n", + cloudWatchClient, + taggingClient); + ResourceTagMapping mapping = + ResourceTagMapping.builder() + .resourceARN("arn:aws:ec2:reg:123456789012:instance/i-1") + .tags(Tag.builder().key("Name").value("example").build()) + .build(); + Mockito.when(taggingClient.getResources(any(GetResourcesRequest.class))) + .thenReturn( + GetResourcesResponse.builder().resourceTagMappingList(mapping, mapping).build()); + Mockito.when(cloudWatchClient.listMetrics(any(ListMetricsRequest.class))) + .thenReturn( + ListMetricsResponse.builder() + .metrics( + Metric.builder() + .dimensions(Dimension.builder().name("InstanceId").value("i-1").build()) + .build()) + .build()); + Mockito.when(cloudWatchClient.getMetricStatistics(any(GetMetricStatisticsRequest.class))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder().timestamp(new Date().toInstant()).average(2.0).build()) + .build()); + + Collector.MetricFamilySamples info = metricFamily(collector.collect(), "aws_resource_info"); + + assertThat(info.samples).hasSize(1); + assertThat(info.samples.get(0).labelNames).contains("instance_id", "tag_Name"); + assertThat(info.samples.get(0).labelValues).contains("i-1", "example"); + } + + private Collector.MetricFamilySamples metricFamily( + List samples, String name) { + return samples.stream().filter(sample -> sample.name.equals(name)).findFirst().orElseThrow(); + } + + private double errorSample(List samples) { + return samples.stream() + .filter(sample -> sample.name.equals("cloudwatch_exporter_scrape_error")) + .findFirst() + .orElseThrow() + .samples + .get(0) + .value; + } } diff --git a/src/test/java/io/prometheus/cloudwatch/DefaultDimensionSourceTest.java b/src/test/java/io/prometheus/cloudwatch/DefaultDimensionSourceTest.java new file mode 100644 index 00000000..d047a674 --- /dev/null +++ b/src/test/java/io/prometheus/cloudwatch/DefaultDimensionSourceTest.java @@ -0,0 +1,74 @@ +package io.prometheus.cloudwatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.prometheus.client.Counter; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; +import software.amazon.awssdk.services.cloudwatch.model.Dimension; +import software.amazon.awssdk.services.cloudwatch.model.ListMetricsRequest; +import software.amazon.awssdk.services.cloudwatch.model.ListMetricsResponse; + +class DefaultDimensionSourceTest { + + @Test + void usesSelectedDimensionValuesWithoutCallingCloudWatchWhenAllDimensionsAreKnown() { + CloudWatchClient client = mock(CloudWatchClient.class); + MetricRule rule = metricRule(); + rule.awsDimensions = List.of("LoadBalancerName", "AvailabilityZone"); + rule.awsDimensionSelect = + Map.of("LoadBalancerName", List.of("lb-a", "lb-b"), "AvailabilityZone", List.of("us-a")); + + DimensionSource.DimensionData data = source(client).getDimensions(rule, List.of()); + + assertThat(data.getDimensions()) + .containsExactlyInAnyOrder( + List.of(dimension("LoadBalancerName", "lb-a"), dimension("AvailabilityZone", "us-a")), + List.of(dimension("LoadBalancerName", "lb-b"), dimension("AvailabilityZone", "us-a"))); + verify(client, never()).listMetrics(any(ListMetricsRequest.class)); + } + + @Test + void returnsEmptyDimensionsWhenListMetricsReturnsNoMatchesAndWarningEnabled() { + CloudWatchClient client = mock(CloudWatchClient.class); + when(client.listMetrics(any(ListMetricsRequest.class))) + .thenReturn(ListMetricsResponse.builder().metrics(List.of()).build()); + MetricRule rule = metricRule(); + rule.awsDimensions = List.of("LoadBalancerName"); + rule.warnOnEmptyListDimensions = true; + + DimensionSource.DimensionData data = source(client).getDimensions(rule, List.of()); + + assertThat(data.getDimensions()).isEmpty(); + verify(client).listMetrics(any(ListMetricsRequest.class)); + } + + private DefaultDimensionSource source(CloudWatchClient client) { + return new DefaultDimensionSource( + client, + Counter.build() + .name("default_dimension_source_test_cloudwatch_requests") + .help("requests") + .labelNames("action", "namespace") + .create()); + } + + private MetricRule metricRule() { + MetricRule rule = new MetricRule(); + rule.awsNamespace = "AWS/ELB"; + rule.awsMetricName = "RequestCount"; + rule.rangeSeconds = 60; + return rule; + } + + private Dimension dimension(String name, String value) { + return Dimension.builder().name(name).value(value).build(); + } +} diff --git a/src/test/java/io/prometheus/cloudwatch/DisallowHttpMethodsTest.java b/src/test/java/io/prometheus/cloudwatch/DisallowHttpMethodsTest.java new file mode 100644 index 00000000..fb7c0ab5 --- /dev/null +++ b/src/test/java/io/prometheus/cloudwatch/DisallowHttpMethodsTest.java @@ -0,0 +1,51 @@ +package io.prometheus.cloudwatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.EnumSet; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.api.Test; + +class DisallowHttpMethodsTest { + + private final Request request = mock(Request.class); + private final Response response = mock(Response.class); + private final Callback callback = mock(Callback.class); + + @Test + void returnsMethodNotAllowedForDisallowedMethod() throws Exception { + when(request.getMethod()).thenReturn("TRACE"); + DisallowHttpMethods handler = new DisallowHttpMethods(EnumSet.of(HttpMethod.TRACE)); + + boolean handled = handler.handle(request, response, callback); + + assertThat(handled).isTrue(); + verify(response).setStatus(HttpStatus.METHOD_NOT_ALLOWED_405); + verify(callback).succeeded(); + } + + @Test + void delegatesAllowedMethodToWrappedHandler() throws Exception { + when(request.getMethod()).thenReturn("GET"); + Handler wrappedHandler = mock(Handler.class); + when(wrappedHandler.handle(request, response, callback)).thenReturn(true); + DisallowHttpMethods handler = new DisallowHttpMethods(EnumSet.of(HttpMethod.TRACE)); + handler.setHandler(wrappedHandler); + + boolean handled = handler.handle(request, response, callback); + + assertThat(handled).isTrue(); + verify(wrappedHandler).handle(request, response, callback); + verify(response, never()).setStatus(HttpStatus.METHOD_NOT_ALLOWED_405); + verify(callback, never()).succeeded(); + } +} diff --git a/src/test/java/io/prometheus/cloudwatch/GetMetricDataGetterTest.java b/src/test/java/io/prometheus/cloudwatch/GetMetricDataGetterTest.java index 2a94821a..dd73d9ae 100644 --- a/src/test/java/io/prometheus/cloudwatch/GetMetricDataGetterTest.java +++ b/src/test/java/io/prometheus/cloudwatch/GetMetricDataGetterTest.java @@ -1,10 +1,22 @@ package io.prometheus.cloudwatch; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import io.prometheus.client.Counter; +import java.time.Instant; import java.util.Collections; import java.util.List; -import org.junit.Test; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; +import software.amazon.awssdk.services.cloudwatch.model.Dimension; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricDataRequest; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricDataResponse; +import software.amazon.awssdk.services.cloudwatch.model.MetricDataResult; public class GetMetricDataGetterTest { @Test @@ -12,8 +24,93 @@ public void testPartition() { List originalList = List.copyOf(Collections.nCopies(28, 0)); List> partitions = GetMetricDataDataGetter.partitionByMaxSize(originalList, 40); for (List p : partitions) { - assertTrue("partition must be smaller than 40", p.size() <= 40); - assertTrue("partition should not be empty", !p.isEmpty()); + assertThat(p.size()).isLessThanOrEqualTo(40); + assertThat(p.size()).isPositive(); } } + + @Test + public void partitionReturnsEmptyListForEmptyInput() { + assertThat(GetMetricDataDataGetter.partitionByMaxSize(List.of(), 40)).isEmpty(); + } + + @Test + public void partitionSplitsInputIntoMaxSizedPartitions() { + List originalList = List.copyOf(Collections.nCopies(85, 0)); + + List> partitions = GetMetricDataDataGetter.partitionByMaxSize(originalList, 40); + + assertThat(partitions).hasSize(3); + assertThat(partitions).extracting(List::size).containsExactly(40, 40, 5); + } + + @Test + public void metricLabelsEncodeStatAndSortedDimensions() { + String label = + GetMetricDataDataGetter.MetricLabels.labelFor( + "Average", + List.of( + Dimension.builder().name("InstanceId").value("i-123").build(), + Dimension.builder().name("AutoScalingGroupName").value("asg").build())); + + assertThat(label).isEqualTo("Average/AutoScalingGroupName=asg,InstanceId=i-123"); + } + + @Test + public void metricLabelsRejectLabelsWithoutStatSeparator() { + assertThatThrownBy(() -> GetMetricDataDataGetter.MetricLabels.decode("Average")) + .isInstanceOf(GetMetricDataDataGetter.MetricLabels.UnexpectedLabel.class) + .hasMessage("Cannot decode label Average"); + } + + @Test + public void metricRuleDataForMapsExtendedStatisticsAndSkipsEmptyResults() { + CloudWatchClient client = mock(CloudWatchClient.class); + when(client.getMetricData(any(GetMetricDataRequest.class))) + .thenReturn( + GetMetricDataResponse.builder() + .metricDataResults( + MetricDataResult.builder() + .label("p99/InstanceId=i-123") + .timestamps(List.of(Instant.parse("2024-01-01T00:00:00Z"))) + .values(List.of(99.0)) + .build(), + MetricDataResult.builder() + .label("p95/InstanceId=i-123") + .timestamps(List.of()) + .values(List.of(95.0)) + .build(), + MetricDataResult.builder() + .label("p90/InstanceId=i-123") + .timestamps(List.of(Instant.parse("2024-01-01T00:00:00Z"))) + .values(List.of()) + .build()) + .build()); + MetricRule rule = new MetricRule(); + rule.awsNamespace = "AWS/EC2"; + rule.awsMetricName = "CPUUtilization"; + rule.awsExtendedStatistics = List.of("p99"); + rule.periodSeconds = 60; + rule.rangeSeconds = 120; + rule.delaySeconds = 30; + Dimension dimension = Dimension.builder().name("InstanceId").value("i-123").build(); + + DataGetter.MetricRuleData data = + new GetMetricDataDataGetter( + client, + 1_704_067_200_000L, + rule, + counter("get_metric_data_api_requests"), + counter("get_metric_data_metrics_requested"), + List.of(List.of(dimension))) + .metricRuleDataFor(List.of(dimension)); + + assertThat(data.timestamp).isEqualTo(Instant.parse("2024-01-01T00:00:00Z")); + assertThat(data.extendedValues).containsOnly(Map.entry("p99", 99.0)); + assertThat(data.statisticValues).isEmpty(); + } + + private Counter counter(String name) { + return Counter.build().name(name).help(name).labelNames("a", "b").create(); + } } diff --git a/src/test/java/io/prometheus/cloudwatch/GetMetricStatisticsDataGetterTest.java b/src/test/java/io/prometheus/cloudwatch/GetMetricStatisticsDataGetterTest.java new file mode 100644 index 00000000..585ba643 --- /dev/null +++ b/src/test/java/io/prometheus/cloudwatch/GetMetricStatisticsDataGetterTest.java @@ -0,0 +1,113 @@ +package io.prometheus.cloudwatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.prometheus.client.Counter; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; +import software.amazon.awssdk.services.cloudwatch.model.Datapoint; +import software.amazon.awssdk.services.cloudwatch.model.Dimension; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricStatisticsRequest; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricStatisticsResponse; +import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; +import software.amazon.awssdk.services.cloudwatch.model.Statistic; + +class GetMetricStatisticsDataGetterTest { + + @Test + void returnsNewestDatapointWithStatisticsAndExtendedStatistics() { + CloudWatchClient client = mock(CloudWatchClient.class); + when(client.getMetricStatistics(any(GetMetricStatisticsRequest.class))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder() + .timestamp(Instant.parse("2024-01-02T00:00:00Z")) + .unit(StandardUnit.COUNT) + .sum(1.0) + .sampleCount(2.0) + .minimum(3.0) + .maximum(4.0) + .average(5.0) + .extendedStatistics(Map.of("p99", 6.0)) + .build(), + Datapoint.builder() + .timestamp(Instant.parse("2024-01-01T00:00:00Z")) + .unit(StandardUnit.COUNT) + .sum(100.0) + .build()) + .build()); + + DataGetter.MetricRuleData data = getter(client).metricRuleDataFor(List.of(dimension())); + + assertThat(data.timestamp).isEqualTo(Instant.parse("2024-01-02T00:00:00Z")); + assertThat(data.unit).isEqualTo("Count"); + assertThat(data.statisticValues) + .containsEntry(Statistic.SUM, 1.0) + .containsEntry(Statistic.SAMPLE_COUNT, 2.0) + .containsEntry(Statistic.MINIMUM, 3.0) + .containsEntry(Statistic.MAXIMUM, 4.0) + .containsEntry(Statistic.AVERAGE, 5.0); + assertThat(data.extendedValues).containsEntry("p99", 6.0); + } + + @Test + void returnsNullWhenCloudWatchReturnsNoDatapoints() { + CloudWatchClient client = mock(CloudWatchClient.class); + when(client.getMetricStatistics(any(GetMetricStatisticsRequest.class))) + .thenReturn(GetMetricStatisticsResponse.builder().datapoints(List.of()).build()); + + assertThat(getter(client).metricRuleDataFor(List.of(dimension()))).isNull(); + } + + @Test + void handlesDatapointWithoutExtendedStatistics() { + CloudWatchClient client = mock(CloudWatchClient.class); + when(client.getMetricStatistics(any(GetMetricStatisticsRequest.class))) + .thenReturn( + GetMetricStatisticsResponse.builder() + .datapoints( + Datapoint.builder() + .timestamp(Instant.parse("2024-01-01T00:00:00Z")) + .unit(StandardUnit.COUNT) + .average(7.0) + .build()) + .build()); + + DataGetter.MetricRuleData data = getter(client).metricRuleDataFor(List.of(dimension())); + + assertThat(data.statisticValues).containsEntry(Statistic.AVERAGE, 7.0); + assertThat(data.extendedValues).isEmpty(); + } + + private GetMetricStatisticsDataGetter getter(CloudWatchClient client) { + MetricRule rule = new MetricRule(); + rule.awsNamespace = "AWS/EC2"; + rule.awsMetricName = "CPUUtilization"; + rule.awsStatistics = List.of(Statistic.SUM, Statistic.AVERAGE); + rule.awsExtendedStatistics = List.of("p99"); + rule.periodSeconds = 60; + rule.rangeSeconds = 120; + rule.delaySeconds = 30; + return new GetMetricStatisticsDataGetter( + client, 1_704_156_600_000L, rule, counter("api_requests"), counter("metrics_requested")); + } + + private Dimension dimension() { + return Dimension.builder().name("InstanceId").value("i-123").build(); + } + + private Counter counter(String name) { + return Counter.build() + .name("get_metric_statistics_test_" + name) + .help(name) + .labelNames("a", "b") + .create(); + } +} diff --git a/src/test/java/io/prometheus/cloudwatch/MetricRuleTest.java b/src/test/java/io/prometheus/cloudwatch/MetricRuleTest.java new file mode 100644 index 00000000..e797dcda --- /dev/null +++ b/src/test/java/io/prometheus/cloudwatch/MetricRuleTest.java @@ -0,0 +1,119 @@ +package io.prometheus.cloudwatch; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.cloudwatch.model.Statistic; + +public class MetricRuleTest { + + @Test + public void equalRulesHaveSameHashCode() { + MetricRule left = populatedRule(); + MetricRule right = populatedRule(); + right.awsTagSelect = left.awsTagSelect; + + assertThat(left).isEqualTo(right); + assertThat(left.hashCode()).isEqualTo(right.hashCode()); + } + + @Test + public void equalsHandlesIdentityNullAndDifferentTypes() { + MetricRule rule = populatedRule(); + + assertThat(rule).isEqualTo(rule); + assertThat(rule).isNotEqualTo(null); + assertThat(rule).isNotEqualTo("not a metric rule"); + } + + @Test + public void equalsDetectsDifferentFields() { + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.periodSeconds = 61)); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.rangeSeconds = 121)); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.delaySeconds = 31)); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.cloudwatchTimestamp = false)); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.useGetMetricData = false)); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.awsNamespace = "AWS/S3")); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.awsMetricName = "Latency")); + assertThat(populatedRule()) + .isNotEqualTo(changedRule(rule -> rule.awsStatistics = List.of(Statistic.SUM))); + assertThat(populatedRule()) + .isNotEqualTo(changedRule(rule -> rule.awsExtendedStatistics = List.of("p99"))); + assertThat(populatedRule()) + .isNotEqualTo(changedRule(rule -> rule.awsDimensions = List.of("BucketName"))); + assertThat(populatedRule()) + .isNotEqualTo( + changedRule( + rule -> rule.awsDimensionSelect = Map.of("LoadBalancerName", List.of("b")))); + assertThat(populatedRule()) + .isNotEqualTo( + changedRule( + rule -> rule.awsDimensionSelectRegex = Map.of("LoadBalancerName", List.of("b.*")))); + assertThat(populatedRule()) + .isNotEqualTo( + changedRule(rule -> rule.awsTagSelect = new CloudWatchCollector.AWSTagSelect())); + assertThat(populatedRule()).isNotEqualTo(changedRule(rule -> rule.help = "other help")); + assertThat(populatedRule()) + .isNotEqualTo(changedRule(rule -> rule.listMetricsCacheTtl = Duration.ofMinutes(2))); + } + + @Test + public void equalsDetectsDifferentFieldsAfterEqualTagSelect() { + MetricRule left = populatedRule(); + MetricRule right = populatedRule(); + right.awsTagSelect = left.awsTagSelect; + right.help = "other help"; + + assertThat(left).isNotEqualTo(right); + } + + @Test + public void warnOnEmptyListDimensionsDoesNotAffectEquality() { + MetricRule left = populatedRule(); + MetricRule right = populatedRule(); + right.awsTagSelect = left.awsTagSelect; + right.warnOnEmptyListDimensions = !left.warnOnEmptyListDimensions; + + assertThat(left).isEqualTo(right); + assertThat(left.hashCode()).isEqualTo(right.hashCode()); + } + + @Test + public void hashCodeHandlesNullFields() { + assertThat(new MetricRule().hashCode()).isZero(); + } + + private MetricRule changedRule(RuleChange change) { + MetricRule rule = populatedRule(); + change.apply(rule); + return rule; + } + + private MetricRule populatedRule() { + MetricRule rule = new MetricRule(); + rule.awsNamespace = "AWS/ELB"; + rule.awsMetricName = "RequestCount"; + rule.periodSeconds = 60; + rule.rangeSeconds = 120; + rule.delaySeconds = 30; + rule.awsStatistics = List.of(Statistic.AVERAGE, Statistic.MAXIMUM); + rule.awsExtendedStatistics = List.of("p95"); + rule.awsDimensions = List.of("LoadBalancerName"); + rule.awsDimensionSelect = Map.of("LoadBalancerName", List.of("a")); + rule.awsDimensionSelectRegex = Map.of("LoadBalancerName", List.of("a.*")); + rule.awsTagSelect = new CloudWatchCollector.AWSTagSelect(); + rule.help = "help text"; + rule.cloudwatchTimestamp = true; + rule.useGetMetricData = true; + rule.listMetricsCacheTtl = Duration.ofMinutes(1); + rule.warnOnEmptyListDimensions = true; + return rule; + } + + private interface RuleChange { + void apply(MetricRule rule); + } +} diff --git a/src/test/java/io/prometheus/cloudwatch/ServletTest.java b/src/test/java/io/prometheus/cloudwatch/ServletTest.java new file mode 100644 index 00000000..aee7ffae --- /dev/null +++ b/src/test/java/io/prometheus/cloudwatch/ServletTest.java @@ -0,0 +1,135 @@ +package io.prometheus.cloudwatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import org.junit.jupiter.api.Test; + +public class ServletTest { + private final HttpServletRequest request = mock(HttpServletRequest.class); + + @Test + public void healthServletReturnsPlainTextOk() throws Exception { + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter responseBody = responseBody(response); + + new HealthServlet().doGet(request, response); + + verify(response).setContentType("text/plain"); + assertThat(responseBody.toString()).isEqualTo("ok"); + } + + @Test + public void homePageServletReturnsHtmlLinks() throws Exception { + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter responseBody = responseBody(response); + + new HomePageServlet().doGet(request, response); + + verify(response).setContentType("text/html"); + assertThat(responseBody.toString()) + .contains("

CloudWatch Exporter

") + .contains("Metrics"); + } + + @Test + public void dynamicReloadServletRejectsGetRequests() throws Exception { + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter responseBody = responseBody(response); + + new DynamicReloadServlet(mock(CloudWatchCollector.class)).doGet(request, response); + + verify(response).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + verify(response).setContentType(DynamicReloadServlet.CONTENT_TYPE); + assertThat(responseBody.toString()).isEqualTo("Only POST requests allowed"); + } + + @Test + public void dynamicReloadServletReloadsConfigForPostRequests() throws Exception { + CloudWatchCollector collector = mock(CloudWatchCollector.class); + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter responseBody = responseBody(response); + + new DynamicReloadServlet(collector).doPost(request, response); + + verify(collector).reloadConfig(); + verify(response, never()).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response).setContentType(DynamicReloadServlet.CONTENT_TYPE); + assertThat(responseBody.toString()).isEqualTo("OK"); + } + + @Test + public void dynamicReloadServletReturnsErrorWhenReloadFails() throws Exception { + CloudWatchCollector collector = mock(CloudWatchCollector.class); + doThrow(new IOException("boom")).when(collector).reloadConfig(); + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter responseBody = responseBody(response); + + new DynamicReloadServlet(collector).doPost(request, response); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response).setContentType(DynamicReloadServlet.CONTENT_TYPE); + assertThat(responseBody.toString()).isEqualTo("Reloading config failed"); + } + + @Test + public void homePageServletIgnoresWriterFailures() throws Exception { + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getWriter()).thenThrow(new IOException("boom")); + + new HomePageServlet().doGet(request, response); + + verify(response).setContentType("text/html"); + } + + @Test + public void dynamicReloadServletIgnoresGetWriterFailures() throws Exception { + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getWriter()).thenThrow(new IOException("boom")); + + new DynamicReloadServlet(mock(CloudWatchCollector.class)).doGet(request, response); + + verify(response).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED); + verify(response).setContentType(DynamicReloadServlet.CONTENT_TYPE); + } + + @Test + public void dynamicReloadServletIgnoresSuccessWriterFailures() throws Exception { + CloudWatchCollector collector = mock(CloudWatchCollector.class); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getWriter()).thenThrow(new IOException("boom")); + + new DynamicReloadServlet(collector).doPost(request, response); + + verify(collector).reloadConfig(); + verify(response).setContentType(DynamicReloadServlet.CONTENT_TYPE); + } + + @Test + public void dynamicReloadServletIgnoresErrorWriterFailures() throws Exception { + CloudWatchCollector collector = mock(CloudWatchCollector.class); + doThrow(new IOException("reload boom")).when(collector).reloadConfig(); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.getWriter()).thenThrow(new IOException("writer boom")); + + new DynamicReloadServlet(collector).doPost(request, response); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + verify(response).setContentType(DynamicReloadServlet.CONTENT_TYPE); + } + + private StringWriter responseBody(HttpServletResponse response) throws IOException { + StringWriter responseBody = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(responseBody)); + return responseBody; + } +} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MemberAccessor b/src/test/resources/mockito-extensions/org.mockito.plugins.MemberAccessor new file mode 100644 index 00000000..71111e33 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MemberAccessor @@ -0,0 +1 @@ +member-accessor-reflection diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..fdbd0b15 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass