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

Add app configuration health indicator #25364

Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ public void testSpringBootActuatorHealth() {
app.property("spring.cloud.azure.keyvault.secret.profile.tenant-id", SPRING_TENANT_ID);
app.property("management.endpoint.health.show-details", "always");
app.property("management.endpoints.web.exposure.include", "*");
app.property("management.health.azure-key-vault.enabled", "true");
app.property("management.health.azure-keyvault.enabled", "true");
app.start();

final String response = REST_TEMPLATE.getForObject(
"http://localhost:" + app.port() + "/actuator/health/keyVault", String.class);
"http://localhost:" + app.port() + "/actuator/health/keyvault", String.class);
assertEquals("{\"status\":\"UP\"}", response);
LOGGER.info("response = {}", response);
}
Expand All @@ -60,7 +60,7 @@ public void testSpringBootActuatorEnv() {
app.property("spring.cloud.azure.keyvault.secret.profile.tenant-id", SPRING_TENANT_ID);
app.property("management.endpoint.health.show-details", "always");
app.property("management.endpoints.web.exposure.include", "*");
app.property("management.health.azure-key-vault.enabled", "true");
app.property("management.health.azure-keyvault.enabled", "true");
app.start();

final String response = REST_TEMPLATE.getForObject(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.actuate.autoconfigure.appconfiguration;

import com.azure.data.appconfiguration.ConfigurationAsyncClient;
import com.azure.spring.cloud.actuate.appconfiguration.AppConfigurationHealthIndicator;
import com.azure.spring.cloud.autoconfigure.appconfiguration.AzureAppConfigurationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Configuration class of App Configuration Health
*/
@Configuration
@ConditionalOnClass({ ConfigurationAsyncClient.class, HealthIndicator.class })
@ConditionalOnBean(ConfigurationAsyncClient.class)
@AutoConfigureAfter(AzureAppConfigurationAutoConfiguration.class)
@ConditionalOnEnabledHealthIndicator("azure-appconfiguration")
public class AppConfigurationHealthConfiguration {

@Bean
AppConfigurationHealthIndicator appconfigurationHealthIndicator(ConfigurationAsyncClient configurationAsyncClient) {
return new AppConfigurationHealthIndicator(configurationAsyncClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

Expand All @@ -24,12 +22,10 @@
@ConditionalOnClass({ CosmosAsyncClient.class, HealthIndicator.class})
@ConditionalOnBean(CosmosAsyncClient.class)
@AutoConfigureAfter(AzureCosmosAutoConfiguration.class)
@ConditionalOnExpression("${spring.cloud.azure.cosmos.enabled:true}")
@ConditionalOnProperty(prefix = "spring.cloud.azure.cosmos", name = { "endpoint", "database" })
@ConditionalOnEnabledHealthIndicator("azure-cosmos")
public class CosmosHealthConfiguration {

@Bean
@ConditionalOnEnabledHealthIndicator("azure-cosmos")
public HealthIndicator cosmosHealthContributor(AzureCosmosProperties azureCosmosProperties,
CosmosAsyncClient cosmosAsyncClient) {
return new CosmosHealthIndicator(cosmosAsyncClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,16 @@
@Configuration
@ConditionalOnClass({ EventHubClientBuilder.class, HealthIndicator.class })
@AutoConfigureAfter(AzureEventHubsAutoConfiguration.class)
@ConditionalOnBean(EventHubClientBuilder.class)
@ConditionalOnEnabledHealthIndicator("azure-eventhubs")
public class EventHubsHealthConfiguration {

@Bean
@ConditionalOnBean(EventHubClientBuilder.class)
@ConditionalOnEnabledHealthIndicator("azure-eventhub")
public EventHubsHealthIndicator eventHubsHealthIndicator(
public EventHubsHealthIndicator eventhubsHealthIndicator(
ObjectProvider<EventHubProducerAsyncClient> producerAsyncClients,
ObjectProvider<EventHubConsumerAsyncClient> consumerAsyncClients) {

return new EventHubsHealthIndicator(producerAsyncClients.getIfAvailable(),
consumerAsyncClients.getIfAvailable());
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
@ConditionalOnClass({ SecretClient.class, HealthIndicator.class })
@ConditionalOnBean(SecretAsyncClient.class)
@AutoConfigureAfter(AzureKeyVaultSecretAutoConfiguration.class)
@ConditionalOnEnabledHealthIndicator("azure-keyvault")
public class KeyVaultHealthConfiguration {

@Bean
@ConditionalOnEnabledHealthIndicator("azure-key-vault")
KeyVaultSecretHealthIndicator keyVaultHealthIndicator(SecretAsyncClient secretAsyncClient) {
KeyVaultSecretHealthIndicator keyvaultHealthIndicator(SecretAsyncClient secretAsyncClient) {
return new KeyVaultSecretHealthIndicator(secretAsyncClient);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@
@ConditionalOnClass({ BlobServiceAsyncClient.class, HealthIndicator.class })
@ConditionalOnBean(BlobServiceAsyncClient.class)
@AutoConfigureAfter(AzureStorageBlobAutoConfiguration.class)
@ConditionalOnEnabledHealthIndicator("azure-storage")
public class StorageBlobHealthConfiguration {

@Bean
@ConditionalOnEnabledHealthIndicator("azure-storage")
@ConditionalOnBean(BlobServiceAsyncClient.class)
public StorageBlobHealthIndicator storageBlobHealthIndicator(BlobServiceAsyncClient blobServiceAsyncClient) {
return new StorageBlobHealthIndicator(blobServiceAsyncClient);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@
@ConditionalOnClass({ ShareServiceAsyncClient.class, HealthIndicator.class })
@AutoConfigureAfter(AzureStorageFileShareAutoConfiguration.class)
@ConditionalOnBean(ShareServiceAsyncClient.class)
@ConditionalOnEnabledHealthIndicator("azure-storage")
public class StorageFileHealthConfiguration {

@Bean
@ConditionalOnEnabledHealthIndicator("azure-storage")
public StorageFileHealthIndicator storageFileHealthIndicator(ShareServiceAsyncClient shareServiceAsyncClient) {
return new StorageFileHealthIndicator(shareServiceAsyncClient);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@
@ConditionalOnClass({ QueueServiceAsyncClient.class, HealthIndicator.class })
@ConditionalOnBean(QueueServiceAsyncClient.class)
@AutoConfigureAfter(AzureStorageQueueAutoConfiguration.class)
@ConditionalOnEnabledHealthIndicator("azure-storage")
public class StorageQueueHealthConfiguration {

@Bean
@ConditionalOnEnabledHealthIndicator("azure-storage")
@ConditionalOnBean(QueueServiceAsyncClient.class)
public StorageQueueHealthIndicator storageQueueHealthIndicator(QueueServiceAsyncClient queueServiceAsyncClient) {
return new StorageQueueHealthIndicator(queueServiceAsyncClient);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.azure.spring.cloud.actuate.autoconfigure.appconfiguration.AppConfigurationHealthConfiguration,\
com.azure.spring.cloud.actuate.autoconfigure.cosmos.CosmosHealthConfiguration,\
com.azure.spring.cloud.actuate.autoconfigure.eventhubs.EventHubsHealthConfiguration,\
com.azure.spring.cloud.actuate.autoconfigure.keyvault.KeyVaultHealthConfiguration,\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.actuate.appconfiguration;

import com.azure.data.appconfiguration.ConfigurationAsyncClient;
import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.spring.cloud.actuate.autoconfigure.appconfiguration.AppConfigurationHealthConfiguration;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;

public class AppConfigurationHealthConfigurationTest {

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.cloud.azure.appconfiguration.endpoint=https://moaryc-appconfig.azconfig.io")
.withConfiguration(AutoConfigurations.of(AppConfigurationHealthConfiguration.class));

@Test
void configureWithNoConfigurationAsyncClient() {
this.contextRunner.run(context -> Assertions.assertThat(context).doesNotHaveBean(AppConfigurationHealthIndicator.class));
}

@Test
void configureWithConfigurationAsyncClientUp() {
this.contextRunner
.withUserConfiguration(AppConfigurationHealthConfigurationTest.TestConfigurationConnectionUp.class)
.run(context -> {
Assertions.assertThat(context).hasSingleBean(AppConfigurationHealthIndicator.class);
final AppConfigurationHealthIndicator healthIndicator = context.getBean(AppConfigurationHealthIndicator.class);
Health health = healthIndicator.getHealth(true);
assertEquals(Status.UP, health.getStatus());
});
}

@Test
void configureWithConfigurationAsyncClientDown() {
this.contextRunner
.withUserConfiguration(AppConfigurationHealthConfigurationTest.TestConfigurationConnectionDown.class)
.run(context -> {
Assertions.assertThat(context).hasSingleBean(AppConfigurationHealthIndicator.class);
final AppConfigurationHealthIndicator healthIndicator = context.getBean(AppConfigurationHealthIndicator.class);
Health health = healthIndicator.getHealth(true);
assertEquals(Status.DOWN, health.getStatus());
});
}

@Configuration(proxyBeanMethods = false)
static class TestConfigurationConnectionUp {

@Bean
ConfigurationAsyncClient configurationAsyncClient() {
ConfigurationAsyncClient mockConfigurationAsyncClient = mock(ConfigurationAsyncClient.class);
ConfigurationSetting mockSetting = mock(ConfigurationSetting.class);
Mockito.when(mockConfigurationAsyncClient.getConfigurationSetting(any(String.class), isNull()))
.thenReturn(Mono.just(mockSetting));
return mockConfigurationAsyncClient;
}
}

@Configuration(proxyBeanMethods = false)
static class TestConfigurationConnectionDown {

@Bean
ConfigurationAsyncClient configurationAsyncClient() {
ConfigurationAsyncClient mockConfigurationAsyncClient = mock(ConfigurationAsyncClient.class);
Mockito.when(mockConfigurationAsyncClient.getConfigurationSetting(any(String.class), isNull()))
.thenReturn(Mono.error(new IllegalArgumentException("The gremlins have cut the cable.")));
return mockConfigurationAsyncClient;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class StorageBlobHealthIndicatorTest {
private static final String MOCK_URL = "https://example.org/bigly_fake_url";

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.cloud.azure.storage.blob.account-name=testaccount")
.withConfiguration(AutoConfigurations.of(StorageBlobHealthConfiguration.class));

@Test
Expand Down Expand Up @@ -68,9 +69,7 @@ static class TestConfigurationConnectionUp {

@Bean
BlobServiceAsyncClient blobAsyncClient() {
@SuppressWarnings("unchecked") Response<BlobServiceProperties> mockResponse =
(Response<BlobServiceProperties>) Mockito.mock(
Response.class);
@SuppressWarnings("unchecked") Response<BlobServiceProperties> mockResponse = (Response<BlobServiceProperties>) Mockito.mock(Response.class);

BlobServiceAsyncClient mockAsyncClient = Mockito.mock(BlobServiceAsyncClient.class);
Mockito.when(mockAsyncClient.getAccountUrl()).thenReturn(MOCK_URL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class StorageFileHealthIndicatorTest {
private static final String MOCK_URL = "https://example.org/bigly_fake_url";

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.cloud.azure.storage.fileshare.account-name=testaccount")
.withConfiguration(AutoConfigurations.of(StorageFileHealthConfiguration.class));

@Test
Expand Down
6 changes: 6 additions & 0 deletions sdk/spring/spring-cloud-azure-actuator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-data-appconfiguration</artifactId>
<version>1.2.4</version> <!-- {x-version-update;com.azure:azure-data-appconfiguration;dependency} -->
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.actuate.appconfiguration;

import com.azure.core.exception.ResourceNotFoundException;
import com.azure.data.appconfiguration.ConfigurationAsyncClient;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;

import java.time.Duration;

import static com.azure.spring.cloud.actuate.util.Constants.DEFAULT_HEALTH_CHECK_TIMEOUT;

/**
* Indicator class of App Configuration
*/
public class AppConfigurationHealthIndicator extends AbstractHealthIndicator {

private Duration timeout = DEFAULT_HEALTH_CHECK_TIMEOUT;
private final ConfigurationAsyncClient configurationAsyncClient;

public AppConfigurationHealthIndicator(ConfigurationAsyncClient configurationAsyncClient) {
this.configurationAsyncClient = configurationAsyncClient;
}

@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
try {
this.configurationAsyncClient.getConfigurationSetting("azure-spring-none-existing-setting", null)
.block(timeout);
builder.up();
} catch (Exception e) {
if (e instanceof ResourceNotFoundException) {
builder.up();
} else {
throw e;
}
}
}

public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.util.Assert;

import java.time.Duration;

import static com.azure.spring.cloud.actuate.util.Constants.DEFAULT_HEALTH_CHECK_TIMEOUT;

/**
* Simple implementation of a {@link HealthIndicator} returning status information for
* Cosmos data stores.
Expand All @@ -23,6 +27,7 @@ public class CosmosHealthIndicator extends AbstractHealthIndicator {
private final CosmosAsyncClient cosmosAsyncClient;
private final String database;
private final String endpoint;
private Duration timeout = DEFAULT_HEALTH_CHECK_TIMEOUT;

public CosmosHealthIndicator(CosmosAsyncClient cosmosAsyncClient, String database, String endpoint) {
super("Cosmos health check failed");
Expand All @@ -34,7 +39,9 @@ public CosmosHealthIndicator(CosmosAsyncClient cosmosAsyncClient, String databas

@Override
protected void doHealthCheck(Builder builder) {
CosmosDatabaseResponse response = this.cosmosAsyncClient.getDatabase(database).read().block();
CosmosDatabaseResponse response = this.cosmosAsyncClient.getDatabase(database)
.read()
.block(timeout);

if (response != null) {
LOGGER.info("The health indicator cost {} RUs, cosmos uri: {}, dbName: {}",
Expand All @@ -45,6 +52,9 @@ protected void doHealthCheck(Builder builder) {
} else {
builder.up().withDetail("database", response.getProperties().getId());
}
}

public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
}
Loading