e-volv
Docs menu

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+:

kotlin
implementation("io.e-volv:logs-jvm:0.1.0")

Maven:

xml
<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:

kotlin
implementation("io.e-volv:logs-jvm-openfeature:0.1.0")
java
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

java
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:

OptionDefaultMeaning
enabledtrueStart the flags client. False gives a defaults-only stub.
urlderivedBase URL ending in /api/public/v1/flags; defaults to the origin of the Observer url option, else the e-volv cloud.
modeSTREAMSTREAM | POLL | OFFLINE. OFFLINE never contacts the control plane (bootstrap snapshot or defaults only).
pollIntervalSeconds30 (min 15)Poll period once streaming has fallen back.
cacheDirJVM temp dirWhere the last-known ruleset is kept (one file per key, hashed name, atomic write). cacheDisabled() turns it off.
bootstrapnoneA bundled ruleset JSON snapshot (byte[]) used before the first fetch completes.
exposuresEnabledtrueRecord an exposure on each evaluation of a flag present in the ruleset.
exposureSampleRate1.0Probability an evaluation is recorded; sent as sampleRate.
dedupeWindowSeconds60Suppress repeats of the same (flag, variant, subject) within the window; suppressed counts ride the next exposure.
sendAttributesfalseSend 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

java
// 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.

java
// 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):

ResponseSDK behaviour
401Key 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:readFlags are disabled for this key with one warning; telemetry is unaffected. No retry until re-init.
404Launch is not enabled for the workspace, or the key is not bound to an environment. Same handling as 401.
429Honours Retry-After (seconds). The delivery rate limit is 3,000 requests/min per key.
5xx, timeout, network errorRetries 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.