quonfig

package module
v0.0.25 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MIT Imports: 31 Imported by: 0

README

quonfig

Go SDK for Quonfig — Feature Flags, Live Config, and Dynamic Log Levels.

Note: This SDK is pre-1.0 and the API is not yet stable.

Installation

go get github.com/quonfig/sdk-go

Quick Start

import "github.com/quonfig/sdk-go"

client, err := quonfig.NewClient(
    quonfig.WithAPIKey("your-sdk-key"),
)
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// Feature flag
if on, _, _ := client.GetBoolValue("new-dashboard", nil); on {
    // show new dashboard
}

// Config value
limit, _, _ := client.GetIntValue("rate-limit", nil)

Connection health

The SDK exposes two diagnostic getters so callers can surface freshness in their own dashboards and alerts:

state := client.ConnectionState()
// One of: Initializing | Connected | Disconnected | FallingBack

last := client.LastSuccessfulRefresh()
// time.Time — zero value before the first install.

ConnectionState reports the SSE transport state. LastSuccessfulRefresh is the wall-clock time of the most recent installed config envelope, from either path (SSE push or Layer 2 fallback poll).

Do NOT wire these into a Kubernetes liveness probe

Do not wire LastSuccessfulRefresh() or ConnectionState() directly into a Kubernetes liveness probe. These signals are diagnostic, not pass/fail. A liveness probe based on SDK freshness will amplify transient network blips into restart cascades.

The SDK intentionally does not expose a Healthy() primitive. A binary health signal wired into a liveness probe will reboot pods every time the SSE stream hiccups — turning a 60-second network blip into a fleet-wide restart storm. If you need a threshold-based view of staleness, compose it yourself from LastSuccessfulRefresh() (e.g. time.Since(last) > 10 * time.Minute) and surface it as a metric or readiness signal, not a liveness one.

Fallback polling

By default the SDK opens an SSE stream and trusts it. To guarantee a poll-based refresh path during sustained disconnects (≥120s), enable Layer 2:

client, err := quonfig.NewClient(
    quonfig.WithAPIKey("your-sdk-key"),
    quonfig.WithFallbackPoll(true, 60*time.Second),
)

The poller is idle while SSE is connected. It only engages after the stream has been disconnected past DefaultFallbackPollThreshold (120s) and disengages once SSE recovers. While engaged, ConnectionState() reports FallingBack.

Datadir mode: auto-reload on file changes

When you initialize the SDK with WithDataDir("./path"), configs are loaded once from disk at NewClient time. Opt in to WithDataDirAutoReload(true) to have the SDK watch the directory and re-read the envelope whenever files change — an editor save, a git pull, or a build step.

client, err := quonfig.NewClient(
    quonfig.WithDataDir("./workspace-data"),
    quonfig.WithEnvironment("development"),
    quonfig.WithDataDirAutoReload(true), // off by default — must be opted in
    quonfig.WithOnConfigUpdate(func() {
        log.Println("Quonfig configs reloaded from disk")
    }),
)
if err != nil {
    log.Fatal(err)
}

// Edit a file under ./workspace-data and OnConfigUpdate fires within ~200ms.

// On shutdown, Close stops the watcher and clears any pending debounce timer.
defer client.Close()
When to enable
  • Local development with the datadir checked out from git.
  • Self-hosted servers that git pull the datadir on a schedule.
  • CI jobs that mutate the datadir between assertions.
When NOT to enable
  • Read-only / immutable filesystems (some containers, AWS Lambda, scratch images). Watch registration may fail; the SDK degrades gracefully (logs the error and continues serving the envelope it loaded at NewClient time) but you are paying for nothing.
  • Build-time-embedded workflows where the datadir is baked into the artifact and never changes at runtime. Watching wastes a file descriptor and a goroutine.
  • Production paths where reload timing matters — e.g. you would rather pin the envelope you shipped with and roll forward through a redeploy than have it shift under traffic.

Default is false; datadir mode is silent until you opt in.

Behavior contract
  • Parse-then-swap. If the new envelope fails to parse (truncated write, mid-git pull state, invalid JSON), the SDK logs the error and keeps serving the previous envelope. OnConfigUpdate is not fired on parse failure — only on a successful swap.
  • Debounced. Bursts of filesystem events (atomic-rename editor saves, git pull touching dozens of files) coalesce into a single re-read. Default window: 200ms (DefaultDataDirAutoReloadDebounce) — long enough to absorb the 3–5 events typical editors emit in <50ms, short enough that interactive edits feel immediate. Tune via WithDataDirAutoReloadDebounce if you need a different window.
  • Graceful degrade. If watch registration fails (read-only fs, immutable container, missing directory, EMFILE), the SDK logs via the configured WithLogger and continues without watching — NewClient does not return an error from a failed registration.
  • Symlinks. The watcher resolves DataDir to its real path at start via filepath.EvalSymlinks. Editing the file the symlink points at is detected; atomic flips that retarget the link itself are not.
  • Shutdown. client.Close() stops the watcher goroutine, releases the underlying fsnotify handle, and clears any pending debounce timer. There is no separate handle to manage — the watcher lifecycle is tied to the client.
Tuning the debounce window
client, err := quonfig.NewClient(
    quonfig.WithDataDir("./workspace-data"),
    quonfig.WithDataDirAutoReload(true),
    quonfig.WithDataDirAutoReloadDebounce(1 * time.Second),
)

The default (200ms) is tuned for interactive editing. Raise it if you have a noisy producer (continuously regenerating files) and would rather see one reload per second than per save. Lower it only if you have measured that 200ms is meaningfully too slow for your use case.

See the open-source / local how-to for the cross-SDK story (sdk-node, sdk-go, sdk-ruby, sdk-python, sdk-java).

