All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.confidence.ConfidenceFeatureProvider Maven / Gradle / Ivy

package com.spotify.confidence;

import static com.spotify.confidence.FlagResolverClientImpl.OPEN_FEATURE_RESOLVE_CONTEXT_KEY;

import com.google.protobuf.Struct;
import com.spotify.confidence.ConfidenceUtils.FlagPath;
import com.spotify.confidence.Exceptions.IllegalValuePath;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolveFlagsResponse;
import com.spotify.confidence.shaded.flags.resolver.v1.ResolvedFlag;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FeatureProvider;
import dev.openfeature.sdk.Metadata;
import dev.openfeature.sdk.ProviderEvaluation;
import dev.openfeature.sdk.Value;
import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.exceptions.GeneralError;
import dev.openfeature.sdk.exceptions.TypeMismatchError;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status.Code;
import io.grpc.StatusRuntimeException;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import org.slf4j.Logger;

/** OpenFeature Provider for feature flagging with the Confidence platform */
public class ConfidenceFeatureProvider implements FeatureProvider {

  private final Confidence confidence;

  /**
   * ConfidenceFeatureProvider constructor
   *
   * @param confidence an instance of the Confidence
   */
  public ConfidenceFeatureProvider(Confidence confidence) {
    this.confidence = confidence;
  }

  private static final Logger log =
      org.slf4j.LoggerFactory.getLogger(ConfidenceFeatureProvider.class);

  /**
   * ConfidenceFeatureProvider constructor
   *
   * @param clientSecret generated from the Confidence portal
   * @param managedChannel gRPC channel
   * @deprecated This constructor is deprecated. Please use {@link
   *     #ConfidenceFeatureProvider(Confidence)} instead.
   */
  @Deprecated()
  public ConfidenceFeatureProvider(String clientSecret, ManagedChannel managedChannel) {
    this(Confidence.builder(clientSecret).flagResolverManagedChannel(managedChannel).build());
  }

  /**
   * ConfidenceFeatureProvider constructor
   *
   * @param clientSecret generated from the Confidence portal
   * @deprecated This constructor is deprecated. Please use {@link
   *     #ConfidenceFeatureProvider(Confidence)} instead.
   */
  @Deprecated()
  public ConfidenceFeatureProvider(String clientSecret) {
    this(clientSecret, ManagedChannelBuilder.forAddress("edge-grpc.spotify.com", 443).build());
  }

  /**
   * ConfidenceFeatureProvider constructor that allows you to override the default gRPC host and
   * port, used for local resolver.
   *
   * @param clientSecret generated from the Confidence portal
   * @param host gRPC host you want to connect to.
   * @param port port of the gRPC host that you want to use.
   * @deprecated This constructor is deprecated. Please use {@link
   *     #ConfidenceFeatureProvider(Confidence)} instead.
   */
  @Deprecated()
  public ConfidenceFeatureProvider(String clientSecret, String host, int port) {
    this(clientSecret, ManagedChannelBuilder.forAddress(host, port).usePlaintext().build());
  }

  @Override
  public Metadata getMetadata() {
    return () -> "com.spotify.confidence.flags.resolver.v1.FlagResolverService";
  }

  @Override
  public ProviderEvaluation getBooleanEvaluation(
      String key, Boolean defaultValue, EvaluationContext ctx) {
    return getCastedEvaluation(key, defaultValue, ctx, Value::asBoolean);
  }

  @Override
  public ProviderEvaluation getStringEvaluation(
      String key, String defaultValue, EvaluationContext ctx) {
    return getCastedEvaluation(key, defaultValue, ctx, Value::asString);
  }

  @Override
  public ProviderEvaluation getIntegerEvaluation(
      String key, Integer defaultValue, EvaluationContext ctx) {
    return getCastedEvaluation(key, defaultValue, ctx, Value::asInteger);
  }

  @Override
  public ProviderEvaluation getDoubleEvaluation(
      String key, Double defaultValue, EvaluationContext ctx) {
    return getCastedEvaluation(key, defaultValue, ctx, Value::asDouble);
  }

  private  ProviderEvaluation getCastedEvaluation(
      String key, T defaultValue, EvaluationContext ctx, Function cast) {
    final Value wrappedDefaultValue;
    try {
      wrappedDefaultValue = new Value(defaultValue);
    } catch (InstantiationException e) {
      // this is not going to happen because we only call the constructor with supported types
      throw new RuntimeException(e);
    }

    final ProviderEvaluation objectEvaluation =
        getObjectEvaluation(key, wrappedDefaultValue, ctx);

    final T castedValue = cast.apply(objectEvaluation.getValue());
    if (castedValue == null) {
      log.warn("Cannot cast value '{}' to expected type", objectEvaluation.getValue().toString());
      throw new TypeMismatchError(
          String.format("Cannot cast value '%s' to expected type", objectEvaluation.getValue()));
    }

    return ProviderEvaluation.builder()
        .value(castedValue)
        .variant(objectEvaluation.getVariant())
        .reason(objectEvaluation.getReason())
        .errorMessage(objectEvaluation.getErrorMessage())
        .errorCode(objectEvaluation.getErrorCode())
        .build();
  }

