Java and Kotlin
io.e-volv:logs-jvm is the e-volv Launch SDK for the JVM — Java 17 and later, Kotlin-friendly throughout. It is a server SDK: with a server key (evk_…) it receives the environment’s full ruleset and evaluates flags locally, so a flag check never performs I/O, never blocks and never throws. Delivery is a stream of change notifications (SSE) with a polling fallback, a last-known cache on disk, and exposure tracking that rides the same transport as your logs. The behaviour is pinned by the Launch SDK contract and both of its conformance suites run in CI.
Install
Gradle (Kotlin DSL), JVM 17+:
implementation("io.e-volv:logs-jvm:0.1.0")Maven:
<dependency> <groupId>io.e-volv</groupId> <artifactId>logs-jvm</artifactId> <version>0.1.0</version> </dependency>
OpenFeature provider
If your stack already speaks OpenFeature, add the provider — it brings dev.openfeature:sdk and the core SDK with it:
implementation("io.e-volv:logs-jvm-openfeature:0.1.0")import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import io.evolve.logs.openfeature.EvolveProvider;
OpenFeatureAPI.getInstance().setProviderAndWait(
new EvolveProvider(client.flags()));
boolean checkout = OpenFeatureAPI.getInstance().getClient().getBooleanValue(
"checkout.new", false,
new MutableContext("u_1", Map.of("plan", Value.of("pro"))));Reason mapping: RULE:* and TARGET_MATCH become TARGETING_MATCH, ROLLOUT becomes SPLIT, OFF/KILLED become DISABLED, and error reasons carry the matching OpenFeature error code. The native e-volv reason is always available on flagMetadata.evolveReason. Provider shutdown() is a no-op — the LogsClient owns the lifecycle.
Native API
import io.evolve.logs.EvolveLogs;
import io.evolve.logs.LogsClient;
import io.evolve.logs.Options;
import io.evolve.logs.flags.Evaluation;
import io.evolve.logs.flags.Flags;
import io.evolve.logs.flags.FlagsMode;
import io.evolve.logs.flags.FlagsOptions;
LogsClient client = EvolveLogs.init(
Options.builder()
.key("evk_…") // server key
.url("https://api.e-volv.io/api/public/v1/logs")
.flags(FlagsOptions.builder()
.mode(FlagsMode.STREAM) // or POLL / OFFLINE
.build())
.build());
Flags flags = client.flags();
flags.ready(2000); // optional: block until the first ruleset arrives
Map<String, Object> context =
Map.<String, Object>of("targetingKey", user.id(), "plan", user.plan());
if (flags.bool("checkout.new", false, context)) {
renderNewCheckout();
}
// The full answer when you need the variant and the reason:
Evaluation d = flags.detail("checkout.banner", "default", context);
// d.getValue(), d.getVariant() ('on' | 'off' | 'control' | …),
// d.getReason() ('RULE:0' | 'ROLLOUT' | 'TARGET_MATCH' | 'DEFAULT' | 'FLAG_NOT_FOUND' | …)Evaluation methods: bool, string, number, json and detail — all synchronous, all safe, the context argument optional. A flag whose served value is not of the requested type returns your default with reason TYPE_MISMATCH; a flag absent from the ruleset returns your default with FLAG_NOT_FOUND. The typed reads drive the wrong-type rule; the untyped detail(key, default, context) is the json path and never reports TYPE_MISMATCH. From Kotlin the same calls read naturally — Evaluation is a data class, so d.value, d.variant and d.reason are properties.
Options
Flags are configured on the flags(FlagsOptions) builder of the existing init() options — the JVM spells the contract’s dotted names as builder methods:
| Option | Default | Meaning |
|---|---|---|
enabled | true | Start the flags client. False gives a defaults-only stub. |
url | derived | Base URL ending in /api/public/v1/flags; defaults to the origin of the Observer url option, else the e-volv cloud. |
mode | STREAM | STREAM | POLL | OFFLINE. OFFLINE never contacts the control plane (bootstrap snapshot or defaults only). |
pollIntervalSeconds | 30 (min 15) | Poll period once streaming has fallen back. |
cacheDir | JVM temp dir | Where the last-known ruleset is kept (one file per key, hashed name, atomic write). cacheDisabled() turns it off. |
bootstrap | none | A bundled ruleset JSON snapshot (byte[]) used before the first fetch completes. |
exposuresEnabled | true | Record an exposure on each evaluation of a flag present in the ruleset. |
exposureSampleRate | 1.0 | Probability an evaluation is recorded; sent as sampleRate. |
dedupeWindowSeconds | 60 | Suppress repeats of the same (flag, variant, subject) within the window; suppressed counts ride the next exposure. |
sendAttributes | false | Send context attributes with exposures (private attributes are always removed). |
privateAttributes | [] | Attribute names never sent anywhere. |
Spring Boot: with logs-jvm-server on the classpath the flags options map from evolve.logs.flags-enabled, evolve.logs.flags-mode, evolve.logs.flags-poll-interval-seconds and evolve.logs.flags-cache-dir (flat keys under evolve.logs; a nested evolve.logs.flags.* key is silently ignored) and an injectable Flags bean is registered.
Install check and lifecycle
// Install check: is this key bound, and to which environment?
Map<String, Object> ping = flags.verify();
// { environment: "production", keyKind: "server", flags: 42, etag: "3f2a…" }
// null when the control plane did not answer.flags.ready(timeoutMs) blocks until the first ruleset arrives (only if you choose to — it is the only call that waits), flags.lastUpdatedAt reports the last confirmed update as a java.time.Instant, and flags.onChange(listener) notifies with the keys whose values changed; close the returned AutoCloseable to unsubscribe. flags.flushExposures() drains pending exposures on demand.
// On shutdown (a JVM shutdown hook already calls this): stop the // delivery thread, flush pending logs and exposures. Idempotent. client.close();
Troubleshooting by status code
Evaluation never breaks because of the control plane — it degrades. What each response means for the SDK (contract §2.6):
| Response | SDK behaviour |
|---|---|
401 | Key invalid or revoked. The SDK keeps serving held values or defaults, logs once on stderr, and retries bootstrap every 5 minutes. |
403 — lacks the scope flags:read | Flags are disabled for this key with one warning; telemetry is unaffected. No retry until re-init. |
404 | Launch is not enabled for the workspace, or the key is not bound to an environment. Same handling as 401. |
429 | Honours Retry-After (seconds). The delivery rate limit is 3,000 requests/min per key. |
5xx, timeout, network error | Retries with full-jitter exponential backoff while serving the last held values. |
Guarantees
- A flag check is local and synchronous — bounded by CPU, not the network;
ready()is the only call that waits, and only when you ask it to. - An unreachable or slow control plane never changes an answer mid-flag: the last held ruleset (or a valid cache, or your defaults) keeps serving while the SDK backs off and reconnects.
- Cold start reads the on-disk cache instantly and swaps to a fresh ruleset without a flicker for unchanged flags; corrupt or other-environment caches are ignored and overwritten.
- Exposures are de-duplicated, sampled, capped and retried with the same backoff as your logs — telemetry you can trust as much as the flag answers themselves.
The SDKs index lists every platform, the e-volv Launch overview covers the product model, and the HTTP API page documents the wire format the SDK speaks.