See also

Documentation

Overview

Package quonfig provides a client for fetching configuration and feature flags from the Quonfig API.

Index

Constants

View Source
const DefaultDataDirAutoReloadDebounce = 200 * time.Millisecond

DefaultDataDirAutoReloadDebounce is the default debounce window used to coalesce filesystem-event bursts (atomic-rename saves, git pull, etc.).

View Source
const DefaultDomain = "quonfig.com"

DefaultDomain is the production domain used when QUONFIG_DOMAIN is unset and no explicit URL options are provided.

View Source
const DefaultFallbackPollInterval = 60 * time.Second

DefaultFallbackPollInterval is the cadence used once Layer 2 has engaged. Matches the legacy WithRefreshInterval recommendation; callers can override.

View Source
const DefaultFallbackPollThreshold = 120 * time.Second

DefaultFallbackPollThreshold is the disconnect window before Layer 2 engages. The cross-SDK spec calls for 120s; tests inject smaller values.

View Source
const QuonfigSDKLoggingContextName = "quonfig-sdk-logging"

QuonfigSDKLoggingContextName is the top-level context name used by the ShouldLogPath convenience to inject the logger path for per-logger rule evaluation. It is load-bearing for api-telemetry's example-context auto-capture, so do not rename without updating the matching constants in the other SDKs.

Variables

View Source
var ErrInitializationTimeout = errors.New("initialization_timeout")

ErrInitializationTimeout is returned when the client could not finish its initial fetch before the configured timeout.

View Source
var ErrMissingEnvVar = errors.New("missing_env_var")

ErrMissingEnvVar is returned when an ENV_VAR-provided config references a missing environment variable.

View Source
var ErrNotFound = errors.New("config not found")

ErrNotFound is returned when a config key does not exist.

View Source
var ErrUnableToCoerce = errors.New("unable_to_coerce_env_var")

ErrUnableToCoerce is returned when an ENV_VAR-provided config cannot be coerced to the target type.

View Source
var ErrUnableToDecrypt = errors.New("unable_to_decrypt")

ErrUnableToDecrypt is returned when a confidential value cannot be decrypted.

Functions

func ContextWithContextSet

func ContextWithContextSet(ctx context.Context, cs *ContextSet) context.Context

ContextWithContextSet returns a derived context carrying the given *ContextSet. QuonfigHandler.Handle pulls this ContextSet out and passes it to Client.ShouldLogPath, on top of the Client's GlobalContext. nil is a valid value and clears any previously attached ContextSet.

func ParseISO8601Duration

func ParseISO8601Duration(s string) (time.Duration, error)

ParseISO8601Duration parses an ISO 8601 duration string and returns a time.Duration. Supports: P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S Examples: PT0.2S, PT90S, PT1.5M, PT0.5H, P1DT6H2M1.5S

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the main Quonfig SDK client.

func NewClient

func NewClient(opts ...Option) (*Client, error)

NewClient creates a new Quonfig client with the given options. If an API key is configured, the client begins an initial config download and wires local evaluation automatically. Background refresh is opt-in via WithFallbackPoll (the legacy WithRefreshInterval is preserved as a shim).

func (*Client) Close

func (c *Client) Close()

Close stops the supervised background workers, shuts down the SSE stream, and flushes pending telemetry.

func (*Client) ConnectionState added in v0.0.21

func (c *Client) ConnectionState() ConnectionState

ConnectionState reports the SDK's customer-visible transport state. Values match the cross-SDK spec in project/plans/sdk-hardening-and-verification.md: initializing, connected, disconnected, falling_back. Returns "initializing" when background workers are disabled (e.g. WithDataDir, no API key).

func (*Client) EvaluateDetails added in v0.0.20

func (c *Client) EvaluateDetails(key string, ctx *ContextSet) EvaluationDetails

EvaluateDetails resolves a config key and returns a full EvaluationDetails record including typed ErrorCode, ErrorMessage, Variant, and FlagMetadata. This is the recommended API for OpenFeature providers and any caller that needs structured evaluation metadata.

func (*Client) EvaluateKey

func (c *Client) EvaluateKey(key string, ctx *ContextSet) (*Value, EvalReason, bool, error)

EvaluateKey resolves a config key and returns the resolved value, evaluation reason, and ok flag. Retained for backward compatibility; new code should prefer EvaluateDetails which returns the full EvaluationDetails record (typed ErrorCode, Variant, FlagMetadata).

func (*Client) FallbackPollerActive added in v0.0.21

func (c *Client) FallbackPollerActive() bool

FallbackPollerActive reports whether the Layer 2 fallback poller is currently engaged (i.e. SSE has been down past the engagement threshold and the SDK is polling instead). Returns false when fallback polling is disabled or has not engaged.

func (*Client) FeatureIsOn

func (c *Client) FeatureIsOn(key string, ctx *ContextSet) (bool, bool)

FeatureIsOn returns whether a feature flag is on. Returns false if the key is not found.

func (*Client) GetBoolValue

func (c *Client) GetBoolValue(key string, ctx *ContextSet) (bool, bool, error)

GetBoolValue returns the bool value for a config key.

func (*Client) GetDurationValue

func (c *Client) GetDurationValue(key string, ctx *ContextSet) (time.Duration, bool, error)

GetDurationValue returns the time.Duration value for a config key. The stored value should be an ISO 8601 duration string (e.g., "PT90S", "PT1.5M", "P1DT6H2M1.5S").

func (*Client) GetFloatValue

func (c *Client) GetFloatValue(key string, ctx *ContextSet) (float64, bool, error)

GetFloatValue returns the float64 value for a config key.

