diff --git a/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/pom.xml b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/pom.xml new file mode 100644 index 000000000..e21e76b6d --- /dev/null +++ b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/pom.xml @@ -0,0 +1,141 @@ + + + + + + + flink-connector-aws-e2e-tests-parent + org.apache.flink + 5.1-SNAPSHOT + + + 4.0.0 + + flink-connector-aws-cloudwatch-e2e-tests + Flink : Connectors : AWS : E2E Tests : Amazon CloudWatch + jar + + + 2.31.18 + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + + + + org.apache.flink + flink-sql-connector-cloudwatch + ${project.version} + test + + + + org.apache.flink + flink-connector-cloudwatch + ${project.version} + + + + org.apache.flink + flink-connector-aws-base + ${project.version} + + + + org.apache.flink + flink-connector-aws-base + ${project.version} + test-jar + + + + org.apache.flink + flink-connector-cloudwatch + ${project.version} + test-jar + + + + + com.google.guava + guava + test + + + + com.fasterxml.jackson.core + jackson-databind + test + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + test + + + + software.amazon.awssdk + s3 + test + + + + software.amazon.awssdk + cloudwatch + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + pre-integration-test + + copy + + + + + + + org.apache.flink + flink-sql-connector-cloudwatch + ${project.version} + sql-cloudwatch.jar + jar + ${project.build.directory}/dependencies + + + + + + + diff --git a/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/java/org/apache/flink/connector/cloudwatch/sink/test/CloudWatchSinkITCase.java b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/java/org/apache/flink/connector/cloudwatch/sink/test/CloudWatchSinkITCase.java new file mode 100644 index 000000000..aa5412fe9 --- /dev/null +++ b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/java/org/apache/flink/connector/cloudwatch/sink/test/CloudWatchSinkITCase.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink.test; + +import org.apache.flink.connector.aws.testutils.AWSServicesTestUtils; +import org.apache.flink.connector.aws.testutils.LocalstackContainer; +import org.apache.flink.connector.aws.util.AWSGeneralUtil; +import org.apache.flink.connector.cloudwatch.sink.CloudWatchSink; +import org.apache.flink.connector.cloudwatch.sink.MetricWriteRequest; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.test.junit5.MiniClusterExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.services.cloudwatch.CloudWatchClient; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricDataRequest; +import software.amazon.awssdk.services.cloudwatch.model.GetMetricDataResponse; +import software.amazon.awssdk.services.cloudwatch.model.Metric; +import software.amazon.awssdk.services.cloudwatch.model.MetricDataQuery; +import software.amazon.awssdk.services.cloudwatch.model.MetricStat; + +import java.time.Instant; +import java.util.UUID; + +import static org.apache.flink.connector.aws.testutils.AWSServicesTestUtils.createConfig; +import static org.assertj.core.api.Assertions.assertThat; + +/** Integration test for {@link CloudWatchSink}. */ +@Testcontainers +@ExtendWith(MiniClusterExtension.class) +public class CloudWatchSinkITCase { + private static final Logger LOG = LoggerFactory.getLogger(CloudWatchSinkITCase.class); + + private static String testMetricName; + private static final int NUMBER_OF_ELEMENTS = 50; + + private static StreamExecutionEnvironment env; + + private CloudWatchClient cloudWatchClient; + private SdkHttpClient httpClient; + private static final Network network = Network.newNetwork(); + private static final String LOCALSTACK_DOCKER_IMAGE_VERSION = "localstack/localstack:3.7.2"; + private static final String TEST_NAMESPACE = "test_namespace"; + + @Container + private static final LocalstackContainer MOCK_CLOUDWATCH_CONTAINER = + new LocalstackContainer(DockerImageName.parse(LOCALSTACK_DOCKER_IMAGE_VERSION)) + .withNetwork(network) + .withNetworkAliases("localstack"); + + @BeforeEach + public void setup() { + System.setProperty(SdkSystemSetting.CBOR_ENABLED.property(), "false"); + + testMetricName = UUID.randomUUID().toString(); + + env = StreamExecutionEnvironment.getExecutionEnvironment(); + env.setParallelism(1); + + httpClient = AWSServicesTestUtils.createHttpClient(); + + cloudWatchClient = + AWSServicesTestUtils.createAwsSyncClient( + MOCK_CLOUDWATCH_CONTAINER.getEndpoint(), + httpClient, + CloudWatchClient.builder()); + + LOG.info("Done setting up the localstack."); + } + + @AfterEach + public void teardown() { + System.clearProperty(SdkSystemSetting.CBOR_ENABLED.property()); + AWSGeneralUtil.closeResources(httpClient, cloudWatchClient); + } + + @Test + public void testRandomDataSuccessfullyWritten() throws Exception { + CloudWatchSink cloudWatchSink = + CloudWatchSink.builder() + .setNamespace(TEST_NAMESPACE) + .setCloudWatchClientProperties( + createConfig(MOCK_CLOUDWATCH_CONTAINER.getEndpoint())) + .build(); + + Instant testTimestamp = Instant.now(); + + env.fromSequence(1, NUMBER_OF_ELEMENTS) + .map( + data -> + MetricWriteRequest.builder() + .withMetricName(testMetricName) + .addValue(1.0d) + .withTimestamp(testTimestamp) + .build()) + .sinkTo(cloudWatchSink); + + env.execute("Integration Test"); + + GetMetricDataResponse response = + cloudWatchClient.getMetricData( + GetMetricDataRequest.builder() + .metricDataQueries( + MetricDataQuery.builder() + .metricStat(getMetricStat("Sum")) + .build(), + MetricDataQuery.builder() + .metricStat(getMetricStat("SampleCount")) + .build()) + .startTime(testTimestamp.minusSeconds(300)) + .endTime(testTimestamp.plusSeconds(300)) + .build()); + + response.metricDataResults() + .forEach( + result -> + assertThat(result.values()) + .containsExactly(Double.valueOf(NUMBER_OF_ELEMENTS))); + } + + private static MetricStat getMetricStat(String stat) { + return MetricStat.builder() + .metric( + Metric.builder() + .namespace(TEST_NAMESPACE) + .metricName(testMetricName) + .build()) + .stat(stat) + .period(300) + .build(); + } +} diff --git a/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/java/org/apache/flink/connector/cloudwatch/table/test/CloudWatchTableAPIITCase.java b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/java/org/apache/flink/connector/cloudwatch/table/test/CloudWatchTableAPIITCase.java new file mode 100644 index 000000000..d0418f508 --- /dev/null +++ b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/java/org/apache/flink/connector/cloudwatch/table/test/CloudWatchTableAPIITCase.java @@ -0,0 +1,257 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table.test; + +import org.apache.flink.api.common.time.Deadline; +import org.apache.flink.connector.aws.testutils.AWSServicesTestUtils; +import org.apache.flink.connector.aws.testutils.LocalstackContainer; +import org.apache.flink.connector.aws.util.AWSGeneralUtil; +import org.apache.flink.connector.cloudwatch.sink.CloudWatchSink; +import org.apache.flink.connector.testframe.container.FlinkContainers; +import org.apache.flink.connector.testframe.container.TestcontainersSettings; +import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; +import org.apache.flink.test.junit5.MiniClusterExtension; +import org.apache.flink.test.resources.ResourceTestUtils; +import org.apache.flink.test.util.SQLJobSubmission; +import org.apache.flink.util.TestLogger; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.core.lookup.StrSubstitutor; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.rules.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.core.SdkSystemSetting; +import software.amazon.awssdk.http.SdkHttpClient; +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.Metric; +import software.amazon.awssdk.services.cloudwatch.model.MetricDataQuery; +import software.amazon.awssdk.services.cloudwatch.model.MetricStat; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEYS; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_NAMESPACE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_COUNT; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MAX_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MIN_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_SUM_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +/** Integration test for {@link CloudWatchSink} TableAPI. */ +@Testcontainers +@ExtendWith(MiniClusterExtension.class) +public class CloudWatchTableAPIITCase extends TestLogger { + private static final Logger LOG = LoggerFactory.getLogger(CloudWatchTableAPIITCase.class); + + private static String testDimensionValue1; + private static String testDimensionValue2; + + private static StreamExecutionEnvironment env; + + private final Path sqlConnectorCloudWatchJar = + ResourceTestUtils.getResource(".*cloudwatch.jar"); + + private CloudWatchClient cloudWatchClient; + private SdkHttpClient httpClient; + private static final Network network = Network.newNetwork(); + private static final String LOCALSTACK_DOCKER_IMAGE_VERSION = "localstack/localstack:3.7.2"; + + @ClassRule public static final Timeout TIMEOUT = new Timeout(10, TimeUnit.MINUTES); + + @Container + private static final LocalstackContainer MOCK_CLOUDWATCH_CONTAINER = + new LocalstackContainer(DockerImageName.parse(LOCALSTACK_DOCKER_IMAGE_VERSION)) + .withEnv("AWS_CBOR_DISABLE", "1") + .withEnv( + "FLINK_ENV_JAVA_OPTS", + "-Dorg.apache.flink.cloudwatch.shaded.com.amazonaws.sdk.disableCertChecking -Daws.cborEnabled=false") + .withLogConsumer((log) -> LOG.info(log.getUtf8String())) + .withNetwork(network) + .withNetworkAliases("localstack"); + + public static final TestcontainersSettings TESTCONTAINERS_SETTINGS = + TestcontainersSettings.builder() + .environmentVariable("AWS_CBOR_DISABLE", "1") + .environmentVariable( + "FLINK_ENV_JAVA_OPTS", + "-Dorg.apache.flink.cloudwatch.shaded.com.amazonaws.sdk.disableCertChecking -Daws.cborEnabled=false") + .network(network) + .logger(LOG) + .dependsOn(MOCK_CLOUDWATCH_CONTAINER) + .build(); + + public static final FlinkContainers FLINK = + FlinkContainers.builder().withTestcontainersSettings(TESTCONTAINERS_SETTINGS).build(); + + @BeforeClass + public static void setupFlink() throws Exception { + FLINK.start(); + } + + @AfterClass + public static void stopFlink() { + FLINK.stop(); + } + + @Before + public void setup() { + System.setProperty(SdkSystemSetting.CBOR_ENABLED.property(), "false"); + + testDimensionValue1 = UUID.randomUUID().toString(); + testDimensionValue2 = UUID.randomUUID().toString(); + + env = StreamExecutionEnvironment.getExecutionEnvironment(); + env.setParallelism(1); + + httpClient = AWSServicesTestUtils.createHttpClient(); + + cloudWatchClient = + AWSServicesTestUtils.createAwsSyncClient( + MOCK_CLOUDWATCH_CONTAINER.getEndpoint(), + httpClient, + CloudWatchClient.builder()); + + LOG.info("Done setting up the localstack."); + } + + @After + public void teardown() { + System.clearProperty(SdkSystemSetting.CBOR_ENABLED.property()); + AWSGeneralUtil.closeResources(httpClient, cloudWatchClient); + } + + @Test + public void testTableApiAndSQL() throws Exception { + executeSqlStatements(Collections.singletonList(getSqlStmt())); + + Instant testTimestamp = Instant.now(); + + GetMetricDataResponse response; + + Deadline deadline = Deadline.fromNow(Duration.ofSeconds(10)); + do { + Thread.sleep(1000); + response = + cloudWatchClient.getMetricData( + GetMetricDataRequest.builder() + .metricDataQueries( + MetricDataQuery.builder() + .metricStat(getMetricStat("Sum")) + .build(), + MetricDataQuery.builder() + .metricStat(getMetricStat("SampleCount")) + .build(), + MetricDataQuery.builder() + .metricStat(getMetricStat("Maximum")) + .build(), + MetricDataQuery.builder() + .metricStat(getMetricStat("Minimum")) + .build()) + .startTime(testTimestamp.minusSeconds(300)) + .endTime(testTimestamp.plusSeconds(300)) + .build()); + } while (deadline.hasTimeLeft() && response.metricDataResults().get(0).values().isEmpty()); + + assertThat(response.metricDataResults().get(0).values()) + .containsExactly(TEST_SAMPLE_VALUE + TEST_STATS_SUM_VALUE); + assertThat(response.metricDataResults().get(1).values()).containsExactly(TEST_SAMPLE_COUNT); + assertThat(response.metricDataResults().get(2).values()) + .containsExactly(TEST_STATS_MAX_VALUE); + assertThat(response.metricDataResults().get(3).values()) + .containsExactly(TEST_STATS_MIN_VALUE); + } + + private void executeSqlStatements(final List sqlLines) throws Exception { + FLINK.submitSQLJob( + new SQLJobSubmission.SQLJobSubmissionBuilder(sqlLines) + .addJars(sqlConnectorCloudWatchJar) + .build()); + } + + private static MetricStat getMetricStat(String stat) { + return MetricStat.builder() + .metric( + Metric.builder() + .namespace(TEST_NAMESPACE) + .metricName(TEST_METRIC_NAME) + .dimensions( + Dimension.builder() + .name(TEST_DIMENSION_KEY_1) + .value(testDimensionValue1) + .build(), + Dimension.builder() + .name(TEST_DIMENSION_KEY_2) + .value(testDimensionValue2) + .build()) + .build()) + .stat(stat) + .period(300) + .build(); + } + + private static String getSqlStmt() throws IOException { + String sqlStmt = + IOUtils.toString( + Objects.requireNonNull( + Thread.currentThread() + .getContextClassLoader() + .getResourceAsStream("test-sink-table.sql")), + StandardCharsets.UTF_8); + + Map valuesMap = new HashMap<>(); + + valuesMap.put("namespace", TEST_NAMESPACE); + valuesMap.put("dimension_keys", TEST_DIMENSION_KEYS); + valuesMap.put("dimension_key_1", TEST_DIMENSION_KEY_1); + valuesMap.put("dimension_key_2", TEST_DIMENSION_KEY_2); + valuesMap.put("dimension_val_1", testDimensionValue1); + valuesMap.put("dimension_val_2", testDimensionValue2); + valuesMap.put("metric_name", TEST_METRIC_NAME); + + return new StrSubstitutor(valuesMap).replace(sqlStmt); + } +} diff --git a/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/resources/log4j2-test.properties b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/resources/log4j2-test.properties new file mode 100644 index 000000000..835c2ec9a --- /dev/null +++ b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/resources/log4j2-test.properties @@ -0,0 +1,28 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 root logger level to OFF to not flood build logs +# set manually to INFO for debugging purposes +rootLogger.level = OFF +rootLogger.appenderRef.test.ref = TestLogger + +appender.testlogger.name = TestLogger +appender.testlogger.type = CONSOLE +appender.testlogger.target = SYSTEM_ERR +appender.testlogger.layout.type = PatternLayout +appender.testlogger.layout.pattern = %-4r [%t] %-5p %c %x - %m%n diff --git a/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/resources/test-sink-table.sql b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/resources/test-sink-table.sql new file mode 100644 index 000000000..b8f12d22e --- /dev/null +++ b/flink-connector-aws-e2e-tests/flink-connector-cloudwatch-e2e-tests/src/test/resources/test-sink-table.sql @@ -0,0 +1,58 @@ +--/* +-- * Licensed to the Apache Software Foundation (ASF) under one +-- * or more contributor license agreements. See the NOTICE file +-- * distributed with this work for additional information +-- * regarding copyright ownership. The ASF licenses this file +-- * to you 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. +-- */ + +CREATE TABLE CloudWatchTable +( + `my_metric_name` STRING, + `${dimension_key_1}` STRING, + `${dimension_key_2}` STRING, + `sample_value` DOUBLE, + `sample_count` DOUBLE, + `unit` STRING, + `storage_res` INT, + `stats_max` DOUBLE, + `stats_min` DOUBLE, + `stats_sum` DOUBLE, + `stats_count` DOUBLE +) + WITH ( + 'connector' = 'cloudwatch', + 'aws.region' = 'ap-southeast-1', + 'aws.endpoint' = 'https://localstack:4566', + 'aws.credentials.provider' = 'BASIC', + 'aws.credentials.provider.basic.accesskeyid' = 'accessKeyId', + 'aws.credentials.provider.basic.secretkey' = 'secretAccessKey', + 'aws.trust.all.certificates' = 'true', + 'sink.http-client.protocol.version' = 'HTTP1_1', + 'sink.batch.max-size' = '1', + 'metric.namespace' = '${namespace}', + 'metric.name.key' = 'my_metric_name', + 'metric.dimension.keys' = '${dimension_keys}', + 'metric.value.key' = 'sample_value', + 'metric.count.key' = 'sample_count', + 'metric.unit.key' = 'unit', + 'metric.storage-resolution.key' = 'storage_res', + 'metric.statistic.max.key' = 'stats_max', + 'metric.statistic.min.key' = 'stats_min', + 'metric.statistic.sum.key' = 'stats_sum', + 'metric.statistic.sample-count.key' = 'stats_count', + 'sink.invalid-metric.retry-mode' = 'RETRY' + ); + +INSERT INTO CloudWatchTable +VALUES ('${metric_name}', '${dimension_val_1}', '${dimension_val_2}', 123, 1, 'Seconds', 60, 999, 1, 10, 11); \ No newline at end of file diff --git a/flink-connector-aws-e2e-tests/pom.xml b/flink-connector-aws-e2e-tests/pom.xml index d7227369e..4c1cef727 100644 --- a/flink-connector-aws-e2e-tests/pom.xml +++ b/flink-connector-aws-e2e-tests/pom.xml @@ -42,6 +42,7 @@ under the License. flink-connector-aws-kinesis-streams-e2e-tests flink-connector-kinesis-e2e-tests flink-connector-aws-sqs-e2e-tests + flink-connector-cloudwatch-e2e-tests flink-formats-avro-glue-schema-registry-e2e-tests flink-formats-json-glue-schema-registry-e2e-tests diff --git a/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/70dddb8b-045a-45a9-80f2-a32ad375d87f b/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/70dddb8b-045a-45a9-80f2-a32ad375d87f new file mode 100644 index 000000000..e69de29bb diff --git a/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/9577f915-0c2c-4536-a06d-561accc2e4b6 b/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/9577f915-0c2c-4536-a06d-561accc2e4b6 new file mode 100644 index 000000000..e69de29bb diff --git a/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/stored.rules b/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/stored.rules new file mode 100644 index 000000000..bf684dc06 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/archunit-violations/stored.rules @@ -0,0 +1,4 @@ +# +#Wed Apr 16 17:34:20 BST 2025 +ITCASE\ tests\ should\ use\ a\ MiniCluster\ resource\ or\ extension=70dddb8b-045a-45a9-80f2-a32ad375d87f +Tests\ inheriting\ from\ AbstractTestBase\ should\ have\ name\ ending\ with\ ITCase=9577f915-0c2c-4536-a06d-561accc2e4b6 diff --git a/flink-connector-aws/flink-connector-cloudwatch/pom.xml b/flink-connector-aws/flink-connector-cloudwatch/pom.xml new file mode 100644 index 000000000..9909e5f2c --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/pom.xml @@ -0,0 +1,246 @@ + + + + + 4.0.0 + + + org.apache.flink + flink-connector-aws-parent + 5.1-SNAPSHOT + + + flink-connector-cloudwatch + Flink : Connectors : AWS : Amazon Cloudwatch + jar + + + 2.31.18 + 1.19.0 + + + + + org.apache.flink + flink-streaming-java + ${flink.version} + provided + + + + org.apache.flink + flink-connector-aws-base + ${project.version} + + + + software.amazon.awssdk + cloudwatch + + + + software.amazon.awssdk + netty-nio-client + + + + + org.apache.flink + flink-test-utils + ${flink.version} + test + + + org.apache.flink + flink-connector-test-utils + ${flink.version} + test + + + + org.apache.flink + flink-connector-aws-base + ${project.version} + test-jar + test + + + + org.apache.flink + flink-connector-base + ${flink.version} + test-jar + test + + + + org.testcontainers + testcontainers + test + + + + + org.apache.flink + flink-table-common + ${flink.version} + provided + + + + org.apache.flink + flink-table-runtime + ${flink.version} + test + + + + org.apache.flink + flink-table-common + ${flink.version} + test + test-jar + + + + + + org.apache.flink + flink-architecture-tests-test + test + + + + software.amazon.awssdk + s3 + test + + + + software.amazon.awssdk + bom + ${aws.sdkv2.version} + pom + import + + + + + + + + + software.amazon.awssdk + bom + ${aws.sdkv2.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + shade-flink + package + + shade + + + + + org.apache.flink:flink-connector-aws-base:* + com.amazonaws:* + software.amazon.awssdk:* + org.reactivestreams:* + io.netty:* + com.typesafe.netty:* + + + + + + org.apache.flink.connector.aws + + org.apache.flink.cloudwatch.shaded.org.apache.flink.connector.aws + + + + com.amazonaws + org.apache.flink.cloudwatch.shaded.com.amazonaws + + + io.netty + org.apache.flink.cloudwatch.shaded.io.netty + + + com.typesafe.netty + org.apache.flink.cloudwatch.shaded.com.typesafe.netty + + + software.amazon + org.apache.flink.cloudwatch.shaded.software.amazon + + + org.reactivestreams + org.apache.flink.cloudwatch.shaded.org.reactivestreams + + + + + *:* + + .gitkeep + + + + org.apache.flink:flink-connector-aws-base:* + + profile + + + + + + + + + + diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchConfigConstants.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchConfigConstants.java new file mode 100644 index 000000000..ce9f34b10 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchConfigConstants.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.PublicEvolving; +import org.apache.flink.configuration.ConfigOption; +import org.apache.flink.configuration.ConfigOptions; + +/** Defaults for {@link CloudWatchSinkWriter}. */ +@PublicEvolving +public class CloudWatchConfigConstants { + + public static final ConfigOption BASE_CLOUDWATCH_USER_AGENT_PREFIX_FORMAT = + ConfigOptions.key("Apache Flink %s (%s) CloudWatch Connector") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch useragent prefix format."); + + public static final ConfigOption CLOUDWATCH_CLIENT_USER_AGENT_PREFIX = + ConfigOptions.key("aws.cloudwatch.client.user-agent-prefix") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch identifier for user agent prefix."); +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchExceptionClassifiers.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchExceptionClassifiers.java new file mode 100644 index 000000000..565071633 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchExceptionClassifiers.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.connector.aws.sink.throwable.AWSExceptionClassifierUtil; +import org.apache.flink.connector.base.sink.throwable.FatalExceptionClassifier; + +import software.amazon.awssdk.services.cloudwatch.model.CloudWatchException; +import software.amazon.awssdk.services.cloudwatch.model.ResourceNotFoundException; + +/** + * Class containing set of {@link FatalExceptionClassifier} for {@link + * software.amazon.awssdk.services.cloudwatch.model.CloudWatchException}. + */ +@Internal +public class CloudWatchExceptionClassifiers { + + public static FatalExceptionClassifier getNotAuthorizedExceptionClassifier() { + return AWSExceptionClassifierUtil.withAWSServiceErrorCode( + CloudWatchException.class, + "NotAuthorized", + err -> + new CloudWatchSinkException( + "Encountered non-recoverable exception: NotAuthorized", err)); + } + + public static FatalExceptionClassifier getAccessDeniedExceptionClassifier() { + return AWSExceptionClassifierUtil.withAWSServiceErrorCode( + CloudWatchException.class, + "AccessDeniedException", + err -> + new CloudWatchSinkException( + "Encountered non-recoverable exception: AccessDeniedException", + err)); + } + + public static FatalExceptionClassifier getResourceNotFoundExceptionClassifier() { + return FatalExceptionClassifier.withRootCauseOfType( + ResourceNotFoundException.class, + err -> + new CloudWatchSinkException( + "Encountered non-recoverable exception relating to not being able to find the specified resources", + err)); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSink.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSink.java new file mode 100644 index 000000000..c6bd448fc --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSink.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.annotation.PublicEvolving; +import org.apache.flink.annotation.VisibleForTesting; +import org.apache.flink.connector.base.sink.AsyncSinkBase; +import org.apache.flink.connector.base.sink.writer.BufferedRequestState; +import org.apache.flink.connector.base.sink.writer.ElementConverter; +import org.apache.flink.connector.cloudwatch.sink.client.CloudWatchAsyncClientProvider; +import org.apache.flink.connector.cloudwatch.sink.client.SdkClientProvider; +import org.apache.flink.core.io.SimpleVersionedSerializer; +import org.apache.flink.util.Preconditions; + +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Properties; + +/** + * A CloudWatch Sink that performs async requests against a destination CloudWatch using the + * buffering protocol specified in {@link AsyncSinkBase}. + * + *

