diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java index 4b7ca99db962..97eb31aed40f 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManagementConfiguration.java @@ -2,10 +2,13 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management; +import org.springframework.beans.BeansException; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; @@ -15,7 +18,9 @@ */ @Configuration @EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class }) -class FeatureManagementConfiguration { +class FeatureManagementConfiguration implements ApplicationContextAware { + + private ApplicationContext appContext; /** * Creates Feature Manager @@ -26,8 +31,13 @@ class FeatureManagementConfiguration { * @return FeatureManager */ @Bean - FeatureManager featureManager(ApplicationContext context, - FeatureManagementProperties featureManagementConfigurations, FeatureManagementConfigProperties properties) { - return new FeatureManager(context, featureManagementConfigurations, properties); + FeatureManager featureManager(FeatureManagementProperties featureManagementConfigurations, + FeatureManagementConfigProperties properties) { + return new FeatureManager(appContext, featureManagementConfigurations, properties); + } + + @Override + public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { + this.appContext = applicationContext; } } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java index 97f864d0060a..1e13cdc543e4 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/FeatureManager.java @@ -23,6 +23,7 @@ import com.azure.spring.cloud.feature.management.implementation.FeatureManagementConfigProperties; import com.azure.spring.cloud.feature.management.implementation.FeatureManagementProperties; import com.azure.spring.cloud.feature.management.models.Conditions; +import com.azure.spring.cloud.feature.management.models.EvaluationEvent; import com.azure.spring.cloud.feature.management.models.Feature; import com.azure.spring.cloud.feature.management.models.FeatureFilterEvaluationContext; import com.azure.spring.cloud.feature.management.models.FilterNotFoundException; @@ -42,8 +43,8 @@ public class FeatureManager { private final FeatureManagementProperties featureManagementConfigurations; private transient FeatureManagementConfigProperties properties; - - private static final Duration DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(100); + + private static final Duration DEFAULT_BLOCK_TIMEOUT = Duration.ofSeconds(100); /** * Can be called to check if a feature is enabled or disabled. @@ -69,7 +70,7 @@ public class FeatureManager { * @throws FilterNotFoundException file not found */ public Mono isEnabledAsync(String feature) { - return checkFeature(feature, null); + return checkFeature(feature, null).map(event -> event.isEnabled()); } /** @@ -82,7 +83,7 @@ public Mono isEnabledAsync(String feature) { * @throws FilterNotFoundException file not found */ public Boolean isEnabled(String feature) throws FilterNotFoundException { - return checkFeature(feature, null).block(DEFAULT_REQUEST_TIMEOUT); + return checkFeature(feature, null).map(event -> event.isEnabled()).block(DEFAULT_BLOCK_TIMEOUT); } /** @@ -96,7 +97,7 @@ public Boolean isEnabled(String feature) throws FilterNotFoundException { * @throws FilterNotFoundException file not found */ public Mono isEnabledAsync(String feature, Object featureContext) { - return checkFeature(feature, featureContext); + return checkFeature(feature, featureContext).map(event -> event.isEnabled()); } /** @@ -110,30 +111,40 @@ public Mono isEnabledAsync(String feature, Object featureContext) { * @throws FilterNotFoundException file not found */ public Boolean isEnabled(String feature, Object featureContext) throws FilterNotFoundException { - return checkFeature(feature, featureContext).block(DEFAULT_REQUEST_TIMEOUT); + return checkFeature(feature, featureContext).map(event -> event.isEnabled()).block(DEFAULT_BLOCK_TIMEOUT); } - private Mono checkFeature(String featureName, Object featureContext) throws FilterNotFoundException { + private Mono checkFeature(String featureName, Object featureContext) + throws FilterNotFoundException { Feature featureFlag = featureManagementConfigurations.getFeatureFlags().stream() .filter(feature -> feature.getId().equals(featureName)).findAny().orElse(null); + EvaluationEvent event = new EvaluationEvent(featureFlag); + if (featureFlag == null) { - return Mono.just(false); + LOGGER.warn("Feature flag %s not found", featureName); + return Mono.just(event); } - if (featureFlag.getConditions().getClientFilters().size() == 0) { - return Mono.just(featureFlag.isEnabled()); + if (!featureFlag.isEnabled()) { + // If a feature flag is disabled and override can't enable it + return Mono.just(event.setEnabled(false)); } - return checkFeatureFilters(featureFlag, featureContext); + Mono result = this.checkFeatureFilters(event, featureContext); + + return result; } - private Mono checkFeatureFilters(Feature featureFlag, Object featureContext) { + private Mono checkFeatureFilters(EvaluationEvent event, Object featureContext) { + Feature featureFlag = event.getFeature(); Conditions conditions = featureFlag.getConditions(); List featureFilters = conditions.getClientFilters(); if (featureFilters.size() == 0) { - return Mono.just(true); + return Mono.just(event.setEnabled(true)); + } else { + event.setEnabled(conditions.getRequirementType().equals(ALL_REQUIREMENT_TYPE)); } List> filterResults = new ArrayList>(); @@ -165,10 +176,14 @@ private Mono checkFeatureFilters(Feature featureFlag, Object featureCon } if (ALL_REQUIREMENT_TYPE.equals(featureFlag.getConditions().getRequirementType())) { - return Flux.merge(filterResults).reduce((a, b) -> a && b).single(); + return Flux.merge(filterResults).reduce((a, b) -> { + return a && b; + }).single().map(result -> { + return event.setEnabled(result); + }); } // Any Filter must be true - return Flux.merge(filterResults).reduce((a, b) -> a || b).single(); + return Flux.merge(filterResults).reduce((a, b) -> a || b).single().map(result -> event.setEnabled(result)); } /** diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java index 8432c1fbc3a9..81fd09d0fe11 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilter.java @@ -8,6 +8,7 @@ * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by * feature management. As a Contextual feature filter any context that is passed in to the feature request will be * passed along to the filter(s). + * @since 6.0.0 */ @FunctionalInterface public interface ContextualFeatureFilter { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java index 6725e88ca6b7..c22930312978 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/ContextualFeatureFilterAsync.java @@ -10,6 +10,7 @@ * A Filter for Feature Management that is attached to Features. The filter needs to have @Component set to be found by * feature management. As a Contextual feature filter any context that is passed in to the feature request will be * passed along to the filter(s). + * @since 6.0.0 */ @FunctionalInterface public interface ContextualFeatureFilterAsync { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java index fcaa357aa947..5d1285aaee06 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/filters/TargetingFilter.java @@ -52,7 +52,7 @@ public class TargetingFilter implements FeatureFilter, ContextualFeatureFilter { * Audience that always returns false */ private static final String EXCLUSION_CAMEL = "Exclusion"; - protected static final String EXCLUSION = "Exclusion"; + /** * Error message for when the total Audience value is greater than 100 percent. */ @@ -111,7 +111,6 @@ public boolean evaluate(FeatureFilterEvaluationContext context, Object appContex } TargetingContext targetingContext = new TargetingFilterContext(); - if (appContext != null && appContext instanceof TargetingContext) { // Use this if, there is an appContext + the contextualAccessor, or there is no contextAccessor. targetingContext = (TargetingContext) appContext; @@ -151,10 +150,10 @@ public boolean evaluate(FeatureFilterEvaluationContext context, Object appContex if (exclusionMap == null) { exclusionMap = new HashMap<>(); } - + Object users = exclusionMap.get(exclusionUserValue); Object groups = exclusionMap.get(exclusionGroupsValue); - + Map exclusion = new HashMap<>(); if (users instanceof Map) { diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java index cb25caac6203..5f8fe1cf9925 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureFilterUtils.java @@ -6,6 +6,7 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collection; import java.util.Map; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java index 64165c5ddedd..605d45ea1dfe 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/implementation/FeatureManagementConstants.java @@ -2,10 +2,10 @@ // Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implementation; -public class FeatureManagementConstants { +public final class FeatureManagementConstants { public static final String DEFAULT_REQUIREMENT_TYPE = "Any"; - + public static final String ALL_REQUIREMENT_TYPE = "All"; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java index a6504e0f219d..069c6bbafab7 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Conditions.java @@ -11,6 +11,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +/** + * Conditions for evaluating a feature flag. + */ @JsonIgnoreProperties(ignoreUnknown = true) public class Conditions { @JsonProperty("client_filters") @@ -28,6 +31,7 @@ public String getRequirementType() { /** * @param requirementType the requirementType to set + * @return Conditions */ public Conditions setRequirementType(String requirementType) { this.requirementType = requirementType; @@ -43,6 +47,7 @@ public List getClientFilters() { /** * @param clientFilters the clientFilters to set + * @return Conditions */ public Conditions setClientFilters(List clientFilters) { this.clientFilters = clientFilters; diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java new file mode 100644 index 000000000000..786269d9ed6b --- /dev/null +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/EvaluationEvent.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.feature.management.models; + +/** + * Event tracking the evaluation of a feature flag + */ +public class EvaluationEvent { + + private final Feature feature; + + private String user = ""; + + private boolean enabled = false; + + /** + * Creates an Evaluation Event for the given feature + * @param feature Feature + */ + public EvaluationEvent(Feature feature) { + this.feature = feature; + } + + /** + * @return the feature + */ + public Feature getFeature() { + return feature; + } + + /** + * @return the user + */ + public String getUser() { + return user; + } + + /** + * @param user the user to set + * @return EvaluationEvent + */ + public EvaluationEvent setUser(String user) { + this.user = user; + return this; + } + + /** + * @return the enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * @param enabled the enabled to set + * @return EvaluationEvent + */ + public EvaluationEvent setEnabled(boolean enabled) { + this.enabled = enabled; + return this; + } +} diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java index e3b43b0013c4..e25f75b77278 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/Feature.java @@ -35,6 +35,7 @@ public String getId() { /** * @param id the id to set + * @return Feature */ public Feature setId(String id) { this.id = id; @@ -50,6 +51,7 @@ public boolean isEnabled() { /** * @param enabled the enabled to set + * @return Feature */ public Feature setEnabled(boolean enabled) { this.enabled = enabled; @@ -58,14 +60,15 @@ public Feature setEnabled(boolean enabled) { /** * @return the description - * */ + */ public String getDescription() { return description; } /** * @param description the description to set - * */ + * @return Feature + */ public Feature setDescription(String description) { this.description = description; return this; @@ -73,17 +76,17 @@ public Feature setDescription(String description) { /** * @return the conditions - * */ + */ public Conditions getConditions() { return conditions; } /** * @param conditions the conditions to set - * */ + * @return Feature + */ public Feature setConditions(Conditions conditions) { this.conditions = conditions; return this; } - } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java index 8204b27cabfa..30ce0305f987 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/main/java/com/azure/spring/cloud/feature/management/models/FeatureManagementException.java @@ -20,7 +20,7 @@ public final class FeatureManagementException extends RuntimeException { * * @param message the error message. */ - FeatureManagementException(String message) { + public FeatureManagementException(String message) { super(message); this.message = message; } diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java index 660e2b774fab..26115986d3fe 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/filters/TargetingFilterTest.java @@ -62,23 +62,25 @@ public void targetedUser() { } @Test - public void targetedUserLower() { + public void targetedUserAudience() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); - Map parameters = new LinkedHashMap(); + Map parameters = new LinkedHashMap<>(); + Map audience = new LinkedHashMap<>(); - Map users = new LinkedHashMap(); + Map users = new LinkedHashMap<>(); users.put("0", "Doe"); - parameters.put(USERS.toLowerCase(), users); - parameters.put(GROUPS.toLowerCase(), new LinkedHashMap()); - parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); - parameters.put("Exclusion", emptyExclusion()); + audience.put(USERS, users); + audience.put(GROUPS, new LinkedHashMap()); + audience.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); + audience.put("Exclusion", emptyExclusion()); Map excludes = new LinkedHashMap<>(); Map excludedGroups = new LinkedHashMap<>(); excludes.put(GROUPS, excludedGroups); + parameters.put("Audience", audience); context.setParameters(parameters); context.setFeatureName("testFeature"); @@ -86,6 +88,9 @@ public void targetedUserLower() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", null)); assertTrue(filter.evaluate(context)); + + filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", null)); + assertTrue(filter.evaluate(context)); } @Test @@ -139,10 +144,11 @@ public void targetedGroup() { } @Test - public void targetedGroupLower() { + public void targetedGroupAudience() { FeatureFilterEvaluationContext context = new FeatureFilterEvaluationContext(); - Map parameters = new LinkedHashMap(); + Map parameters = new LinkedHashMap<>(); + Map audience = new LinkedHashMap<>(); Map groups = new LinkedHashMap(); Map g1 = new LinkedHashMap(); @@ -150,10 +156,12 @@ public void targetedGroupLower() { g1.put("rolloutPercentage", "100"); groups.put("0", g1); - parameters.put(USERS.toLowerCase(), new LinkedHashMap()); - parameters.put(GROUPS.toLowerCase(), groups); - parameters.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); - parameters.put("Exclusion", emptyExclusion()); + audience.put(USERS, new LinkedHashMap()); + audience.put(GROUPS, groups); + audience.put(DEFAULT_ROLLOUT_PERCENTAGE, 0); + audience.put("Exclusion", emptyExclusion()); + + parameters.put("Audience", audience); context.setParameters(parameters); context.setFeatureName("testFeature"); @@ -247,7 +255,7 @@ public void targetedGroupFiftyFalse() { TargetingFilter filter = new TargetingFilter(new TargetingFilterTestContextAccessor("Doe", targetedGroups)); - assertTrue(filter.evaluate(context)); + assertFalse(filter.evaluate(context)); } @Test diff --git a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java index ab85d8a7c543..759cf5873635 100644 --- a/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java +++ b/sdk/spring/spring-cloud-azure-feature-management/src/test/java/com/azure/spring/cloud/feature/management/implemenation/TargetingFilterUtilsTest.java @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. package com.azure.spring.cloud.feature.management.implemenation; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -13,8 +15,8 @@ public void isTargetedPercentageTest() { assertEquals(FeatureFilterUtils.isTargetedPercentage(null), 9.875071074318855); assertEquals(FeatureFilterUtils.isTargetedPercentage(""), 26.0813765987012); assertEquals(FeatureFilterUtils.isTargetedPercentage("Alice"), 38.306839656621875); - assertEquals(FeatureFilterUtils.isTargetedPercentage("Quinn\nDeb"), 38.306839656621875); - assertEquals(FeatureFilterUtils.isTargetedPercentage("\nProd"), 79.98622464481421); + assertEquals(FeatureFilterUtils.isTargetedPercentage("Quinn\nDeb"), 79.98622464481421); + assertEquals(FeatureFilterUtils.isTargetedPercentage("\nProd"), 73.47059517015484); } }