func (*Client) GetIntValue

func (c *Client) GetIntValue(key string, ctx *ContextSet) (int64, bool, error)

GetIntValue returns the int64 value for a config key.

func (*Client) GetJSONValue

func (c *Client) GetJSONValue(key string, ctx *ContextSet) (interface{}, bool, error)

GetJSONValue returns the parsed JSON value for a config key. Values are stored natively (object/array/number/boolean/null); this is a direct pass-through of Value.Value.

func (*Client) GetStringSliceValue

func (c *Client) GetStringSliceValue(key string, ctx *ContextSet) ([]string, bool, error)

GetStringSliceValue returns the []string value for a config key.

func (*Client) GetStringValue

func (c *Client) GetStringValue(key string, ctx *ContextSet) (string, bool, error)

GetStringValue returns the string value for a config key.

func (*Client) Keys

func (c *Client) Keys() []string

Keys returns all config keys currently in the store.

func (*Client) LastSuccessfulRefresh added in v0.0.21

func (c *Client) LastSuccessfulRefresh() time.Time

LastSuccessfulRefresh returns the wall-clock time of the most recent successful config install (either path). Zero value before the first install or when background workers are disabled.

func (*Client) Refresh

func (c *Client) Refresh() error

Refresh performs a manual poll of GET /api/v2/configs using ETag caching.

func (*Client) ShouldLog

func (c *Client) ShouldLog(configKey string, desiredLevel string, ctx *ContextSet) bool

ShouldLog returns true if a message at desiredLevel should be logged for the given configKey. The caller must pass the full stored key (e.g. "log-level.my-app") — the SDK does not auto-prefix "log-level.". desiredLevel is case-insensitive (e.g. "debug", "INFO"). Returns true if no config is found (log everything by default).

func (*Client) ShouldLogPath

func (c *Client) ShouldLogPath(loggerPath string, desiredLevel string, ctx *ContextSet) bool

ShouldLogPath returns true if a message at desiredLevel should be logged for the given loggerPath. It is a higher-level convenience on top of ShouldLog: it uses the Client's LoggerKey (set via WithLoggerKey or Options.LoggerKey) as the underlying config key, and injects loggerPath into ctx under contexts["quonfig-sdk-logging"] = { "key": loggerPath } so a single log-level config can drive per-logger overrides via the normal rule engine.

loggerPath is passed through verbatim — the SDK does not normalize it, so "MyApp::Services::Auth" stays as "MyApp::Services::Auth". Callers may pass any identifier shape their host language prefers (dotted, colon, slash, etc.) and author matching rules in the config against that exact shape.

Panics if the Client has no LoggerKey set. Use the existing ShouldLog(configKey, ...) primitive directly if you need a different error-handling policy or want to evaluate an ad-hoc config key.

func (*Client) WithContext

func (c *Client) WithContext(ctx *ContextSet) *ContextBoundClient

WithContext returns a ContextBoundClient that merges the given context into every call.

type ConfigEnvelope

type ConfigEnvelope struct {
	Configs []ConfigResponse `json:"configs"`
	Meta    Meta             `json:"meta"`
}

ConfigEnvelope is the response wrapper for config downloads.

type ConfigEvaluator

type ConfigEvaluator interface {
	// EvaluateConfigResponse evaluates a ConfigResponse for the given environment and context.
	// Returns the full evaluation result including match metadata for telemetry and reasons.
	EvaluateConfigResponse(cfg *ConfigResponse, envID string, ctx *ContextSet) *EvalResult
}

ConfigEvaluator evaluates a config against a context. This interface breaks the import cycle between quonfig and internal/eval.

type ConfigResponse

type ConfigResponse struct {
	ID              string       `json:"id"`
	Key             string       `json:"key"`
	Type            ConfigType   `json:"type"`
	ValueType       ValueType    `json:"valueType"`
	SendToClientSDK bool         `json:"sendToClientSdk"`
	Default         RuleSet      `json:"default"`
	Environment     *Environment `json:"environment,omitempty"`
}

ConfigResponse is a single config in the download response, filtered to one environment.

type ConfigType

type ConfigType string

ConfigType represents the type of a config entry.

const (
	ConfigTypeFeatureFlag ConfigType = "feature_flag"
	ConfigTypeConfig      ConfigType = "config"
	ConfigTypeSegment     ConfigType = "segment"
	ConfigTypeLogLevel    ConfigType = "log_level"
	ConfigTypeSchema      ConfigType = "schema"
)

type ConnectionState added in v0.0.21

type ConnectionState string

ConnectionState is the customer-visible health surface; values match the cross-SDK spec in project/plans/sdk-hardening-and-verification.md.

const (
	// ConnStateInitializing is the pre-Start state and the state during the
	// first connection attempt before any worker has reported success.
	ConnStateInitializing ConnectionState = "initializing"
	// ConnStateConnected means an SSE stream (Layer 1) is live.
	ConnStateConnected ConnectionState = "connected"
	// ConnStateDisconnected means the Layer 1 worker is between connection
	// attempts (after a drop, before the next reconnect succeeds).
	ConnStateDisconnected ConnectionState = "disconnected"
	// ConnStateFallingBack means Layer 1 is unable to maintain a connection
	// and the Layer 2 fallback poller is active.
	ConnStateFallingBack ConnectionState = "falling_back"
)

type ContextBoundClient

type ContextBoundClient struct {
	// contains filtered or unexported fields
}

ContextBoundClient is a Client bound to a specific context.

func (*ContextBoundClient) FeatureIsOn

func (cb *ContextBoundClient) FeatureIsOn(key string) (bool, bool)

FeatureIsOn returns whether a feature flag is on using the bound context.

func (*ContextBoundClient) GetBoolValue

func (cb *ContextBoundClient) GetBoolValue(key string) (bool, bool, error)

GetBoolValue returns the bool value for a config key using the bound context.

func (*ContextBoundClient) GetDurationValue

func (cb *ContextBoundClient) GetDurationValue(key string) (time.Duration, bool, error)

GetDurationValue returns the time.Duration value for a config key using the bound context.

func (*ContextBoundClient) GetFloatValue

func (cb *ContextBoundClient) GetFloatValue(key string) (float64, bool, error)

GetFloatValue returns the float64 value for a config key using the bound context.

func (*ContextBoundClient) GetIntValue

func (cb *ContextBoundClient) GetIntValue(key string) (int64, bool, error)

GetIntValue returns the int64 value for a config key using the bound context.

func (*ContextBoundClient) GetJSONValue

func (cb *ContextBoundClient) GetJSONValue(key string) (interface{}, bool, error)

GetJSONValue returns the parsed JSON value for a config key using the bound context.

func (*ContextBoundClient) GetStringSliceValue

func (cb *ContextBoundClient) GetStringSliceValue(key string) ([]string, bool, error)

GetStringSliceValue returns the []string value for a config key using the bound context.

func (*ContextBoundClient) GetStringValue

func (cb *ContextBoundClient) GetStringValue(key string) (string, bool, error)

GetStringValue returns the string value for a config key using the bound context.

func (*ContextBoundClient) ShouldLog

func (cb *ContextBoundClient) ShouldLog(configKey string, desiredLevel string) bool

ShouldLog returns true if a message at desiredLevel should be logged for the given configKey, using the bound context.

func (*ContextBoundClient) ShouldLogPath

func (cb *ContextBoundClient) ShouldLogPath(loggerPath string, desiredLevel string) bool

ShouldLogPath returns true if a message at desiredLevel should be logged for the given loggerPath, using the bound context. See Client.ShouldLogPath for full semantics.

func (*ContextBoundClient) WithContext

func (cb *ContextBoundClient) WithContext(ctx *ContextSet) *ContextBoundClient

WithContext returns a new ContextBoundClient with the given context merged in.

type ContextSet

type ContextSet struct {
	// contains filtered or unexported fields
}

ContextSet is a set of named contexts used for config evaluation.

func ContextSetFromContext

func ContextSetFromContext(ctx context.Context) *ContextSet

ContextSetFromContext returns the *ContextSet previously attached with ContextWithContextSet, or nil if none is attached.

func Merge

func Merge(sets ...*ContextSet) *ContextSet

Merge returns a new ContextSet that combines all provided context sets. Later sets take precedence over earlier ones for the same context name.

func NewContextSet

func NewContextSet() *ContextSet

NewContextSet creates a new empty ContextSet.

func (*ContextSet) GetContextValue

func (cs *ContextSet) GetContextValue(propertyName string) (interface{}, bool)

GetContextValue looks up a value by dotted property name. The part before the first dot is the context name; the part after is the key within that context. If there is no dot, the unnamed ("") context is searched.

func (*ContextSet) SetNamedContext

func (cs *ContextSet) SetNamedContext(nc *NamedContext)

SetNamedContext adds or replaces a named context.

func (*ContextSet) WithNamedContextValues

func (cs *ContextSet) WithNamedContextValues(name string, values map[string]interface{}) *ContextSet

WithNamedContextValues adds or replaces a named context with the given values, returning the ContextSet for chaining.

type ContextTelemetryMode

type ContextTelemetryMode string

ContextTelemetryMode controls what context data the SDK sends to the telemetry backend.

const (
	// ContextTelemetryNone disables context telemetry.
	ContextTelemetryNone ContextTelemetryMode = ""
	// ContextTelemetryShapes sends only context field names and types.
	ContextTelemetryShapes ContextTelemetryMode = "shapes"
	// ContextTelemetryPeriodicExample sends context shapes and periodic example values.
	ContextTelemetryPeriodicExample ContextTelemetryMode = "periodic_example"
)

type Criterion

type Criterion struct {
	PropertyName string `json:"propertyName,omitempty"`
	Operator     string `json:"operator"`
	ValueToMatch *Value `json:"valueToMatch,omitempty"`
}

Criterion is a single condition in a rule.

type EnvLookupFunc

type EnvLookupFunc func(key string) (string, bool)

EnvLookupFunc looks up an environment variable by name. Returns the value and whether it was found.

type Environment

type Environment struct {
	ID    string `json:"id"`
	Rules []Rule `json:"rules"`
}

Environment is an environment-specific rule set.

type ErrorCode added in v0.0.20

type ErrorCode string

ErrorCode is a typed enumeration of evaluation error categories. Values mirror the OpenFeature spec's error codes so providers can forward them without inferring meaning from error message text.

const (
	// ErrorCodeNone indicates a successful evaluation (no error).
	ErrorCodeNone ErrorCode = ""
	// ErrorCodeFlagNotFound indicates the requested key is not present in the store.
	ErrorCodeFlagNotFound ErrorCode = "FLAG_NOT_FOUND"
	// ErrorCodeTypeMismatch indicates a value could not be coerced to the requested type.
	ErrorCodeTypeMismatch ErrorCode = "TYPE_MISMATCH"
	// ErrorCodeProviderNotReady indicates the SDK has not finished initialization.
	ErrorCodeProviderNotReady ErrorCode = "PROVIDER_NOT_READY"
	// ErrorCodeGeneral covers errors that don't fit the more specific categories
	// (missing env vars, decryption failures, etc.).
	ErrorCodeGeneral ErrorCode = "GENERAL"
)

type EvalReason

type EvalReason int

EvalReason describes why a particular value was returned from evaluation. Maps to OpenFeature evaluation reasons and the telemetry payload's reason field.

const (
	// ReasonUnknown is the zero value; used when reason is not determined.
	ReasonUnknown EvalReason = 0
	// ReasonStatic means the config has no targeting rules -- just a static value.
	ReasonStatic EvalReason = 1
	// ReasonTargetingMatch means a rule's criteria matched the evaluation context.
	ReasonTargetingMatch EvalReason = 2
	// ReasonSplit means a weighted value (A/B test) was resolved.
	ReasonSplit EvalReason = 3
	// ReasonDefault means the SDK-provided default was returned (no match or error).
	ReasonDefault EvalReason = 4
	// ReasonError means evaluation failed (type mismatch, missing config, etc.).
	ReasonError EvalReason = 5
)

func (EvalReason) String

func (r EvalReason) String() string

String returns the OpenFeature-compatible reason string.

type EvalResult

type EvalResult struct {
	Value              *Value
	ConfigID           string
	ConfigKey          string
	ConfigType         ConfigType
	RuleIndex          int
	WeightedValueIndex int
	Reason             EvalReason
	IsMatch            bool
}

EvalResult is the internal result of evaluating a config, carrying full metadata needed for telemetry reporting and (future) OpenFeature evaluation details.

type EvaluationDetails added in v0.0.20

type EvaluationDetails struct {
	// Value is the resolved value, or nil on error / not-found.
	Value *Value
	// Reason describes why this value was returned.
	Reason EvalReason
	// ErrorCode is the typed error category. Empty (ErrorCodeNone) on success.
	ErrorCode ErrorCode
	// ErrorMessage is a human-readable error description. Empty on success.
	ErrorMessage string
	// Variant is the OpenFeature-style variant identifier (e.g. "static",
	// "targeting:0", "split:1", "default"). Always set, never empty.
	Variant string
	// FlagMetadata carries provider-specific data (configId, configType,
	// environment, ruleIndex, weightedValueIndex). Always non-nil; keys
	// follow camelCase (Go idiom) per the cross-SDK spec.
	FlagMetadata map[string]any
}

EvaluationDetails is the public, OpenFeature-shaped record of an evaluation. It bundles the resolved Value with the reason, typed error code, variant, and flagMetadata so callers (and OpenFeature providers) can populate ProviderResolutionDetail without inferring fields from error message text.

type LogLevel

type LogLevel string

LogLevel is the type for log level names.

const (
	LogLevelTrace LogLevel = "TRACE"
	LogLevelDebug LogLevel = "DEBUG"
	LogLevelInfo  LogLevel = "INFO"
	LogLevelWarn  LogLevel = "WARN"
	LogLevelError LogLevel = "ERROR"
	LogLevelFatal LogLevel = "FATAL"
)

type Meta

type Meta struct {
	Version     string `json:"version"`
	Environment string `json:"environment"`
	WorkspaceID string `json:"workspaceId,omitempty"`
}

Meta holds response metadata.

type NamedContext

type NamedContext struct {
	Name string
	Data map[string]interface{}
}

NamedContext is a named context providing key-value data about a user, team, device, etc.

type OnInitFailure

type OnInitFailure int

OnInitFailure controls behavior when initialization times out.

const (
	// ReturnError causes getter methods to return an error if initialization times out.
	ReturnError OnInitFailure = iota
	// ReturnZeroValue causes getter methods to return zero values if initialization times out.
	ReturnZeroValue
)

type Option

type Option func(*Options) error

Option is a functional option for configuring the Client.

func WithAPIKey

func WithAPIKey(key string) Option

WithAPIKey sets the API key for authentication.

func WithAPIURLs

func WithAPIURLs(urls []string) Option

WithAPIURLs sets an ordered list of base URLs for the Quonfig API. The client tries each URL in order, falling back to the next on failure. Setting this option is treated as an explicit override and takes precedence over the QUONFIG_DOMAIN env var.

func WithAllTelemetryDisabled

func WithAllTelemetryDisabled() Option

WithAllTelemetryDisabled disables all telemetry collection.

func WithCollectEvaluationSummaries

func WithCollectEvaluationSummaries(enabled bool) Option

WithCollectEvaluationSummaries enables or disables evaluation summary telemetry.

func WithContextTelemetryMode

func WithContextTelemetryMode(mode ContextTelemetryMode) Option

WithContextTelemetryMode sets the context telemetry mode.

func WithDataDir

func WithDataDir(path string) Option

WithDataDir sets the local Quonfig workspace directory to load from disk.

func WithDataDirAutoReload added in v0.0.24

func WithDataDirAutoReload(enabled bool) Option

WithDataDirAutoReload enables filesystem watching for the configured DataDir. Default false — datadir mode is silent until you opt in.

When enabled, the SDK walks the resolved datadir at startup, registers every subdirectory with fsnotify, debounces filesystem-event bursts (default DefaultDataDirAutoReloadDebounce, 200ms — tune with WithDataDirAutoReloadDebounce), then re-reads the workspace, parses it, and atomically swaps in the new envelope on success. The existing OnConfigUpdate callback fires on each successful swap; on parse failure the SDK keeps serving the previous envelope and the callback is NOT fired.

Symlinks are resolved once at Start via filepath.EvalSymlinks — editing the file the symlink points at is detected, but atomic flips that retarget the symlink itself are not.

Graceful degrade: if watch registration fails (read-only filesystem, immutable container, missing directory, EMFILE), the SDK logs at WARN via the configured WithLogger and continues serving the envelope it loaded at NewClient. It never panics on a watcher failure and NewClient does not return an error from a failed registration.

Shutdown: Client.Close stops the watcher goroutine, releases the underlying fsnotify handle, and clears any pending debounce timer. There is no separate handle to manage — the watcher lifecycle is tied to the client.

func WithDataDirAutoReloadDebounce added in v0.0.24

func WithDataDirAutoReloadDebounce(d time.Duration) Option

WithDataDirAutoReloadDebounce tunes how long the watcher waits after the most recent filesystem event before re-reading the datadir. Bursts of events (atomic-rename editor saves, `git pull` touching dozens of files) coalesce into a single re-read inside this window.

Defaults to DefaultDataDirAutoReloadDebounce (200ms) — long enough to absorb the 3–5 events typical editors emit in <50ms, short enough that interactive edits feel immediate. Raise it if you have a noisy producer (continuously regenerating files) and would rather see one reload per second than per save. Lower it only if you have measured 200ms is meaningfully too slow.

Has no effect unless WithDataDirAutoReload(true) is also set. A negative duration is rejected at option-apply time; zero falls back to the default.

func WithEnvLookup

func WithEnvLookup(fn EnvLookupFunc) Option

WithEnvLookup sets a custom environment variable lookup function. By default, os.LookupEnv is used. This is useful for testing.

func WithEnvironment

func WithEnvironment(environment string) Option

WithEnvironment sets the environment ID/name used when loading from a local data dir.

func WithFallbackPoll added in v0.0.21

func WithFallbackPoll(enabled bool, interval time.Duration) Option

WithFallbackPoll configures the Layer 2 fallback poller. The poller is idle while the SSE stream is connected; it engages only after SSE has been disconnected for the default 120s threshold, and ticks at the given interval until SSE recovers. Pass enabled=false to disable Layer 2 entirely (the default).

Replaces the deprecated WithRefreshInterval, which ran parallel polling on top of SSE.

func WithGlobalContext

func WithGlobalContext(ctx *ContextSet) Option

WithGlobalContext sets the global context that is merged into every evaluation.

func WithHTTPClient

func WithHTTPClient(client *http.Client) Option

WithHTTPClient overrides the HTTP client used for config downloads.

func WithInitTimeout

func WithInitTimeout(d time.Duration) Option

WithInitTimeout sets how long to wait for initial config loading before applying the OnInitFailure policy.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the *slog.Logger the SDK uses to emit warnings. When unset, the SDK falls back to slog.Default(). Symmetric with WithHTTPClient: pass the highest-level convenience type so callers who want a custom handler can do slog.New(myHandler) themselves.

Note: this is distinct from WithLoggerKey, which configures the per-logger log-level config key consumed by ShouldLogPath.

func WithLoggerKey

func WithLoggerKey(key string) Option

WithLoggerKey sets the config key used by ShouldLogPath to look up a per-logger level rule (e.g. "log-level.my-app"). When set, callers can use ShouldLogPath(loggerPath, desiredLevel, ctx) and the SDK evaluates LoggerKey with contexts["quonfig-sdk-logging"] = { "key": loggerPath } merged into ctx. The existing ShouldLog(configKey, ...) primitive does not require this option.

func WithOnConfigUpdate

func WithOnConfigUpdate(fn func()) Option

WithOnConfigUpdate sets a callback function that is called whenever the client receives and installs a new config envelope. This is useful for OpenFeature providers and other integrations that need to emit change events.

func WithOnInitFailure

func WithOnInitFailure(f OnInitFailure) Option

WithOnInitFailure sets the behavior when initialization times out.

func WithQuonfigUserContext

func WithQuonfigUserContext(enabled bool) Option

WithQuonfigUserContext enables (or disables) injecting quonfig-user.email from ~/.quonfig/tokens.json into GlobalContext on NewClient. Customer-supplied GlobalContext keys win on collision. Default off; the env var QUONFIG_DEV_CONTEXT=true also enables it when no explicit option is set.

func WithRefreshInterval deprecated

func WithRefreshInterval(d time.Duration) Option

WithRefreshInterval enables background polling refreshes.

Deprecated: prior to v0.0.21 this option ran PARALLEL polling on top of SSE. As of v0.0.21 the SDK polls fallback-only — the poller is idle while SSE is connected and engages only after SSE has been disconnected for >=120s. WithRefreshInterval(d) is preserved as a thin shim over WithFallbackPoll(true, d); new code should call WithFallbackPoll directly. A one-shot deprecation warning is logged at NewClient time so deployers can spot the call site.

func WithSSE

func WithSSE(enabled bool) Option

WithSSE enables or disables the background SSE streaming client. Default is true. When disabled, the SDK relies on the initial HTTP fetch plus any polling configured via WithFallbackPoll. Note that fallback polling only engages after sustained SSE disconnect — with SSE disabled it effectively starts immediately, but you typically still want WithFallbackPoll(true, …) to make poll cadence explicit.

func WithSSEStateCallback

func WithSSEStateCallback(fn func(connected bool)) Option

WithSSEStateCallback registers a function that is invoked whenever the background SSE stream transitions between connected and disconnected. The callback receives true when a stream is live and false when it is not. Useful for emitting accurate connection-health metrics.

func WithTelemetrySyncInterval

func WithTelemetrySyncInterval(d time.Duration) Option

WithTelemetrySyncInterval sets how often telemetry is submitted to the backend.

func WithTelemetryURL

func WithTelemetryURL(url string) Option

WithTelemetryURL sets the telemetry ingestion endpoint. Setting this option is treated as an explicit override and takes precedence over the QUONFIG_DOMAIN env var.

type Options

type Options struct {
	APIKey        string
	APIURLs       []string
	DataDir       string
	Environment   string
	GlobalContext *ContextSet
	InitTimeout   time.Duration
	OnInitFailure OnInitFailure
	EnvLookup     EnvLookupFunc
	HTTPClient    *http.Client

	// FallbackPollEnabled controls whether the Layer 2 fallback poller is
	// allowed to engage. The poller is idle while SSE is connected; it
	// engages only after SSE has been disconnected for FallbackPollThreshold
	// (default 120s) and ticks at FallbackPollInterval until SSE recovers.
	//
	// Default false: an SSE-only deployment trusts the stream and accepts
	// degraded freshness during outages. Set true (and pick an interval) to
	// guarantee a poll-based refresh path during sustained disconnects.
	FallbackPollEnabled bool
	// FallbackPollInterval is how often the Layer 2 poller fetches once
	// engaged. Must be >0 when FallbackPollEnabled is true. Defaults to 60s.
	FallbackPollInterval time.Duration

	// Logger is the *slog.Logger used by the SDK to emit warnings (e.g. the
	// dev-context tokens loader). Defaults to slog.Default() when not set via
	// WithLogger. Internal helpers/goroutines that may emit warnings should
	// read from this field rather than calling slog.Default() directly so a
	// host app can route SDK output through its own handler.
	Logger *slog.Logger

	// OnConfigUpdate is called whenever the client installs a new config envelope
	// (i.e. after a successful fetch or data-dir load). It is called with the
	// client's internal mutex NOT held, so it is safe to call client methods
	// from within the callback.
	OnConfigUpdate func()

	// DataDirAutoReload enables filesystem watching when DataDir is set. The
	// SDK re-reads the datadir whenever a file inside changes, atomically
	// swaps the envelope on a successful parse, and fires OnConfigUpdate.
	// Default false — opt in for dev / git-pull workflows.
	DataDirAutoReload bool

	// DataDirAutoReloadDebounce coalesces filesystem-event bursts (atomic-rename
	// editor saves, git pull touching dozens of files). The SDK waits this long
	// after the most recent event before re-reading. Zero means use
	// DefaultDataDirAutoReloadDebounce (200ms).
	DataDirAutoReloadDebounce time.Duration

	// SSEEnabled controls whether a background SSE streamer is opened after
	// initialization. Default true. Set false for pure HTTP-poll behavior.
	// When DataDir is set (local dev) or no APIKey is configured, SSE is a
	// no-op regardless of this flag.
	SSEEnabled bool

	// OnSSEStateChange, if non-nil, is invoked whenever the background SSE
	// connection transitions between connected=true and connected=false.
	// Useful for emitting accurate "stream is up" metrics on the caller's
	// side (see load-gen's load_gen.sse_connected gauge). The callback may
	// run on any goroutine and should be cheap / non-blocking.
	OnSSEStateChange func(connected bool)

	// LoggerKey is the config key used by Client.ShouldLogPath to look up a
	// per-logger level rule (e.g. "log-level.my-app"). When set, callers can
	// use the higher-level ShouldLogPath(loggerPath, ...) convenience, which
	// injects loggerPath into the evaluation context as
	// contexts["quonfig-sdk-logging"] = { "key": loggerPath } so a single
	// log-level config can drive per-logger overrides.
	LoggerKey string

	// EnableQuonfigUserContext, when true, makes NewClient read
	// ~/.quonfig/tokens.json (written by `qfg login`) and merge
	// { "quonfig-user": { "email": <userEmail> } } into GlobalContext under
	// any caller-supplied keys. Default false. The env var
	// QUONFIG_DEV_CONTEXT=true also enables it. Production servers do not
	// have the tokens file, so this is a no-op there by construction.
	EnableQuonfigUserContext bool

	// Telemetry options
	CollectEvaluationSummaries bool
	ContextTelemetryMode       ContextTelemetryMode
	TelemetrySyncInterval      time.Duration
	TelemetryURL               string
	// contains filtered or unexported fields
}

Options holds all client configuration.

func (*Options) TelemetryEnabled

func (o *Options) TelemetryEnabled() bool

TelemetryEnabled returns true if a TelemetryURL is configured and any telemetry collection is enabled.

type ProvidedData

type ProvidedData struct {
	Source string `json:"source"`
	Lookup string `json:"lookup"`
}

ProvidedData holds the source info for ENV_VAR-provided values.

type QuonfigHandler

type QuonfigHandler struct {
	// contains filtered or unexported fields
}

QuonfigHandler is a slog.Handler that gates records through Quonfig's dynamic per-logger level configuration. It wraps another slog.Handler (supplied by the caller, e.g. slog.NewJSONHandler(os.Stdout, nil)) and suppresses records whose level is below the Quonfig-configured level for the handler's loggerPath.

Enabled, WithAttrs, and WithGroup delegate to the inner handler; Handle consults Client.ShouldLogPath before forwarding.

func NewQuonfigHandler

func NewQuonfigHandler(client *Client, inner slog.Handler, loggerPath string) *QuonfigHandler

NewQuonfigHandler returns a QuonfigHandler bound to the given client and loggerPath. The loggerPath is passed through verbatim to ShouldLogPath — no normalization is applied, so rules may match the caller's native identifier shape (dotted, colon-delimited, slashed, etc.).

Panics if client.opts.LoggerKey is empty. Configure it with quonfig.WithLoggerKey("log-level.my-app") when constructing the Client.

func (*QuonfigHandler) Enabled

