Skip to content

Files

Latest commit

f6fd04b · Nov 14, 2023

History

History
559 lines (423 loc) · 19.6 KB

README.adoc

File metadata and controls

559 lines (423 loc) · 19.6 KB

Java Client SDK for FeatureHub

Welcome to the Java SDK implementation for FeatureHub.io - Open source Feature flags management, A/B testing and remote configuration platform.

Below explains how you can use the FeatureHub SDK in Java for Java backend applications or Android mobile applications.

To control the feature flags from the FeatureHub Admin console, either use our [demo](https://demo.featurehub.io) version for evaluation or install the app using our guide [here](http://docs.featurehub.io/#_installation)

There are 2 ways to request for feature updates via this SDK:

  • SSE (Server Sent Events) realtime updates mechanism

In this mode, you will make a connection to the FeatureHub Edge server using an EventSource library which this SDK is based on, and any updates to any features will come through to you in near realtime, automatically updating the feature values in the repository. This is always the recommended method for backend applications, and we have an implementation in Jersey.

  • FeatureHub GET client (GET request updates)

In this mode, you make a GET request, which you control how often it runs. The SDK provides no timer based repeat functionality to keep making this request. There is an implementation using OKHttp. We have deliberately left the timer choice to you as there are many different timer functions, including one built into the JDK (java.util.Timer).

SDK Installation

To install the SDK, choose your method of connection. The Core library will be included transitively. The Core library uses Java Service Loaders to automatically discover what client library you have chosen, so please ensure you include only one.

If you want to specify and deliberately configure it, you can use:

fhConfig.setEdgeService(() => new EdgeProvider...());

Where EdgeProvider is the name of your class that knows how to connect to the Edge and pull feature details.

  • SSE (Server Sent Events) realtime updates mechanism

There are three options for SSE connections - Jersey 2, Jersey 3 and OKHttp SSE. OKHttp is recommended for stacks that do not already use Jersey as their transport of choice as it is considerably smaller, and less general purpose. We recommend SSE for long lived server or batch processes.

The dependency includes (Maven style) are below (choose one):

Jersey 2
----
<dependency>
  <groupId>io.featurehub.sdk</groupId>
  <artifactId>java-client-jersey</artifactId>
  <version>[2.1,3)</version>
</dependency>
----
Jersey 3
----
<dependency>
  <groupId>io.featurehub.sdk</groupId>
  <artifactId>java-client-jersey3</artifactId>
  <version>[2.1,3)</version>
</dependency>
----
OKHttp SSE
----
<dependency>
  <groupId>io.featurehub.sdk</groupId>
  <artifactId>java-client-sse</artifactId>
  <version>[1.2,2)</version>
</dependency>
----

If you do not already use Jersey in your code base, you should also include our runtime dependencies for Jersey and Jackson.

set of jersey2 dependencies
----
<dependency>
  <groupId>io.featurehub.sdk.composites</groupId>
  <artifactId>sdk-composite-jersey2</artifactId>
  <version>[1.1, 2)</version>
</dependency>
----
  • FeatureHub polling client (GET request updates)

This is recommended for Mobile as it will only request updates when you ask for them and not keep the radio on. We also recommend them for short batch jobs, Functions as a Service (such as Knative, Cloud Functions, Lamba or Azure Cloud Functions or similar frameworks) where you can ensure you get the features up front and then carry on.

    <dependency>
      <groupId>io.featurehub.sdk</groupId>
      <artifactId>java-client-android</artifactId>
      <version>[2.1,3)</version>
    </dependency>

Quick start

Connecting to FeatureHub

There are 3 steps to connecting:

1) Copy FeatureHub API Key from the FeatureHub Admin Console

2) Create FeatureHub config

3) Check FeatureHub Repository readyness and request feature state

1. Copy API Key from the FeatureHub Admin Console

Find and copy your API Key from the FeatureHub Admin Console on the Service Accounts Keys page - you will use this in your code to configure feature updates for your environments. It should look similar to this: default/71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE.

There are two options - a Server Evaluated API Key and a Client Evaluated API Key. More on this here

Client Side evaluation is intended for use in secure environments (such as microservices) and is intended for rapid client side evaluation, per request for example.

