Go
The Go module github.com/e-volv/logs-go is the e-volv Launch SDK for Go 1.22 and later — standard library only, no third-party dependencies. 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, 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. The logging half of the SDK — batching, traces on context.Context, net/http middleware — is documented on the Go Observer page.
Install
go get github.com/e-volv/logs-go
OpenFeature provider
If your stack already speaks OpenFeature, the provider ships as its own Go module so the OpenFeature dependency stays out of an Observer-only go.sum:
go get github.com/e-volv/logs-go/openfeature github.com/open-feature/go-sdk
import (
"context"
"log"
"github.com/open-feature/go-sdk/openfeature"
evolveopenfeature "github.com/e-volv/logs-go/openfeature"
logs "github.com/e-volv/logs-go"
)
client := logs.New(logs.Options{
Key: "evk_…",
URL: "https://api.e-volv.io/api/public/v1/logs",
})
// SetProviderAndWait returns once the first ruleset is held. SetProvider
// returns at once, and an evaluation before then is PROVIDER_NOT_READY.
if err := openfeature.SetProviderAndWait(evolveopenfeature.NewProvider(client.Flags())); err != nil {
log.Printf("e-volv flags provider not ready: %v", err) // defaults keep serving
}
flags := openfeature.NewClient("checkout")
value, _ := flags.BooleanValue(
context.Background(), "checkout.new", false,
openfeature.NewEvaluationContext("u_1", map[string]any{"plan": "pro"}))Reason mapping: RULE:* and TARGET_MATCH become TARGETING_MATCH, ROLLOUT becomes SPLIT, OFF/KILLED become DISABLED, error reasons carry the matching OpenFeature error code, and everything else (DEFAULT, PREREQUISITE_FAILED) becomes DEFAULT. The native e-volv reason is always available on FlagMetadata[“evolveReason”].
Native API
package main
import (
"context"
logs "github.com/e-volv/logs-go"
"github.com/e-volv/logs-go/flags"
)
func main() {
client := logs.New(logs.Options{
Key: "evk_…",
URL: "https://api.e-volv.io/api/public/v1/logs",
Flags: flags.Options{Mode: flags.ModeStream}, // stream | poll | offline
})
defer client.Close()
ctx := context.Background()
_ = client.Flags().Ready(ctx) // optional: nil once the first ruleset arrived
if client.Flags().Bool("checkout.new", false, map[string]any{
"targetingKey": "u_1",
"plan": "pro",
}) {
renderNewCheckout()
}
// The full detail when you need the variant and the reason:
d := client.Flags().Detail("checkout.banner", "default", map[string]any{
"targetingKey": "u_1",
})
// d.Value, d.Variant (*string — nil when the fallback was served),
// d.Reason ("RULE:0" | "ROLLOUT" | "TARGET_MATCH" | "DEFAULT" | …)
}logs.Init installs the package-level default client; logs.New builds a non-default one. In both cases client.Flags() returns the flags handle — never nil, so user code can call it unconditionally. Evaluation methods: Bool, String, Number, JSON and Detail — all synchronous, all safe. 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. DetailTyped keeps value, variant and reason while applying the SDK’s type rule (the conformance runner’s entry point). Every evaluation takes a flags.Context — a map[string]any whose targetingKey is the identity rollouts bucket on.
Options
Flags are configured under the Flags field of the existing logs.Options, as a flags.Options value (the Go spellings of the contract’s §4 options):
| Option | Default | Meaning |
|---|---|---|
Disabled | false | Serve defaults without starting delivery. |
Mode | flags.ModeStream | ModeStream / ModePoll / ModeOffline. Offline never contacts the control plane (bootstrap snapshot or defaults only). |
PollInterval | 30s (min 15s) | Poll period once streaming has fallen back. |
URL | derived | Base URL ending in /api/public/v1/flags; defaults to the origin of the Observer URL option, else the e-volv cloud. |
CacheDir | os.TempDir()/evolve-flags, "-" disables | Where the last-known ruleset is kept (one file per key, hashed name, atomic write). |
Bootstrap | none | A raw ruleset JSON snapshot used before the first fetch completes. |
DisableExposures | false | Turn exposure recording off entirely. |
ExposureSampleRate | 1.0 | Keep-rate in [0, 1]; sent as sampleRate. |
DedupeWindow | 60s | Suppress repeats of the same (flagKey, variant, contextKind, subject) within the window; suppressed counts ride the next exposure. |
SendAttributes | false | Attach context attributes to exposures (private attributes are always removed). |
PrivateAttributes | [] | Attribute names never sent anywhere. |
Zero values mean “the default”: an unset PollInterval, ExposureSampleRate or DedupeWindow falls back to 30s, 1.0 and 60s, and an empty Mode means ModeStream.
Install check and lifecycle
// Install check: is this key bound, and to which environment?
ping, err := client.Flags().Verify(ctx)
// &flags.PingResult{Environment: "production", KeyKind: "server",
// Flags: 42, Etag: "3f2a…"}
// err != nil on a transport failure; (nil, nil) when the control plane
// answered with anything other than 200.Flags().Ready(ctx) waits for the first ruleset (only if you choose to): it blocks until the network delivers one or ctx ends, while an offline, bootstrap-fed or cache-fed client answers immediately — nil when a ruleset is held, flags.ErrNotReady otherwise. Flags().OnChange(fn) returns an unsubscribe function and receives the sorted keys whose flags changed, Flags().LastUpdatedAt() reports the last control-plane confirmation (the zero time before anything was held), and Flags().Mode() reports the live delivery mode.
// On shutdown: stop the flusher and the delivery goroutine, then flush // pending events and exposures. Close is idempotent; delivery is // best-effort, so call client.Flush(ctx) first if you need to observe the // outcome. defer client.Close()
client.Flush(ctx) sends pending events, then pending exposures; Flags().FlushExposures(ctx)drains just the exposure queue, 1,000 at a time, through the Observer’s HTTP stack and retry policy.
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 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;
Readyis the only call that waits, and only when you call it. - 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. After two consecutive stream failures the SDK polls every
PollIntervaland retries the stream every five minutes. - 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 recorded per evaluation of a flag present in the ruleset (never for
FLAG_NOT_FOUND,TYPE_MISMATCHorERROR), de-duplicated, sampled, capped (10,000 pending, 50,000 de-dupe entries, oldest dropped first) and retried with the same backoff as your logs.
Next: the e-volv Launch overview for concepts and terminology, the HTTP API page for the wire format the SDK speaks, and the SDKs index for every platform.