configclient

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 14 Imported by: 0

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

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

func New(ctx context.Context, opts Options) (*Client, error)

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) Close

func (c *Client) Close() error

Close stops all watchers and closes the connection.

func (*Client) FlagEnabled

func (c *Client) FlagEnabled(flagKey string) bool

FlagEnabled reports whether a flag is on. Unknown flags are false.

func (*Client) FlagValue

func (c *Client) FlagValue(flagKey string) (string, bool)

FlagValue returns a flag's string value and whether it was found.

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) Snapshot

func (c *Client) Snapshot() Snapshot

Snapshot returns a copy of the current cache.

func (*Client) Status

func (c *Client) Status() Status

Status reports the client's current health. Cheap enough to expose from a /healthz handler.

func (*Client) Translate

func (c *Client) Translate(microserviceID int64, locale, key string) (string, bool)

Translate resolves a single localization key for a service+locale. Returns the translated string and whether it was found. Out-of-scope microservices are reported the same way as in ServiceSettings.

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?

Jump to

Keyboard shortcuts

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