Skip to content

Commit

Permalink
Added Health Check for Azure App Configuration (#22949)
Browse files Browse the repository at this point in the history
* Added Health Check

* Removed unneeded check

* Fixed names, tests, and review comments.

* Updated nit
  • Loading branch information
mrm9084 authored Aug 12, 2021
1 parent 9d48bd2 commit b36d4a0
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
// Licensed under the MIT License.
package com.azure.spring.cloud.config;

import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.endpoint.RefreshEndpoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import com.azure.spring.cloud.config.health.AppConfigurationHealthIndicator;
import com.azure.spring.cloud.config.properties.AppConfigurationProperties;
import com.azure.spring.cloud.config.stores.ClientStore;

Expand All @@ -29,4 +33,19 @@ public AppConfigurationRefresh getConfigWatch(AppConfigurationProperties propert
return new AppConfigurationRefresh(properties, clientStore);
}
}

/**
* Health Indicator for Azure App Configuration store connections.
*/
@Configuration
@ConditionalOnClass({ HealthIndicator.class })
@ConditionalOnEnabledHealthIndicator("azure-app-configuration")
static class AppConfigurationtHealthConfiguration {

@Bean
@ConditionalOnBean
AppConfigurationHealthIndicator appConfigurationHealthIndicator(AppConfigurationRefresh refresh) {
return new AppConfigurationHealthIndicator(refresh);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,11 @@ private List<AppConfigurationPropertySource> create(String context, ConfigStore

ConfigurationSetting watchKey = clients.getWatchKey(settingSelector,
store.getEndpoint());
watchKeysSettings.add(watchKey);
if (watchKey != null) {
watchKeysSettings.add(watchKey);
} else {
watchKeysSettings.add(new ConfigurationSetting().setKey(trigger.getKey()).setLabel(trigger.getLabel()));
}
}
if (store.getFeatureFlags().getEnabled()) {
SettingSelector settingSelector = new SettingSelector()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;

Expand All @@ -19,6 +21,7 @@

import com.azure.data.appconfiguration.models.ConfigurationSetting;
import com.azure.data.appconfiguration.models.SettingSelector;
import com.azure.spring.cloud.config.health.AppConfigurationStoreHealth;
import com.azure.spring.cloud.config.properties.AppConfigurationProperties;
import com.azure.spring.cloud.config.properties.AppConfigurationStoreMonitoring;
import com.azure.spring.cloud.config.properties.ConfigStore;
Expand All @@ -37,15 +40,27 @@ public class AppConfigurationRefresh implements ApplicationEventPublisherAware {
private final AtomicBoolean running = new AtomicBoolean(false);

private final List<ConfigStore> configStores;

private ApplicationEventPublisher publisher;

private final ClientStore clientStore;

private Map<String, AppConfigurationStoreHealth> clientHealth;

private String eventDataInfo;

public AppConfigurationRefresh(AppConfigurationProperties properties, ClientStore clientStore) {
this.configStores = properties.getStores();
this.clientStore = clientStore;
this.eventDataInfo = "";
this.clientHealth = new HashMap<>();
configStores.stream().forEach(store -> {
if (getStoreHealthState(store)) {
this.clientHealth.put(store.getEndpoint(), AppConfigurationStoreHealth.UP);
} else {
this.clientHealth.put(store.getEndpoint(), AppConfigurationStoreHealth.NOT_LOADED);
}
});
}

@Override
Expand All @@ -54,7 +69,8 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv
}

/**
* Checks configurations to see if configurations should be reloaded. If the refresh interval has passed and a trigger has been updated configuration are reloaded.
* Checks configurations to see if configurations should be reloaded. If the refresh interval has passed and a
* trigger has been updated configuration are reloaded.
*
* @return Future with a boolean of if a RefreshEvent was published. If refreshConfigurations is currently being run
* elsewhere this method will return right away as <b>false</b>.
Expand Down Expand Up @@ -83,33 +99,48 @@ public void expireRefreshInterval(String endpoint) {
private boolean refreshStores() {
boolean didRefresh = false;
if (running.compareAndSet(false, true)) {
Map<String, AppConfigurationStoreHealth> clientHealthUpdate = new HashMap<>();
configStores.stream().forEach(store -> {
if (getStoreHealthState(store)) {
clientHealthUpdate.put(store.getEndpoint(), AppConfigurationStoreHealth.DOWN);
} else {
clientHealthUpdate.put(store.getEndpoint(), AppConfigurationStoreHealth.NOT_LOADED);
}
});
try {
for (ConfigStore configStore : configStores) {
if (configStore.isEnabled()) {
String endpoint = configStore.getEndpoint();
AppConfigurationStoreMonitoring monitor = configStore.getMonitoring();

if (StateHolder.getLoadState(endpoint) && monitor.isEnabled()
&& refresh(StateHolder.getState(endpoint), endpoint, monitor.getRefreshInterval())) {
didRefresh = true;
break;
} else {
LOGGER.debug("Skipping configuration refresh check for " + endpoint);
if (StateHolder.getLoadState(endpoint)) {
if (monitor.isEnabled()
&& refresh(StateHolder.getState(endpoint), endpoint, monitor.getRefreshInterval())) {
didRefresh = true;
break;
} else {
LOGGER.debug("Skipping configuration refresh check for " + endpoint);
}
clientHealthUpdate.put(configStore.getEndpoint(), AppConfigurationStoreHealth.UP);
}

FeatureFlagStore featureStore = configStore.getFeatureFlags();

if (featureStore.getEnabled() && StateHolder.getLoadStateFeatureFlag(endpoint) && refresh(
StateHolder.getStateFeatureFlag(endpoint), endpoint, monitor.getFeatureFlagRefreshInterval())) {
didRefresh = true;
break;
} else {
LOGGER.debug("Skipping feature flag refresh check for " + endpoint);
if (StateHolder.getLoadStateFeatureFlag(endpoint)) {
if (featureStore.getEnabled() && refresh(StateHolder.getStateFeatureFlag(endpoint),
endpoint, monitor.getFeatureFlagRefreshInterval())) {
didRefresh = true;
break;
} else {
LOGGER.debug("Skipping feature flag refresh check for " + endpoint);
}
clientHealthUpdate.put(configStore.getEndpoint(), AppConfigurationStoreHealth.UP);
}
}
}
} finally {
running.set(false);
clientHealth = clientHealthUpdate;
}
}
return didRefresh;
Expand Down Expand Up @@ -158,12 +189,23 @@ private boolean refresh(State state, String endpoint, Duration refreshInterval)
return true;
}
}
StateHolder.setState(endpoint, state.getWatchKeys(), refreshInterval);

// Just need to reset refreshInterval, if a refresh was triggered it will updated after loading the new configurations.
StateHolder.setState(state, refreshInterval);
}

return false;
}

public Map<String, AppConfigurationStoreHealth> getAppConfigurationStoresHealth() {
return this.clientHealth;
}

private Boolean getStoreHealthState(ConfigStore store) {
return store.isEnabled() && (StateHolder.getLoadState(store.getEndpoint())
|| StateHolder.getLoadStateFeatureFlag(store.getEndpoint()));
}

/**
* For each refresh, multiple etags can change, but even one etag is changed, refresh is required.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ class State {
private final List<ConfigurationSetting> watchKeys;

private final Date nextRefreshCheck;

private final String key;

State(List<ConfigurationSetting> watchKeys, int refreshInterval) {
State(List<ConfigurationSetting> watchKeys, int refreshInterval, String key) {
this.watchKeys = watchKeys;
nextRefreshCheck = DateUtils.addSeconds(new Date(), refreshInterval);
this.key = key;
}

/**
Expand All @@ -34,4 +37,10 @@ public Date getNextRefreshCheck() {
return nextRefreshCheck;
}

/**
* @return the key
*/
public String getKey() {
return key;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static State getStateFeatureFlag(String endpoint) {
*/
static void setState(String endpoint, List<ConfigurationSetting> watchKeys,
Duration duration) {
STATE.put(endpoint, new State(watchKeys, Math.toIntExact(duration.getSeconds())));
STATE.put(endpoint, new State(watchKeys, Math.toIntExact(duration.getSeconds()), endpoint));
}

/**
Expand All @@ -56,14 +56,22 @@ static void setStateFeatureFlag(String endpoint, List<ConfigurationSetting> watc
setState(endpoint + FEATURE_ENDPOINT, watchKeys, duration);
}

/**
* @param state previous state to base off
* @param duration nextRefreshPeriod
*/
static void setState(State state, Duration duration) {
STATE.put(state.getKey(), new State(state.getWatchKeys(), Math.toIntExact(duration.getSeconds()), state.getKey()));
}

static void expireState(String endpoint) {
String key = endpoint;
State oldState = STATE.get(key);
SecureRandom random = new SecureRandom();
long wait = (long) (random.nextDouble() * MAX_JITTER);
long timeLeft = (int) ((oldState.getNextRefreshCheck().getTime() - (new Date().getTime())) / 1000);
if (wait < timeLeft) {
STATE.put(key, new State(oldState.getWatchKeys(), (int) wait));
STATE.put(key, new State(oldState.getWatchKeys(), (int) wait, oldState.getKey()));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.config.health;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;

import com.azure.spring.cloud.config.AppConfigurationRefresh;

/**
* Indicator class of App Configuration
*/
public class AppConfigurationHealthIndicator implements HealthIndicator {

private final AppConfigurationRefresh refresh;

public AppConfigurationHealthIndicator(AppConfigurationRefresh refresh) {
this.refresh = refresh;
}

@Override
public Health health() {
Health.Builder healthBuilder = new Health.Builder();
Boolean healthy = true;

for (String store : refresh.getAppConfigurationStoresHealth().keySet()) {
if (AppConfigurationStoreHealth.DOWN.equals(refresh.getAppConfigurationStoresHealth().get(store))) {
healthy = false;
healthBuilder.withDetail(store, "DOWN");
} else if (refresh.getAppConfigurationStoresHealth().get(store).equals(AppConfigurationStoreHealth.NOT_LOADED)) {
healthBuilder.withDetail(store, "NOT LOADED");
} else {
healthBuilder.withDetail(store, "UP");
}
}

if (!healthy) {
return healthBuilder.down().build();
}
return healthBuilder.up().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.cloud.config.health;

/**
* App Configuration Health states
*/
public enum AppConfigurationStoreHealth {

UP,
DOWN,
NOT_LOADED

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
/**
* Package contains classes for converting Azure App Configuration Health information.
*/
package com.azure.spring.cloud.config.health;
Loading

0 comments on commit b36d4a0

Please sign in to comment.