Cargo Features
The core crate ships nearly everything behind a feature, so a build
carries only what it asked for: dynamic-config with default features
pulls in no cryptography, no HTTP client and no async runtime. This
chapter is what each flag actually buys — the surface it unlocks, the
dependency it pulls, and the reason it is a choice rather than a default.
[dependencies]
dynamic-config = { version = "<version>", features = ["toml", "watch", "async"] }
How a missing feature fails is part of the design. Surface whose
feature is off does not exist — watch() without watch is a compile
error, not a runtime surprise. A format whose feature is off fails at
load time instead, naming the feature to add, because the file's path is
runtime data now: the message says exactly what to put in
features = [..], so the one machine that reads YAML diagnoses itself.
Formats
| Feature | Default | Unlocks |
|---|---|---|
json | ✅ | .json sources, via serde_json |
toml | .toml sources | |
yaml | .yaml / .yml sources | |
ini | .ini sources — a parser in this crate, and the backend's | |
properties | .properties sources — the only parser anywhere, so every reader reads it | |
ron | .ron sources, read only, through the config-rs reader | |
json5 | .json5 sources, read only, likewise |
One per format because each is its own parser dependency — except
properties, whose parser lives in this crate and costs nothing but
code, and ron/json5, which have no parser here at all and turn the
config-rs reader's on. A format feature turns the matching parser on in
every backend present, so there is no second list to keep in step;
Engines & Readers has the table and the two places
the dialects differ. Neither flat format can be a save target, and
neither can RON or JSON5 — no backend here writes them.
json is the default as the least controversial single choice; turn it
off with default-features = false if the build reads only TOML. The extension
picks the parser at load time, which is why these are load-time rather
than compile-time failures when missing.
Watching
watch — builder.watch() / watch_with(), the generated
start_watch(), and Dynamic::watch: the debounced, directory-level
file watcher. Pulls notify, the platform
notification backend. Since the 1.88 floor no feature moves the MSRV —
the ladder this sentence used to describe collapsed in 0.8.0.
Async
async — load_async / init_async, changes(),
Dynamic::changes, and the AsyncRemoteSource trait the async store
crates implement. No dependency at all: Changes is a hand-written
Future, so tokio, smol, Embassy and a hand-rolled executor all drive
it; blocking work goes to a fresh thread unless an executor is installed
with set_blocking_executor.
tokio — async, plus tokio's blocking pool instead of a thread
per load. This is a routing choice, not a requirement: an application
already on tokio avoids a thread spawn per reload. Does not raise the
MSRV.
Integration
clap — bind_clap(&matches, ..): copies named command-line
arguments into the flags layer. The only feature that pins another
crate's major version, which is exactly why it is opt-in and separate —
with it off, a clap major release is not this crate's breaking change.
config is not a feature — it is one of three dependencies every
build has, because it carries the fold and this crate has
none of its own. It is also an opt-in reader,
bringing YAML through the maintained yaml-rust2 plus two formats this
crate has no parser for.
Its format parsers follow this crate's own format features rather than a
second list: features = ["yaml"] turns on the backend's YAML too, so
there is no default-features set to keep in step with.
ron, json5 — two read-only formats, parsed by the config-rs
reader and by nothing in this crate. save() refuses them, as it refuses
INI.
figment — Source::provider(&dyn figment::Provider) and figment
itself re-exported: the escape hatch for the long tail of sources this
crate will never ship (a database, an in-house format). Interop only —
the resolution is this crate's own, and with the feature off figment is
not in the dependency graph at all. It is the only feature that puts
figment in a public signature; with it off, a figment major bump is not a
breaking change here, and with it on you have opted into that coupling
knowingly. See Stability Tiers.
dotenv — .env_file(..): a .env file read as the environment
layer, below the real environment. Does not call setenv —
mutating the process environment to configure one struct is a side
effect nobody asked for, and it is not thread-safe.
Schema
schema — builder.schema() and schema::merge: a JSON Schema for
the config files, secrets marked writeOnly, which is what gives
editors validation and completion. Pulls schemars (the type needs
derive(JsonSchema)).
Encryption
decrypt — the Decryptor/Encryptor traits, set_decryptor, and
encrypted_file(..) on the builder: bring your own scheme (SOPS through
sops -d, a KMS). Pulls zeroize, so decrypted plaintext is wiped on
drop whatever the scheme is.
age — decrypt, plus the stock implementation: transparent
decryption (and save_encrypted) for age-encrypted files. See
MSRV & Features for why the two differ.
Observability
log — the same diagnostics through the log crate's global
logger, for programs that standardised on log. An installed
set_log_sink outranks it; tracing outranks both. Without any of the
three, stderr — unchanged since 0.1, and now governable at runtime by
set_log_level.
tracing — the watcher's diagnostics become tracing events
instead of stderr lines, every install becomes a dynamic_config.reload
span carrying the reason, the generation and the outcome, with a WARN
event for a reload that installed nothing, and every remote fetch becomes
a dynamic_config.fetch span carrying the outcome and, on a failure, the
error kind. Nothing is emitted on the read path, and no field carries a
value, a key path or a store's address. See Telemetry.
telemetry — telemetry::Exposition: ConfigStatus and
RemoteStatus rendered as Prometheus text, so a process can serve
/metrics without this crate choosing its metrics ecosystem. No
dependency at all — an exposition format is a wire encoding, not a
crate — and the metric names are API: six for the configuration, six more
for a remote source, each labelled by the caller rather than by anything
the crate read. Also see Telemetry.
The bundle
full — everything above. For applications that want the whole
surface; a library depending on this crate should name what it needs
instead, so its users' builds stay small.
What is not a feature
The remote stores: each is a companion crate, not a
flag, so reaching for Consul does not put a gRPC stack, the AWS SDK and
three HTTP clients into a build that asked for none of them. The
no_std engine is likewise its own crate — a
microcontroller needs a different shape, not fewer flags. MSRV per
configuration is its own chapter.