Server Side evaluation is more suitable when you are using an insecure client. (e.g. Browser or Mobile). This also means you evaluate one user per client.

2. Create FeatureHub config:

Create an instance of EdgeFeatureHubConfig. You need to provide the API Key and the URL of the end-point you will be connecting to (the Edge server URL).

import io.featurehub.client.EdgeFeatureHubConfig;

// typically you would get these from environment variables
String edgeUrl = "http://localhost:8085/";
String apiKey = "71ed3c04-122b-4312-9ea8-06b2b8d6ceac/fsTmCrcZZoGyl56kPHxfKAkbHrJ7xZMKO3dlBiab5IqUXjgKvqpjxYdI8zdXiJqYCpv92Jrki0jY5taE";

FeatureHubConfig fhConfig = new EdgeFeatureHubConfig(edgeUrl, apiKey);

3. Check FeatureHub Repository readyness and request feature state

Feature flag rollout strategies and user targeting are all determined by the active user context. If you are not intending to use rollout strategies, you can pass empty context to the SDK.

Client Side evaluation

What you do next depends on your framework. In many modern frameworks, you don’t get to choose when the server starts, it starts and you just have deal with it. It is recommended that you ensure that your heartbeat or readyness check is dependent on whether the feature service is connected.

Remember client side evaluation is used for services, those processing requests (from users or via eventing systems) or batch processing for example. As such, they are typically wired up using Dependency Injection (DI) frameworks and we show that approach here as it is what people are most likely to use.

As you would typically have a dependency injection system (like Spring or CDI) looking after you, you need to inject the FeatureHubConfig you created above. Our SpringBoot, pure Jersey and Quarkus examples can be found in our featurehub-examples repository.

SpringBoot - wiring the FeatureHubConfig
  @Bean // using environment variables
  public FeatureHubConfig featureHubConfig() {
    String host = System.getenv("FEATUREHUB_EDGE_URL");
    String apiKey = System.getenv("FEATUREHUB_API_KEY");
    FeatureHubConfig config = new EdgeFeatureHubConfig(host, apiKey);
    config.init();

    return config;
  }
Quarkus/CDI - wiring the FeatureHubConfig
/**
 * We do this at the top level because we need a Produces for the FeatureHub config as we
 * specifically want this bean and not have to delegate through, and we need the external config.
 */
@Startup
@ApplicationScoped
public class FeatureSource {
  private static final Logger log = LoggerFactory.getLogger(FeatureSource.class);

  @ConfigProperty(name = "feature-hub.url")
  String url;

  @ConfigProperty(name = "feature-hub.api-key")
  String apiKey;

  /**
   * We need a FeatureHubConfig bean available for all sundry uses, the health check and any other
   * incoming calls. So we create it at startup and seed it into the CDI Context.
   *
   * @return FeatureHubConfig - the config ready for use.
   */
  @Startup
  @Produces
  @ApplicationScoped
  public FeatureHubConfig fhConfig() {
    final EdgeFeatureHubConfig config = new EdgeFeatureHubConfig(url, apiKey);
    config.init();
    log.info("FeatureHub started");
    return config;
  }
}

We then recommend you consider adding FeatureHub to your heartbeat or liveness check.

SpringBoot - liveness
@RestController
@RequestMapping("/health")
public class HealthResource {
  private final FeatureHubConfig featureHubConfig;
  private static final Logger log = LoggerFactory.getLogger(HealthResource.class);

  @Inject
  public HealthResource(FeatureHubConfig featureHubConfig) {
    this.featureHubConfig = featureHubConfig;
  }

  @RequestMapping("/liveness")
  public String liveness() {
    if (featureHubConfig.getReadyness() == Readyness.Ready) {
      return "yes";
    }

    log.warn("FeatureHub connection not yet available, reporting not live.");
    throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE);
  }
}
Quarkus/CDI - liveness
@Path("/health/liveness")
public class HealthResource {
  private final FeatureHubConfig config;

  @Inject
  public HealthResource(FeatureHubConfig config) {
    this.config = config;
  }