The sink internally uses a {@link CloudWatchAsyncClient} to communicate with the AWS endpoint. + * + *

Please see the writer implementation in {@link CloudWatchSinkWriter} + * + *

maxBatchSize is calculated in terms of requestEntries (MetricWriteRequest). In CloudWatch, + * each PutMetricDataRequest can have maximum of 1000 MetricWriteRequest, hence the maxBatchSize + * cannot be more than 1000. + * + *

maxBatchSizeInBytes is calculated in terms of size of requestEntries (MetricWriteRequest). In + * CloudWatch, each PutMetricDataRequest can have maximum of 1MB of payload, hence the + * maxBatchSizeInBytes cannot be more than 1 MB. + * + * @param Type of the elements handled by this sink + */ +@PublicEvolving +public class CloudWatchSink extends AsyncSinkBase { + + private final String namespace; + private final Properties cloudWatchClientProperties; + private transient SdkClientProvider asyncClientSdkClientProviderOverride; + private final InvalidMetricDataRetryMode invalidMetricDataRetryMode; + + CloudWatchSink( + ElementConverter elementConverter, + Integer maxBatchSize, + Integer maxInFlightRequests, + Integer maxBufferedRequests, + Long maxBatchSizeInBytes, + Long maxTimeInBufferMS, + Long maxRecordSizeInBytes, + String namespace, + Properties cloudWatchClientProperties, + InvalidMetricDataRetryMode invalidMetricDataRetryMode) { + super( + elementConverter, + maxBatchSize, + maxInFlightRequests, + maxBufferedRequests, + maxBatchSizeInBytes, + maxTimeInBufferMS, + maxRecordSizeInBytes); + this.namespace = + Preconditions.checkNotNull( + namespace, + "The cloudWatch namespace must not be null when initializing the CloudWatch Sink."); + this.invalidMetricDataRetryMode = invalidMetricDataRetryMode; + Preconditions.checkArgument( + !this.namespace.isEmpty(), + "The cloudWatch namespace must be set when initializing the CloudWatch Sink."); + + Preconditions.checkArgument( + (this.getMaxBatchSize() <= 1000), + "The cloudWatch MaxBatchSize must not be greater than 1,000."); + + Preconditions.checkArgument( + (this.getMaxBatchSizeInBytes() <= 1000 * 1000), + "The cloudWatch MaxBatchSizeInBytes must not be greater than 1,000,000."); + + this.cloudWatchClientProperties = cloudWatchClientProperties; + } + + /** + * Create a {@link CloudWatchSinkBuilder} to allow the fluent construction of a new {@code + * CloudWatchSink}. + * + * @param type of incoming records + * @return {@link CloudWatchSinkBuilder} + */ + public static CloudWatchSinkBuilder builder() { + return new CloudWatchSinkBuilder<>(); + } + + @Override + public StatefulSinkWriter> createWriter( + InitContext context) throws IOException { + return new CloudWatchSinkWriter<>( + getElementConverter(), + context, + getMaxBatchSize(), + getMaxInFlightRequests(), + getMaxBufferedRequests(), + getMaxBatchSizeInBytes(), + getMaxTimeInBufferMS(), + getMaxRecordSizeInBytes(), + namespace, + getAsyncClientProvider(cloudWatchClientProperties), + Collections.emptyList(), + invalidMetricDataRetryMode); + } + + @Override + public StatefulSinkWriter> restoreWriter( + InitContext context, + Collection> recoveredState) + throws IOException { + return new CloudWatchSinkWriter<>( + getElementConverter(), + context, + getMaxBatchSize(), + getMaxInFlightRequests(), + getMaxBufferedRequests(), + getMaxBatchSizeInBytes(), + getMaxTimeInBufferMS(), + getMaxRecordSizeInBytes(), + namespace, + getAsyncClientProvider(cloudWatchClientProperties), + recoveredState, + invalidMetricDataRetryMode); + } + + private SdkClientProvider getAsyncClientProvider( + Properties clientProperties) { + if (asyncClientSdkClientProviderOverride != null) { + return asyncClientSdkClientProviderOverride; + } + return new CloudWatchAsyncClientProvider(clientProperties); + } + + @Internal + @VisibleForTesting + void setCloudWatchAsyncClientProvider( + SdkClientProvider asyncClientSdkClientProviderOverride) { + this.asyncClientSdkClientProviderOverride = asyncClientSdkClientProviderOverride; + } + + @Override + public SimpleVersionedSerializer> + getWriterStateSerializer() { + return new CloudWatchStateSerializer(); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkBuilder.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkBuilder.java new file mode 100644 index 000000000..e91db051a --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkBuilder.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.PublicEvolving; +import org.apache.flink.annotation.VisibleForTesting; +import org.apache.flink.connector.base.sink.AsyncSinkBaseBuilder; +import org.apache.flink.connector.base.sink.writer.ElementConverter; + +import software.amazon.awssdk.http.Protocol; + +import java.util.Optional; +import java.util.Properties; + +import static org.apache.flink.connector.aws.config.AWSConfigConstants.HTTP_PROTOCOL_VERSION; +import static software.amazon.awssdk.http.Protocol.HTTP1_1; + +/** + * Builder to construct {@link CloudWatchSink}. + * + *

The following example shows the minimum setup to create a {@link CloudWatchSink} that writes + * String values to a CloudWatch named cloudWatchUrl. + * + *

{@code
+ * Properties sinkProperties = new Properties();
+ * sinkProperties.put(AWSConfigConstants.AWS_REGION, "eu-west-1");
+ *
+ * CloudWatchSink cloudWatchSink =
+ *         CloudWatchSink.builder()
+ *                 .setNamespace("namespace")
+ *                 .setCloudWatchClientProperties(sinkProperties)
+ *                 .build();
+ * }
+ * + *

If the following parameters are not set in this builder, the following defaults will be used: + * + *

    + *
  • {@code maxBatchSize} will be 100 + *
  • {@code maxInFlightRequests} will be 50 + *
  • {@code maxBufferedRequests} will be 5000 + *
  • {@code maxBatchSizeInBytes} will be 100 KB + *
  • {@code maxTimeInBufferMs} will be 5000ms + *
  • {@code maxRecordSizeInBytes} will be 1 KB + *
  • {@code invalidMetricDataRetryMode} will be FAIL_ON_ERROR + *
+ * + * @param type of elements that should be persisted in the destination + */ +@PublicEvolving +public class CloudWatchSinkBuilder + extends AsyncSinkBaseBuilder> { + + private static final int DEFAULT_MAX_BATCH_SIZE = 100; + private static final int DEFAULT_MAX_IN_FLIGHT_REQUESTS = 50; + private static final int DEFAULT_MAX_BUFFERED_REQUESTS = 5_000; + private static final long DEFAULT_MAX_BATCH_SIZE_IN_B = 100 * 1000; + private static final long DEFAULT_MAX_TIME_IN_BUFFER_MS = 5000; + private static final long DEFAULT_MAX_RECORD_SIZE_IN_B = 1000; + private static final InvalidMetricDataRetryMode DEFAULT_INVALID_METRIC_DATA_RETRY_MODE = + InvalidMetricDataRetryMode.FAIL_ON_ERROR; + private static final Protocol DEFAULT_HTTP_PROTOCOL = HTTP1_1; + + private String namespace; + private Properties cloudWatchClientProperties; + private InvalidMetricDataRetryMode invalidMetricDataRetryMode; + + private ElementConverter elementConverter; + + public CloudWatchSinkBuilder setElementConverter( + final ElementConverter elementConverter) { + this.elementConverter = elementConverter; + return this; + } + + public CloudWatchSinkBuilder setNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * If writing to CloudWatch results in a failure being returned due to Invalid Metric Data + * provided, the retry mode will be determined based on this config. + * + * @param invalidMetricDataRetryMode retry mode + * @return {@link CloudWatchSinkBuilder} itself + */ + public CloudWatchSinkBuilder setInvalidMetricDataRetryMode( + InvalidMetricDataRetryMode invalidMetricDataRetryMode) { + this.invalidMetricDataRetryMode = invalidMetricDataRetryMode; + return this; + } + + /** + * A set of properties used by the sink to create the CloudWatch client. This may be used to set + * the aws region, credentials etc. See the docs for usage and syntax. + * + * @param cloudWatchClientProps CloudWatch client properties + * @return {@link CloudWatchSinkBuilder} itself + */ + public CloudWatchSinkBuilder setCloudWatchClientProperties( + final Properties cloudWatchClientProps) { + cloudWatchClientProperties = cloudWatchClientProps; + return this; + } + + protected InvalidMetricDataRetryMode getInvalidMetricDataRetryMode() { + return this.invalidMetricDataRetryMode; + } + + @VisibleForTesting + Properties getClientPropertiesWithDefaultHttpProtocol() { + Properties clientProperties = + Optional.ofNullable(cloudWatchClientProperties).orElse(new Properties()); + clientProperties.putIfAbsent(HTTP_PROTOCOL_VERSION, DEFAULT_HTTP_PROTOCOL.toString()); + return clientProperties; + } + + @Override + public CloudWatchSink build() { + return new CloudWatchSink<>( + Optional.ofNullable(elementConverter) + .orElse( + (DefaultMetricWriteRequestElementConverter) + DefaultMetricWriteRequestElementConverter.builder() + .build()), + Optional.ofNullable(getMaxBatchSize()).orElse(DEFAULT_MAX_BATCH_SIZE), + Optional.ofNullable(getMaxInFlightRequests()) + .orElse(DEFAULT_MAX_IN_FLIGHT_REQUESTS), + Optional.ofNullable(getMaxBufferedRequests()).orElse(DEFAULT_MAX_BUFFERED_REQUESTS), + Optional.ofNullable(getMaxBatchSizeInBytes()).orElse(DEFAULT_MAX_BATCH_SIZE_IN_B), + Optional.ofNullable(getMaxTimeInBufferMS()).orElse(DEFAULT_MAX_TIME_IN_BUFFER_MS), + Optional.ofNullable(getMaxRecordSizeInBytes()).orElse(DEFAULT_MAX_RECORD_SIZE_IN_B), + namespace, + getClientPropertiesWithDefaultHttpProtocol(), + Optional.ofNullable(getInvalidMetricDataRetryMode()) + .orElse(DEFAULT_INVALID_METRIC_DATA_RETRY_MODE)); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkException.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkException.java new file mode 100644 index 000000000..e07b2ce09 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkException.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.PublicEvolving; + +/** + * A {@link RuntimeException} wrapper indicating the exception was thrown from the CloudWatch Sink. + */ +@PublicEvolving +class CloudWatchSinkException extends RuntimeException { + + public CloudWatchSinkException(final String message) { + super(message); + } + + public CloudWatchSinkException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * When the flag {@code failOnError} is set in {@link CloudWatchSinkWriter}, this exception is + * raised as soon as any exception occurs when writing to CloudWatch. + */ + static class CloudWatchFailFastSinkException extends CloudWatchSinkException { + + private static final String ERROR_MESSAGE = + "Encountered an exception while persisting records, not retrying due to {failOnError} being set."; + + public CloudWatchFailFastSinkException() { + super(ERROR_MESSAGE); + } + + public CloudWatchFailFastSinkException(final String errorMessage) { + super(errorMessage); + } + + public CloudWatchFailFastSinkException(final String errorMessage, final Throwable cause) { + super(errorMessage, cause); + } + + public CloudWatchFailFastSinkException(final Throwable cause) { + super(ERROR_MESSAGE, cause); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkWriter.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkWriter.java new file mode 100644 index 000000000..75e04e2af --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkWriter.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.api.connector.sink2.Sink; +import org.apache.flink.connector.aws.sink.throwable.AWSExceptionHandler; +import org.apache.flink.connector.aws.util.AWSGeneralUtil; +import org.apache.flink.connector.base.sink.throwable.FatalExceptionClassifier; +import org.apache.flink.connector.base.sink.writer.AsyncSinkWriter; +import org.apache.flink.connector.base.sink.writer.BufferedRequestState; +import org.apache.flink.connector.base.sink.writer.ElementConverter; +import org.apache.flink.connector.base.sink.writer.config.AsyncSinkWriterConfiguration; +import org.apache.flink.connector.cloudwatch.sink.client.SdkClientProvider; +import org.apache.flink.metrics.Counter; +import org.apache.flink.metrics.groups.SinkWriterMetricGroup; +import org.apache.flink.util.ExceptionUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; +import software.amazon.awssdk.services.cloudwatch.model.InvalidFormatException; +import software.amazon.awssdk.services.cloudwatch.model.InvalidParameterCombinationException; +import software.amazon.awssdk.services.cloudwatch.model.InvalidParameterValueException; +import software.amazon.awssdk.services.cloudwatch.model.MissingRequiredParameterException; +import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest; +import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataResponse; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.apache.flink.connector.aws.util.AWSCredentialFatalExceptionClassifiers.getInvalidCredentialsExceptionClassifier; +import static org.apache.flink.connector.aws.util.AWSCredentialFatalExceptionClassifiers.getSdkClientMisconfiguredExceptionClassifier; +import static org.apache.flink.connector.base.sink.writer.AsyncSinkFatalExceptionClassifiers.getInterruptedExceptionClassifier; + +/** + * Sink writer created by {@link CloudWatchSink} to write to CloudWatch. More details on the + * operation of this sink writer may be found in the doc for {@link CloudWatchSink}. More details on + * the internals of this sink writer may be found in {@link AsyncSinkWriter}. + * + *

The {@link CloudWatchAsyncClient} used here may be configured in the standard way for the AWS + * SDK 2.x. e.g. the provision of {@code AWS_REGION}, {@code AWS_ACCESS_KEY_ID} and {@code + * AWS_SECRET_ACCESS_KEY} through environment variables etc. + * + *

The batching of this sink is in terms of estimated size of a MetricWriteRequest in bytes. The + * goal is adaptively increase the number of MetricWriteRequest in each batch, a + * PutMetricDataRequest sent to CloudWatch, to a configurable number. This is the parameter + * maxBatchSizeInBytes which is calculated based on getSizeInBytes. + * + *

getSizeInBytes(requestEntry) returns the size of a MetricWriteRequest in bytes which is + * estimated by assuming each double takes 8 bytes, and each string char takes 1 byte (UTF_8 + * encoded). + */ +@Internal +class CloudWatchSinkWriter extends AsyncSinkWriter { + + private static final Logger LOG = LoggerFactory.getLogger(CloudWatchSinkWriter.class); + + private final SdkClientProvider clientProvider; + + private static final AWSExceptionHandler CLOUDWATCH_FATAL_EXCEPTION_HANDLER = + AWSExceptionHandler.withClassifier( + FatalExceptionClassifier.createChain( + getInterruptedExceptionClassifier(), + getInvalidCredentialsExceptionClassifier(), + CloudWatchExceptionClassifiers.getResourceNotFoundExceptionClassifier(), + CloudWatchExceptionClassifiers.getAccessDeniedExceptionClassifier(), + CloudWatchExceptionClassifiers.getNotAuthorizedExceptionClassifier(), + getSdkClientMisconfiguredExceptionClassifier())); + + private static final List CLOUDWATCH_INVALID_METRIC_EXCEPTION = + Arrays.asList( + InvalidFormatException.class, + InvalidParameterCombinationException.class, + InvalidParameterValueException.class, + MissingRequiredParameterException.class); + + private static final int BYTES_PER_DOUBLE = 8; + + private final Counter numRecordsOutErrorsCounter; + + /* Namespace of CloudWatch metric */ + private final String namespace; + + /* The sink writer metric group */ + private final SinkWriterMetricGroup metrics; + + /* The retry mode when an invalid metric caused failure */ + private final InvalidMetricDataRetryMode invalidMetricDataRetryMode; + + CloudWatchSinkWriter( + ElementConverter elementConverter, + Sink.InitContext context, + int maxBatchSize, + int maxInFlightRequests, + int maxBufferedRequests, + long maxBatchSizeInBytes, + long maxTimeInBufferMS, + long maxRecordSizeInBytes, + String namespace, + SdkClientProvider clientProvider, + Collection> initialStates, + InvalidMetricDataRetryMode invalidMetricDataRetryMode) { + super( + elementConverter, + context, + AsyncSinkWriterConfiguration.builder() + .setMaxBatchSize(maxBatchSize) + .setMaxBatchSizeInBytes(maxBatchSizeInBytes) + .setMaxInFlightRequests(maxInFlightRequests) + .setMaxBufferedRequests(maxBufferedRequests) + .setMaxTimeInBufferMS(maxTimeInBufferMS) + .setMaxRecordSizeInBytes(maxRecordSizeInBytes) + .build(), + initialStates); + this.namespace = namespace; + this.metrics = context.metricGroup(); + this.numRecordsOutErrorsCounter = metrics.getNumRecordsOutErrorsCounter(); + this.clientProvider = clientProvider; + this.invalidMetricDataRetryMode = invalidMetricDataRetryMode; + } + + @Override + protected void submitRequestEntries( + List requestEntries, + Consumer> requestResult) { + + final PutMetricDataRequest putMetricDataRequest = + PutMetricDataRequest.builder() + .namespace(namespace) + .metricData( + requestEntries.stream() + .map(MetricWriteRequest::toMetricDatum) + .collect(Collectors.toList())) + .strictEntityValidation(true) + .build(); + + CompletableFuture future = + clientProvider.getClient().putMetricData(putMetricDataRequest); + + // CloudWatchAsyncClient PutMetricDataRequest does not fail partially. + // If there is only one poison pill Metric Datum, the whole request fails fully. + future.whenComplete( + (response, err) -> { + if (err != null) { + handleFullyFailedRequest(err, requestEntries, requestResult); + } else { + requestResult.accept(Collections.emptyList()); + } + }) + .exceptionally( + ex -> { + getFatalExceptionCons() + .accept( + new CloudWatchSinkException + .CloudWatchFailFastSinkException( + ex.getMessage(), ex)); + return null; + }); + } + + @Override + protected long getSizeInBytes(MetricWriteRequest requestEntry) { + long sizeInBytes = 0L; + sizeInBytes += requestEntry.getMetricName().getBytes(StandardCharsets.UTF_8).length; + sizeInBytes += (long) requestEntry.getValues().length * BYTES_PER_DOUBLE; + sizeInBytes += (long) requestEntry.getCounts().length * BYTES_PER_DOUBLE; + + sizeInBytes += + Arrays.stream(requestEntry.getDimensions()) + .map( + dimension -> + dimension.getName().getBytes(StandardCharsets.UTF_8).length + + dimension + .getValue() + .getBytes(StandardCharsets.UTF_8) + .length) + .reduce(Integer::sum) + .orElse(0); + + sizeInBytes += + Stream.of( + requestEntry.getStatisticSum(), + requestEntry.getStatisticCount(), + requestEntry.getStatisticMax(), + requestEntry.getStatisticMin(), + requestEntry.getStorageResolution(), + requestEntry.getTimestamp()) + .filter(Objects::nonNull) + .count() + * BYTES_PER_DOUBLE; + + return sizeInBytes; + } + + @Override + public void close() { + AWSGeneralUtil.closeResources(clientProvider); + } + + private void handleFullyFailedRequest( + Throwable err, + List requestEntries, + Consumer> requestResult) { + + numRecordsOutErrorsCounter.inc(requestEntries.size()); + boolean isFatal = + CLOUDWATCH_FATAL_EXCEPTION_HANDLER.consumeIfFatal(err, getFatalExceptionCons()); + if (isFatal) { + return; + } + + if (CLOUDWATCH_INVALID_METRIC_EXCEPTION.stream() + .anyMatch(clazz -> ExceptionUtils.findThrowable(err, clazz).isPresent())) { + handleInvalidMetricRequest(err, requestEntries, requestResult); + return; + } + + LOG.warn( + "CloudWatch Sink failed to write and will retry all {} entries to CloudWatch, First request was {}", + requestEntries.size(), + requestEntries.get(0).toString(), + err); + requestResult.accept(requestEntries); + } + + private void handleInvalidMetricRequest( + Throwable err, + List requestEntries, + Consumer> requestResult) { + + switch (invalidMetricDataRetryMode) { + case SKIP_METRIC_ON_ERROR: + LOG.warn( + "CloudWatch Sink failed to write and will skip sending all {} entries", + requestEntries.size(), + err); + requestResult.accept(Collections.emptyList()); + return; + case FAIL_ON_ERROR: + LOG.warn( + "CloudWatch Sink failed to write all {} entries and will fail the job", + requestEntries.size(), + err); + getFatalExceptionCons() + .accept(new CloudWatchSinkException.CloudWatchFailFastSinkException(err)); + return; + case RETRY: + default: + LOG.warn( + "CloudWatch Sink failed to write and will retry all {} entries to CloudWatch, First request was {}", + requestEntries.size(), + requestEntries.get(0).toString(), + err); + requestResult.accept(requestEntries); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchStateSerializer.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchStateSerializer.java new file mode 100644 index 000000000..ca5fea94a --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchStateSerializer.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.connector.base.sink.writer.AsyncSinkWriterStateSerializer; + +import software.amazon.awssdk.utils.StringUtils; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** CloudWatch implementation {@link AsyncSinkWriterStateSerializer}. */ +@Internal +public class CloudWatchStateSerializer extends AsyncSinkWriterStateSerializer { + @Override + protected void serializeRequestToStream( + final MetricWriteRequest request, final DataOutputStream out) throws IOException { + + serializeMetricName(request.getMetricName(), out); + serializeValues(request, out); + serializeCounts(request, out); + serializeDimensions(request, out); + serializeDoubleValue(request.getStatisticMax(), out); + serializeDoubleValue(request.getStatisticMin(), out); + serializeDoubleValue(request.getStatisticSum(), out); + serializeDoubleValue(request.getStatisticCount(), out); + serializeUnit(request.getUnit(), out); + serializeStorageResolution(request.getStorageResolution(), out); + serializeTimestamp(request.getTimestamp(), out); + } + + @Override + protected MetricWriteRequest deserializeRequestFromStream( + final long requestSize, final DataInputStream in) throws IOException { + MetricWriteRequest.Builder builder = MetricWriteRequest.builder(); + + builder.withMetricName(deserializeMetricName(in)); + deserializeValues(in).forEach(builder::addValue); + deserializeCounts(in).forEach(builder::addCount); + deserializeDimensions(in) + .forEach( + dimension -> + builder.addDimension(dimension.getName(), dimension.getValue())); + Optional.ofNullable(deserializeDoubleValue(in)).ifPresent(builder::withStatisticMax); + Optional.ofNullable(deserializeDoubleValue(in)).ifPresent(builder::withStatisticMin); + Optional.ofNullable(deserializeDoubleValue(in)).ifPresent(builder::withStatisticSum); + Optional.ofNullable(deserializeDoubleValue(in)).ifPresent(builder::withStatisticCount); + Optional.ofNullable(deserializeUnit(in)).ifPresent(builder::withUnit); + Optional.ofNullable(deserializeStorageResolution(in)) + .ifPresent(builder::withStorageResolution); + Optional.ofNullable(deserializeTimestamp(in)).ifPresent(builder::withTimestamp); + + return builder.build(); + } + + private void serializeMetricName(String metricName, DataOutputStream out) throws IOException { + out.writeUTF(metricName); + } + + private String deserializeMetricName(DataInputStream in) throws IOException { + return in.readUTF(); + } + + private void serializeValues(MetricWriteRequest request, DataOutputStream out) + throws IOException { + boolean hasValues = request.getValues() != null && request.getValues().length > 0; + out.writeBoolean(hasValues); + if (hasValues) { + out.writeInt(request.getValues().length); + for (Double value : request.getValues()) { + out.writeDouble(value); + } + } + } + + private List deserializeValues(DataInputStream in) throws IOException { + return getDoubles(in); + } + + private void serializeCounts(MetricWriteRequest request, DataOutputStream out) + throws IOException { + boolean hasCounts = request.getCounts() != null && request.getCounts().length > 0; + out.writeBoolean(hasCounts); + if (hasCounts) { + out.writeInt(request.getCounts().length); + for (Double count : request.getCounts()) { + out.writeDouble(count); + } + } + } + + private List deserializeCounts(DataInputStream in) throws IOException { + return getDoubles(in); + } + + private List getDoubles(DataInputStream in) throws IOException { + boolean hasDoubles = in.readBoolean(); + if (!hasDoubles) { + return new ArrayList<>(); + } + + int size = in.readInt(); + List doubles = new ArrayList<>(); + for (int i = 0; i < size; i++) { + doubles.add(in.readDouble()); + } + return doubles; + } + + private void serializeDimensions(MetricWriteRequest request, DataOutputStream out) + throws IOException { + boolean hasDimensions = + request.getDimensions() != null && request.getDimensions().length > 0; + out.writeBoolean(hasDimensions); + if (hasDimensions) { + out.writeInt(request.getDimensions().length); + for (MetricWriteRequest.Dimension dimension : request.getDimensions()) { + out.writeUTF(dimension.getName()); + out.writeUTF(dimension.getValue()); + } + } + } + + private List deserializeDimensions(DataInputStream in) + throws IOException { + boolean hasDimensions = in.readBoolean(); + if (!hasDimensions) { + return new ArrayList<>(); + } + + int size = in.readInt(); + List dimensions = new ArrayList<>(); + for (int i = 0; i < size; i++) { + String name = in.readUTF(); + String value = in.readUTF(); + dimensions.add(new MetricWriteRequest.Dimension(name, value)); + } + return dimensions; + } + + private void serializeDoubleValue(Double value, DataOutputStream out) throws IOException { + boolean hasValue = value != null; + + out.writeBoolean(hasValue); + if (hasValue) { + out.writeDouble(value); + } + } + + private Double deserializeDoubleValue(DataInputStream in) throws IOException { + boolean hasValue = in.readBoolean(); + if (!hasValue) { + return null; + } + return in.readDouble(); + } + + private void serializeUnit(String unit, DataOutputStream out) throws IOException { + boolean hasUnit = !StringUtils.isEmpty(unit); + out.writeBoolean(hasUnit); + if (hasUnit) { + out.writeUTF(unit); + } + } + + private String deserializeUnit(DataInputStream in) throws IOException { + boolean hasUnit = in.readBoolean(); + if (!hasUnit) { + return null; + } + return in.readUTF(); + } + + private void serializeStorageResolution(Integer storageRes, DataOutputStream out) + throws IOException { + boolean hasStorageResolution = storageRes != null; + out.writeBoolean(hasStorageResolution); + if (hasStorageResolution) { + out.writeInt(storageRes); + } + } + + private Integer deserializeStorageResolution(DataInputStream in) throws IOException { + boolean hasStorageResolution = in.readBoolean(); + if (!hasStorageResolution) { + return null; + } + return in.readInt(); + } + + private void serializeTimestamp(Instant timestamp, DataOutputStream out) throws IOException { + boolean hasTimestamp = timestamp != null; + out.writeBoolean(hasTimestamp); + if (hasTimestamp) { + out.writeLong(timestamp.toEpochMilli()); + } + } + + private Instant deserializeTimestamp(DataInputStream in) throws IOException { + boolean hasTimestamp = in.readBoolean(); + if (!hasTimestamp) { + return null; + } + return Instant.ofEpochMilli(in.readLong()); + } + + @Override + public int getVersion() { + return 1; + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/DefaultMetricWriteRequestElementConverter.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/DefaultMetricWriteRequestElementConverter.java new file mode 100644 index 000000000..09bd354f3 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/DefaultMetricWriteRequestElementConverter.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.api.connector.sink2.SinkWriter; +import org.apache.flink.connector.base.sink.writer.ElementConverter; + +import java.util.UnknownFormatConversionException; + +/** + * An implementation of the {@link ElementConverter} that uses the AWS CloudWatch SDK v2. The user + * needs to provide the {@code InputT} in the form of {@link MetricWriteRequest}. + * + *

The InputT needs to be structured and only supports {@link MetricWriteRequest} + */ +@Internal +public class DefaultMetricWriteRequestElementConverter + extends MetricWriteRequestElementConverter { + + @Override + public MetricWriteRequest apply(InputT element, SinkWriter.Context context) { + if (!(element instanceof MetricWriteRequest)) { + throw new UnknownFormatConversionException( + "DefaultMetricWriteRequestElementConverter only supports MetricWriteRequest element."); + } + + return (MetricWriteRequest) element; + } + + public static Builder builder() { + return new Builder<>(); + } + + /** A builder for the DefaultMetricWriteRequestElementConverter. */ + public static class Builder { + + public DefaultMetricWriteRequestElementConverter build() { + return new DefaultMetricWriteRequestElementConverter<>(); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/InvalidMetricDataRetryMode.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/InvalidMetricDataRetryMode.java new file mode 100644 index 000000000..75c9eb5be --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/InvalidMetricDataRetryMode.java @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +/** RetryMode to handle invalid CloudWatch Metric Data. */ +public enum InvalidMetricDataRetryMode { + FAIL_ON_ERROR, + SKIP_METRIC_ON_ERROR, + RETRY +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequest.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequest.java new file mode 100644 index 000000000..d884665d2 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequest.java @@ -0,0 +1,335 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.PublicEvolving; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import software.amazon.awssdk.annotations.NotNull; +import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; +import software.amazon.awssdk.services.cloudwatch.model.StatisticSet; + +import java.io.Serializable; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** Pojo used as sink input, containing information for a single CloudWatch MetricDatum Object. */ +@PublicEvolving +public class MetricWriteRequest implements Serializable { + + public String metricName; + public Dimension[] dimensions; + public Double[] values; + public Double[] counts; + public Instant timestamp; + public String unit; + public Integer storageResolution; + public Double statisticMax; + public Double statisticMin; + public Double statisticSum; + public Double statisticCount; + + public MetricWriteRequest() {} + + public MetricWriteRequest( + @NotNull String metricName, + Dimension[] dimensions, + Double[] values, + Double[] counts, + Instant timestamp, + String unit, + Integer storageResolution, + Double statisticMax, + Double statisticMin, + Double statisticSum, + Double statisticCount) { + this.metricName = metricName; + this.dimensions = dimensions; + this.values = values; + this.counts = counts; + this.timestamp = timestamp; + this.unit = unit; + this.storageResolution = storageResolution; + this.statisticMax = statisticMax; + this.statisticMin = statisticMin; + this.statisticSum = statisticSum; + this.statisticCount = statisticCount; + } + + public Dimension[] getDimensions() { + return dimensions; + } + + public Double[] getValues() { + return values; + } + + public String getMetricName() { + return metricName; + } + + public Double[] getCounts() { + return counts; + } + + public Instant getTimestamp() { + return timestamp; + } + + public String getUnit() { + return unit; + } + + public Integer getStorageResolution() { + return storageResolution; + } + + public Double getStatisticMax() { + return statisticMax; + } + + public Double getStatisticMin() { + return statisticMin; + } + + public Double getStatisticSum() { + return statisticSum; + } + + public Double getStatisticCount() { + return statisticCount; + } + + public static Builder builder() { + return new Builder(); + } + + /** A single Dimension. */ + public static class Dimension implements Serializable { + public String name; + public String value; + + public Dimension() {} + + public Dimension(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public software.amazon.awssdk.services.cloudwatch.model.Dimension toCloudWatchDimension() { + return software.amazon.awssdk.services.cloudwatch.model.Dimension.builder() + .name(this.name) + .value(this.value) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Dimension label = (Dimension) o; + return new EqualsBuilder() + .append(name, label.name) + .append(value, label.value) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(name).append(value).toHashCode(); + } + } + + /** Builder for sink input pojo instance. */ + public static final class Builder { + private final List dimensions = new ArrayList<>(); + private final List values = new ArrayList<>(); + private final List counts = new ArrayList<>(); + private String metricName; + private Instant timestamp; + private String unit; + private Integer storageResolution; + private Double statisticMax; + private Double statisticMin; + private Double statisticSum; + private Double statisticCount; + + private Builder() {} + + public Builder withMetricName(String metricName) { + this.metricName = metricName; + return this; + } + + public Builder addDimension(String dimensionName, String dimensionValue) { + dimensions.add(new Dimension(dimensionName, dimensionValue)); + return this; + } + + public Builder addValue(Double value) { + values.add(value); + return this; + } + + public Builder addCount(Double count) { + counts.add(count); + return this; + } + + public Builder withTimestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder withUnit(String unit) { + this.unit = unit; + return this; + } + + public Builder withStorageResolution(Integer storageResolution) { + this.storageResolution = storageResolution; + return this; + } + + public Builder withStatisticMax(Double statisticMax) { + this.statisticMax = statisticMax; + return this; + } + + public Builder withStatisticMin(Double statisticMin) { + this.statisticMin = statisticMin; + return this; + } + + public Builder withStatisticSum(Double statisticSum) { + this.statisticSum = statisticSum; + return this; + } + + public Builder withStatisticCount(Double statisticCount) { + this.statisticCount = statisticCount; + return this; + } + + public MetricWriteRequest build() { + return new MetricWriteRequest( + metricName, + dimensions.toArray(new Dimension[dimensions.size()]), + values.toArray(new Double[values.size()]), + counts.toArray(new Double[counts.size()]), + timestamp, + unit, + storageResolution, + statisticMax, + statisticMin, + statisticSum, + statisticCount); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + MetricWriteRequest that = (MetricWriteRequest) o; + return Arrays.equals(dimensions, that.dimensions) + && Arrays.equals(values, that.values) + && Objects.equals(metricName, that.metricName) + && Objects.equals(timestamp, that.timestamp) + && Arrays.equals(counts, that.counts) + && Objects.equals(unit, that.unit) + && Objects.equals(storageResolution, that.storageResolution) + && Objects.equals(statisticMax, that.statisticMax) + && Objects.equals(statisticMin, that.statisticMin) + && Objects.equals(statisticSum, that.statisticSum) + && Objects.equals(statisticCount, that.statisticCount); + } + + @Override + public int hashCode() { + Integer result = Objects.hash(metricName); + result = 31 * result + Arrays.hashCode(dimensions); + result = 31 * result + Arrays.hashCode(values); + result = 31 * result + Arrays.hashCode(counts); + result = 31 * result + Objects.hash(timestamp); + result = 31 * result + Objects.hashCode(unit); + result = 31 * result + Objects.hashCode(storageResolution); + result = 31 * result + Objects.hashCode(statisticMax); + result = 31 * result + Objects.hashCode(statisticMin); + result = 31 * result + Objects.hashCode(statisticSum); + result = 31 * result + Objects.hashCode(statisticCount); + return result; + } + + @Override + public String toString() { + return toMetricDatum().toString(); + } + + public MetricDatum toMetricDatum() { + MetricDatum.Builder builder = + MetricDatum.builder().metricName(metricName).values(values).counts(counts); + + Optional.ofNullable(timestamp).ifPresent(builder::timestamp); + Optional.ofNullable(unit).ifPresent(builder::unit); + Optional.ofNullable(storageResolution).ifPresent(builder::storageResolution); + + if (dimensions.length > 0) { + builder.dimensions( + Arrays.stream(dimensions) + .map(Dimension::toCloudWatchDimension) + .collect(Collectors.toList())); + } + + if (statisticMax != null + | statisticMin != null + | statisticSum != null + | statisticCount != null) { + StatisticSet.Builder statisticBuilder = StatisticSet.builder(); + Optional.ofNullable(statisticMax).ifPresent(statisticBuilder::maximum); + Optional.ofNullable(statisticMin).ifPresent(statisticBuilder::minimum); + Optional.ofNullable(statisticSum).ifPresent(statisticBuilder::sum); + Optional.ofNullable(statisticCount).ifPresent(statisticBuilder::sampleCount); + builder.statisticValues(statisticBuilder.build()); + } + + return builder.build(); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestElementConverter.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestElementConverter.java new file mode 100644 index 000000000..1e3f6fc34 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestElementConverter.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.api.connector.sink2.SinkWriter; +import org.apache.flink.connector.base.sink.writer.ElementConverter; + +/** + * An implementation of the {@link ElementConverter} that uses the AWS CloudWatch SDK v2. The user + * only needs to provide the {@code InputT} to transform it into a {@link MetricWriteRequest} that + * may be persisted. + */ +@Internal +public abstract class MetricWriteRequestElementConverter + implements ElementConverter { + + @Override + public abstract MetricWriteRequest apply(InputT element, SinkWriter.Context context); +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/client/CloudWatchAsyncClientProvider.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/client/CloudWatchAsyncClientProvider.java new file mode 100644 index 000000000..61312af7e --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/client/CloudWatchAsyncClientProvider.java @@ -0,0 +1,64 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.flink.connector.cloudwatch.sink.client; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.connector.aws.util.AWSClientUtil; +import org.apache.flink.connector.aws.util.AWSGeneralUtil; +import org.apache.flink.connector.cloudwatch.sink.CloudWatchConfigConstants; + +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; + +import java.util.Properties; + +/** Provides a {@link CloudWatchAsyncClient}. */ +@Internal +public class CloudWatchAsyncClientProvider implements SdkClientProvider { + + private final SdkAsyncHttpClient httpClient; + private final CloudWatchAsyncClient cloudWatchAsyncClient; + + public CloudWatchAsyncClientProvider(Properties clientProperties) { + this.httpClient = AWSGeneralUtil.createAsyncHttpClient(clientProperties); + this.cloudWatchAsyncClient = buildClient(clientProperties, httpClient); + } + + @Override + public CloudWatchAsyncClient getClient() { + return cloudWatchAsyncClient; + } + + @Override + public void close() { + AWSGeneralUtil.closeResources(httpClient, cloudWatchAsyncClient); + } + + private CloudWatchAsyncClient buildClient( + Properties cloudWatchClientProperties, SdkAsyncHttpClient httpClient) { + AWSGeneralUtil.validateAwsCredentials(cloudWatchClientProperties); + + return AWSClientUtil.createAwsAsyncClient( + cloudWatchClientProperties, + httpClient, + CloudWatchAsyncClient.builder(), + CloudWatchConfigConstants.BASE_CLOUDWATCH_USER_AGENT_PREFIX_FORMAT.key(), + CloudWatchConfigConstants.CLOUDWATCH_CLIENT_USER_AGENT_PREFIX.key()); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/client/SdkClientProvider.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/client/SdkClientProvider.java new file mode 100644 index 000000000..1497d4533 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/sink/client/SdkClientProvider.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.flink.connector.cloudwatch.sink.client; + +import org.apache.flink.annotation.Internal; + +import software.amazon.awssdk.core.SdkClient; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** Provides a {@link SdkClient}. */ +@Internal +public interface SdkClientProvider extends SdkAutoCloseable { + + /** + * Returns {@link T}. + * + * @return the AWS SDK client + */ + T getClient(); +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchConnectorOptions.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchConnectorOptions.java new file mode 100644 index 000000000..3e7336b2e --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchConnectorOptions.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.annotation.PublicEvolving; +import org.apache.flink.configuration.ConfigOption; +import org.apache.flink.configuration.ConfigOptions; +import org.apache.flink.connector.cloudwatch.sink.InvalidMetricDataRetryMode; + +import java.util.List; +import java.util.Map; + +/** Options for the CloudWatch connector. */ +@PublicEvolving +public class CloudWatchConnectorOptions { + public static final ConfigOption AWS_REGION = + ConfigOptions.key("aws.region") + .stringType() + .noDefaultValue() + .withDescription("AWS region of used Cloudwatch metric."); + + public static final ConfigOption> AWS_CONFIG_PROPERTIES = + ConfigOptions.key("aws") + .mapType() + .noDefaultValue() + .withDescription("AWS configuration properties."); + + public static final ConfigOption METRIC_NAMESPACE = + ConfigOptions.key("metric.namespace") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric namespace."); + + public static final ConfigOption METRIC_NAME_KEY = + ConfigOptions.key("metric.name.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric name."); + + public static final ConfigOption> METRIC_DIMENSION_KEYS = + ConfigOptions.key("metric.dimension.keys") + .stringType() + .asList() + .noDefaultValue() + .withDescription("CloudWatch metric dimension key name."); + + public static final ConfigOption METRIC_VALUE_KEY = + ConfigOptions.key("metric.value.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric value key."); + + public static final ConfigOption METRIC_COUNT_KEY = + ConfigOptions.key("metric.count.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric count key."); + + public static final ConfigOption METRIC_UNIT_KEY = + ConfigOptions.key("metric.unit.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric unit key."); + + public static final ConfigOption METRIC_STORAGE_RESOLUTION_KEY = + ConfigOptions.key("metric.storage-resolution.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric storage resolution key."); + + public static final ConfigOption METRIC_TIMESTAMP_KEY = + ConfigOptions.key("metric.timestamp.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric sample timestamp."); + + public static final ConfigOption METRIC_STATISTIC_MAX_KEY = + ConfigOptions.key("metric.statistic.max.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric statistic max key."); + + public static final ConfigOption METRIC_STATISTIC_MIN_KEY = + ConfigOptions.key("metric.statistic.min.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric statistic min key."); + + public static final ConfigOption METRIC_STATISTIC_SUM_KEY = + ConfigOptions.key("metric.statistic.sum.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric statistic sum key."); + + public static final ConfigOption METRIC_STATISTIC_SAMPLE_COUNT_KEY = + ConfigOptions.key("metric.statistic.sample-count.key") + .stringType() + .noDefaultValue() + .withDescription("CloudWatch metric statistic sample count key."); + + public static final ConfigOption INVALID_METRIC_DATA_RETRY_MODE = + ConfigOptions.key("sink.invalid-metric.retry-mode") + .enumType(InvalidMetricDataRetryMode.class) + .noDefaultValue() + .withDescription( + "Retry mode when an invalid CloudWatch Metric is encountered."); +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicSink.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicSink.java new file mode 100644 index 000000000..16e06fd1d --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicSink.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.connector.base.table.sink.AsyncDynamicTableSink; +import org.apache.flink.connector.base.table.sink.AsyncDynamicTableSinkBuilder; +import org.apache.flink.connector.cloudwatch.sink.CloudWatchSinkBuilder; +import org.apache.flink.connector.cloudwatch.sink.InvalidMetricDataRetryMode; +import org.apache.flink.table.connector.ChangelogMode; +import org.apache.flink.table.connector.sink.DynamicTableSink; +import org.apache.flink.table.connector.sink.SinkV2Provider; +import org.apache.flink.table.data.RowData; +import org.apache.flink.table.types.DataType; +import org.apache.flink.util.Preconditions; +import org.apache.flink.util.StringUtils; + +import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; + +import javax.annotation.Nullable; + +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; + +/** A {@link DynamicTableSink} for CloudWatch. */ +@Internal +public class CloudWatchDynamicSink extends AsyncDynamicTableSink { + + /** Physical data type of the table. */ + private final DataType physicalDataType; + + /** Url of CloudWatch queue to write to. */ + private final String namespace; + + /** Properties for the CloudWatch Aws Client. */ + private final Properties cloudWatchClientProps; + + /** Retry mode when an invalid CloudWatch Metric is encountered. */ + private final InvalidMetricDataRetryMode invalidMetricDataRetryMode; + + /** Properties for the CloudWatch Table Config. */ + private final CloudWatchTableConfig cloudWatchTableConfig; + + protected CloudWatchDynamicSink( + @Nullable Integer maxBatchSize, + @Nullable Integer maxInFlightRequests, + @Nullable Integer maxBufferedRequests, + @Nullable Long maxBufferSizeInBytes, + @Nullable Long maxTimeInBufferMS, + @Nullable InvalidMetricDataRetryMode invalidMetricDataRetryMode, + @Nullable DataType physicalDataType, + String namespace, + @Nullable Properties cloudWatchClientProps, + CloudWatchTableConfig cloudWatchTableConfig) { + super( + maxBatchSize, + maxInFlightRequests, + maxBufferedRequests, + maxBufferSizeInBytes, + maxTimeInBufferMS); + this.cloudWatchTableConfig = cloudWatchTableConfig; + Preconditions.checkArgument( + !StringUtils.isNullOrWhitespaceOnly(namespace), + "CloudWatch namespace must not be null or empty when creating CloudWatch sink."); + this.physicalDataType = physicalDataType; + this.namespace = namespace; + this.cloudWatchClientProps = cloudWatchClientProps; + this.invalidMetricDataRetryMode = invalidMetricDataRetryMode; + } + + @Override + public ChangelogMode getChangelogMode(ChangelogMode changelogMode) { + return ChangelogMode.upsert(); + } + + @Override + public SinkRuntimeProvider getSinkRuntimeProvider(Context context) { + CloudWatchSinkBuilder builder = + new CloudWatchSinkBuilder() + .setNamespace(namespace) + .setElementConverter( + new RowDataElementConverter( + physicalDataType, cloudWatchTableConfig)); + + Optional.ofNullable(cloudWatchClientProps) + .ifPresent(builder::setCloudWatchClientProperties); + Optional.ofNullable(invalidMetricDataRetryMode) + .ifPresent(builder::setInvalidMetricDataRetryMode); + return SinkV2Provider.of(builder.build()); + } + + @Override + public DynamicTableSink copy() { + return new CloudWatchDynamicSink( + maxBatchSize, + maxInFlightRequests, + maxBufferedRequests, + maxBufferSizeInBytes, + maxTimeInBufferMS, + invalidMetricDataRetryMode, + physicalDataType, + namespace, + cloudWatchClientProps, + cloudWatchTableConfig); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CloudWatchDynamicSink that = (CloudWatchDynamicSink) o; + return super.equals(o) + && Objects.equals(invalidMetricDataRetryMode, that.invalidMetricDataRetryMode) + && Objects.equals(physicalDataType, that.physicalDataType) + && Objects.equals(namespace, that.namespace) + && Objects.equals(cloudWatchClientProps, that.cloudWatchClientProps); + } + + @Override + public int hashCode() { + return Objects.hash( + super.hashCode(), + physicalDataType, + namespace, + cloudWatchClientProps, + invalidMetricDataRetryMode); + } + + @Override + public String asSummaryString() { + StringBuilder sb = new StringBuilder(); + sb.append("CloudWatchDynamicSink{"); + sb.append("cloudWatchUrl='").append(namespace).append('\''); + sb.append(", physicalDataType=").append(physicalDataType); + sb.append(", cloudWatchTableConfig=").append(cloudWatchTableConfig); + sb.append(", invalidMetricDataRetryMode=").append(invalidMetricDataRetryMode); + Optional.ofNullable(cloudWatchClientProps) + .ifPresent( + props -> + props.forEach( + (k, v) -> sb.append(", ").append(k).append("=").append(v))); + sb.append(", maxBatchSize=").append(maxBatchSize); + sb.append(", maxInFlightRequests=").append(maxInFlightRequests); + sb.append(", maxBufferedRequests=").append(maxBufferedRequests); + sb.append(", maxBufferSizeInBytes=").append(maxBufferSizeInBytes); + sb.append(", maxTimeInBufferMS=").append(maxTimeInBufferMS); + sb.append('}'); + return sb.toString(); + } + + @Override + public String toString() { + return asSummaryString(); + } + + public static CloudWatchDynamicSinkBuilder builder() { + return new CloudWatchDynamicSinkBuilder(); + } + + /** Builder for {@link CloudWatchDynamicSink}. */ + @Internal + public static class CloudWatchDynamicSinkBuilder + extends AsyncDynamicTableSinkBuilder { + + private String namespace; + + private Properties cloudWatchClientProps; + + private InvalidMetricDataRetryMode invalidMetricDataRetryMode; + + private DataType physicalDataType; + + private CloudWatchTableConfig cloudWatchTableConfig; + + public CloudWatchDynamicSinkBuilder setCloudWatchMetricNamespace(String namespace) { + this.namespace = namespace; + return this; + } + + public CloudWatchDynamicSinkBuilder setCloudWatchTableConfig( + CloudWatchTableConfig cloudWatchTableConfig) { + this.cloudWatchTableConfig = cloudWatchTableConfig; + return this; + } + + public CloudWatchDynamicSinkBuilder setInvalidMetricDataRetryMode( + InvalidMetricDataRetryMode invalidMetricDataRetryMode) { + this.invalidMetricDataRetryMode = invalidMetricDataRetryMode; + return this; + } + + public CloudWatchDynamicSinkBuilder setCloudWatchClientProperties( + Properties cloudWatchClientProps) { + this.cloudWatchClientProps = cloudWatchClientProps; + return this; + } + + public CloudWatchDynamicSinkBuilder setPhysicalDataType(DataType physicalDataType) { + this.physicalDataType = physicalDataType; + return this; + } + + @Override + public CloudWatchDynamicSink build() { + return new CloudWatchDynamicSink( + getMaxBatchSize(), + getMaxInFlightRequests(), + getMaxBufferedRequests(), + getMaxBufferSizeInBytes(), + getMaxTimeInBufferMS(), + invalidMetricDataRetryMode, + physicalDataType, + namespace, + cloudWatchClientProps, + cloudWatchTableConfig); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicTableFactory.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicTableFactory.java new file mode 100644 index 000000000..2382f31ce --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicTableFactory.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.configuration.ConfigOption; +import org.apache.flink.configuration.ReadableConfig; +import org.apache.flink.connector.aws.util.AWSGeneralUtil; +import org.apache.flink.connector.base.table.AsyncDynamicTableSinkFactory; +import org.apache.flink.connector.base.table.AsyncSinkConnectorOptions; +import org.apache.flink.table.catalog.ResolvedCatalogTable; +import org.apache.flink.table.connector.sink.DynamicTableSink; +import org.apache.flink.table.factories.FactoryUtil; +import org.apache.flink.table.types.DataType; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.Set; + +/** Factory for creating configured instances of {@link CloudWatchDynamicSink}. */ +@Internal +public class CloudWatchDynamicTableFactory extends AsyncDynamicTableSinkFactory { + static final String IDENTIFIER = "cloudwatch"; + + @Override + public DynamicTableSink createDynamicTableSink(Context context) { + FactoryUtil.TableFactoryHelper factoryHelper = + FactoryUtil.createTableFactoryHelper(this, context); + ResolvedCatalogTable catalogTable = context.getCatalogTable(); + DataType physicalDataType = catalogTable.getResolvedSchema().toPhysicalRowDataType(); + + FactoryUtil.validateFactoryOptions(this, factoryHelper.getOptions()); + + Properties clientProperties = getCloudWatchClientProperties(factoryHelper.getOptions()); + AWSGeneralUtil.validateAwsConfiguration(clientProperties); + + CloudWatchTableConfig cloudWatchTableConfig = + new CloudWatchTableConfig(factoryHelper.getOptions()); + + CloudWatchDynamicSink.CloudWatchDynamicSinkBuilder builder = + CloudWatchDynamicSink.builder() + .setCloudWatchMetricNamespace(cloudWatchTableConfig.getNamespace()) + .setCloudWatchTableConfig(cloudWatchTableConfig) + .setCloudWatchClientProperties(clientProperties) + .setPhysicalDataType(physicalDataType) + .setInvalidMetricDataRetryMode( + factoryHelper + .getOptions() + .get( + CloudWatchConnectorOptions + .INVALID_METRIC_DATA_RETRY_MODE)); + + addAsyncOptionsToBuilder(getAsyncSinkOptions(factoryHelper.getOptions()), builder); + return builder.build(); + } + + @Override + public String factoryIdentifier() { + return IDENTIFIER; + } + + @Override + public Set> requiredOptions() { + Set> options = new HashSet<>(); + options.add(CloudWatchConnectorOptions.AWS_REGION); + options.add(CloudWatchConnectorOptions.METRIC_NAMESPACE); + options.add(CloudWatchConnectorOptions.METRIC_NAME_KEY); + + return options; + } + + @Override + public Set> optionalOptions() { + Set> options = super.optionalOptions(); + options.add(CloudWatchConnectorOptions.INVALID_METRIC_DATA_RETRY_MODE); + options.add(CloudWatchConnectorOptions.AWS_CONFIG_PROPERTIES); + + options.add(CloudWatchConnectorOptions.METRIC_VALUE_KEY); + options.add(CloudWatchConnectorOptions.METRIC_COUNT_KEY); + options.add(CloudWatchConnectorOptions.METRIC_DIMENSION_KEYS); + options.add(CloudWatchConnectorOptions.METRIC_STORAGE_RESOLUTION_KEY); + options.add(CloudWatchConnectorOptions.METRIC_TIMESTAMP_KEY); + options.add(CloudWatchConnectorOptions.METRIC_UNIT_KEY); + options.add(CloudWatchConnectorOptions.METRIC_STATISTIC_MAX_KEY); + options.add(CloudWatchConnectorOptions.METRIC_STATISTIC_MIN_KEY); + options.add(CloudWatchConnectorOptions.METRIC_STATISTIC_SUM_KEY); + options.add(CloudWatchConnectorOptions.METRIC_STATISTIC_SAMPLE_COUNT_KEY); + return options; + } + + @Override + public Set> forwardOptions() { + Set> options = new HashSet<>(); + options.add(CloudWatchConnectorOptions.AWS_REGION); + options.add(CloudWatchConnectorOptions.AWS_CONFIG_PROPERTIES); + return options; + } + + private Properties getCloudWatchClientProperties(ReadableConfig config) { + Properties properties = new Properties(); + properties.putAll( + appendAwsPrefixToOptions( + config.get(CloudWatchConnectorOptions.AWS_CONFIG_PROPERTIES))); + + return properties; + } + + private Map appendAwsPrefixToOptions(Map options) { + Map prefixedProperties = new HashMap<>(); + options.forEach((key, value) -> prefixedProperties.put("aws" + "." + key, value)); + return prefixedProperties; + } + + private Properties getAsyncSinkOptions(ReadableConfig config) { + Properties properties = new Properties(); + Optional.ofNullable(config.get(AsyncSinkConnectorOptions.FLUSH_BUFFER_SIZE)) + .ifPresent( + flushBufferSize -> + properties.put( + AsyncSinkConnectorOptions.FLUSH_BUFFER_SIZE.key(), + flushBufferSize)); + Optional.ofNullable(config.get(AsyncSinkConnectorOptions.MAX_BATCH_SIZE)) + .ifPresent( + maxBatchSize -> + properties.put( + AsyncSinkConnectorOptions.MAX_BATCH_SIZE.key(), + maxBatchSize)); + Optional.ofNullable(config.get(AsyncSinkConnectorOptions.MAX_IN_FLIGHT_REQUESTS)) + .ifPresent( + maxInflightRequests -> + properties.put( + AsyncSinkConnectorOptions.MAX_IN_FLIGHT_REQUESTS.key(), + maxInflightRequests)); + Optional.ofNullable(config.get(AsyncSinkConnectorOptions.MAX_BUFFERED_REQUESTS)) + .ifPresent( + maxBufferedRequests -> + properties.put( + AsyncSinkConnectorOptions.MAX_BUFFERED_REQUESTS.key(), + maxBufferedRequests)); + Optional.ofNullable(config.get(AsyncSinkConnectorOptions.FLUSH_BUFFER_TIMEOUT)) + .ifPresent( + timeout -> + properties.put( + AsyncSinkConnectorOptions.FLUSH_BUFFER_TIMEOUT.key(), + timeout)); + return properties; + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchTableConfig.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchTableConfig.java new file mode 100644 index 000000000..19cd29ebc --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/CloudWatchTableConfig.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.configuration.ReadableConfig; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_DIMENSION_KEYS; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_NAMESPACE; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_NAME_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_MAX_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_MIN_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_SAMPLE_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_SUM_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STORAGE_RESOLUTION_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_TIMESTAMP_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_UNIT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_VALUE_KEY; + +/** CloudWatch specific configuration. */ +@Internal +public class CloudWatchTableConfig implements Serializable { + private final ReadableConfig options; + + public CloudWatchTableConfig(ReadableConfig options) { + this.options = options; + } + + public String getNamespace() { + return options.get(METRIC_NAMESPACE); + } + + public String getMetricName() { + return options.get(METRIC_NAME_KEY); + } + + public Set getMetricDimensionKeys() { + return new HashSet<>(options.get(METRIC_DIMENSION_KEYS)); + } + + public String getMetricTimestampKey() { + return options.get(METRIC_TIMESTAMP_KEY); + } + + public String getMetricUnitKey() { + return options.get(METRIC_UNIT_KEY); + } + + public String getMetricCountKey() { + return options.get(METRIC_COUNT_KEY); + } + + public String getMetricValueKey() { + return options.get(METRIC_VALUE_KEY); + } + + public String getMetricStatisticMaxKey() { + return options.get(METRIC_STATISTIC_MAX_KEY); + } + + public String getMetricStatisticMinKey() { + return options.get(METRIC_STATISTIC_MIN_KEY); + } + + public String getMetricStatisticSumKey() { + return options.get(METRIC_STATISTIC_SUM_KEY); + } + + public String getMetricStatisticSampleCountKey() { + return options.get(METRIC_STATISTIC_SAMPLE_COUNT_KEY); + } + + public String getMetricStorageResolutionKey() { + return options.get(METRIC_STORAGE_RESOLUTION_KEY); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/RowDataElementConverter.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/RowDataElementConverter.java new file mode 100644 index 000000000..5a9760561 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/RowDataElementConverter.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.annotation.PublicEvolving; +import org.apache.flink.api.connector.sink2.SinkWriter; +import org.apache.flink.connector.base.sink.writer.ElementConverter; +import org.apache.flink.connector.cloudwatch.sink.MetricWriteRequest; +import org.apache.flink.table.data.RowData; +import org.apache.flink.table.types.DataType; + +/** + * Converts the sink input {@link RowData} into the Protobuf {@link MetricWriteRequest} that are + * sent to CloudWatch. + */ +@PublicEvolving +public class RowDataElementConverter implements ElementConverter { + private final DataType physicalDataType; + private final CloudWatchTableConfig cloudWatchTableConfig; + private transient RowDataToMetricWriteRequestConverter rowDataToCloudWatchMetricInputConverter; + + public RowDataElementConverter( + DataType physicalDataType, CloudWatchTableConfig cloudWatchTableConfig) { + this.physicalDataType = physicalDataType; + this.cloudWatchTableConfig = cloudWatchTableConfig; + this.rowDataToCloudWatchMetricInputConverter = + new RowDataToMetricWriteRequestConverter(physicalDataType, cloudWatchTableConfig); + } + + public MetricWriteRequest apply(RowData element, SinkWriter.Context context) { + if (rowDataToCloudWatchMetricInputConverter == null) { + rowDataToCloudWatchMetricInputConverter = + new RowDataToMetricWriteRequestConverter( + physicalDataType, cloudWatchTableConfig); + } + + return rowDataToCloudWatchMetricInputConverter.convertRowData(element); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/RowDataToMetricWriteRequestConverter.java b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/RowDataToMetricWriteRequestConverter.java new file mode 100644 index 000000000..05c79fe67 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/java/org/apache/flink/connector/cloudwatch/table/RowDataToMetricWriteRequestConverter.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.annotation.Internal; +import org.apache.flink.connector.cloudwatch.sink.MetricWriteRequest; +import org.apache.flink.table.api.DataTypes; +import org.apache.flink.table.data.RowData; +import org.apache.flink.table.data.TimestampData; +import org.apache.flink.table.types.DataType; + +import java.time.Instant; +import java.util.List; + +import static org.apache.flink.table.data.RowData.createFieldGetter; + +/** Converts from Flink Table API internal type of {@link RowData} to {@link MetricWriteRequest}. */ +@Internal +public class RowDataToMetricWriteRequestConverter { + + private final DataType physicalDataType; + private final CloudWatchTableConfig tableConfig; + + public RowDataToMetricWriteRequestConverter( + DataType physicalDataType, CloudWatchTableConfig tableConfig) { + this.physicalDataType = physicalDataType; + this.tableConfig = tableConfig; + } + + public MetricWriteRequest convertRowData(RowData row) { + List fields = DataType.getFields(physicalDataType); + + MetricWriteRequest.Builder builder = MetricWriteRequest.builder(); + + for (int i = 0; i < fields.size(); i++) { + DataTypes.Field field = fields.get(i); + RowData.FieldGetter fieldGetter = + createFieldGetter(fields.get(i).getDataType().getLogicalType(), i); + FieldValue fieldValue = new FieldValue(fieldGetter.getFieldOrNull(row)); + String fieldName = field.getName(); + + if (fieldName.equals(tableConfig.getMetricName())) { + builder.withMetricName(fieldValue.getStringValue()); + } else if (fieldName.equals(tableConfig.getMetricValueKey())) { + builder.addValue(fieldValue.getDoubleValue()); + } else if (tableConfig.getMetricDimensionKeys().contains(fieldName)) { + builder.addDimension(fieldName, fieldValue.getStringValue()); + } else if (fieldName.equals(tableConfig.getMetricCountKey())) { + builder.addCount(fieldValue.getDoubleValue()); + } else if (fieldName.equals(tableConfig.getMetricUnitKey())) { + builder.withUnit(fieldValue.getStringValue()); + } else if (fieldName.equals(tableConfig.getMetricTimestampKey())) { + builder.withTimestamp(Instant.ofEpochMilli(fieldValue.getLongValue())); + } else if (fieldName.equals(tableConfig.getMetricStorageResolutionKey())) { + builder.withStorageResolution(fieldValue.getIntegerValue()); + } else if (fieldName.equals(tableConfig.getMetricStatisticMaxKey())) { + builder.withStatisticMax(fieldValue.getDoubleValue()); + } else if (fieldName.equals(tableConfig.getMetricStatisticMinKey())) { + builder.withStatisticMin(fieldValue.getDoubleValue()); + } else if (fieldName.equals(tableConfig.getMetricStatisticSumKey())) { + builder.withStatisticSum(fieldValue.getDoubleValue()); + } else if (fieldName.equals(tableConfig.getMetricStatisticSampleCountKey())) { + builder.withStatisticCount(fieldValue.getDoubleValue()); + } else { + throw new IllegalArgumentException("Unsupported field: " + fieldName); + } + } + + return builder.build(); + } + + private static class FieldValue { + private final Object value; + + private FieldValue(Object value) { + this.value = value; + } + + private String getStringValue() { + return value.toString(); + } + + private Double getDoubleValue() { + return Double.valueOf(value.toString()); + } + + private Integer getIntegerValue() { + return Integer.valueOf(value.toString()); + } + + private Long getLongValue() { + if (value instanceof TimestampData) { + return ((TimestampData) value).getMillisecond(); + } + return Long.valueOf(value.toString()); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory b/flink-connector-aws/flink-connector-cloudwatch/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory new file mode 100644 index 000000000..1ec4e8a9b --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +org.apache.flink.connector.cloudwatch.table.CloudWatchDynamicTableFactory \ No newline at end of file diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/main/resources/log4j2.properties b/flink-connector-aws/flink-connector-cloudwatch/src/main/resources/log4j2.properties new file mode 100644 index 000000000..c64a340a8 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/main/resources/log4j2.properties @@ -0,0 +1,25 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +################################################################################ + +rootLogger.level = OFF +rootLogger.appenderRef.console.ref = ConsoleAppender + +appender.console.name = ConsoleAppender +appender.console.type = CONSOLE +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{HH:mm:ss,SSS} %-5p %-60c %x - %m%n diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/architecture/TestCodeArchitectureTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/architecture/TestCodeArchitectureTest.java new file mode 100644 index 000000000..a32e171f3 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/architecture/TestCodeArchitectureTest.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.flink.architecture; + +import org.apache.flink.architecture.common.ImportOptions; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchTests; + +/** Architecture tests for test code. */ +@AnalyzeClasses( + packages = "org.apache.flink.connector.cloudwatch", + importOptions = { + ImportOption.OnlyIncludeTests.class, + ImportOptions.ExcludeScalaImportOption.class, + ImportOptions.ExcludeShadedImportOption.class + }) +public class TestCodeArchitectureTest { + + @ArchTest + public static final ArchTests COMMON_TESTS = ArchTests.in(TestCodeArchitectureTestBase.class); +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkTest.java new file mode 100644 index 000000000..e4571d0ff --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkTest.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.connector.base.sink.writer.TestSinkInitContext; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.exception.SdkClientException; + +import java.util.Properties; + +import static org.apache.flink.connector.aws.config.AWSConfigConstants.AWS_CREDENTIALS_PROVIDER; +import static org.apache.flink.connector.aws.config.AWSConfigConstants.AWS_REGION; +import static org.apache.flink.connector.aws.config.AWSConfigConstants.CredentialProvider.BASIC; +import static org.apache.flink.connector.aws.config.AWSConfigConstants.CredentialProvider.ENV_VAR; +import static org.apache.flink.connector.aws.config.AWSConfigConstants.CredentialProvider.SYS_PROP; +import static org.apache.flink.connector.aws.config.AWSConfigConstants.CredentialProvider.WEB_IDENTITY_TOKEN; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_NAMESPACE; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class CloudWatchSinkTest { + @Test + public void testSuccessfullyCreateWithMinimalConfiguration() { + CloudWatchSink.builder().setNamespace(TEST_NAMESPACE).build(); + } + + @Test + public void testElementConverterUsesDefaultConverterIfNotSet() { + CloudWatchSink sink = + CloudWatchSink.builder().setNamespace(TEST_NAMESPACE).build(); + + assertThat(sink) + .extracting("elementConverter") + .isInstanceOf(DefaultMetricWriteRequestElementConverter.class); + } + + @Test + public void testNamespaceRequired() { + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> CloudWatchSink.builder().build()) + .withMessageContaining( + "The cloudWatch namespace must not be null when initializing the CloudWatch Sink."); + } + + @Test + public void testNamespaceNotBlank() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> CloudWatchSink.builder().setNamespace("").build()) + .withMessageContaining( + "The cloudWatch namespace must be set when initializing the CloudWatch Sink."); + } + + @Test + public void testInvalidMaxBatchSize() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> + CloudWatchSink.builder() + .setNamespace(TEST_NAMESPACE) + .setMaxBatchSize(1001) + .build()) + .withMessageContaining( + "The cloudWatch MaxBatchSize must not be greater than 1,000."); + } + + @Test + public void testInvalidMaxBatchSizeInBytes() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy( + () -> + CloudWatchSink.builder() + .setNamespace(TEST_NAMESPACE) + .setMaxBatchSizeInBytes(1000001) + .build()) + .withMessageContaining( + "The cloudWatch MaxBatchSizeInBytes must not be greater than 1,000,000."); + } + + @Test + public void testInvalidAwsRegionThrowsException() { + Properties properties = new Properties(); + properties.setProperty(AWS_REGION, "some-invalid-region"); + CloudWatchSink sink = + CloudWatchSink.builder() + .setCloudWatchClientProperties(properties) + .setNamespace(TEST_NAMESPACE) + .build(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> sink.createWriter(new TestSinkInitContext())) + .withMessageContaining("Invalid AWS region set in config."); + } + + @Test + public void testIncompleteEnvironmentCredentialsProviderThrowsException() { + Properties properties = new Properties(); + properties.put(AWS_CREDENTIALS_PROVIDER, ENV_VAR.toString()); + CloudWatchSink sink = + CloudWatchSink.builder() + .setCloudWatchClientProperties(properties) + .setNamespace(TEST_NAMESPACE) + .build(); + + assertThatExceptionOfType(SdkClientException.class) + .isThrownBy(() -> sink.createWriter(new TestSinkInitContext())) + .withMessageContaining("Unable to load credentials from system settings."); + } + + @Test + public void testIncompleteSystemPropertyCredentialsProviderThrowsException() { + Properties properties = new Properties(); + properties.put(AWS_CREDENTIALS_PROVIDER, SYS_PROP.toString()); + CloudWatchSink sink = + CloudWatchSink.builder() + .setCloudWatchClientProperties(properties) + .setNamespace(TEST_NAMESPACE) + .build(); + + assertThatExceptionOfType(SdkClientException.class) + .isThrownBy(() -> sink.createWriter(new TestSinkInitContext())) + .withMessageContaining("Unable to load credentials from system settings."); + } + + @Test + public void testIncompleteBasicCredentialsProviderThrowsException() { + Properties properties = new Properties(); + properties.put(AWS_CREDENTIALS_PROVIDER, BASIC.toString()); + CloudWatchSink sink = + CloudWatchSink.builder() + .setCloudWatchClientProperties(properties) + .setNamespace(TEST_NAMESPACE) + .build(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> sink.createWriter(new TestSinkInitContext())) + .withMessageContaining( + "Please set values for AWS Access Key ID ('aws.credentials.provider.basic.accesskeyid') and Secret Key ('aws.credentials.provider.basic.secretkey') when using the BASIC AWS credential provider type."); + } + + @Test + public void testIncompleteWebIdentityTokenCredentialsProviderThrowsException() { + Properties properties = new Properties(); + properties.put(AWS_CREDENTIALS_PROVIDER, WEB_IDENTITY_TOKEN.toString()); + CloudWatchSink sink = + CloudWatchSink.builder() + .setCloudWatchClientProperties(properties) + .setNamespace(TEST_NAMESPACE) + .build(); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> sink.createWriter(new TestSinkInitContext())) + .withMessageContaining( + "Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set."); + } + + @Test + public void testInvalidCredentialsProviderThrowsException() { + Properties properties = new Properties(); + properties.put(AWS_CREDENTIALS_PROVIDER, "INVALID_CREDENTIALS_PROVIDER"); + CloudWatchSink sink = + CloudWatchSink.builder() + .setCloudWatchClientProperties(properties) + .setNamespace(TEST_NAMESPACE) + .build(); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> sink.createWriter(new TestSinkInitContext())) + .withMessageContaining("Invalid AWS Credential Provider Type set in config."); + } + + @Test + public void testGetWriterStateSerializer() { + CloudWatchSink sink = + CloudWatchSink.builder().setNamespace(TEST_NAMESPACE).build(); + + assertThat(sink.getWriterStateSerializer()) + .usingRecursiveComparison() + .isEqualTo(new CloudWatchStateSerializer()); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkWriterTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkWriterTest.java new file mode 100644 index 000000000..fe1eff625 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchSinkWriterTest.java @@ -0,0 +1,383 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.connector.base.sink.writer.TestSinkInitContext; +import org.apache.flink.connector.cloudwatch.sink.client.SdkClientProvider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; +import software.amazon.awssdk.services.cloudwatch.CloudWatchServiceClientConfiguration; +import software.amazon.awssdk.services.cloudwatch.model.CloudWatchException; +import software.amazon.awssdk.services.cloudwatch.model.InternalServiceException; +import software.amazon.awssdk.services.cloudwatch.model.InvalidFormatException; +import software.amazon.awssdk.services.cloudwatch.model.InvalidParameterCombinationException; +import software.amazon.awssdk.services.cloudwatch.model.InvalidParameterValueException; +import software.amazon.awssdk.services.cloudwatch.model.LimitExceededException; +import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; +import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest; +import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataResponse; +import software.amazon.awssdk.services.cloudwatch.model.ResourceNotFoundException; +import software.amazon.awssdk.services.sts.model.InvalidAuthorizationMessageException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_NAMESPACE; +import static org.assertj.core.api.Assertions.assertThat; + +class CloudWatchSinkWriterTest { + private static final long FUTURE_TIMEOUT_MS = 10000; + private static final int BYTES_PER_DOUBLE = 8; + + @Test + public void testSuccessfulRequest() throws Exception { + List inputRequests = + Arrays.asList( + MetricWriteRequest.builder() + .withMetricName("test1") + .addValue(1d) + .addCount(1d) + .build(), + MetricWriteRequest.builder() + .withMetricName("test2") + .addValue(2d) + .addCount(2d) + .build()); + + List expectedClientRequests = + Collections.singletonList( + PutMetricDataRequest.builder() + .namespace(TEST_NAMESPACE) + .metricData( + MetricDatum.builder() + .metricName("test1") + .values(1d) + .counts(1d) + .build(), + MetricDatum.builder() + .metricName("test2") + .values(2d) + .counts(2d) + .build()) + .strictEntityValidation(true) + .build()); + + TrackingCloudWatchAsyncClient trackingCloudWatchAsyncClient = + new TrackingCloudWatchAsyncClient(); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(trackingCloudWatchAsyncClient)); + CompletableFuture> failedRequests = new CompletableFuture<>(); + Consumer> failedRequestConsumer = failedRequests::complete; + + cloudWatchSinkWriter.submitRequestEntries(inputRequests, failedRequestConsumer); + assertThat(trackingCloudWatchAsyncClient.getRequestHistory()) + .isNotEmpty() + .containsAll(expectedClientRequests); + assertThat(failedRequests.get(FUTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isEmpty(); + } + + @ParameterizedTest + @MethodSource("provideRetryableException") + public void testRetryableExceptionWillRetry(Exception retryableException) throws Exception { + Optional exceptionToThrow = Optional.of(retryableException); + ThrowingCloudWatchAsyncClient throwingCloudWatchAsyncClient = + new ThrowingCloudWatchAsyncClient<>(exceptionToThrow, str -> true); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(throwingCloudWatchAsyncClient)); + + assertThatRequestsAreRetried(cloudWatchSinkWriter); + } + + private static Stream provideRetryableException() { + return Stream.of( + Arguments.of(LimitExceededException.builder().build()), + Arguments.of(InternalServiceException.builder().build())); + } + + @ParameterizedTest + @MethodSource("provideNonRetryableException") + public void testNonRetryableExceptionWillNotRetry(Exception nonRetryableException) + throws Exception { + Optional exceptionToThrow = Optional.of(nonRetryableException); + ThrowingCloudWatchAsyncClient throwingCloudWatchAsyncClient = + new ThrowingCloudWatchAsyncClient<>(exceptionToThrow, str -> true); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(throwingCloudWatchAsyncClient)); + + assertThatRequestsAreNotRetried(cloudWatchSinkWriter); + } + + private static Stream provideNonRetryableException() { + return Stream.of( + Arguments.of(ResourceNotFoundException.builder().build()), + Arguments.of(InvalidAuthorizationMessageException.builder().build())); + } + + @ParameterizedTest + @MethodSource("provideInvalidMetricException") + public void testInvalidMetricExceptionWillRetryWhenRetryModeEnabled() throws Exception { + Optional exceptionToThrow = + Optional.ofNullable(InvalidFormatException.builder().build()); + ThrowingCloudWatchAsyncClient throwingCloudWatchAsyncClient = + new ThrowingCloudWatchAsyncClient<>(exceptionToThrow, str -> true); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(throwingCloudWatchAsyncClient), + InvalidMetricDataRetryMode.RETRY); + + assertThatRequestsAreRetried(cloudWatchSinkWriter); + } + + @ParameterizedTest + @MethodSource("provideInvalidMetricException") + public void testInvalidMetricExceptionWillSkipWhenSkipModeEnabled() throws Exception { + Optional exceptionToThrow = + Optional.ofNullable(InvalidFormatException.builder().build()); + ThrowingCloudWatchAsyncClient throwingCloudWatchAsyncClient = + new ThrowingCloudWatchAsyncClient<>(exceptionToThrow, str -> true); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(throwingCloudWatchAsyncClient), + InvalidMetricDataRetryMode.SKIP_METRIC_ON_ERROR); + + assertThatRequestsAreSkipped(cloudWatchSinkWriter); + } + + @ParameterizedTest + @MethodSource("provideInvalidMetricException") + public void testInvalidMetricExceptionWillNotRetryWhenFailModeEnabled( + CloudWatchException invalidMetricException) throws Exception { + Optional exceptionToThrow = Optional.ofNullable(invalidMetricException); + ThrowingCloudWatchAsyncClient throwingCloudWatchAsyncClient = + new ThrowingCloudWatchAsyncClient<>(exceptionToThrow, str -> true); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(throwingCloudWatchAsyncClient), + InvalidMetricDataRetryMode.FAIL_ON_ERROR); + + assertThatRequestsAreNotRetried(cloudWatchSinkWriter); + } + + private static Stream provideInvalidMetricException() { + return Stream.of( + Arguments.of(InvalidFormatException.builder().build()), + Arguments.of(InvalidParameterCombinationException.builder().build()), + Arguments.of(InvalidParameterValueException.builder().build())); + } + + @Test + public void testGetSizeInBytesNotImplemented() throws IOException { + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter( + new TestAsyncCloudWatchClientProvider(new TrackingCloudWatchAsyncClient()), + InvalidMetricDataRetryMode.FAIL_ON_ERROR); + + assertThat( + cloudWatchSinkWriter.getSizeInBytes( + MetricWriteRequest.builder() + .withMetricName(TEST_METRIC_NAME) + .addValue(123d) + .build())) + .isEqualTo(BYTES_PER_DOUBLE + TEST_METRIC_NAME.length()); + } + + @Test + public void testClientClosesWhenWriterIsClosed() throws IOException { + TestAsyncCloudWatchClientProvider testAsyncCloudWatchClientProvider = + new TestAsyncCloudWatchClientProvider(new TrackingCloudWatchAsyncClient()); + CloudWatchSinkWriter cloudWatchSinkWriter = + getDefaultSinkWriter(testAsyncCloudWatchClientProvider); + cloudWatchSinkWriter.close(); + + assertThat(testAsyncCloudWatchClientProvider.getCloseCount()).isEqualTo(1); + } + + private void assertThatRequestsAreRetried(CloudWatchSinkWriter cloudWatchSinkWriter) + throws Exception { + CompletableFuture> failedRequests = new CompletableFuture<>(); + Consumer> failedRequestConsumer = failedRequests::complete; + + cloudWatchSinkWriter.submitRequestEntries(getDefaultInputRequests(), failedRequestConsumer); + + assertThat(failedRequests.get(FUTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) + .containsExactlyInAnyOrderElementsOf(getDefaultInputRequests()); + } + + private void assertThatRequestsAreNotRetried(CloudWatchSinkWriter cloudWatchSinkWriter) { + CompletableFuture> failedRequests = new CompletableFuture<>(); + Consumer> failedRequestConsumer = failedRequests::complete; + + cloudWatchSinkWriter.submitRequestEntries(getDefaultInputRequests(), failedRequestConsumer); + + assertThat(failedRequests).isNotCompleted(); + } + + private void assertThatRequestsAreSkipped(CloudWatchSinkWriter cloudWatchSinkWriter) + throws Exception { + CompletableFuture> failedRequests = new CompletableFuture<>(); + Consumer> failedRequestConsumer = failedRequests::complete; + + cloudWatchSinkWriter.submitRequestEntries(getDefaultInputRequests(), failedRequestConsumer); + + assertThat(failedRequests.get(FUTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isEmpty(); + } + + private List getDefaultInputRequests() { + return Arrays.asList( + MetricWriteRequest.builder() + .withMetricName(TEST_METRIC_NAME) + .addValue(1d) + .addCount(1d) + .build(), + MetricWriteRequest.builder() + .withMetricName(TEST_METRIC_NAME) + .withStatisticMax(123d) + .build()); + } + + private CloudWatchSinkWriter getDefaultSinkWriter( + SdkClientProvider cloudWatchAsyncClientSdkClientProvider) + throws IOException { + return getDefaultSinkWriter( + cloudWatchAsyncClientSdkClientProvider, InvalidMetricDataRetryMode.RETRY); + } + + private CloudWatchSinkWriter getDefaultSinkWriter( + SdkClientProvider cloudWatchAsyncClientSdkClientProvider, + InvalidMetricDataRetryMode invalidMetricDataRetryMode) + throws IOException { + TestSinkInitContext initContext = new TestSinkInitContext(); + CloudWatchSink sink = + CloudWatchSink.builder() + .setNamespace(TEST_NAMESPACE) + .setInvalidMetricDataRetryMode(invalidMetricDataRetryMode) + .build(); + + sink.setCloudWatchAsyncClientProvider(cloudWatchAsyncClientSdkClientProvider); + return (CloudWatchSinkWriter) sink.createWriter(initContext); + } + + private static class ThrowingCloudWatchAsyncClient + implements CloudWatchAsyncClient { + + private final Optional errorToReturn; + private final Predicate failWhenMatched; + + private ThrowingCloudWatchAsyncClient( + Optional errorToReturn, Predicate failWhenMatched) { + this.errorToReturn = errorToReturn; + this.failWhenMatched = failWhenMatched; + } + + @Override + public String serviceName() { + return "CloudWatch"; + } + + @Override + public void close() {} + + @Override + public CompletableFuture putMetricData( + PutMetricDataRequest putMetricDataRequest) { + if (errorToReturn.isPresent()) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally( + CloudWatchException.builder().cause((errorToReturn.get())).build()); + return future; + } + return CompletableFuture.completedFuture(PutMetricDataResponse.builder().build()); + } + + @Override + public CloudWatchServiceClientConfiguration serviceClientConfiguration() { + return CloudWatchServiceClientConfiguration.builder().build(); + } + } + + private static class TrackingCloudWatchAsyncClient implements CloudWatchAsyncClient { + + private List requestHistory = new ArrayList<>(); + + @Override + public String serviceName() { + return "CloudWatch"; + } + + @Override + public void close() {} + + @Override + public CompletableFuture putMetricData( + PutMetricDataRequest putMetricDataRequest) { + requestHistory.add(putMetricDataRequest); + return CompletableFuture.completedFuture(PutMetricDataResponse.builder().build()); + } + + @Override + public CloudWatchServiceClientConfiguration serviceClientConfiguration() { + return CloudWatchServiceClientConfiguration.builder().build(); + } + + public List getRequestHistory() { + return requestHistory; + } + } + + private static class TestAsyncCloudWatchClientProvider + implements SdkClientProvider { + + private final CloudWatchAsyncClient cloudWatchAsyncClient; + private int closeCount = 0; + + private TestAsyncCloudWatchClientProvider(CloudWatchAsyncClient cloudWatchAsyncClient) { + this.cloudWatchAsyncClient = cloudWatchAsyncClient; + } + + @Override + public CloudWatchAsyncClient getClient() { + return cloudWatchAsyncClient; + } + + @Override + public void close() { + closeCount++; + } + + public int getCloseCount() { + return closeCount; + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchStateSerializerTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchStateSerializerTest.java new file mode 100644 index 000000000..a04d9a81c --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/CloudWatchStateSerializerTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.connector.base.sink.writer.BufferedRequestState; +import org.apache.flink.connector.base.sink.writer.ElementConverter; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.apache.flink.connector.base.sink.writer.AsyncSinkWriterTestUtils.getTestState; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class CloudWatchStateSerializerTest { + + private static final ElementConverter ELEMENT_CONVERTER = + (element, context) -> + MetricWriteRequest.builder() + .withMetricName("test_metric") + .addValue(1d) + .addCount(2d) + .build(); + + @Test + public void testSerializeAndDeserialize() throws IOException { + BufferedRequestState expectedState = + getTestState(ELEMENT_CONVERTER, this::getRequestSize); + + CloudWatchStateSerializer serializer = new CloudWatchStateSerializer(); + BufferedRequestState actualState = + serializer.deserialize(1, serializer.serialize(expectedState)); + + assertThat(actualState).usingRecursiveComparison().isEqualTo(expectedState); + } + + @Test + public void testVersion() { + CloudWatchStateSerializer serializer = new CloudWatchStateSerializer(); + assertThat(serializer.getVersion()).isEqualTo(1); + } + + private int getRequestSize(MetricWriteRequest requestEntry) { + return requestEntry.getMetricName().toString().getBytes(StandardCharsets.UTF_8).length + + 8 * 2; + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/DefaultMetricWriteRequestElementConverterTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/DefaultMetricWriteRequestElementConverterTest.java new file mode 100644 index 000000000..e51392527 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/DefaultMetricWriteRequestElementConverterTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.junit.jupiter.api.Test; + +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class DefaultMetricWriteRequestElementConverterTest { + @Test + void defaultConverterSupportsMetricWriteRequest() { + DefaultMetricWriteRequestElementConverter converter = + new DefaultMetricWriteRequestElementConverter<>(); + + MetricWriteRequest metricWriteRequest = + MetricWriteRequest.builder() + .withMetricName(TEST_METRIC_NAME) + .addValue(TEST_SAMPLE_VALUE) + .build(); + + MetricWriteRequest request = converter.apply(metricWriteRequest, null); + assertThat(converter).hasNoNullFieldsOrProperties(); + assertThat(request.getMetricName()).isEqualTo(TEST_METRIC_NAME); + assertThat(request.getValues()).containsExactly(TEST_SAMPLE_VALUE); + } + + @Test + void defaultConverterThrowsExceptionForNonMetricWriteRequest() { + DefaultMetricWriteRequestElementConverter converter = + new DefaultMetricWriteRequestElementConverter<>(); + String str = "test"; + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> converter.apply(str, null)) + .withMessageContaining( + "DefaultMetricWriteRequestElementConverter only supports MetricWriteRequest element."); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestElementConverterTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestElementConverterTest.java new file mode 100644 index 000000000..949d9cd60 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestElementConverterTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.apache.flink.api.connector.sink2.SinkWriter; +import org.apache.flink.connector.cloudwatch.utils.Sample; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_VALUE_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_VALUE_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_COUNT; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_VALUE; +import static org.assertj.core.api.Assertions.assertThat; + +class MetricWriteRequestElementConverterTest { + + @Test + void sampleTypeConversion() { + MetricWriteRequestElementConverter elementConverter = new TestSampleConverter(); + + Map dimensions = new HashMap<>(); + dimensions.put(TEST_DIMENSION_KEY_1, TEST_DIMENSION_VALUE_1); + dimensions.put(TEST_DIMENSION_KEY_2, TEST_DIMENSION_VALUE_2); + + Sample sample = new Sample(TEST_METRIC_NAME, dimensions, TEST_SAMPLE_VALUE); + + MetricWriteRequest actual = elementConverter.apply(sample, null); + + assertThat(actual.getMetricName()).isEqualTo(TEST_METRIC_NAME); + assertThat(actual.getDimensions()) + .containsAll( + Arrays.asList( + new MetricWriteRequest.Dimension( + TEST_DIMENSION_KEY_1, TEST_DIMENSION_VALUE_1), + new MetricWriteRequest.Dimension( + TEST_DIMENSION_KEY_2, TEST_DIMENSION_VALUE_2))); + assertThat(actual.getValues()).containsExactly(TEST_SAMPLE_VALUE); + assertThat(actual.getCounts()).containsExactly(TEST_SAMPLE_COUNT); + } + + private static class TestSampleConverter extends MetricWriteRequestElementConverter { + + @Override + public MetricWriteRequest apply(Sample element, SinkWriter.Context context) { + MetricWriteRequest.Builder builder = + MetricWriteRequest.builder() + .withMetricName(element.getName()) + .addValue(element.getValue()) + .addCount(TEST_SAMPLE_COUNT); + + element.getLabel().forEach(builder::addDimension); + + return builder.build(); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestTest.java new file mode 100644 index 000000000..c2f718afe --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/sink/MetricWriteRequestTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.sink; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; +import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_VALUE_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_VALUE_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.types.PojoTestUtils.assertSerializedAsPojo; +import static org.apache.flink.types.PojoTestUtils.assertSerializedAsPojoWithoutKryo; +import static org.assertj.core.api.Assertions.assertThat; + +class MetricWriteRequestTest { + @Test + public void testMetricDatumExpectedFields() { + List fields = + Arrays.stream(MetricDatum.class.getDeclaredFields()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .collect(Collectors.toList()); + + assertThat(fields) + .as( + "If this test fails the CloudWatch AWS SDK may have changed. " + + "We need to check this, and update the CloudWatchStateSerializer if required.") + .hasSize(9); + } + + @Test + public void testToString() { + MetricWriteRequest metricWriteRequest = + MetricWriteRequest.builder() + .withMetricName(TEST_METRIC_NAME) + .addDimension(TEST_DIMENSION_KEY_1, TEST_DIMENSION_VALUE_1) + .addDimension(TEST_DIMENSION_KEY_2, TEST_DIMENSION_VALUE_2) + .addValue(123d) + .addCount(234d) + .withTimestamp(Instant.ofEpochMilli(345)) + .withStatisticMax(456d) + .withUnit("Seconds") + .build(); + + assertThat(metricWriteRequest.toString()) + .contains(TEST_METRIC_NAME) + .contains(TEST_DIMENSION_KEY_1) + .contains(TEST_DIMENSION_KEY_2) + .contains(TEST_DIMENSION_VALUE_1) + .contains(TEST_DIMENSION_VALUE_2) + .contains("123") + .contains("234") + .contains(Instant.ofEpochMilli(345).toString()) + .contains("456") + .contains(StandardUnit.SECONDS.toString()); + } + + @Test + public void testSerializedAsPojo() { + assertSerializedAsPojo(MetricWriteRequest.class); + } + + @Test + public void testSerializedAsPojoWithoutKryo() { + assertSerializedAsPojoWithoutKryo(MetricWriteRequest.class); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicTableFactoryTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicTableFactoryTest.java new file mode 100644 index 000000000..7a69230df --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/table/CloudWatchDynamicTableFactoryTest.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.api.connector.sink2.Sink; +import org.apache.flink.configuration.Configuration; +import org.apache.flink.connector.cloudwatch.sink.CloudWatchSink; +import org.apache.flink.table.api.DataTypes; +import org.apache.flink.table.api.ValidationException; +import org.apache.flink.table.catalog.Column; +import org.apache.flink.table.catalog.ResolvedSchema; +import org.apache.flink.table.connector.ChangelogMode; +import org.apache.flink.table.connector.sink.SinkV2Provider; +import org.apache.flink.table.data.RowData; +import org.apache.flink.table.factories.TableOptionsBuilder; +import org.apache.flink.table.factories.TestFormatFactory; +import org.apache.flink.table.runtime.connector.sink.SinkRuntimeProviderContext; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Map; +import java.util.Properties; + +import static org.apache.flink.connector.base.table.AsyncSinkConnectorOptions.FLUSH_BUFFER_SIZE; +import static org.apache.flink.connector.base.table.AsyncSinkConnectorOptions.FLUSH_BUFFER_TIMEOUT; +import static org.apache.flink.connector.base.table.AsyncSinkConnectorOptions.MAX_BATCH_SIZE; +import static org.apache.flink.connector.base.table.AsyncSinkConnectorOptions.MAX_BUFFERED_REQUESTS; +import static org.apache.flink.connector.base.table.AsyncSinkConnectorOptions.MAX_IN_FLIGHT_REQUESTS; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.AWS_REGION; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_DIMENSION_KEYS; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_NAMESPACE; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_NAME_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_MAX_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_MIN_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_SAMPLE_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_SUM_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STORAGE_RESOLUTION_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_TIMESTAMP_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_UNIT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_VALUE_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.DATA_TYPE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEYS; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_NAMESPACE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_REGION; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MAX_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MIN_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_SUM_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STORAGE_RESOLUTION_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_TIMESTAMP_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_UNIT_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_VALUE_KEY; +import static org.apache.flink.table.factories.utils.FactoryMocks.createTableSink; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class CloudWatchDynamicTableFactoryTest { + @Test + void testGoodTableSink() { + ResolvedSchema sinkSchema = defaultSinkSchema(); + Map sinkOptions = defaultTableOptions().build(); + + // Construct actual sink + CloudWatchDynamicSink actualSink = + (CloudWatchDynamicSink) createTableSink(sinkSchema, sinkOptions); + + // Construct expected sink + Properties cloudWatchClientProperties = new Properties(); + cloudWatchClientProperties.setProperty(AWS_REGION.key(), TEST_REGION); + + CloudWatchDynamicSink expectedSink = + (CloudWatchDynamicSink) + CloudWatchDynamicSink.builder() + .setCloudWatchMetricNamespace(TEST_NAMESPACE) + .setCloudWatchTableConfig( + new CloudWatchTableConfig( + Configuration.fromMap(sinkOptions))) + .setCloudWatchClientProperties(cloudWatchClientProperties) + .setPhysicalDataType(DATA_TYPE) + .build(); + + assertThat(actualSink).usingRecursiveComparison().isEqualTo(expectedSink); + assertThat(actualSink.getChangelogMode(ChangelogMode.insertOnly())) + .isEqualTo(ChangelogMode.upsert()); + + Sink createdSink = + ((SinkV2Provider) + actualSink.getSinkRuntimeProvider( + new SinkRuntimeProviderContext(false))) + .createSink(); + assertThat(createdSink).isInstanceOf(CloudWatchSink.class); + } + + @Test + void testGoodTableSinkWithAsyncOptions() { + ResolvedSchema sinkSchema = defaultSinkSchema(); + Map sinkOptions = + defaultTableOptions() + .withTableOption(MAX_BATCH_SIZE, "100") + .withTableOption(MAX_IN_FLIGHT_REQUESTS, "200") + .withTableOption(MAX_BUFFERED_REQUESTS, "300") + .withTableOption(FLUSH_BUFFER_SIZE, "1024") + .withTableOption(FLUSH_BUFFER_TIMEOUT, "1000") + .build(); + + // Construct actual sink + CloudWatchDynamicSink actualSink = + (CloudWatchDynamicSink) createTableSink(sinkSchema, sinkOptions); + + // Construct expected sink + Properties cloudWatchClientProperties = new Properties(); + cloudWatchClientProperties.setProperty(AWS_REGION.key(), TEST_REGION); + + CloudWatchDynamicSink expectedSink = + (CloudWatchDynamicSink) + CloudWatchDynamicSink.builder() + .setCloudWatchMetricNamespace(TEST_NAMESPACE) + .setCloudWatchTableConfig( + new CloudWatchTableConfig( + Configuration.fromMap(sinkOptions))) + .setCloudWatchClientProperties(cloudWatchClientProperties) + .setMaxBatchSize(100) + .setMaxInFlightRequests(200) + .setMaxBufferedRequests(300) + .setMaxBufferSizeInBytes(1024) + .setMaxTimeInBufferMS(1000) + .setPhysicalDataType(DATA_TYPE) + .build(); + + assertThat(actualSink).usingRecursiveComparison().isEqualTo(expectedSink); + assertThat(actualSink.getChangelogMode(ChangelogMode.insertOnly())) + .isEqualTo(ChangelogMode.upsert()); + + Sink createdSink = + ((SinkV2Provider) + actualSink.getSinkRuntimeProvider( + new SinkRuntimeProviderContext(false))) + .createSink(); + assertThat(createdSink).isInstanceOf(CloudWatchSink.class); + } + + @Test + void testBadTableSinkWithoutRequiredOptions() { + ResolvedSchema sinkSchema = defaultSinkSchema(); + Map sinkOptions = + new TableOptionsBuilder( + CloudWatchDynamicTableFactory.IDENTIFIER, + TestFormatFactory.IDENTIFIER) + .build(); + + assertThatExceptionOfType(ValidationException.class) + .isThrownBy(() -> createTableSink(sinkSchema, Collections.emptyList(), sinkOptions)) + .havingCause() + .withMessageContaining("One or more required options are missing.") + .withMessageContaining(METRIC_NAMESPACE.key()) + .withMessageContaining(METRIC_NAME_KEY.key()) + .withMessageContaining(AWS_REGION.key()); + } + + private ResolvedSchema defaultSinkSchema() { + return ResolvedSchema.of( + Column.physical(TEST_METRIC_NAME, DataTypes.STRING()), + Column.physical(TEST_DIMENSION_KEY_1, DataTypes.STRING()), + Column.physical(TEST_DIMENSION_KEY_2, DataTypes.STRING()), + Column.physical(TEST_VALUE_KEY, DataTypes.DOUBLE()), + Column.physical(TEST_COUNT_KEY, DataTypes.DOUBLE()), + Column.physical(TEST_TIMESTAMP_KEY, DataTypes.TIMESTAMP(6)), + Column.physical(TEST_UNIT_KEY, DataTypes.STRING()), + Column.physical(TEST_STORAGE_RESOLUTION_KEY, DataTypes.INT()), + Column.physical(TEST_STATS_MAX_KEY, DataTypes.DOUBLE()), + Column.physical(TEST_STATS_MIN_KEY, DataTypes.DOUBLE()), + Column.physical(TEST_STATS_SUM_KEY, DataTypes.DOUBLE()), + Column.physical(TEST_STATS_COUNT_KEY, DataTypes.DOUBLE())); + } + + private TableOptionsBuilder defaultTableOptions() { + String connector = CloudWatchDynamicTableFactory.IDENTIFIER; + String format = TestFormatFactory.IDENTIFIER; + return new TableOptionsBuilder(connector, format) + // default table options + .withTableOption(METRIC_NAMESPACE, TEST_NAMESPACE) + .withTableOption(METRIC_NAME_KEY, TEST_METRIC_NAME) + .withTableOption(METRIC_DIMENSION_KEYS, TEST_DIMENSION_KEYS) + .withTableOption(METRIC_VALUE_KEY, TEST_VALUE_KEY) + .withTableOption(METRIC_COUNT_KEY, TEST_COUNT_KEY) + .withTableOption(METRIC_TIMESTAMP_KEY, TEST_TIMESTAMP_KEY) + .withTableOption(METRIC_UNIT_KEY, TEST_UNIT_KEY) + .withTableOption(METRIC_STORAGE_RESOLUTION_KEY, TEST_STORAGE_RESOLUTION_KEY) + .withTableOption(METRIC_STATISTIC_MAX_KEY, TEST_STATS_MAX_KEY) + .withTableOption(METRIC_STATISTIC_MIN_KEY, TEST_STATS_MIN_KEY) + .withTableOption(METRIC_STATISTIC_SUM_KEY, TEST_STATS_SUM_KEY) + .withTableOption(METRIC_STATISTIC_SAMPLE_COUNT_KEY, TEST_STATS_COUNT_KEY) + .withTableOption(AWS_REGION, TEST_REGION); + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/table/RowDataElementConverterTest.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/table/RowDataElementConverterTest.java new file mode 100644 index 000000000..fa56992d8 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/table/RowDataElementConverterTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.table; + +import org.apache.flink.api.connector.sink2.SinkWriter; +import org.apache.flink.connector.cloudwatch.sink.MetricWriteRequest; +import org.apache.flink.connector.cloudwatch.utils.TestConstants; +import org.apache.flink.table.api.TableConfig; +import org.apache.flink.table.data.GenericRowData; +import org.apache.flink.table.data.RowData; +import org.apache.flink.table.data.StringData; +import org.apache.flink.table.data.TimestampData; +import org.apache.flink.types.RowKind; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Arrays; + +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.AWS_REGION; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_DIMENSION_KEYS; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_NAMESPACE; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_NAME_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_MAX_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_MIN_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_SAMPLE_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STATISTIC_SUM_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_STORAGE_RESOLUTION_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_TIMESTAMP_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_UNIT_KEY; +import static org.apache.flink.connector.cloudwatch.table.CloudWatchConnectorOptions.METRIC_VALUE_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.DATA_TYPE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_KEY_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_VALUE_1; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_DIMENSION_VALUE_2; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_METRIC_NAME; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_NAMESPACE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_REGION; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_COUNT; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_TS_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_SAMPLE_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_COUNT_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_COUNT_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MAX_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MAX_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MIN_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_MIN_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_SUM_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STATS_SUM_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STORAGE_RESOLUTION_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_STORAGE_RESOLUTION_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_TIMESTAMP_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_UNIT_KEY; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_UNIT_VALUE; +import static org.apache.flink.connector.cloudwatch.utils.TestConstants.TEST_VALUE_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +class RowDataElementConverterTest { + private CloudWatchTableConfig cloudWatchTableConfig; + private RowDataElementConverter elementConverter; + private static final SinkWriter.Context context = new UnusedSinkWriterContext(); + + @BeforeEach + void setUp() { + TableConfig tableConfig = TableConfig.getDefault(); + + tableConfig.set(METRIC_NAMESPACE, TEST_NAMESPACE); + tableConfig.set(METRIC_NAME_KEY, TEST_METRIC_NAME); + tableConfig.set( + METRIC_DIMENSION_KEYS, Arrays.asList(TEST_DIMENSION_KEY_1, TEST_DIMENSION_KEY_2)); + tableConfig.set(METRIC_VALUE_KEY, TEST_VALUE_KEY); + tableConfig.set(METRIC_COUNT_KEY, TEST_COUNT_KEY); + tableConfig.set(METRIC_TIMESTAMP_KEY, TEST_TIMESTAMP_KEY); + tableConfig.set(METRIC_UNIT_KEY, TEST_UNIT_KEY); + tableConfig.set(METRIC_STORAGE_RESOLUTION_KEY, TEST_STORAGE_RESOLUTION_KEY); + tableConfig.set(METRIC_STATISTIC_MAX_KEY, TEST_STATS_MAX_KEY); + tableConfig.set(METRIC_STATISTIC_MIN_KEY, TEST_STATS_MIN_KEY); + tableConfig.set(METRIC_STATISTIC_SUM_KEY, TEST_STATS_SUM_KEY); + tableConfig.set(METRIC_STATISTIC_SAMPLE_COUNT_KEY, TEST_STATS_COUNT_KEY); + tableConfig.set(AWS_REGION, TEST_REGION); + + cloudWatchTableConfig = new CloudWatchTableConfig(tableConfig); + elementConverter = new RowDataElementConverter(DATA_TYPE, cloudWatchTableConfig); + } + + @Test + void testApply() { + RowData rowData = createElement(); + MetricWriteRequest actualTimeSeries = elementConverter.apply(rowData, context); + MetricWriteRequest expectedTimeSeries = + MetricWriteRequest.builder() + .withMetricName(TEST_METRIC_NAME) + .addDimension(TEST_DIMENSION_KEY_1, TEST_DIMENSION_VALUE_1) + .addDimension(TEST_DIMENSION_KEY_2, TEST_DIMENSION_VALUE_2) + .addValue(TEST_SAMPLE_VALUE) + .addCount(TEST_SAMPLE_COUNT) + .withTimestamp(Instant.ofEpochMilli(TEST_SAMPLE_TS_VALUE)) + .withUnit(TEST_UNIT_VALUE) + .withStorageResolution(TEST_STORAGE_RESOLUTION_VALUE) + .withStatisticMax(TEST_STATS_MAX_VALUE) + .withStatisticMin(TEST_STATS_MIN_VALUE) + .withStatisticSum(TEST_STATS_SUM_VALUE) + .withStatisticCount(TEST_STATS_COUNT_VALUE) + .build(); + + assertThat(actualTimeSeries).usingRecursiveComparison().isEqualTo(expectedTimeSeries); + } + + private static RowData createElement() { + GenericRowData element = new GenericRowData(RowKind.INSERT, 12); + element.setField(0, StringData.fromString(TestConstants.TEST_METRIC_NAME)); + element.setField(1, StringData.fromString(TestConstants.TEST_DIMENSION_VALUE_1)); + element.setField(2, StringData.fromString(TestConstants.TEST_DIMENSION_VALUE_2)); + element.setField(3, TestConstants.TEST_SAMPLE_VALUE); + element.setField(4, TestConstants.TEST_SAMPLE_COUNT); + element.setField(5, TimestampData.fromEpochMillis(TestConstants.TEST_SAMPLE_TS_VALUE)); + element.setField(6, StringData.fromString(TestConstants.TEST_UNIT_VALUE)); + element.setField(7, TestConstants.TEST_STORAGE_RESOLUTION_VALUE); + element.setField(8, TestConstants.TEST_STATS_MAX_VALUE); + element.setField(9, TestConstants.TEST_STATS_MIN_VALUE); + element.setField(10, TestConstants.TEST_STATS_SUM_VALUE); + element.setField(11, TestConstants.TEST_STATS_COUNT_VALUE); + return element; + } + + private static class UnusedSinkWriterContext implements SinkWriter.Context { + + @Override + public long currentWatermark() { + throw new UnsupportedOperationException(); + } + + @Override + public Long timestamp() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/utils/Sample.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/utils/Sample.java new file mode 100644 index 000000000..a1703a374 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/utils/Sample.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.flink.connector.cloudwatch.utils; + +import org.apache.flink.connector.cloudwatch.sink.MetricWriteRequestElementConverter; + +import java.util.Map; + +/** A test POJO for use with {@link MetricWriteRequestElementConverter}. */ +public class Sample { + + private final String name; + private final Map label; + private final double value; + + public String getName() { + return name; + } + + public Map getLabel() { + return label; + } + + public double getValue() { + return value; + } + + public Sample(String name, Map label, double value) { + this.name = name; + this.label = label; + this.value = value; + } +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/utils/TestConstants.java b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/utils/TestConstants.java new file mode 100644 index 000000000..56a6913ba --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/java/org/apache/flink/connector/cloudwatch/utils/TestConstants.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.flink.connector.cloudwatch.utils; + +import org.apache.flink.table.api.DataTypes; +import org.apache.flink.table.types.DataType; + +/** Constants used for testing. */ +public class TestConstants { + public static final String TEST_REGION = "us-east-1"; + + public static final String TEST_NAMESPACE = "test_namespace"; + public static final String TEST_METRIC_NAME = "test_metric_name"; + public static final String TEST_DIMENSION_KEY_1 = "test_dim_key_1"; + public static final String TEST_DIMENSION_KEY_2 = "test_dim_key_2"; + public static final String TEST_DIMENSION_KEYS = + String.format("[%s,%s]", TEST_DIMENSION_KEY_1, TEST_DIMENSION_KEY_2); + public static final String TEST_DIMENSION_VALUE_1 = "test_dim_val_1"; + public static final String TEST_DIMENSION_VALUE_2 = "test_dim_val_2"; + public static final String TEST_VALUE_KEY = "test_value_key"; + public static final String TEST_COUNT_KEY = "test_count_key"; + public static final double TEST_SAMPLE_VALUE = 123d; + public static final double TEST_SAMPLE_COUNT = 12d; + public static final String TEST_TIMESTAMP_KEY = "test_sample_ts_key"; + public static final long TEST_SAMPLE_TS_VALUE = 234L; + public static final String TEST_UNIT_KEY = "test_unit_key"; + public static final String TEST_UNIT_VALUE = "Second"; + public static final String TEST_STORAGE_RESOLUTION_KEY = "test_storage_res_key"; + public static final int TEST_STORAGE_RESOLUTION_VALUE = 9; + public static final String TEST_STATS_MAX_KEY = "test_stats_max_key"; + public static final double TEST_STATS_MAX_VALUE = 999d; + public static final String TEST_STATS_MIN_KEY = "test_stats_min_key"; + public static final double TEST_STATS_MIN_VALUE = 1d; + public static final String TEST_STATS_SUM_KEY = "test_stats_sum_key"; + public static final double TEST_STATS_SUM_VALUE = 10d; + public static final String TEST_STATS_COUNT_KEY = "test_stats_count_key"; + public static final double TEST_STATS_COUNT_VALUE = 11d; + + public static final DataType DATA_TYPE = + DataTypes.ROW( + DataTypes.FIELD(TEST_METRIC_NAME, DataTypes.STRING()), + DataTypes.FIELD(TEST_DIMENSION_KEY_1, DataTypes.STRING()), + DataTypes.FIELD(TEST_DIMENSION_KEY_2, DataTypes.STRING()), + DataTypes.FIELD(TEST_VALUE_KEY, DataTypes.DOUBLE()), + DataTypes.FIELD(TEST_COUNT_KEY, DataTypes.DOUBLE()), + DataTypes.FIELD(TEST_TIMESTAMP_KEY, DataTypes.TIMESTAMP()), + DataTypes.FIELD(TEST_UNIT_KEY, DataTypes.STRING()), + DataTypes.FIELD(TEST_STORAGE_RESOLUTION_KEY, DataTypes.INT()), + DataTypes.FIELD(TEST_STATS_MAX_KEY, DataTypes.DOUBLE()), + DataTypes.FIELD(TEST_STATS_MIN_KEY, DataTypes.DOUBLE()), + DataTypes.FIELD(TEST_STATS_SUM_KEY, DataTypes.DOUBLE()), + DataTypes.FIELD(TEST_STATS_COUNT_KEY, DataTypes.DOUBLE())) + .notNull(); +} diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/resources/archunit.properties b/flink-connector-aws/flink-connector-cloudwatch/src/test/resources/archunit.properties new file mode 100644 index 000000000..15be88c95 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/resources/archunit.properties @@ -0,0 +1,31 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +# By default we allow removing existing violations, but fail when new violations are added. +freeze.store.default.allowStoreUpdate=true + +# Enable this if a new (frozen) rule has been added in order to create the initial store and record the existing violations. +#freeze.store.default.allowStoreCreation=true + +# Enable this to add allow new violations to be recorded. +# NOTE: Adding new violations should be avoided when possible. If the rule was correct to flag a new +# violation, please try to avoid creating the violation. If the violation was created due to a +# shortcoming of the rule, file a JIRA issue so the rule can be improved. +#freeze.refreeze=true + +freeze.store.default.path=archunit-violations diff --git a/flink-connector-aws/flink-connector-cloudwatch/src/test/resources/log4j2-test.properties b/flink-connector-aws/flink-connector-cloudwatch/src/test/resources/log4j2-test.properties new file mode 100644 index 000000000..c4fa18706 --- /dev/null +++ b/flink-connector-aws/flink-connector-cloudwatch/src/test/resources/log4j2-test.properties @@ -0,0 +1,28 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 root logger level to OFF to not flood build logs +# set manually to INFO for debugging purposes +rootLogger.level = OFF +rootLogger.appenderRef.test.ref = TestLogger + +appender.testlogger.name = TestLogger +appender.testlogger.type = CONSOLE +appender.testlogger.target = SYSTEM_ERR +appender.testlogger.layout.type = PatternLayout +appender.testlogger.layout.pattern = %-4r [%t] %-5p %c %x - %m%n diff --git a/flink-connector-aws/flink-sql-connector-cloudwatch/pom.xml b/flink-connector-aws/flink-sql-connector-cloudwatch/pom.xml new file mode 100644 index 000000000..1d69c58b7 --- /dev/null +++ b/flink-connector-aws/flink-sql-connector-cloudwatch/pom.xml @@ -0,0 +1,143 @@ + + + + + 4.0.0 + + + org.apache.flink + flink-connector-aws-parent + 5.1-SNAPSHOT + + + flink-sql-connector-cloudwatch + Flink : Connectors : AWS : SQL : Amazon CloudWatch + + jar + + + true + 2.31.18 + + + + + org.apache.flink + flink-connector-cloudwatch + ${project.version} + + + org.apache.flink + flink-test-utils + ${flink.version} + test + + + + + + + software.amazon.awssdk + bom + ${aws.sdkv2.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + shade-flink + package + + shade + + + + + org.apache.flink:flink-connector-aws-base + org.apache.flink:flink-connector-base + org.apache.flink:flink-connector-cloudwatch + software.amazon.awssdk:* + com.google.guava:guava + commons-codec:commons-codec + commons-io:commons-io + commons-lang:commons-lang + commons-logging:commons-logging + org.apache.commons:commons-lang3 + joda-time:joda-time + + + + com.google.guava:listenablefuture + org.checkerframework:checker-qual + com.google.errorprone:error_prone_annotations + com.google.j2objc:j2objc-annotations + com.google.code.findbugs:jsr305 + + + + + *:* + + codegen-resources/** + mozilla/public-suffix-list.txt + VersionInfo.java + mime.types + LICENSE + .gitkeep + + + + + + software.amazon + org.apache.flink.cloudwatch.shaded.software.amazon + + + org.apache.commons + org.apache.flink.cloudwatch.shaded.org.apache.commons + + + com.google + org.apache.flink.cloudwatch.shaded.com.google + + + org.joda.time + org.apache.flink.cloudwatch.shaded.org.joda.time + + + + + + + + + diff --git a/flink-connector-aws/flink-sql-connector-cloudwatch/src/main/resources/META-INF/NOTICE b/flink-connector-aws/flink-sql-connector-cloudwatch/src/main/resources/META-INF/NOTICE new file mode 100644 index 000000000..420d728a5 --- /dev/null +++ b/flink-connector-aws/flink-sql-connector-cloudwatch/src/main/resources/META-INF/NOTICE @@ -0,0 +1,15 @@ +flink-sql-connector-cloudwatch +Copyright 2014-2024 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This project bundles the following dependencies under the Apache Software License 2.0. (http://www.apache.org/licenses/LICENSE-2.0.txt) + +- org.apache.commons:commons-lang3:3.12.0 +- joda-time:joda-time:2.8.1 +- commons-logging:commons-logging:1.1.3 +- commons-lang:commons-lang:2.6 +- commons-io:commons-io:2.15.1 +- commons-codec:commons-codec:1.15 +- com.google.guava:guava:32.1.3-jre diff --git a/flink-connector-aws/flink-sql-connector-cloudwatch/src/test/java/org/apache/flink/connectors/cloudwatch/PackagingITCase.java b/flink-connector-aws/flink-sql-connector-cloudwatch/src/test/java/org/apache/flink/connectors/cloudwatch/PackagingITCase.java new file mode 100644 index 000000000..4d04f6993 --- /dev/null +++ b/flink-connector-aws/flink-sql-connector-cloudwatch/src/test/java/org/apache/flink/connectors/cloudwatch/PackagingITCase.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.flink.connectors.cloudwatch; + +import org.apache.flink.packaging.PackagingTestUtils; +import org.apache.flink.table.factories.Factory; +import org.apache.flink.test.resources.ResourceTestUtils; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.Arrays; + +class PackagingITCase { + + @Test + void testPackaging() throws Exception { + final Path jar = + ResourceTestUtils.getResource(".*/flink-sql-connector-cloudwatch[^/]*\\.jar"); + + PackagingTestUtils.assertJarContainsOnlyFilesMatching( + jar, Arrays.asList("org/apache/flink/", "META-INF/")); + PackagingTestUtils.assertJarContainsServiceEntry(jar, Factory.class); + } +} diff --git a/flink-connector-aws/pom.xml b/flink-connector-aws/pom.xml index 9a62b32b1..3a7ad9b07 100644 --- a/flink-connector-aws/pom.xml +++ b/flink-connector-aws/pom.xml @@ -38,12 +38,14 @@ under the License. flink-connector-aws-kinesis-firehose flink-connector-aws-kinesis-streams flink-connector-kinesis + flink-connector-cloudwatch flink-connector-sqs flink-sql-connector-dynamodb flink-sql-connector-aws-kinesis-firehose flink-sql-connector-aws-kinesis-streams flink-sql-connector-kinesis + flink-sql-connector-cloudwatch \ No newline at end of file