Rust
e-volv-logs (imported as evolve_logs) is the e-volv Launch SDK for Rust 1.75 and later. 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 panics. Delivery is a stream of change notifications (SSE) with a polling fallback, kept fresh by one dedicated std::thread — with or without a Tokio runtime, because the blocking client must never run on a runtime thread — plus a last-known cache on disk and exposure tracking that rides the same init, the same key and the same options as your logs. The behaviour is pinned by the Launch SDK contract and both of its conformance suites run in CI.
Install
cargo add e-volv-logs
The flags module sits behind the default-on cargo feature flags; the default features (tokio, reqwest,flags) give a Tokio-backed batcher and a reqwest transport. default-features = false keeps a plain std::thread path with no async runtime. Flags configure exactly like the e-volv Observer Rust SDK — one Options struct feeds both.
With flags but without reqwest compiled in, the client serves a bundled bootstrap and the on-disk cache only — no HTTP — and says so once on stderr.
Native API
use std::time::Duration;
let layer = evolve_logs::layer(evolve_logs::Options {
key: "evk_…".into(), // a server key, from the environment
url: "https://api.e-volv.io/api/public/v1/logs".into(),
service: "orders-api".into(),
environment: "production".into(),
release: "1.4.2".into(),
..Default::default()
});
let client = layer.client();
let flags = client.flags();
let context = serde_json::json!({ "targetingKey": "u_1", "plan": "pro" });
// Optional: block (up to the timeout) until the first ruleset
// arrives; the bool reports whether one is held.
let _ = flags.ready(Duration::from_secs(5));
if flags.bool("checkout.new", false, &context) {
render_new_checkout();
}
// The full answer when you need the variant and the reason:
let d = flags.detail("checkout.banner", serde_json::json!("default"), &context);
// d.value, d.variant (Some("on") / Some("off") / …), d.reason
// ("RULE:0" | "ROLLOUT" | "TARGET_MATCH" | "DEFAULT" | "FLAG_NOT_FOUND" | …)Evaluation methods: bool, string, number, json and detail, plus typed_detail for the runner protocol — all synchronous, all safe. A flag whose served value is not of the requested type returns your default with reason TYPE_MISMATCH and records no exposure; a flag absent from the ruleset returns your default with FLAG_NOT_FOUND. The context is the same JSON value your logs carry: targetingKey (or key), plus any attributes your rules match on.
Options
Flags are configured on the flags field of the existing Options:
| Option | Default | Meaning |
|---|---|---|
flags.disabled | false | Serve defaults without starting delivery; every evaluation returns the default with reason FLAG_NOT_FOUND (Flags::disabled() builds this stub by hand). |
flags.mode | FlagsMode::Stream | Stream | Poll | Offline. Offline never contacts the control plane (bootstrap snapshot or defaults only). |
flags.poll_interval | 30 s (min 15) | Poll period once streaming has fallen back. |
flags.url | None (derived) | Base URL ending in /api/public/v1/flags; defaults to the origin of the Observer url option, else the e-volv cloud. |
flags.cache | CacheSetting::Default | Where the last-known ruleset is kept: the temp dir's evolve-flags subdirectory, one sha1-named file per key, atomic write. Dir(path) overrides; Disabled turns caching off. |
flags.bootstrap | None | A bundled ruleset used before the first fetch completes. |
flags.exposures.enabled | true | Record an exposure on each evaluation of a flag present in the ruleset. |
flags.exposures.sample_rate | 1.0 | Probability an evaluation is recorded; sent as sampleRate (clamped to [0, 1]). |
flags.exposures.dedupe_window | 60 s | Suppress repeats of the same (flagKey, variant, contextKind, subject) within the window; suppressed counts ride the next exposure. |
flags.exposures.send_attributes | false | Send context attributes with exposures (private attributes are always removed). |
flags.private_attributes | [] | Attribute names never sent anywhere. |
Install check and lifecycle
// Install check: is this key bound, and to which environment?
let ping = flags.verify();
// Some(PingResult { environment: "production", key_kind: "server",
// flags: 42, etag: Some("3f2a…".into()) })
// None when the control plane did not answer.flags.ready(timeout) waits for the first ruleset (only if you choose to), flags.last_updated_at() reports the last confirmed update, flags.mode() reports the delivery mode, and flags.on_change(Box<dyn Fn(&[String]) + Send + Sync>) notifies with the sorted keys whose values changed — drop the returned Subscription to unsubscribe. client.flush() also drains pending exposures, even when telemetry is disabled.
Called from inside a Tokio runtime, the blocking lifecycle calls (verify, flush_exposures, close) move their work onto a short-lived thread, since the blocking HTTP client panics on a runtime thread. Evaluation itself never touches the runtime.
// On shutdown: stop the delivery thread and flush pending exposures. flags.close(); // idempotent
Dropping the last Client or Guard handle closes flags the same way, so a typical service just lets the guard drop at shutdown.
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, and retries bootstrap every 5 minutes. |
403 — lacks the scope flags:read | Flags are off 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. On the stream it switches to polling and retries the stream afterwards (at least 5 minutes). |
5xx, timeout, network error | Reconnects with full-jitter backoff — random(0, min(60, 2^attempt)) seconds — while serving the last held values; two consecutive stream failures switch to polling, and the stream is retried every 5 minutes. |
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 delivery thread 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, oversized or other-environment caches are ignored and overwritten.
- Exposures are de-duplicated, sampled and capped (a 10,000-entry buffer drops oldest first, 1,000 per request) and retried with the same backoff as your logs — telemetry you can trust as much as the flag answers themselves.
The e-volv Launch overview covers concepts and targeting, the HTTP API page documents the wire format the SDK speaks, and the SDKs index lists every platform.