  @GET
  public Response liveness() {
    if (config.getReadyness() == Readyness.Ready) {
      return Response.ok().build();
    }

    return Response.status(503).build();
  }
}

This will prevent most services like Application Load Balancers or Kubernetes from routing traffic to your server before it has connected to the feature service and is ready.

There are other ways to do this - for example not starting your server until you have a readyness success, but this is the most strongly recommended because it ensures that a system in a properly structured Java service will behave as expected.

The next thing you would normally do is to ensure that the ClientContext is ready and set up for downstream systems to get a hold of and use. In Java this is normally done by using a filter and providing some kind of request level scope - a Request Level injectable object.

In our examples, we simply put the Authorization header into the UserKey of the context, allowing you to just pass the name of the user to keep it simple.

SpringBoot - creating and using the fhClient
@Configuration
public class UserConfiguration {
  @Bean
  @Scope("request")
  ClientContext createClient(FeatureHubConfig fhConfig, HttpServletRequest request) {
    ClientContext fhClient = fhConfig.newContext();

    if (request.getHeader("Authorization") != null) {
      // you would always authenticate some other way, this is just an example
      fhClient.userKey(request.getHeader("Authorization"));
    }

    return fhClient;
  }
}

@RestController
public class HelloResource {
  private final Provider<ClientContext> clientProvider;

  @Inject
  public HelloResource(Provider<ClientContext> clientProvider) {
    this.clientProvider = clientProvider;
  }

  @RequestMapping("/")
  public String index() {
    ClientContext fhClient = clientProvider.get();
    return "Hello World " + fhClient.feature("SUBMIT_COLOR_BUTTON").getString();
  }
}
Quarkus/CDI - creating and using the fhClient
  /**
   * This lets us create the ClientContext, which will always be empty, or the AuthFilter will add the user if it
   * discovers it. (This is part of the FeatureSource class from above)
   *
   * @param config - the FeatureHub Config
   * @return - a blank client context usable by any resource.
   */
  @Produces
  @RequestScoped
  public ClientContext createClient(FeatureHubConfig config) {
    try {
      return config.newContext().build().get();
    } catch (Exception e) {
      log.error("Cannot create context!", e);
      throw new RuntimeException(e);
    }
  }

/**
 * This filter checks if there is an Authorization header and if so, will add it to the user context
 * (which is mutable) allowing downstream resources to correctly calculate their features.
 *
 */
@Provider
@PreMatching
public class AuthFilter implements ContainerRequestFilter {
  private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

  @Inject
  javax.inject.Provider<ClientContext> clientProvider;

  @Override
  public void filter(ContainerRequestContext req) {
    if (req.getHeaders().containsKey("Authorization")) {
      String user = req.getHeaderString("Authorization");

      try {
        clientProvider.get().userKey(user).build().get();
      } catch (Exception e) {
        log.error("Unable to set user key on user");
      }
    }
  }
}

@Path("/")
public class HelloResource {
  private final Provider<ClientContext> clientProvider;

  @Inject
  public HelloResource(Provider<ClientContext> clientProvider) {
    this.clientProvider = clientProvider;
  }


  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String hello() {
    return "hello world! " + contextProvider.get().feature("SUBMIT_COLOR_BUTTON").getString();
  }
}

These examples show us how we can wire the FeatureHub functionality into our system in two different cases, the standard CDI (with extensions) way that Quarkus (and to a degree Jersey) works, and the way that Spring/SpringBoot works.

Server side evaluation

In the server side evaluation (e.g. an Android Mobile app), the context is created once as you evaluate one user per client. This config is likely loaded into resources that are baked into your Mobile image and once you load them, you can progress from there.

You should not use Server Sent Events for Mobile as they attempt to keep the radio on and will drain battery. Use the java-client-android artifact and this will be automatically used for you.

As such, it is recommended that you create your ClientContext as early as sensible and build it. This will trigger a poll to the server and it will get the feature statuses and you will be ready to go. Each time you need an update, you can simply .build() your context again and it will force a poll.

ClientContext fhClient = fhConfig.newContext().build().get();

Local Feature Overrides

If you set a system property feature-toggles.FEATURE_NAME then you can override the value of what the value is for feature flags. This is a further convenience feature and can be useful for an individual developer working on a new feature, where it is off for everyone else but not for them.

