Know how your app will die—before it does.
A configuration library that performs a premortem on your app's config—finding all the ways it would die before it ever runs.
The name is a bit tongue-in-cheek—but only a bit. Configuration errors are one of the leading causes of production outages. Bad config doesn't just cause bugs; it causes incidents, pages, and 3am debugging sessions.
A postmortem is what you do after something dies—gathering everyone to analyze what went wrong. Traditional config libraries give you the postmortem experience:
$ ./myapp
Error: missing field `database.host`
$ ./myapp # fixed it, try again
Error: invalid port value
$ ./myapp # fixed that too
Error: pool_size must be positive
# Three deaths to find three problems
premortem gives you all the fatal issues upfront:
$ ./myapp
Configuration errors (3):
[config.toml:8] missing required field 'database.host'
[env:APP_PORT] value "abc" is not a valid integer
[config.toml:10] 'pool_size' value -5 must be >= 1
One run. All errors. Know how your app would die—before it does.
Try it yourself: cargo run --example error-demo
- Accumulate all errors — Never stop at the first problem
- Trace value origins — Know exactly which source provided each value
- Multi-source loading — Files, environment, CLI args, remote sources
- Holistic validation — Type, range, format, cross-field, and business rules
- Derive macro — Declarative validation with
#[derive(Validate)] - Hot reload — Watch for config changes (optional feature)
use premortem::{Config, Toml, Env, Validate};
use serde::Deserialize;
#[derive(Debug, Deserialize, Validate)]
struct AppConfig {
#[validate(non_empty)]
pub host: String,
#[validate(range(1..=65535))]
pub port: u16,
#[validate(range(1..=100))]
pub pool_size: u32,
}
fn main() {
let config = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.source(Env::prefix("APP_"))
.build()
.unwrap_or_else(|errors| {
eprintln!("Configuration errors ({}):", errors.len());
for e in &errors {
eprintln!(" {}", e);
}
std::process::exit(1);
});
println!("Starting server on {}:{}", config.host, config.port);
}[dependencies]
premortem = "0.6"With optional features:
[dependencies]
premortem = { version = "0.6", features = ["json", "watch"] }| Feature | Description |
|---|---|
toml |
TOML file support (default) |
json |
JSON file support |
yaml |
YAML file support |
watch |
Hot reload / file watching |
remote |
Remote sources (planned) |
full |
All features |
See the examples/ directory for runnable examples:
| Example | Description |
|---|---|
| error-demo | Error output with source location tracking |
| env-validation | Required environment variables with error accumulation |
| layered-config | Multi-source config with value tracing |
| layered | Environment-based layered configuration |
| basic | Minimal configuration loading |
| validation | Comprehensive validation patterns |
| testing | Configuration testing with MockEnv |
| tracing | Value origin tracing demonstration |
| watch | Hot reload with automatic file watching |
| web-server | Axum web server configuration |
| yaml | YAML configuration file loading |
Run an example:
cargo run --example error-demo
cargo run --example layered-config
cargo run --example yaml --features yaml- Common Patterns — Layered config, secrets, nested structs
- Testing Guide — Testing with MockEnv
Sources are applied in order, with later sources overriding earlier ones:
let config = Config::<AppConfig>::builder()
.source(Defaults::from(AppConfig::default())) // Lowest priority
.source(Toml::file("config.toml"))
.source(Env::prefix("APP_")) // Highest priority
.build()?;Mark environment variables as required at the source level with error accumulation:
let config = Config::<AppConfig>::builder()
.source(
Env::prefix("APP_")
.require_all(&["JWT_SECRET", "DATABASE_URL", "API_KEY"])
)
.build()?;All missing required variables are reported together:
Configuration errors (3):
[env:APP_JWT_SECRET] Missing required field: jwt.secret
[env:APP_DATABASE_URL] Missing required field: database.url
[env:APP_API_KEY] Missing required field: api.key
This separates presence validation (does the variable exist?) from value validation (does it meet constraints?).
All I/O is abstracted through ConfigEnv, enabling testing with MockEnv:
let env = MockEnv::new()
.with_file("config.toml", "port = 8080")
.with_env("APP_HOST", "localhost");
let config = Config::<AppConfig>::builder()
.source(Toml::file("config.toml"))
.source(Env::prefix("APP_"))
.build_with_env(&env)?;Debug where configuration values came from:
let traced = Config::<AppConfig>::builder()
.source(Defaults::from(AppConfig::default()))
.source(Toml::file("config.toml"))
.source(Env::prefix("APP_"))
.build_traced()?;
// Check what was overridden
for path in traced.overridden_paths() {
let trace = traced.trace(path).unwrap();
println!("{}: {:?} from {}", path, trace.final_value.value, trace.final_value.source);
}MIT Glen Baker iepathos@gmail.com