Librebar
Opinionated application foundation for Rust CLIs and services. Wire up CLI flags, layered config, and structured logging in about 30 lines.
use Result;
use Parser;
use ;
Librebar is a library, not a framework. You own main(). You own your CLI struct. You own your config struct. Librebar handles the wiring that is identical across every project.
Installation
Add librebar to your Cargo.toml with the features you need:
[]
= { = "0.1", = ["cli", "config", "logging"] }
No default features. You opt into exactly what you need.
Features
Core application
| Feature | What it does |
|---|---|
cli |
CommonArgs with --quiet, -v, --json, --color, -C, --version-only |
config |
Layered config discovery, deep merge, TOML/YAML/JSON parsing |
logging |
JSONL structured logging with daily rotation and platform-aware log directories |
shutdown |
Graceful shutdown with SIGINT/SIGTERM handling via tokio::sync::watch |
crash |
Panic hook with structured JSON crash dumps written to the XDG cache directory |
Networking and data
| Feature | What it does |
|---|---|
http |
HTTPS client with tracing, timeouts, user-agent (rustls + Mozilla CA roots) |
cache |
File-based key-value cache with TTL (XDG cache directory) |
update |
"Update available" notifications via the GitHub releases API (24-hour cache) |
Integration
| Feature | What it does |
|---|---|
otel |
OpenTelemetry tracing export via OTLP/HTTP |
otel-grpc |
OpenTelemetry via gRPC (adds Tonic transport) |
mcp |
Model Context Protocol server support (rmcp wrapper) |
Operational
| Feature | What it does |
|---|---|
lockfile |
Exclusive file locks to prevent concurrent instances |
dispatch |
Git-style {app}-{subcommand} plugin lookup on PATH |
diagnostics |
doctor command framework + .tar.gz debug bundle builder |
Benchmarking (dev-only)
| Feature | What it does |
|---|---|
bench |
Wall-clock benchmarks via divan (any platform) |
bench-gungraun |
Instruction-count benchmarks via gungraun / Valgrind (Linux/Intel) |
Some features automatically enable their dependencies: update → http + cache; dispatch → cli; diagnostics → config + logging; otel → logging; otel-grpc → otel.
CLI
Embed CommonArgs into your own clap struct with #[command(flatten)]:
This gives every librebar-based app a consistent set of flags:
| Flag | Short | Effect |
|---|---|---|
--quiet |
-q |
Only print errors |
--verbose |
-v |
More detail (repeatable: -vv for trace) |
--json |
Output as JSON for scripting | |
--color |
auto, always, or never |
|
--chdir |
-C |
Run as if started in a different directory |
--version-only |
Print version number and exit |
For compact help (-h shows short help, --help shows long help):
use CommandFactory;
let cmd = with_help_short;
let cli = from_arg_matches?;
Config
Define your config struct with serde:
use ;
Discovery
The builder's .config::<Config>() discovers config files automatically. It walks up from the current directory looking for (in order):
.config/{app}.{ext}.{app}.{ext}{app}.{ext}
Then checks the user config directory (~/.config/{app}/config.{ext} on macOS/Linux).
Supported extensions: .toml, .yaml, .yml, .json. Walking stops at a .git boundary by default.
Layered merge
When multiple config files are found, values merge with later files winning. Objects merge recursively. Scalars and arrays replace entirely. Your struct's #[serde(default)] values serve as the base layer.
defaults from Config::default()
← ~/.config/myapp/config.toml (user config)
← ./myapp.toml (project config)
← explicit file via --config (highest precedence)
Explicit files
Load from a specific path instead of discovery:
let app = init
.
.start?;
Escape hatch
Skip the builder entirely and use the config module directly:
let = new
.with_project_search
.with_boundary_marker
.?;
Or load a pre-built config:
let app = init
.with_config
.start?;
Logging
The logging feature provides JSONL structured logging to file with daily rotation. Logs go to files or stderr, never stdout (which stays clear for application output like MCP communication).
Log directory resolution
The logging system finds a writable log directory using this priority:
{APP}_LOG_PATHenv var (exact file path){APP}_LOG_DIRenv var (directory, appends{app}.jsonl)log_dirfrom config- Platform default:
- macOS:
~/Library/Logs/{app}/ - Linux:
$XDG_STATE_HOME/{app}/logs/
- macOS:
- Current directory
- stderr (if no writable directory is found)
Where {APP} is the uppercased, hyphen-to-underscore app name (e.g., my-tool becomes MY_TOOL_LOG_PATH).
Log level precedence
--quiet → error only
-v → debug
-vv → trace
RUST_LOG=... → custom filter
(none) → info (default)
Direct usage
Use the logging module without the builder:
let log_cfg = from_app_name;
let filter = env_filter;
let _guard = init?;
Hold the guard for the application's lifetime. When it drops, logs flush.
Builder API
The builder wires everything in the correct initialization order:
- Load config (if requested)
- Initialize logging (reads log settings from config if available)
- Return
App<C>with everything wired up
// Full setup — CLI, config, and logging
let app = init
.with_cli
.
.logging
.start?;
// Access initialized state
let cfg: &Config = app.config;
let sources = app.config_sources;
let cli_args = app.cli;
Without config, .start() returns App<()>:
let app = init
.with_cli
.logging
.start?;
Testing
The default test suite runs fully offline and finishes in under a second:
Opt-in network tests
Two tests in tests/http_test.rs hit the public internet
(api.github.com/zen, httpbin.org/get) and are marked #[ignore] so
they don't pretend to pass when they haven't actually run. Opt in with:
# Run only the ignored tests (nextest):
# Or with the stock runner:
End-to-end OTEL export
The otel feature builds an OTLP/HTTP exporter that lights up whenever
OTEL_EXPORTER_OTLP_ENDPOINT is set to a non-empty value. To watch spans
arrive from the service example, stand up a receiver. The .NET Aspire
Dashboard bundles an OTLP/HTTP ingestor plus a unified UI for logs,
traces, and metrics in a single image:
# Start the dashboard (UI on 18888, OTLP/HTTP ingestor on 18890):
# Run the service with export enabled:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:18890 \
# Open the UI; spans from the `service` service show up under that name:
# http://localhost:18888
#
# First-launch login token is printed to the container logs:
# docker logs $(docker ps -q --filter ancestor=mcr.microsoft.com/dotnet/aspire-dashboard)
Any OTLP/HTTP receiver works the same way — Jaeger's all-in-one image,
the OpenTelemetry Collector, or commercial backends (Honeycomb, Grafana
Cloud, etc.). Point OTEL_EXPORTER_OTLP_ENDPOINT at their ingest URL and
spans flow.
Versioning
librebar follows semantic versioning with the Rust-ecosystem pre-1.0 convention:
| Release track | Behavior |
|---|---|
| 0.x (current) | Minor bumps (0.1.0 → 0.2.0) may contain breaking changes. Patch bumps (0.1.0 → 0.1.1) are additive or bug-fix only. |
| 1.0 and beyond | Strict semver. Breaking changes require a major bump. |
What counts as breaking
During the 0.x line, the following changes warrant a minor bump:
- Removing or renaming any public item (type, function, method, module, feature flag).
- Changing a public function's signature in a way that breaks existing call sites — including parameter type changes, return-type changes, or trait-bound tightening.
- Removing or renaming a variant on the
Errorenum (or any of its per-module companions:HttpError,CacheError,ConfigParseError). These enums are all#[non_exhaustive], so adding a variant is additive and ships in a patch. - Changing the semantics of a stable API (e.g., a method that previously
returned
Ok(None)now returnsErr). - Raising the MSRV beyond what is documented in
rust-versioninCargo.toml.
The following changes are not breaking and can land in a patch:
- Adding new public items (types, functions, methods, feature flags).
- Adding new variants to
Error,HttpError,CacheError, orConfigParseError(all#[non_exhaustive]). - Adding new optional config fields that have
#[serde(default)]. - Internal refactoring, performance improvements, and dependency bumps that don't change the public surface.
MSRV
The minimum supported Rust version is pinned in Cargo.toml's
rust-version field (currently 1.89.0) and tested against in CI.
MSRV increases are treated as breaking and batched into minor bumps
during the 0.x line and into major bumps after 1.0.
When does 1.0 ship
When the public API holds stable across two consecutive minor releases with no breaking changes. No external gate, no calendar deadline.
Using librebar anywhere? Open an issue on GitHub with a one-line "using it for X" — not a gate for 1.0, just an invitation. External consumers surface ergonomic issues that self-dogfooding can't, and earlier signal makes for a better 1.0.
Until then, pin to a specific minor version (librebar = "0.1") if you
want the patch-only guarantee.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.