Documentation
¶
Overview ¶
Package configclient is the consumer-side library for central-config.
A microservice imports this to read configuration without ever touching the database. It warms an in-memory cache from JetStream KV, keeps it live with a Watch, and serves reads from memory (no network per read, survives NATS blips). The central-config database remains the source of truth.
Scope: set Options.MicroserviceID. The client then watches only the keys the service actually needs — every flag in its environment (flags are env-wide by design), its own SERVICESETTINGS key, and its own locale bundles. Leaving it unset selects the fleet-wide watch; see Options.MicroserviceID.
Cold start: if NATS is unreachable at boot, Options.HTTPFallback can hydrate the cache once from central-config's HTTP GET endpoints. It is off by default and never sits on the read path — see httpfallback.go for what it can and cannot reach, and for the bearer token those endpoints require.
Index ¶
- type Client
- func (c *Client) Close() error
- func (c *Client) FlagEnabled(flagKey string) bool
- func (c *Client) FlagValue(flagKey string) (string, bool)
- func (c *Client) ServiceSettings(microserviceID int64) (json.RawMessage, bool)
- func (c *Client) Snapshot() Snapshot
- func (c *Client) Status() Status
- func (c *Client) Translate(microserviceID int64, locale, key string) (string, bool)
- type FlagPayload
- type HTTPFallback
- type Options
- type Snapshot
- type Status
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client holds live, watched caches for one environment.
func New ¶
New connects, warms the caches, and starts watching. It blocks until the initial values for the client's scope have been loaded.
If JetStream is unreachable and Options.HTTPFallback is set, New hydrates what it can over HTTP and returns a client whose Status().Watching is false — i.e. a running-on-a-cold-snapshot client that will never see updates. Without a fallback configured, that case is still an error.
A fallback that could not work — no BaseURL, or no credential for an API that requires one — is an error here even when JetStream is healthy and the fallback is never reached.
Example ¶
ExampleNew shows the path a consuming service takes: warm the cache once at boot, then serve every read from memory for the life of the process.
// A throwaway control plane so this example runs; a real deployment has
// central-config publishing to its own NATS cluster.
natsURL, stop := exampleControlPlane()
defer stop()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := configclient.New(ctx, configclient.Options{
NATSURL: natsURL, // e.g. nats://nats:4222
EnvironmentID: 3,
MicroserviceID: 1,
})
if err != nil {
log.Fatalf("configclient: %v", err)
}
defer client.Close()
fmt.Println("search_v2:", client.FlagEnabled("search_v2"))
if settings, ok := client.ServiceSettings(1); ok {
fmt.Println("appsettings:", string(settings))
}
if title, ok := client.Translate(1, "pt-BR", "catalog.title"); ok {
fmt.Println("catalog.title:", title)
}
Output: search_v2: true appsettings: {"timeout":30} catalog.title: Catálogo
func (*Client) FlagEnabled ¶
FlagEnabled reports whether a flag is on. Unknown flags are false.
func (*Client) ServiceSettings ¶
func (c *Client) ServiceSettings(microserviceID int64) (json.RawMessage, bool)
ServiceSettings returns the appsettings JSON blob for a microservice. If the client is scoped to one microservice (Options.MicroserviceID) and a different one is asked for, this returns false and counts an out-of-scope read: that data is not watched, so any cached copy would be a lie.
func (*Client) Status ¶
Status reports the client's current health. Cheap enough to expose from a /healthz handler.
type FlagPayload ¶
type FlagPayload struct {
Enabled bool `json:"enabled"`
Value string `json:"value"`
UpdatedAt string `json:"updatedAt"`
}
FlagPayload mirrors the value central-config stores in the FLAGS bucket.
type HTTPFallback ¶
type HTTPFallback struct {
// BaseURL of central-config's HTTP API, e.g. http://central-config:8080.
BaseURL string
// Token is the bearer credential sent on every fallback request. It must be
// scoped to the client's environment: the API answers a read outside a
// token's scope with 404, the same as a row that does not exist.
//
// It is a secret and is treated as one — it is never logged, never put in
// an error message and never reported by Status.
Token string
// AllowUnauthenticated sends the fallback requests with no Authorization
// header. It is for a deployment running with auth switched off, which is a
// dev-only mode the control plane warns about at startup. Setting it
// anywhere else converts a boot-time configuration error into a 401
// discovered on the day JetStream is already down.
AllowUnauthenticated bool
// Timeout for the whole hydration pass. Defaults to 5s.
Timeout time.Duration
// HTTPClient overrides the default client (tests, custom transports).
HTTPClient *http.Client
// ConfigValueID is the /configs/values/{id} row id holding this service's
// appsettings. Required to fall back for SERVICESETTINGS.
ConfigValueID int64
// FlagValueIDs maps flag key -> /flags/values/{id} row id, for the flags
// this service actually reads. Required to fall back for FLAGS.
FlagValueIDs map[string]int64
// Locales this service serves, e.g. []string{"en-US", "pt-BR"}. Required
// to fall back for LOCALIZATION, and only usable on a client scoped with
// Options.MicroserviceID (the endpoint is keyed by microservice).
Locales []string
}
HTTPFallback hydrates the cache from central-config's HTTP API when JetStream cannot supply it. It runs only from New — never from a read — so once the cache is warm this type costs nothing.
What it can reach, and why not more: KV is keyed the way a consumer thinks (flag key, microservice, locale) but two of the three HTTP endpoints are keyed by database row id:
GET /localization/lookup/{msId}/{envId}/{locale} — reachable from what the
client already knows, given the locales to fetch (Locales).
GET /configs/values/{id} — {id} is the config-value row id, not the
microservice id. Only reachable if the caller supplies ConfigValueID.
GET /flags/values/{id} — {id} is the flag-value row id, not the flag
key. Only reachable if the caller supplies FlagValueIDs.
The control plane does list flag values for an environment (GET /flags/values?environmentId=), but this fallback fetches by row id instead, so the ids have to be named here. Configure what you have; what is left unset is simply not fetched.
Credentials: the admin API authenticates every route except /health, /livez and /metrics, and a token's environment scope narrows what it may read as well as what it may write. Set Token to a credential scoped to Options.EnvironmentID.
type Options ¶
type Options struct {
NATSURL string // required, e.g. nats://nats:4222
NATSCreds string // optional path to a .creds file
EnvironmentID int64 // required, the environment this service runs in
// MicroserviceID is the service this client runs inside. When set, the
// client watches only "{env}.{id}" on SERVICESETTINGS and "{env}.{id}.>" on
// LOCALIZATION, so its memory grows with its own config rather than with
// the size of the fleet. FLAGS is still watched env-wide because flags are
// shared across services.
//
// Leaving it at 0 selects a fleet-wide watch: every service's appsettings
// and every locale bundle in the environment. That is a deliberate escape
// hatch for diagnostic consumers (the test console, admin views) that
// genuinely want to observe the whole environment — it is the wrong setting
// for a normal service, and Status().FleetWide reports it so the choice is
// visible rather than silent. It is the zero value because the field is
// optional, not because it is the setting to reach for.
//
// When it is set, ServiceSettings and Translate for any *other* microservice
// return false (that data is not watched) and increment
// Status().OutOfScopeReads, so an out-of-scope read shows up as a reported
// mistake instead of an empty result that looks like "no config yet".
MicroserviceID int64
// HTTPFallback, if set, hydrates the cache from central-config's HTTP API
// when JetStream is unreachable at boot or a scoped key is missing from KV.
// Optional and boot-only; see HTTPFallback for its limits. It carries its
// own credential and New rejects one that cannot work, whether or not the
// fallback ends up being needed.
HTTPFallback *HTTPFallback
// Logger, if set, receives the client's diagnostics: watch failures, the
// fallback being used, a malformed payload. It is optional on purpose —
// this is a library, and a library that logs by default is a library that
// prints into somebody else's log stream. Unset means silent, with the same
// information still available through Status().
Logger *slog.Logger
// OnChange, if set, is called after each applied KV update: during the
// initial snapshot and for every later push. bucket is FLAGS, SERVICESETTINGS
// or LOCALIZATION; key is the full KV key; value is nil for a delete.
// It runs on the watcher goroutine, so it must not block.
//
// Delivery is at-least-once, and a delivery does not mean the value
// changed. The same value arrives again whenever the watcher reconnects
// (the ordered consumer replays the current value of every watched key) and
// whenever the control plane genuinely republishes it. Handlers must
// therefore be idempotent: doing real work per call — rebuilding an HTTP
// client, resetting a log level, invalidating a cache — must be safe to
// repeat with an unchanged value. Compare against what you already hold if
// the work is expensive.
OnChange func(bucket, key string, value []byte)
}
Options configures the client.
type Snapshot ¶
type Snapshot struct {
EnvironmentID int64 `json:"environmentId"`
Flags map[string]FlagPayload `json:"flags"`
ServiceSettings map[int64]json.RawMessage `json:"serviceSettings"`
Localization map[int64]map[string]json.RawMessage `json:"localization"`
}
Snapshot is a point-in-time copy of everything the client has cached for its environment. Intended for diagnostics and admin views, not the hot path.
type Status ¶
type Status struct {
EnvironmentID int64 `json:"environmentId"`
MicroserviceID int64 `json:"microserviceId"` // 0 when fleet-wide
FleetWide bool `json:"fleetWide"` // watching every service's config
Connected bool `json:"connected"` // NATS connection is up right now
Watching bool `json:"watching"` // KV watchers are running
UsedHTTPFallback bool `json:"usedHttpFallback"`
LastUpdate string `json:"lastUpdate,omitempty"` // RFC3339 of the last applied KV entry
StaleFor string `json:"staleFor,omitempty"` // time since that update
LastError string `json:"lastError,omitempty"`
OutOfScopeReads int64 `json:"outOfScopeReads"`
Counts struct {
Flags int `json:"flags"`
ServiceSettings int `json:"serviceSettings"`
Localization int `json:"localization"`
} `json:"counts"`
}
Status describes how healthy the cache is. It exists so a consumer can answer the two questions that matter after an incident: am I still being pushed updates, and where did this config come from?