func (h *QuonfigHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the handler should process a record at the given level. It consults Quonfig via ShouldLogPath; if Quonfig says no, slog will skip record construction entirely, which is the cheap pre-filter the slog API is designed around.

func (*QuonfigHandler) Handle

func (h *QuonfigHandler) Handle(ctx context.Context, r slog.Record) error

Handle forwards a record to the inner handler if Quonfig says this record's level should log. This is belt-and-suspenders with Enabled: a caller that builds records by hand or bypasses slog.Logger still gets gated.

func (*QuonfigHandler) WithAttrs

func (h *QuonfigHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs returns a new QuonfigHandler whose inner handler carries the given attributes.

func (*QuonfigHandler) WithGroup

func (h *QuonfigHandler) WithGroup(name string) slog.Handler

WithGroup returns a new QuonfigHandler whose inner handler is nested under the given group.

type QuonfigLeveler

type QuonfigLeveler struct {
	// contains filtered or unexported fields
}

QuonfigLeveler is a slog.Leveler backed by Quonfig's dynamic per-logger level configuration. Hand it to slog.HandlerOptions.Level to have slog's built-in handlers pre-filter based on the current Quonfig-configured level for loggerPath.

Level() is called on every record, so every record sees fresh configuration — flipping a level in the Quonfig UI takes effect immediately after the client's next config refresh, with no logger rebuild needed.

func NewQuonfigLeveler

func NewQuonfigLeveler(client *Client, loggerPath string) *QuonfigLeveler

NewQuonfigLeveler returns a QuonfigLeveler bound to the given client and loggerPath. Panics if client.opts.LoggerKey is empty.

Note: QuonfigLeveler does NOT thread a context.Context through to the Quonfig evaluator — slog.HandlerOptions.Level is contextless. If your rules depend on per-request context (tenant, user, etc.), use QuonfigHandler instead and attach a ContextSet via ContextWithContextSet.

func (*QuonfigLeveler) Level

func (l *QuonfigLeveler) Level() slog.Level

Level returns the current slog.Level for the bound loggerPath, derived by reading the Client's LoggerKey config with the logger-path context injected. Unknown/missing config falls back to INFO.

type Rule

type Rule struct {
	Criteria []Criterion `json:"criteria"`
	Value    Value       `json:"value"`
}

Rule is a set of criteria (AND logic) that produce a value.

type RuleSet

type RuleSet struct {
	Rules []Rule `json:"rules"`
}

RuleSet is a collection of rules (tried top to bottom, first match wins).

type SchemaData

type SchemaData struct {
	SchemaType string `json:"schemaType"`
	Schema     string `json:"schema"`
}

SchemaData holds schema validation data.

type Value

type Value struct {
	Type         ValueType   `json:"type"`
	Value        interface{} `json:"value"`
	Confidential bool        `json:"confidential,omitempty"`
	DecryptWith  string      `json:"decryptWith,omitempty"`
}

Value is the universal value wrapper.

func (Value) BoolValue

func (v Value) BoolValue() bool

BoolValue returns the value as bool, or false.

func (Value) DoubleValue

func (v Value) DoubleValue() float64

DoubleValue returns the value as float64, or 0.

func (Value) IntValue

func (v Value) IntValue() int64

IntValue returns the value as int64, or 0.

func (Value) ProvidedValue

func (v Value) ProvidedValue() *ProvidedData

ProvidedValue returns the provided data, or nil.

func (Value) StringListValue

func (v Value) StringListValue() []string

StringListValue returns the value as []string, or nil.

func (Value) StringValue

func (v Value) StringValue() string

StringValue returns the value as string, or "".

func (*Value) UnmarshalJSON

func (v *Value) UnmarshalJSON(data []byte) error

UnmarshalJSON handles the polymorphic value field.

func (Value) WeightedValuesValue

func (v Value) WeightedValuesValue() *WeightedValuesData

WeightedValuesValue returns the weighted values data, or nil.

type ValueResolver

type ValueResolver interface {
	// ResolveValue resolves a matched value, handling ENV_VAR provided values and decryption.
	// The configKey and valueType are used for coercion and error messages.
	ResolveValue(val *Value, configKey string, valueType ValueType, envID string, ctx *ContextSet) (*Value, error)
}

ValueResolver resolves a matched value (e.g., ENV_VAR lookup, decryption).

type ValueType

type ValueType string

ValueType represents the type of a config value.

const (
	ValueTypeBool           ValueType = "bool"
	ValueTypeInt            ValueType = "int"
	ValueTypeDouble         ValueType = "double"
	ValueTypeString         ValueType = "string"
	ValueTypeJSON           ValueType = "json"
	ValueTypeStringList     ValueType = "string_list"
	ValueTypeLogLevel       ValueType = "log_level"
	ValueTypeWeightedValues ValueType = "weighted_values"
	ValueTypeSchema         ValueType = "schema"
	ValueTypeProvided       ValueType = "provided"
	ValueTypeDuration       ValueType = "duration"
)

type WeightedValue

type WeightedValue struct {
	Weight int   `json:"weight"`
	Value  Value `json:"value"`
}

WeightedValue is a single entry in a weighted distribution.

type WeightedValuesData

type WeightedValuesData struct {
	WeightedValues     []WeightedValue `json:"weightedValues"`
	HashByPropertyName string          `json:"hashByPropertyName,omitempty"`
}

WeightedValuesData holds weighted distribution data for A/B tests.

Directories

Path Synopsis
Package evalcore provides the shared evaluation engine used by sdk-go and api-delivery.
Package evalcore provides the shared evaluation engine used by sdk-go and api-delivery.
internal
resolver
Package resolver handles post-evaluation value resolution including environment variable lookup, type coercion, and decryption of confidential values.
Package resolver handles post-evaluation value resolution including environment variable lookup, type coercion, and decryption of confidential values.
version
Package version exposes the SDK module version for telemetry and HTTP headers.
Package version exposes the SDK module version for telemetry and HTTP headers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL