Go
Install the latest version
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_levelconfig per app, keyed likelog-level.my-app. Value is one ofTRACE,DEBUG,INFO,WARN,ERROR,FATAL. - Tell the client which config to consult with
WithLoggerKey(...). ShouldLogPath(loggerPath, desiredLevel, ctx)pushesloggerPathinto the evaluation context asquonfig-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.keyare 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
| Name | Example | Description |
|---|---|---|
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:
| Name | Description | Default |
|---|---|---|
| collectEvaluationSummaries | Send counts of config/flag evaluation results back to Quonfig to view in web app | true |
| contextTelemetryMode | Upload either context "shapes" (the names and data types your app uses in Quonfig contexts) or periodically send full example contexts | PERIODIC_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 informationquonfig.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:
WithDataDirloads entirely from disk — the SDK does not contact the API or open an SSE stream, and telemetry is effectively idle.WithEnvironment(or theQUONFIG_ENVIRONMENTenv 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
| Name | Description | Default |
|---|---|---|
| WithSdkKey | Your Quonfig SDK key (not needed if QUONFIG_BACKEND_SDK_KEY env var is set) | from env var |
| WithAPIURLs | Ordered list of API base URLs. SSE URL is derived by prepending stream. to the hostname | ["https://primary.quonfig.com"] |
| WithDataDir | Load configuration from a local Quonfig workspace directory instead of the API/SSE (offline/testing) | "" (API mode) |
| WithEnvironment | Which environment to evaluate when loading from a local data dir (overrides QUONFIG_ENVIRONMENT) | from env var |
| WithGlobalContext | Set a static context to be used as the base layer in all configuration evaluation | empty |
| WithCollectEvaluationSummaries | Send counts of config/flag evaluation results back to Quonfig to view in web app | true |
| WithContextTelemetryMode | Upload either context "shapes" (the names and data types your app uses in Quonfig contexts) or periodically send full example contexts | PERIODIC_EXAMPLE |
| WithAllTelemetryDisabled | Disable all telemetry (evaluation summaries and context telemetry) | n/a |
| WithOnInitFailure | Behavior if the initial config fetch fails/times out: quonfig.ReturnError (default) or quonfig.ReturnZeroValue | ReturnError |
| WithInitTimeout | Timeout for the initial config fetch, as a time.Duration (e.g. 10*time.Second) | 10s |
| WithLoggerKey | The log_level config key consulted by ShouldLogPath and the slog adapter. No default — set it to enable the loggerPath convenience. | "" |
| WithQuonfigUserContext | Inject 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) |