Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +18,9 @@
*/
@Configuration
@EnableConfigurationProperties({ FeatureManagementConfigProperties.class, FeatureManagementProperties.class })
class FeatureManagementConfiguration {
class FeatureManagementConfiguration implements ApplicationContextAware {

private ApplicationContext appContext;

/**
* Creates Feature Manager
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -69,7 +70,7 @@ public class FeatureManager {
* @throws FilterNotFoundException file not found
*/
public Mono<Boolean> isEnabledAsync(String feature) {
return checkFeature(feature, null);
return checkFeature(feature, null).map(event -> event.isEnabled());
}

/**
Expand All @@ -82,7 +83,7 @@ public Mono<Boolean> 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);
}

/**
Expand All @@ -96,7 +97,7 @@ public Boolean isEnabled(String feature) throws FilterNotFoundException {
* @throws FilterNotFoundException file not found
*/
public Mono<Boolean> isEnabledAsync(String feature, Object featureContext) {
return checkFeature(feature, featureContext);
return checkFeature(feature, featureContext).map(event -> event.isEnabled());
}

/**
Expand All @@ -110,30 +111,40 @@ public Mono<Boolean> 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<Boolean> checkFeature(String featureName, Object featureContext) throws FilterNotFoundException {
private Mono<EvaluationEvent> 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<EvaluationEvent> result = this.checkFeatureFilters(event, featureContext);

return result;
}

private Mono<Boolean> checkFeatureFilters(Feature featureFlag, Object featureContext) {
private Mono<EvaluationEvent> checkFeatureFilters(EvaluationEvent event, Object featureContext) {
Feature featureFlag = event.getFeature();
Conditions conditions = featureFlag.getConditions();
List<FeatureFilterEvaluationContext> 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<Mono<Boolean>> filterResults = new ArrayList<Mono<Boolean>>();
Expand Down Expand Up @@ -165,10 +176,14 @@ private Mono<Boolean> 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));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, Object> exclusion = new HashMap<>();

if (users instanceof Map) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -28,6 +31,7 @@ public String getRequirementType() {

/**
* @param requirementType the requirementType to set
* @return Conditions
*/
public Conditions setRequirementType(String requirementType) {
this.requirementType = requirementType;
Expand All @@ -43,6 +47,7 @@ public List<FeatureFilterEvaluationContext> getClientFilters() {

/**
* @param clientFilters the clientFilters to set
* @return Conditions
*/
public Conditions setClientFilters(List<FeatureFilterEvaluationContext> clientFilters) {
this.clientFilters = clientFilters;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public String getId() {

/**
* @param id the id to set
* @return Feature
*/
public Feature setId(String id) {
this.id = id;
Expand All @@ -50,6 +51,7 @@ public boolean isEnabled() {

/**
* @param enabled the enabled to set
* @return Feature
*/
public Feature setEnabled(boolean enabled) {
this.enabled = enabled;
Expand All @@ -58,32 +60,33 @@ 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;
}

/**
* @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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading