Skip to main content

Go

Install the latest version

Github | pkg

go get github.com/quonfig/sdk-go@latest

Initialize Client

Add quonfig "github.com/quonfig/sdk-go" to your imports.

Then, initialize the client with your SDK key:

sdk, err := quonfig.NewClient(quonfig.WithSdkKey(sdkKey))

Typical Usage

We recommend using the SDK client as a singleton in your application.

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

var quonfigSdk *quonfig.Client

func init() {
// Note: WithSdkKey is not needed if QUONFIG_BACKEND_SDK_KEY env var is set
sdk, err := quonfig.NewClient()
if err != nil {
panic(err)
}
quonfigSdk = sdk
}

API URLs

By default the SDK connects to https://primary.quonfig.com for config fetches and automatically connects to https://stream.primary.quonfig.com for live SSE updates — the stream URL is derived from each API URL by prepending stream. to the hostname, so you don't configure it separately. A fallback secondary.quonfig.com will be added to the default list once the fallback app exists. Override the list with WithAPIURLs:

sdk, err := quonfig.NewClient(
quonfig.WithSdkKey(sdkKey),
quonfig.WithAPIURLs([]string{"https://primary.quonfig.com"}),
)

Feature Flags

For boolean flags, you can use the FeatureIsOn function:

enabled, ok := sdk.FeatureIsOn("my.feature.name", quonfig.NewContextSet())

Flags that don't exist yet are considered off, so you can happily add FeatureIsOn checks to your code before the flag is created.

Feature flags don't have to return just true or false.

You can get other data types using Get* functions:

value, ok, err := sdk.GetStringValue("my.string.feature.name", quonfig.NewContextSet())
value, ok, err := sdk.GetJSONValue("my.json.feature.name", quonfig.NewContextSet())

Context

Feature flags become more powerful when we give the flag evaluation rules more information to work with. We do this by providing context of the current user (and/or team, request, etc.)

Global Context

When initializing the client, you can set a global context that will be used for all evaluations.

globalContext := quonfig.NewContextSet().
WithNamedContextValues("host", map[string]interface{}{
"name": os.Getenv("HOSTNAME"),
"region": os.Getenv("REGION"),
"cpu": runtime.NumCPU(),
})


sdk, err := quonfig.NewClient(
quonfig.WithSdkKey(sdkKey),
quonfig.WithGlobalContext(globalContext),
)

Global context is the least specific context and will be overridden by more specific context passed in at the time of evaluation.

Bound Context

To make the best use of Quonfig in a web setting, we recommend setting context per-request. Setting this context for the life-cycle of the request means the Quonfig logger can be aware of your user/etc. for feature flags and targeted log levels and you won't have to explicitly pass context into your .FeatureIsOn and .Get* calls.

requestContext := quonfig.NewContextSet().
WithNamedContextValues("user", map[string]interface{}{
"name": currentUser.GetName(),
"email": currentUser.GetEmail(),
})

boundSdk := sdk.WithContext(requestContext)
enabled, ok := boundSdk.FeatureIsOn("my.feature.name")

Just-in-time Context

You can also pass context when evaluating individual flags or config values. Just-in-time context is passed to the unbound client's FeatureIsOn/Get* methods (the context-bound client returned by WithContext takes only a key):

enabled, ok := sdk.FeatureIsOn("my.feature.name", quonfig.NewContextSet().
WithNamedContextValues("team", map[string]interface{}{
"name": currentTeam.GetName(),
"email": currentTeam.GetEmail(),
}))

Dynamic Config

Config values are available via the Get* functions:

value, ok, err := sdk.GetJSONValue("slack.bot.config", quonfig.NewContextSet())

value, ok, err := sdk.GetStringValue("some.string.config", quonfig.NewContextSet())

value, ok, err := sdk.GetFloatValue("some.float.config", quonfig.NewContextSet())

Default Values for Configs

The Get* functions return a found boolean alongside the value, so you can supply your own default when a config isn't available. Here we ask for the value of a config named max-jobs-per-second, falling back to 10 if it's missing.

maxJobsPerSecond, found, err := sdk.GetIntValue("max-jobs-per-second", quonfig.NewContextSet())
if err != nil {
// handle the error (e.g. log it) — value is unusable
}
if !found {
maxJobsPerSecond = 10 // default
}

If max-jobs-per-second is available, found will be true and maxJobsPerSecond will be the value of the config. If it's not available, found will be false and we fall back to 10.

Developer overrides (qfg override)

The qfg override CLI flips a flag for your developer machine without affecting anyone else. It does this by writing a top-priority rule on the flag keyed on the property quonfig-user.email. The SDK injects that property automatically whenever the qfg login token file is present, so the rule is dead code in production by construction (a server that never ran qfg login has no quonfig-user.email on its eval context, and the rule cannot fire).

Injection is on by default — when ~/.quonfig/tokens.json (written by qfg login) exists, the SDK reads it on init and merges quonfig-user.email = <userEmail> into the global context. Customer-supplied quonfig-user keys (set via WithGlobalContext) win on collision. If the file is missing or unparseable the SDK is a no-op — init still succeeds.

To opt out (e.g. on a shared box where qfg login has run):

sdk, err := quonfig.NewClient(
quonfig.WithSdkKey(sdkKey),
quonfig.WithQuonfigUserContext(false), // opt out
)

Or set QUONFIG_DEV_CONTEXT=false in the environment. Precedence: the explicit option wins, then QUONFIG_DEV_CONTEXT, then the default (true).

Telemetry note: quonfig-user.email flows through telemetry like any other context attribute. It only appears in dev-machine telemetry because production never injects it.

Dynamic Log Levels

Log levels in Quonfig are stored as a log_level config (e.g. log-level.my-app). The SDK consults that config on every log call, so changes made in Quonfig take effect immediately via SSE with no polling or restart.

Concept

  • One log_level config per app, keyed like log-level.my-app. Value is one of TRACE, DEBUG, INFO, WARN, ERROR, FATAL.
  • Tell the client which config to consult with WithLoggerKey(...).
  • ShouldLogPath(loggerPath, desiredLevel, ctx) pushes loggerPath into the evaluation context as quonfig-sdk-logging.key (verbatim — no normalization) so a single config can drive per-logger rules.
  • The ShouldLog(configKey, desiredLevel, ctx) primitive is also available when you want to evaluate a specific config without the convenience layer.
  • Logger names flowing through quonfig-sdk-logging.key are auto-captured by example-context telemetry, so the dashboard can auto-suggest rule targets.

Basic usage

import (
"log/slog"
"os"
quonfig "github.com/quonfig/sdk-go"
)

client, _ := quonfig.NewClient(
quonfig.WithSdkKey("your-key"),
quonfig.WithLoggerKey("log-level.my-app"),
)

if client.ShouldLogPath("com.example.auth", "DEBUG", nil) {
// …
}

Rule example

Create a log_level config with key log-level.my-app and target individual loggers via quonfig-sdk-logging.key:

# Default to INFO for every logger in this app
default: INFO

rules:
# Bump a subsystem to DEBUG
- criteria:
quonfig-sdk-logging.key:
starts-with: "com.example.auth"
value: DEBUG

# Silence a chatty third-party package
- criteria:
quonfig-sdk-logging.key:
starts-with: "github.com/somelib"
value: ERROR

# Turn DEBUG on for one developer, everywhere
- criteria:
user.email: "developer@example.com"
value: DEBUG

Because the evaluator sees your full context — global context set via WithGlobalContext, per-call context passed into ShouldLogPath, and quonfig-sdk-logging.key — you can combine logger rules with user, environment, or request context to crank verbosity up for one user, one staging deploy, or one bad request, without touching anyone else.

slog handler

The SDK ships a slog.Handler that wraps any inner handler and gates each record through ShouldLogPath:

import (
"log/slog"
"os"
quonfig "github.com/quonfig/sdk-go"
)

client, _ := quonfig.NewClient(
quonfig.WithSdkKey("your-key"),
quonfig.WithLoggerKey("log-level.my-app"),
)

inner := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
handler := quonfig.NewQuonfigHandler(client, inner, "com.example.auth")
logger := slog.New(handler)

logger.Debug("debug line")
logger.Info("info line")

If you'd rather let slog drive the level decision itself, use QuonfigLeveler:

leveler := quonfig.NewQuonfigLeveler(client, "com.example.auth")
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: leveler,
})
logger := slog.New(handler)

Attaching per-request context

Both NewQuonfigHandler and NewQuonfigLeveler read any *ContextSet attached to the context.Context passed through slog, so per-request user/team context flows into rule evaluation:

cs := quonfig.NewContextSet()
cs.WithNamedContextValues("user", map[string]interface{}{
"email": "developer@example.com",
})
ctx := quonfig.ContextWithContextSet(context.Background(), cs)

logger.DebugContext(ctx, "debug line — evaluated with user context")

Reference

NameExampleDescription
WithLoggerKey(key)quonfig.WithLoggerKey("log-level.my-app")Tells the client which log_level config ShouldLogPath and the slog adapter should consult. Required for either.
ShouldLogPath(loggerPath, desiredLevel, ctx)client.ShouldLogPath("com.example.auth", "DEBUG", nil)Convenience. Uses LoggerKey + injects quonfig-sdk-logging.key = loggerPath so rules can target individual loggers.
ShouldLog(configKey, desiredLevel, ctx)client.ShouldLog("log-level.my-app", "DEBUG", nil)Primitive. Evaluates the named config directly — no auto-injection. Use when building a custom adapter.
NewQuonfigHandler(client, inner, loggerPath)slog.New(quonfig.NewQuonfigHandler(client, inner, "com.example.auth"))slog.Handler that gates each record through ShouldLogPath.
NewQuonfigLeveler(client, loggerPath)slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: leveler})slog.Leveler backed by Quonfig. Use when you want slog's own enabled-check path to see the dynamic level.
ContextWithContextSet(ctx, cs)ctx := quonfig.ContextWithContextSet(ctx, cs)Attach a *ContextSet to a context.Context so the slog adapter picks it up on each record.

Telemetry

By default, Quonfig uploads telemetry that enables a number of useful features. You can alter or disable this behavior using the following options:

NameDescriptionDefault
collectEvaluationSummariesSend counts of config/flag evaluation results back to Quonfig to view in web apptrue
contextTelemetryModeUpload either context "shapes" (the names and data types your app uses in Quonfig contexts) or periodically send full example contextsPERIODIC_EXAMPLE

If you want to change any of these options, you can pass options when initializing the Quonfig SDK:

sdk, err := quonfig.NewClient(
quonfig.WithSdkKey(sdkKey),
quonfig.WithCollectEvaluationSummaries(true),
quonfig.WithContextTelemetryMode(quonfig.ContextTelemetryPeriodicExample),
)

Available context telemetry modes:

  • quonfig.ContextTelemetryNone - Don't upload any context information
  • quonfig.ContextTelemetryShapes - Upload only context shapes (names and data types)
  • quonfig.ContextTelemetryPeriodicExample - Periodically send full example contexts (default)

To disable all telemetry at once:

sdk, err := quonfig.NewClient(
quonfig.WithAllTelemetryDisabled(),
)

Offline and Testing Modes

Local Data Directory (Datafiles)

For offline development, testing, or air-gapped environments, you can load configuration from a local Quonfig workspace directory on disk instead of connecting to the Quonfig API. See Testing with DataFiles for more information on generating local data.

Point the SDK at the workspace directory with WithDataDir, and select which environment to evaluate with WithEnvironment:

sdk, err := quonfig.NewClient(
quonfig.WithDataDir("/path/to/quonfig-workspace"),
quonfig.WithEnvironment("production"), // or "staging", "development"
)

Important notes:

  • WithDataDir loads entirely from disk — the SDK does not contact the API or open an SSE stream, and telemetry is effectively idle.
  • WithEnvironment (or the QUONFIG_ENVIRONMENT env var, which it overrides) selects which environment's values are evaluated from the local data.
  • To pick up edits to the directory while running, opt into WithDataDirAutoReload(true).

Reference

Options

client, err := quonfig.NewClient(
quonfig.WithSdkKey(os.Getenv("QUONFIG_BACKEND_SDK_KEY")), // or omit — auto-loaded from QUONFIG_BACKEND_SDK_KEY
quonfig.WithGlobalContext(globalContext),
quonfig.WithCollectEvaluationSummaries(true),
quonfig.WithContextTelemetryMode(quonfig.ContextTelemetryPeriodicExample),
)

Option Definitions

NameDescriptionDefault
WithSdkKeyYour Quonfig SDK key (not needed if QUONFIG_BACKEND_SDK_KEY env var is set)from env var
WithAPIURLsOrdered list of API base URLs. SSE URL is derived by prepending stream. to the hostname["https://primary.quonfig.com"]
WithDataDirLoad configuration from a local Quonfig workspace directory instead of the API/SSE (offline/testing)"" (API mode)
WithEnvironmentWhich environment to evaluate when loading from a local data dir (overrides QUONFIG_ENVIRONMENT)from env var
WithGlobalContextSet a static context to be used as the base layer in all configuration evaluationempty
WithCollectEvaluationSummariesSend counts of config/flag evaluation results back to Quonfig to view in web apptrue
WithContextTelemetryModeUpload either context "shapes" (the names and data types your app uses in Quonfig contexts) or periodically send full example contextsPERIODIC_EXAMPLE
WithAllTelemetryDisabledDisable all telemetry (evaluation summaries and context telemetry)n/a
WithOnInitFailureBehavior if the initial config fetch fails/times out: quonfig.ReturnError (default) or quonfig.ReturnZeroValueReturnError
WithInitTimeoutTimeout for the initial config fetch, as a time.Duration (e.g. 10*time.Second)10s
WithLoggerKeyThe log_level config key consulted by ShouldLogPath and the slog adapter. No default — set it to enable the loggerPath convenience.""
WithQuonfigUserContextInject quonfig-user.email from ~/.quonfig/tokens.json (written by qfg login) into the global context. Pairs with qfg override. Default on, gated on the token file's presence (inert in prod). Pass false or set QUONFIG_DEV_CONTEXT=false to opt out.on (token-gated)