Java & Scala
It's simple to build your client using the APIs. If you're application is built on jvm you can also use the built-in Izanami client.
This client offer nice strategies for better performances.
You need to add repository
- Gradle
- Sbt
repositories {
jcenter()
maven {
url 'https://raw.githubusercontent.com/mathieuancelin/json-lib-javaslang/master/repository/releases/'
}
}
resolvers ++= Seq(
"jsonlib-repo" at "https://raw.githubusercontent.com/mathieuancelin/json-lib-javaslang/master/repository/releases",
Resolver.jcenterRepo
)
Add the following dependency to your project
- sbt
- Maven
- Gradle
libraryDependencies += "fr.maif" %% "izanami-client" % "1.11.0"
<dependency>
<groupId>fr.maif</groupId>
<artifactId>izanami-client_2.13</artifactId>
<version>1.11.0</version>
</dependency>
dependencies {
compile group: 'fr.maif', name: 'izanami-client_2.13', version: '1.11.0'
}
The client can be used in java or scala. There is two distinct dsl. Be sure to import the correct one :
- Java
- Scala
import izanami.*;
import izanami.javadsl.*;
import izanami._
import izanami.scaladsl._
Izanami client is built with
- Scala: As programming language
- Akka: to handle global state, scheduler ...
- Akka http: for http request, sse ...
The scaladsl rely on :
- play json: for json handling
The javadsl rely on :
- vavr: For functional structures like future, either, option ...
- play json java: For json handling
Setup the Izanami client
The first thing to do is to create a client. The client own the shared http client between config client, feature client and the experiment client.
You need to create a single client for all your application.
- Java
- Scala
izanamiClient = IzanamiClient.client(
system,
ClientConfig.create("http://localhost:8089")
.withClientId("xxxx")
.withClientIdHeaderName("Another-Client-Id-Header")
.withClientSecret("xxxx")
.withClientSecretHeaderName("Another-Client-Secret-Header")
.sseBackend()
.withDispatcher("izanami-example.blocking-io-dispatcher")
.withPageSize(50)
.withZoneId(ZoneId.of("Europe/Paris"))
);
implicit val system = ActorSystem(
"izanami-client",
ConfigFactory.parseString("""
izanami-example.blocking-io-dispatcher {
type = Dispatcher
executor = "thread-pool-executor"
thread-pool-executor {
fixed-pool-size = 32
}
throughput = 1
}
""")
)
val client = IzanamiClient(
ClientConfig(
host = "http://localhost:9000",
clientId = Some("xxxx"),
clientIdHeaderName = "Another-Client-Id-Header",
clientSecret = Some("xxxx"),
clientSecretHeaderName = "Another-Client-Id-Header",
backend = SseBackend,
pageSize = 50,
zoneId = ZoneId.of("Europe/Paris"),
dispatcher = "izanami-example.blocking-io-dispatcher"
)
)
Field | Description |
---|---|
clientId | The client id to access izanami APIs see Manage APIs keys |
clientSecretIdName | A custom header for the client id |
clientSecret | The client secret to access izanami APIs see Manage APIs keys |
clientSecretHeaderName | A custom header for the client secret |
sseBackend | Enable sse to get events from the server |
dispatcher | Reference a dispatcher to manage thread pool |
pageSize | Change the size of the pages when fetching from the server |
zoneId | Zone Id to handle date |
Configs client
The config client is used to access the shared config in Izanami. To understand how configs work, just visit this page
Setup the client
- Java
- Scala
ConfigClient configClient = izanamiClient.configClient(
Strategies.fetchStrategy(),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);
val configClient = client.configClient(
strategy = FetchStrategy(),
fallback = Configs(
"test2" -> Json.obj("value" -> 2)
)
)
When you set up a client you have to choose a strategy :
Strategy | Description |
---|---|
Fetch | Call izanami for each request |
Fetch with cache | Keep response in cache |
Smart cache with polling | Keep data in memory and poll izanami to refresh the cache asynchronously. |
Smart cache with sse | Keep data in memory and refresh the cache with the events from the izanami server. |
The fetch strategy
The fetch strategy will call izanami for each request.
- Java
- Scala
ConfigClient configFetchStrategy = izanamiClient.configClient(
Strategies.fetchStrategy(),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);
val configClient = client.configClient(
strategy = FetchStrategy(),
fallback = Configs(
"test2" -> Json.obj("value" -> 2)
)
)
The fetch with cache strategy
The fetch with cache will do dumb cache by http call. You have to provide a the max elements in cache and a TTL.
- Java
- Scala
Integer maxElementInCache = 100;
FiniteDuration ttl = FiniteDuration.create(20, TimeUnit.MINUTES);
ConfigClient fetchWithCacheStrategy = izanamiClient.configClient(
Strategies.fetchWithCacheStrategy(maxElementInCache, ttl),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);
val strategy = IzanamiClient(
ClientConfig(ctx.host)
).configClient(
strategy = FetchWithCacheStrategy(maxElement = 2, duration = 1.second),
fallback = Configs(
"test2" -> Json.obj("value" -> 2)
)
)
The smart cache strategy
When you choose the smart cache, you have to provide patterns :
- Java
- Scala
ConfigClient configClient = izanamiClient.configClient(
Strategies.smartCacheWithPollingStrategy(
FiniteDuration.create(20, TimeUnit.SECONDS),
"my:configs:*", "other:pattern"
),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
)
);
val configClient = client.configClient(
strategy = CacheWithPollingStrategy(
patterns = Seq("*"),
pollingInterval = 3.second
),
fallback = fallback
)
The client will cache all the configs matching this patterns.
- With a poll strategy, the client will request the server to get change so you have to set a delay.
- With a SSE strategy, the client will listen events from the server to refresh the cache.
::: warning
There is no TTL using this strategy so you have to choose the right patterns to be sure that all datas fit in memory.
:::
Handling errors
An error handling strategy could be provided. You can choose between :
RecoverWithFallback
: If the call crash, the fallback is usedCrash
: The call will finish on error if an error occured while evaluating the feature, config or experiment.
- Java
- Scala
ConfigClient configErrorStrategy = izanamiClient.configClient(
Strategies.fetchStrategy(ErrorStrategies.crash())
);
val configClient = izanamiClient.configClient(
strategy = FetchStrategy(Crash),
fallback = Configs(
"test2" -> Json.obj("value" -> 2)
)
)
Client usage
Get configs for a pattern
- Java
- Scala
Future<Configs> futureConfigs = configClient.configs("my:*");
futureConfigs.onSuccess((Configs configs) -> {
JsValue config = configs.config("my:config");
System.out.println(config.field("value").asString());
});
val configs: Future[Configs] = configClient.configs("*")
configs.onComplete {
case Success(c) => println(c)
case Failure(e) => e.printStackTrace()
}
Get one config
- Java
- Scala
Future<JsValue> futureConfig = configClient.config("my:config");
futureConfig.onSuccess((JsValue config) -> {
System.out.println(config.field("value").asString());
});
val futureConfig: Future[JsValue] = izanamiClient.config("test")
futureConfig.onComplete {
case Success(c) => println(c)
case Failure(e) => e.printStackTrace()
}
Create / Update / Delete configs
Create config using json
- Java
- Scala
JsValue createdJson = configClient.createConfig("my:config", Json.obj(Syntax.$("value", "A configuration"))).get();
val configCreated = client.createConfig("test", Json.obj("value" -> 1))
Create config using a config object
- Java
- Scala
Config created = configClient.createConfig(Config.config("my:config", Json.obj(Syntax.$("value", "A configuration")))).get();
val jsoncreated = client.createConfig(Config("test", Json.obj("value" -> 1)))
Update config using json
- Java
- Scala
JsValue updatedJson = configClient.updateConfig("my:previous:config", "my:config", Json.obj(Syntax.$("value", "A configuration"))).get();
val configUpdated = client.updateConfig("test", "newtest", Json.obj("value" -> 1))
Update config using a config object
- Java
- Scala
Config updated = configClient.updateConfig("my:previous:config", Config.config("my:config", Json.obj(Syntax.$("value", "A configuration")))).get();
val configUpdated = client.updateConfig("test", Config("newtest", Json.obj("value" -> 1)))
Delete a config
- Java
- Scala
Done deleted = configClient.deleteConfig("my:config").get();
val configDeleted = client.deleteConfig("test")
Autocreate configs
You can autocreate configs that are define as fallback. To enable this you need set the autocreate
parameter when the client is created.
- Java
- Scala
Boolean autocreate = true;
ConfigClient configClient = izanamiClient.configClient(
Strategies.fetchStrategy(),
Configs.configs(
Config.config("my:config", Json.obj(
Syntax.$("value", "Fallback value")
))
),
autocreate
);
val izanamiClient = client.configClient(
strategy = Strategies.fetchStrategy(),
fallback = Configs(
"test" -> Json.obj("value" -> 2)
),
autocreate = true
)
Features client
To understand how features work, just visit this page
Setup the client
- Java
- Scala
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.fetchStrategy(),
Features.features(
Features.feature("my:feature", false)
)
);
val featureClient = client.featureClient(
strategy = FetchStrategy(),
fallback = Features(
DefaultFeature("test2", true)
)
)
When you set up a client you have to choose a strategy :
Strategy | Description |
---|---|
Fetch | Call izanami for each request |
Fetch with cache | Keep response in cache |
Smart cache with polling | Keep data in memory and poll izanami to refresh the cache asynchronously. The features that need a context are not cached because it can needs a huge amount of memory |
Smart cache with sse | Keep data in memory and refresh the cache with the events from the izanami server. The features that need a context are not cached because it can needs a huge amount of memory |
The fetch strategy
The fetch strategy will call izanami for each request
- Java
- Scala
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.fetchStrategy(),
Features.features(
Features.feature("my:feature", false)
)
);
val strategy = IzanamiClient(
ClientConfig(host, pageSize = 2)
).featureClient(
Strategies.fetchStrategy()
)
The fetch with cache strategy
The fetch with cache will do dumb cache by http call. You have to provide a the max elements in cache and a TTL.
- Java
- Scala
Integer maxElementInCache = 100;
FiniteDuration ttl = FiniteDuration.create(20, TimeUnit.MINUTES);
FeatureClient featureClientWithCache = izanamiClient.featureClient(
Strategies.fetchWithCacheStrategy(maxElementInCache, ttl),
Features.features(
Features.feature("my:feature", false)
)
);
val strategy = IzanamiClient(
ClientConfig(ctx.host)
).featureClient(
strategy = FetchWithCacheStrategy(maxElement = 2, duration = 1.second),
fallback = Features(
DefaultFeature("test2", true)
)
)
The smart cache strategy
When you choose the smart cache, you have to provide patterns to select the keys that will be in cache:
- Java
- Scala
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.smartCacheWithSseStrategy(
scala.Option.apply(FiniteDuration.create(1, TimeUnit.MINUTES)),
"my:features:*", "other:pattern"
),
Features.features(
Features.feature("my:feature", false)
)
);
val featureClient = client.featureClient(
strategy = CacheWithPollingStrategy(
patterns = Seq("*"),
pollingInterval = 3.second
),
fallback = Features(fallback: _*)
)
The client will cache all the configs matching this patterns.
- With a poll strategy, the client will request the server to get change so you have to set a delay.
- With a SSE strategy, the client will listen events from the server to refresh the cache.
::: note
The feature that need a context to be evaluated are not cached. The cache is used only for simple features or feature with release date.
:::
::: warning
There is no TTL using this strategy so you have to choose the right patterns to be sure that all datas fit in memory.
:::
Handling errors
An error handling strategy could be provided. You can choose between :
RecoverWithFallback
: If the call crash, the fallback is usedCrash
: The call will finish on error if an error occured while evaluating the feature, config or experiment.
- Java
- Scala
FeatureClient featureClientWithErrorHandling = izanamiClient.featureClient(
Strategies.fetchStrategy(ErrorStrategies.crash())
);
val featureClient = izanamiClient.featureClient(
FetchStrategy(Crash)
)
Client usage
List features
- Java
- Scala
Future<Features> futureFeatures = featureClient.features("my:feature:*");
futureFeatures.onSuccess(features -> {
boolean active = features.isActive("my:feature:test");
if (active) {
System.out.println("Feature my:feature:test is active");
} else {
System.out.println("Feature my:feature:test is active");
}
JsObject tree = features.tree();
System.out.println("Tree is " + Json.prettyPrint(tree));
});
val futureFeatures: Future[Features] = featureClient.features("*")
futureFeatures.onComplete {
case Success(features) =>
val active: Boolean = features.isActive("test")
if (active)
println(s"Feature test is active")
else
println(s"Feature test is not active")
val tree: JsObject = features.tree()
println(s"All features: ${Json.prettyPrint(tree)}")
case Failure(e) =>
e.printStackTrace()
}
Check feature
- Java
- Scala
Future<Boolean> futureCheck = featureClient.checkFeature("my:feature");
val futureCheck: Future[Boolean] = featureClient.checkFeature("test")
If the feature needs a context to be evaluated:
- Java
- Scala
Future<Boolean> futureCheckContext = featureClient.checkFeature("my:feature", Json.obj(
Syntax.$("context", true)
));
val context = Json.obj("context" -> true)
val checkWithContext: Future[Boolean] = featureClient.checkFeature("test", context)
Conditional code on feature
This execute a code and return a value if a feature is active:
- Java
- Scala
Future<String> conditonal = featureClient.featureOrElse("my:feature",
() -> "Feature is active",
() -> "Feature is not active"
);
val conditonal: Future[String] = featureClient.featureOrElse("test") {
"Feature is active"
} {
"Feature is not active"
}
Or with a context
- Java
- Scala
Future<String> conditonalContext = featureClient.featureOrElse(
"my:feature",
Json.obj(Syntax.$("context", true)),
() -> "Feature is active",
() -> "Feature is not active"
);
val conditonalWithContext: Future[String] = featureClient.featureOrElse("test", context) {
"Feature is active"
} {
"Feature is not active"
}
Create / update / delete
With the client you can mutate features.
Create with raw data:
- Java
- Scala
Feature createdJson = featureClient.createJsonFeature(
"my:feature",
true,
Features.hourRangeType(),
Option.of(Json.obj(
Syntax.$("startAt", "05:25"),
Syntax.$("endAt", "16:30")
))
).get();
Feature createdJson = featureClient.createJsonFeature(
"my:feature",
true,
Features.hourRangeType(),
Option.of(Json.obj(
Syntax.$("startAt", "05:25"),
Syntax.$("endAt", "16:30")
))
).get();
Or with a feature object:
- Java
- Scala
Feature created = featureClient.createFeature(
Features.hourRange("my:feature", true, LocalTime.of(5, 25), LocalTime.of(16, 30))
).get();
val featureCreated = featureClient.createFeature(
DateRangeFeature("test2", true, LocalDateTime.of(2019, 4, 12, 0, 0, 0), LocalDateTime.of(2019, 5, 13, 0, 0, 0))
)
Update a feature :
- Java
- Scala
Feature updated = featureClient.updateFeature("my:previous:feature", Features.hourRange("my:feature:test", true, LocalTime.of(5, 25), LocalTime.of(16, 30))).get();
val featureCreated = featureClient.updateFeature(
"test",
DateRangeFeature("test2", true, LocalDateTime.of(2019, 4, 12, 0, 0, 0), LocalDateTime.of(2019, 5, 13, 0, 0, 0))
)
Delete a feature :
- Java
- Scala
Done deleted = featureClient.deleteFeature("my:feature").get();
val deleted = featureClient.deleteFeature("test")
You can also activate or deactivate a feature
- Java
- Scala
Feature activated = featureClient.switchFeature("my:feature", true).get();
val activated = featureClient.switchFeature("test", false)
Autocreate features
You can autocreate features that are define as fallback. To enable this you need set the autocreate
parameter when the client is created.
- Java
- Scala
Boolean autocreate = true;
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.fetchStrategy(),
Features.features(
Features.feature("my:feature", false)
),
autocreate
);
val featureClient = client
.featureClient(
FetchStrategy(Crash),
autocreate = true,
fallback = Features(feature)
)
Experiments client
To understand how experiments work, just visit this page
Setup the client
- Java
- Scala
ExperimentsClient experimentsClient = izanamiClient.experimentClient(Strategies.fetchStrategy());
val experimentClient = IzanamiClient(ClientConfig(host))
.experimentClient(Strategies.fetchStrategy())
For experiments, there is only two strategies available : fetch or dev.
Variants
Get a variant for a client
- Java
- Scala
Future<Option<Variant>> futureVariant = experimentsClient.getVariantFor("my:experiment", "clientId");
futureVariant.onSuccess(mayBeVariant ->
Match(mayBeVariant).of(
Case($Some($()), exist -> {
String phrase = "Variant is " + exist;
System.out.println(phrase);
return phrase;
}),
Case($None(), __ -> {
String phrase = "Variant not found";
System.out.println(phrase);
return phrase;
})
)
);
val mayBeFutureVariant: Future[Option[Variant]] = experimentClient.getVariantFor("test", "client1")
mayBeFutureVariant.onComplete {
case Success(mayBeVariant) => println(mayBeVariant)
case Failure(e) => e.printStackTrace()
}
Mark variant displayed
- Java
- Scala
Future<ExperimentVariantDisplayed> futureDisplayed = experimentsClient.markVariantDisplayed("my:experiment", "clientId");
futureDisplayed.onSuccess(event ->
System.out.println(event)
);
val futureDisplayed: Future[ExperimentVariantDisplayed] =
experimentClient.markVariantDisplayed("test", "client1")
futureDisplayed.onComplete {
case Success(event) => println(event)
case Failure(e) => e.printStackTrace()
}
Mark variant won
- Java
- Scala
Future<ExperimentVariantWon> futureWon = experimentsClient.markVariantWon("my:experiment", "clientId");
futureWon.onSuccess(event ->
System.out.println(event)
);
val futureWon: Future[ExperimentVariantWon] = experimentClient.markVariantWon("test", "client1")
futureWon.onComplete {
case Success(event) => println(event)
case Failure(e) => e.printStackTrace()
}
Work with experiment
Get the experiment
- Java
- Scala
Future<Option<ExperimentClient>> futureExperiment = experimentsClient.experiment("my:experiment");
futureExperiment.onSuccess(mayBeExperiment ->
Match(mayBeExperiment).of(
Case($Some($()), exist -> {
String phrase = "Experiment is " + exist;
System.out.println(phrase);
return phrase;
}),
Case($None(), __ -> {
String phrase = "Experiment not found";
System.out.println(phrase);
return phrase;
})
)
);
val futureExperiment: Future[Option[ExperimentClient]] = experimentClient.experiment("test")
futureExperiment.onComplete {
case Success(Some(exp)) => println(s"Experiment is $exp")
case Success(None) => println("Experiment not Found")
case Failure(e) => e.printStackTrace()
}
Once you get the experiment, you can get a variant for a client, mark variant displayed or mark variant won :
- Java
- Scala
ExperimentClient experiment = mayExperiment.get();
Future<Option<Variant>> clientId = experiment.getVariantFor("clientId");
Future<ExperimentVariantDisplayed> displayed = experiment.markVariantDisplayed("clientId");
Future<ExperimentVariantWon> won = experiment.markVariantWon("clientId");
val experiment: ExperimentClient = mayBeExperiment.get
val futureVariant: Future[Option[Variant]] = experiment.getVariantFor("client1")
val displayed: Future[ExperimentVariantDisplayed] = experiment.markVariantDisplayed("client1")
val won: Future[ExperimentVariantWon] = experiment.markVariantWon("client1")
Experiment tree
You can get the experiments tree with associated variant for a client :
- Java
- Scala
Future<JsValue> futureTree = experimentsClient.tree("*", "clientId");
futureTree.onSuccess(tree -> {
assertThat(tree).isEqualTo(
Json.obj(
Syntax.$("my", Json.obj(
Syntax.$("experiment", Json.obj(
Syntax.$("variant", "A")
))
))
)
);
});
val experimentsTree = client.tree("*", "client1").futureValue
experimentsTree must be(
Json.obj(
"izanami" -> Json.obj(
"ab" -> Json.obj(
"test" -> Json.obj(
"variant" -> "A"
)
)
)
)
)
val experimentsTree = client.tree("*", "client1").futureValue
experimentsTree must be(
Json.obj()
)
Exposing izanami with a Proxy
When you have to use Izanami from the client side, you can't call Izanami directly from the browser because it means the API keys are exposed to anyone.
The best solution is to use your backend as a proxy. You can do this with the jvm client.
- Java
- Scala
ConfigClient configClient = izanamiClient.configClient(
Strategies.dev(),
Configs.configs(
Config.config("configs:test", Json.obj(
Syntax.$("value", 2)
))
)
);
FeatureClient featureClient = izanamiClient.featureClient(
Strategies.dev(),
Features.features(
Features.feature("features:test1", true)
)
);
ExperimentsClient experimentsClient = izanamiClient.experimentClient(
Strategies.dev(),
Experiments.create(
ExperimentFallback.create(
"experiments:id",
"Experiment",
"An Experiment",
true,
Variant.create("A", "Variant A", scala.Option.apply("Variant A"))
)));
Proxy proxy = izanamiClient.proxy()
.withConfigClient(configClient)
.withConfigPattern("configs:*")
.withFeatureClient(featureClient)
.withFeaturePattern("features:*")
.withExperimentsClient(experimentsClient)
.withExperimentPattern("experiments:*");
Future<Tuple2<Integer, JsValue>> fJsonResponse = proxy.statusAndJsonResponse();
fJsonResponse.onSuccess(t ->
System.out.println("Code = " + t._1 + ", json body = " + t._2)
);
//Or with string response and additional infos :
Future<Tuple2<Integer, String>> fStringResponse = proxy.statusAndStringResponse(Json.obj().with("id", "ragnard.lodbrock@gmail.com"), "ragnard.lodbrock@gmail.com");
fStringResponse.onSuccess(t ->
System.out.println("Code = " + t._1 + ", string body = " + t._2)
);
// Experiment proxy
Future<Tuple2<Integer, JsValue>> markVariantDisplayed = proxy.markVariantDisplayed("experiments:id", "ragnars.lodbrock@gmail.com");
Future<Tuple2<Integer, JsValue>> markVariantWon = proxy.markVariantWon("experiments:id", "ragnars.lodbrock@gmail.com");
val client = IzanamiClient(
ClientConfig("")
)
val featureClient: FeatureClient = client.featureClient(
strategy = Strategies.dev(),
fallback = Features(
DefaultFeature("features:test1", true)
)
)
val configClient: ConfigClient = client.configClient(
Strategies.dev(),
fallback = Configs(
"configs:test" -> Json.obj("value" -> 2)
)
)
val experimentsClient: ExperimentsClient = client.experimentClient(
strategy = Strategies.dev(),
fallback = Experiments(
ExperimentFallback(
"experiments:id",
"Experiment",
"An experiment",
true,
Variant("A", "Variant A", Some("Variant A"))
)
)
)
val proxy: Proxy = client
.proxy()
.withConfigClient(configClient)
.withConfigPattern("configs:*")
.withFeatureClient(featureClient)
.withFeaturePattern("features:*")
.withExperimentsClient(experimentsClient)
.withExperimentPattern("experiments:*")
val fResponseJson: Future[(Int, JsValue)] = proxy.statusAndJsonResponse()
fResponseJson.onComplete {
case Success((status, responseBody)) =>
println(s"Izanami respond with status $status and json body $responseBody")
case _ => println("Oups something wrong happened")
}
//Or for a string response and additional infos
val fResponseString: Future[(Int, String)] = proxy.statusAndStringResponse(
context = Some(Json.obj("user" -> "ragnard.lodbrock@gmail.com")),
userId = Some("ragnard.lodbrock@gmail.com")
)
fResponseString.onComplete {
case Success((status, responseBody)) =>
println(s"Izanami respond with status $status and string body $responseBody")
case _ => println("Oups something wrong happened")
}
// Experiment proxy
val fDisplayed: Future[(Int, JsValue)] =
proxy.markVariantDisplayed("experiments:id", "ragnard.lodbrock@gmail.com")
val fWon: Future[(Int, JsValue)] = proxy.markVariantWon("experiments:id", "ragnard.lodbrock@gmail.com")