  @Override
  public ProviderEvaluation getObjectEvaluation(
      String key, Value defaultValue, EvaluationContext ctx) {

    final FlagPath flagPath;
    try {
      flagPath = FlagPath.getPath(key);
    } catch (IllegalValuePath e) {
      log.warn(e.getMessage());
      throw new RuntimeException(e);
    }

    final Struct evaluationContext = OpenFeatureUtils.convertToProto(ctx);
    // resolve the flag by calling the resolver API
    final ResolveFlagsResponse resolveFlagResponse;
    try {
      final String requestFlagName = "flags/" + flagPath.getFlag();

      resolveFlagResponse =
          confidence
              .withContext(
                  Map.of(
                      OPEN_FEATURE_RESOLVE_CONTEXT_KEY,
                      ConfidenceValue.Struct.fromProto(evaluationContext)))
              .resolveFlags(requestFlagName)
              .get();

      if (resolveFlagResponse.getResolvedFlagsList().isEmpty()) {
        log.warn("No active flag '{}' was found", flagPath.getFlag());
        throw new FlagNotFoundError(
            String.format("No active flag '%s' was found", flagPath.getFlag()));
      }

      final String responseFlagName = resolveFlagResponse.getResolvedFlags(0).getFlag();
      if (!requestFlagName.equals(responseFlagName)) {
        log.warn("Unexpected flag '{}' from remote", responseFlagName.replaceFirst("^flags/", ""));
        throw new FlagNotFoundError(
            String.format(
                "Unexpected flag '%s' from remote", responseFlagName.replaceFirst("^flags/", "")));
      }

      final ResolvedFlag resolvedFlag = resolveFlagResponse.getResolvedFlags(0);

      if (resolvedFlag.getVariant().isEmpty()) {
        log.debug(
            String.format(
                "The server returned no assignment for the flag '%s'. Typically, this happens "
                    + "if no configured rules matches the given evaluation context.",
                flagPath.getFlag()));
        return ProviderEvaluation.builder()
            .value(defaultValue)
            .reason(
                "The server returned no assignment for the flag. Typically, this happens "
                    + "if no configured rules matches the given evaluation context.")
            .build();
      } else {
        final Value fullValue =
            OpenFeatureTypeMapper.from(resolvedFlag.getValue(), resolvedFlag.getFlagSchema());

        // if a path is given, extract expected portion from the structured value
        Value value = OpenFeatureUtils.getValueForPath(flagPath.getPath(), fullValue);

        if (value.isNull()) {
          value = defaultValue;
        }

        // regular resolve was successful
        return ProviderEvaluation.builder()
            .value(value)
            .variant(resolvedFlag.getVariant())
            .build();
      }
    } catch (StatusRuntimeException | InterruptedException | ExecutionException e) {
      // If the remote API is unreachable, for now we fall back to the default value. However, we
      // should consider maintaining a local resolve-history to avoid flickering experience in case
      // of a temporarily unavailable backend
      if (e instanceof StatusRuntimeException) {
        handleStatusRuntimeException((StatusRuntimeException) e);
      } else if (e.getCause() instanceof StatusRuntimeException) {
        handleStatusRuntimeException((StatusRuntimeException) e.getCause());
      }
      throw new GeneralError("Unknown error occurred when calling the provider backend");
    }
  }

  private static void handleStatusRuntimeException(StatusRuntimeException e) {
    if (e.getStatus().getCode() == Code.DEADLINE_EXCEEDED) {
      log.error("Deadline exceeded when calling provider backend", e);
      throw new GeneralError("Deadline exceeded when calling provider backend");
    } else if (e.getStatus().getCode() == Code.UNAVAILABLE) {
      log.error("Provider backend is unavailable", e);
      throw new GeneralError("Provider backend is unavailable");
    } else if (e.getStatus().getCode() == Code.UNAUTHENTICATED) {
      log.error("UNAUTHENTICATED", e);
      throw new GeneralError("UNAUTHENTICATED");
    } else {
      log.error(
          "Unknown error occurred when calling the provider backend. Grpc status code {}",
          e.getStatus().getCode(),
          e);
      throw new GeneralError(
          String.format(
              "Unknown error occurred when calling the provider backend. Exception: %s",
              e.getMessage()));
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy