Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Cloudwatch publisher opt in #445

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions spring-cloud-aws-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@
<artifactId>s3-transfer-manager</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudwatch-metric-publisher</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-s3-cross-region-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.awspring.cloud.autoconfigure;

import io.awspring.cloud.autoconfigure.core.CloudWatchMetricsPublisherProperties;
import java.net.URI;
import org.springframework.lang.Nullable;

Expand All @@ -37,6 +38,12 @@ public abstract class AwsClientProperties {
@Nullable
private String region;

/**
* Overrides the global enablement of the CloudWatch MetricsPublisher.
*/
@Nullable
private CloudWatchMetricsPublisherProperties metrics;

@Nullable
public URI getEndpoint() {
return this.endpoint;
Expand All @@ -54,4 +61,13 @@ public String getRegion() {
public void setRegion(String region) {
this.region = region;
}

@Nullable
public CloudWatchMetricsPublisherProperties getMetrics() {
return metrics;
}

public void setMetrics(@Nullable CloudWatchMetricsPublisherProperties metrics) {
this.metrics = metrics;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import io.awspring.cloud.autoconfigure.core.RegionProperties;
import io.awspring.cloud.autoconfigure.core.RegionProviderAutoConfiguration;
import io.awspring.cloud.core.SpringCloudClientConfiguration;
import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand All @@ -34,14 +36,19 @@
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.ConfigDataResource;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.providers.AwsRegionProvider;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;

/**
* Base class for AWS specific {@link ConfigDataLocationResolver}s.
Expand All @@ -55,6 +62,10 @@ public abstract class AbstractAwsConfigDataLocationResolver<T extends ConfigData

protected abstract String getPrefix();

private final boolean CLOUDWATCH_METRICS_PUBLISHER_IN_CLASSPATH = ClassUtils.isPresent(
"software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher",
getClass().getClassLoader());

@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return location.hasPrefix(getPrefix());
Expand Down Expand Up @@ -151,8 +162,51 @@ else if (awsProperties.getEndpoint() != null) {
builder.endpointOverride(awsProperties.getEndpoint());
}
builder.credentialsProvider(credentialsProvider);
builder.overrideConfiguration(new SpringCloudClientConfiguration().clientOverrideConfiguration());

if (CLOUDWATCH_METRICS_PUBLISHER_IN_CLASSPATH) {
CloudWatchMetricPublisher metricPublisher = createMetricPublisher(awsProperties, properties, regionProvider,
builder, credentialsProvider);
ClientOverrideConfiguration.Builder clientOverrideConfigurationBuilder = new SpringCloudClientConfiguration()
.clientOverrideConfiguration().toBuilder();
clientOverrideConfigurationBuilder.addMetricPublisher(metricPublisher);
builder.overrideConfiguration(clientOverrideConfigurationBuilder.build());
}

return builder;
}

// TODO: This is very ugly, also not registering any bean but rather just creating the client.
private <T extends AwsClientBuilder<?, ?>> CloudWatchMetricPublisher createMetricPublisher(
AwsProperties awsProperties, AwsClientProperties awsClientProperties, AwsRegionProvider regionProvider,
T clientBuilder, AwsCredentialsProvider credentialsProvider) {
CloudWatchMetricPublisher.Builder builder = CloudWatchMetricPublisher.builder();
PropertyMapper map = PropertyMapper.get();
if ((awsClientProperties.getMetrics() != null && awsClientProperties.getMetrics().getEnabled())) {
CloudWatchAsyncClient cloudwatchAsyncClient = createCloudwatchAsyncClient(awsClientProperties.getRegion(),
awsClientProperties.getEndpoint(), credentialsProvider);

map.from(awsClientProperties.getMetrics()::getNamespace).whenNonNull().to(builder::namespace);
map.from(awsClientProperties.getMetrics()::getUploadFrequencyInSeconds).whenNonNull()
.to(v -> builder.uploadFrequency(Duration.ofSeconds(v)));
builder.cloudWatchClient(cloudwatchAsyncClient);
}
else if (awsProperties.getMetrics() == null
|| (awsProperties.getMetrics() != null && awsProperties.getMetrics().getEnabled())) {
CloudWatchAsyncClient cloudwatchAsyncClient = createCloudwatchAsyncClient(regionProvider.getRegion().id(),
awsProperties.getEndpoint(), credentialsProvider);
if (awsProperties.getMetrics() != null) {
map.from(awsProperties.getMetrics()::getNamespace).whenNonNull().to(builder::namespace);
map.from(awsProperties.getMetrics()::getUploadFrequencyInSeconds).whenNonNull()
.to(v -> builder.uploadFrequency(Duration.ofSeconds(v)));
}
builder.cloudWatchClient(cloudwatchAsyncClient);
}
return builder.build();
}

private <T extends AwsClientBuilder<?, ?>> CloudWatchAsyncClient createCloudwatchAsyncClient(String region,
URI endpoint, AwsCredentialsProvider credentialsProvider) {
return CloudWatchAsyncClient.builder().region(Region.of(region)).endpointOverride(endpoint)
.credentialsProvider(credentialsProvider).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ protected String getPrefix() {
public List<ParameterStoreConfigDataResource> resolveProfileSpecific(
ConfigDataLocationResolverContext resolverContext, ConfigDataLocation location, Profiles profiles)
throws ConfigDataLocationNotFoundException {
registerBean(resolverContext, AwsProperties.class, loadAwsProperties(resolverContext.getBinder()));
registerBean(resolverContext, ParameterStoreProperties.class, loadProperties(resolverContext.getBinder()));
AwsProperties awsProperties = loadAwsProperties(resolverContext.getBinder());
ParameterStoreProperties parameterStoreProperties = loadProperties(resolverContext.getBinder());
RegionProperties regionProperties = loadRegionProperties(resolverContext.getBinder());

registerBean(resolverContext, AwsProperties.class, awsProperties);
registerBean(resolverContext, ParameterStoreProperties.class, parameterStoreProperties);
registerBean(resolverContext, CredentialsProperties.class,
loadCredentialsProperties(resolverContext.getBinder()));
registerBean(resolverContext, RegionProperties.class, loadRegionProperties(resolverContext.getBinder()));

registerBean(resolverContext, RegionProperties.class, regionProperties);
registerAndPromoteBean(resolverContext, SsmClient.class, this::createSimpleSystemManagementClient);

ParameterStorePropertySources sources = new ParameterStorePropertySources();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ public List<SecretsManagerConfigDataResource> resolveProfileSpecific(
registerBean(resolverContext, CredentialsProperties.class,
loadCredentialsProperties(resolverContext.getBinder()));
registerBean(resolverContext, RegionProperties.class, loadRegionProperties(resolverContext.getBinder()));

registerAndPromoteBean(resolverContext, SecretsManagerClient.class, this::createAwsSecretsManagerClient);

SecretsManagerPropertySources propertySources = new SecretsManagerPropertySources();

List<String> contexts = getCustomContexts(location.getNonPrefixedValue(PREFIX));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,24 @@
package io.awspring.cloud.autoconfigure.core;

import io.awspring.cloud.autoconfigure.AwsClientProperties;
import io.awspring.cloud.autoconfigure.metrics.CloudWatchProperties;
import io.awspring.cloud.core.SpringCloudClientConfiguration;
import java.time.Duration;
import java.util.Optional;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.metrics.MetricPublisher;
import software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.regions.providers.AwsRegionProvider;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClientBuilder;

/**
* Provides a convenience method to apply common configuration to any {@link AwsClientBuilder}.
Expand All @@ -34,6 +42,8 @@
* @since 3.0
*/
public class AwsClientBuilderConfigurer {
public static final boolean IS_CLOUDWATCH_METRIC_PUBLISHER_PRESENT = ClassUtils
.isPresent("software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher", null);
private final AwsCredentialsProvider credentialsProvider;
private final AwsRegionProvider regionProvider;
private final AwsProperties awsProperties;
Expand All @@ -48,16 +58,20 @@ public class AwsClientBuilderConfigurer {
}

public <T extends AwsClientBuilder<?, ?>> T configure(T builder) {
return configure(builder, null, null);
return configure(builder, null, null, null);
}

public <T extends AwsClientBuilder<?, ?>> T configure(T builder, @Nullable AwsClientProperties clientProperties,
@Nullable AwsClientCustomizer<T> customizer) {
@Nullable AwsClientCustomizer<T> customizer, @Nullable MetricPublisher metricPublisher) {
ClientOverrideConfiguration.Builder clientOverrideConfigurationBuilder = clientOverrideConfiguration
.toBuilder();
Assert.notNull(builder, "builder is required");
Assert.notNull(clientProperties, "clientProperties are required");

if (metricPublisher != null) {
clientOverrideConfigurationBuilder.addMetricPublisher(metricPublisher).build();
}
builder.credentialsProvider(this.credentialsProvider).region(resolveRegion(clientProperties))
.overrideConfiguration(this.clientOverrideConfiguration);
.overrideConfiguration(clientOverrideConfigurationBuilder.build());
Optional.ofNullable(this.awsProperties.getEndpoint()).ifPresent(builder::endpointOverride);
Optional.ofNullable(clientProperties).map(AwsClientProperties::getEndpoint)
.ifPresent(builder::endpointOverride);
Expand All @@ -68,6 +82,7 @@ public class AwsClientBuilderConfigurer {
if (customizer != null) {
AwsClientCustomizer.apply(customizer, builder);
}

return builder;
}

Expand All @@ -76,4 +91,33 @@ public Region resolveRegion(@Nullable AwsClientProperties clientProperties) {
? Region.of(clientProperties.getRegion())
: this.regionProvider.getRegion();
}

public static @Nullable MetricPublisher createSpecificMetricPublisher(MetricPublisher metricPublisher,
AwsClientProperties properties, AwsClientBuilderConfigurer awsClientBuilderConfigurer) {

if (IS_CLOUDWATCH_METRIC_PUBLISHER_PRESENT && properties.getMetrics() != null) {
if (properties.getMetrics().getEnabled()) {
PropertyMapper propertyMapper = PropertyMapper.get();

CloudWatchAsyncClientBuilder cloudWatchAsyncClientBuilder = CloudWatchAsyncClient.builder();
CloudWatchProperties cloudWatchProperties = new CloudWatchProperties();
propertyMapper.from(properties.getEndpoint()).whenNonNull().to(cloudWatchProperties::setEndpoint);
propertyMapper.from(properties.getRegion()).whenNonNull().to(cloudWatchProperties::setRegion);
CloudWatchAsyncClient cloudWatchAsyncClient = awsClientBuilderConfigurer
.configure(cloudWatchAsyncClientBuilder, cloudWatchProperties, null, null).build();

CloudWatchMetricPublisher.Builder builder = CloudWatchMetricPublisher.builder();
builder.cloudWatchClient(cloudWatchAsyncClient);
propertyMapper.from(properties.getMetrics()::getNamespace).whenNonNull().to(builder::namespace);
propertyMapper.from(properties.getMetrics()::getUploadFrequencyInSeconds).whenNonNull()
.to(v -> builder.uploadFrequency(Duration.ofSeconds(v)));
return builder.build();
}
else {
return null;
}

}
return metricPublisher;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.lang.Nullable;
import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;

Expand Down Expand Up @@ -54,6 +55,13 @@ public class AwsProperties {
@Nullable
private Boolean dualstackEnabled;

/**
* Configure metrics properties to send to CloudWatch if needed
*/
@NestedConfigurationProperty
@Nullable
private CloudWatchMetricsPublisherProperties metrics;

/**
* Configure whether the SDK should use the AWS fips endpoints.
*/
Expand Down Expand Up @@ -95,4 +103,13 @@ public Boolean getFipsEnabled() {
public void setFipsEnabled(@Nullable Boolean fipsEnabled) {
this.fipsEnabled = fipsEnabled;
}

@Nullable
public CloudWatchMetricsPublisherProperties getMetrics() {
return metrics;
}

public void setMetrics(@Nullable CloudWatchMetricsPublisherProperties metrics) {
this.metrics = metrics;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2013-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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 io.awspring.cloud.autoconfigure.core;

import io.awspring.cloud.autoconfigure.metrics.CloudWatchProperties;
import java.time.Duration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.metrics.MetricPublisher;
import software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher;
import software.amazon.awssdk.regions.providers.AwsRegionProvider;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClientBuilder;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(AwsProperties.class)
@ConditionalOnClass({ CloudWatchMetricPublisher.class })
@AutoConfigureAfter({ CredentialsProviderAutoConfiguration.class, RegionProviderAutoConfiguration.class })
public class CloudWatchMetricsPublisherAutoConfiguration {

private final AwsProperties awsProperties;

public CloudWatchMetricsPublisherAutoConfiguration(AwsProperties awsProperties) {
this.awsProperties = awsProperties;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "spring.cloud.aws.metrics.enabled", havingValue = "true", matchIfMissing = true)
MetricPublisher cloudWatchMetricPublisher(AwsClientBuilderConfigurer awsClientBuilderConfigurer,
AwsRegionProvider awsRegionProvider,
ObjectProvider<AwsClientCustomizer<CloudWatchAsyncClientBuilder>> configurer) {
PropertyMapper propertyMapper = PropertyMapper.get();

CloudWatchAsyncClientBuilder cloudWatchAsyncClientBuilder = CloudWatchAsyncClient.builder();
CloudWatchProperties cloudWatchProperties = new CloudWatchProperties();
propertyMapper.from(awsProperties.getEndpoint()).whenNonNull().to(cloudWatchProperties::setEndpoint);
propertyMapper.from(awsRegionProvider.getRegion().id()).whenNonNull().to(cloudWatchProperties::setRegion);
CloudWatchAsyncClient cloudWatchAsyncClient = awsClientBuilderConfigurer
.configure(cloudWatchAsyncClientBuilder, cloudWatchProperties, configurer.getIfAvailable(), null)
.build();

CloudWatchMetricPublisher.Builder builder = CloudWatchMetricPublisher.builder();
builder.cloudWatchClient(cloudWatchAsyncClient);

if (awsProperties.getMetrics() != null) {
propertyMapper.from(awsProperties.getMetrics()::getNamespace).whenNonNull().to(builder::namespace);
propertyMapper.from(awsProperties.getMetrics()::getUploadFrequencyInSeconds).whenNonNull()
.to(v -> builder.uploadFrequency(Duration.ofSeconds(v)));
}
return builder.build();
}
}
Loading