Analytics

The Analytics client layer currently only supports directly exporting data to Google Analytics. It has the capability to add further adapters but this is not our medium term strategy to do it this way.

To configure it, you need three things:

  • a Google analytics key - usually in the form UA-

  • [optional] a CID - a customer id this is associate with this. We recommend you set on for the server and override it if you know what you are tracking against for the individual request.

  • a client implementation. We provide one for Jersey currently.

fhConfig.addAnalyticCollector(new GoogleAnalyticsCollector(analyticsKey, analyticsCid, new GoogleAnalyticsJerseyApiClient()));

When you wish to lodge an event, simply call logAnalyticsEvent on the featurehub repository instance. You can simply pass the event, or you can pass the event plus some extra data, including the overridden CID and a gaValue for the value field in Google Analytics.

Rollout Strategies

Starting from version 1.1.0 FeatureHub supports server side evaluation of complex rollout strategies that are applied to individual feature values in a specific environment. This includes support of preset rules, e.g. per user key, country, device type, platform type as well as percentage splits rules and custom rules that you can create according to your application needs.

For more details on rollout strategies, targeting rules and feature experiments see the core documentation.

We are actively working on supporting client side evaluation of strategies in the future releases as this scales better when you have 10000+ consumers.

Coding for Rollout strategies

There are several preset strategies rules we track specifically: user key, country, device and platform. However, if those do not satisfy your requirements you also have an ability to attach a custom rule. Custom rules can be created as following types: string, number, boolean, date, date-time, semantic-version, ip-address

FeatureHub SDK will match your users according to those rules, so you need to provide attributes to match on in the SDK:

Sending preset attributes:

Provide the following attribute to support userKey rule:

fhClient.userKey("ideally-unique-id");

to support country rule:

fhClient.country(StrategyAttributeCountryName.NewZealand);

to support device rule:

fhClient.device(StrategyAttributeDeviceName.Browser);

to support platform rule:

fhClient.platform(StrategyAttributePlatformName.Android);

to support semantic-version rule:

fhClient.version("1.2.0");

or if you are using multiple rules, you can combine attributes as follows:

fhClient.userKey("ideally-unique-id")
      .country(StrategyAttributeCountryName.NewZealand)
      .device(StrategyAttributeDeviceName.Browser)
      .platform(StrategyAttributePlatformName.Android)
      .version("1.2.0");

If you are using Server Evaluated API Keys then you should always run .build() which will execute a background poll. If you wish to ensure the next line of code has the upated statuses, wait for the future to complete with .get()

Server Evaluated API Key - ensuring the repository is updated
  ClientContext fhClient = fhConfig.newContext().userKey("user@mailinator.com").build.get();

You do not have to do the build().get() (but you can) for client evaluated keys as the context is mutable and changes are immediate. As the context is evaluated locally, it will always be ready the very next line of code.

Sending custom attributes:

To add a custom key/value pair, use attr(key, value)

    fhClient.attr("first-language", "russian");

Or with array of values (only applicable to custom rules):

fhClient.attrs(“languages”, Arrays.asList(“russian”, “english”, “german”));

You can also use fhClient.clear() to empty your context.

Remember, for Server Evaluated Keys you must always call .build() to trigger a request to update the feature values based on the context changes.

Coding for percentage splits: For percentage rollout you are only required to provide the userKey or sessionKey.

fhClient.userKey("ideally-unique-id");

or

fhClient.sessionKey("session-id");

For more details on percentage splits and feature experiments see Percentage Split Rule.

Feature Interceptors

Feature Interceptors are the ability to intercept the request for a feature. They only operate in imperative state. For an overview check out the Documentation on them.

We currently support two feature interceptors:

  • io.featurehub.client.interceptor.SystemPropertyValueInterceptor - this will read properties from system properties and if they match the name of a key (case significant) then they will return that value. You need to have specified a system property featurehub.features.allow-override=true

We have removed support for OpenTracing.

Maintenance

Please note the io.featurehub.strategies package is mirrored from the main repository and is not maintained here. PRs for it should go to the main FeatureHub repository.