Structured Tracing
Runique exposes a structured tracing system via RuniqueLog. By default a console subscriber is installed and domains are opt-in: until a domain is enabled, its events are not emitted. A few critical sites always emit (see Unconditional errors).
Quick activation in development
RuniqueApp::builder(config)
.with_log(|l| l.dev()) // everything at DEBUG if RUNIQUE_ENV=dev
// ...
.dev() is a no-op if RUNIQUE_ENV is not dev — safe to use unconditionally.
Granular configuration
.with_log(|l| l
.forms(|f| f
.validate(LogLevel::DEBUG)
.finalize(LogLevel::DEBUG)
)
.admin(|a| a
.crud(LogLevel::INFO)
.auth(LogLevel::WARN)
)
.auth(|a| a
.login(LogLevel::INFO)
.reset(LogLevel::WARN)
)
.mailer(|m| m.send(LogLevel::INFO))
.builder(|b| b
.templates(LogLevel::INFO)
.middleware(LogLevel::DEBUG)
.routes(LogLevel::INFO)
.statics(LogLevel::INFO)
)
.middleware(|m| m.rate_limit(LogLevel::WARN))
)
Available domains
forms — Form pipeline
| Field | When | Logged data |
|---|---|---|
field | Field registration | name, type, required |
set_value | Value assigned by fill() | name, value (password masked) |
validate | Validation result | field, ok/error, global error count |
render | HTML rendering | field, ok/error |
finalize | Hash/file move | field, ok/error |
admin — Admin panel
| Field | When | Logged data |
|---|---|---|
auth | Access check + CSRF fail | resource, action |
crud | Dispatch + create/edit/delete result | resource, action, ok/error |
list | Dispatch + list result | resource, rows, total, page |
bulk | Bulk actions | resource, action |
auth — Authentication
| Field | When | Logged data |
|---|---|---|
login | Session creation | user_id, username, is_superuser, exclusive, db_persist |
reset | Password reset flow | email, step (token generated / email sent / invalid / ok / error) |
password_init | password_init() called more than once | — |
mailer
| Field | When | Logged data |
|---|---|---|
send | Email dispatch | backend, to, subject, ok/error |
builder — Startup (one-time)
| Field | When | Logged data |
|---|---|---|
templates | Tera loading | internal, user, total |
registry | Admin resources | count |
middleware | Middleware stack | count + slot + name for each entry |
routes | URL registry | count |
statics | Static files | static_url, static_dir, media_url, media_dir |
templates — Template engine
| Field | When | Data logged |
|---|---|---|
load | Templates loading | — |
render | A template rendering | template, ok/error |
migration — Migrations
| Field | When | Data logged |
|---|---|---|
plan | Migration plan computing | — |
apply | Applying | — |
rollback | Rolling back | — |
errors — HTTP error pages
| Field | When | Logged data |
|---|---|---|
http | Handled HTTP error (404/validation/forbidden) | method, path, type / error |
render | Error-template render failure (404/429/500) | template, error — WARN floor (always visible, see below) |
.with_log(|l| l.errors(|e| e.http(LogLevel::INFO).render(LogLevel::WARN)))
middleware — Security (.middleware(|m| m....))
| Field | When | Logged data |
|---|---|---|
csrf | CSRF token detected in a GET URL | path |
csp | CSP violation or construction | — |
cors | CORS decision | — |
rate_limit | Request blocked | ip, retry_after |
host_validation | Host rejected | host |
open_redirect | Redirect refused | — |
anti_bot | Honeypot triggered | — |
https | HTTPS redirect | — |
session — Store (.session(|s| s....))
| Field | When | Logged data |
|---|---|---|
store | Session store operations (watermarks, purge) | event |
cleanup | Expired session purge | — |
exclusive_login | Invalidation of other sessions | — |
db — Database (.db(\|d\| d....))
| Field | When | Logged data |
|---|---|---|
connect | DB connection in progress / established | — |
query | Database queries | query, duration |
These three domains have no direct method on RuniqueLog — each goes through its own closure (.middleware(|m| ...), .session(|s| ...), .db(|d| ...)), same as forms/admin/auth above.
Log outputs
By default Runique installs a console subscriber (Stdout, colors). Configure one or more cumulative outputs via .output():
use runique::prelude::{LogOutput, LogRotation};
.with_log(|l| l
.output(LogOutput::stdout()) // colored console
.output(LogOutput::file("logs/app.json")) // JSON (inferred from the .json extension)
.output(LogOutput::file("logs/app.log") // plain text
.rotation(LogRotation::Daily)))
- The format is inferred from the extension:
.json→ structured JSON (one event per line), otherwise plain text. - File writing is non-blocking; logs are flushed cleanly on shutdown.
- Rotation:
Daily(default),Hourly,Never. RUNIQUE_LOG_FILE=/path/app.jsonadds a file output at runtime, without recompiling.
Custom sink
To route logs to an arbitrary destination (database, HTTP collector, message queue), implement LogSink — no tracing type is exposed:
use runique::prelude::{LogOutput, LogRecord, LogSink};
struct MySink;
impl LogSink for MySink {
fn log(&self, record: &LogRecord) {
// record.level / target / message / file / line / fields
// Must not block: for async, enqueue into your own channel.
}
}
.with_log(|l| l.output(LogOutput::sink(MySink)))
The sink receives all events in the process (Runique and your application); tell them apart via record.target (Runique events have a target starting with runique). Runique deliberately ships no database sink (it would overload the DB) — LogSink is the hook to wire one yourself.
External subscriber
If your application owns its own tracing subscriber (custom layer stack, OpenTelemetry…), declare .external(): Runique installs nothing and leaves you the single global slot, while still emitting its events to the tracing facade (your subscriber receives them).
Minimal:
.with_log(|l| l.external())
Full:
use runique::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// You install YOUR subscriber, before build()
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.init();
let db = DatabaseConfig::from_env()?.build().connect().await?;
RuniqueApp::builder(RuniqueConfig::from_env())
.with_database(db)
.routes(url::routes())
.with_log(|l| l.external()) // Runique does not install its subscriber
.build().await?
.run().await
}
To ignore Runique's internal logs, filter their target in your EnvFilter:
tracing_subscriber::fmt()
.with_env_filter("info,runique=off") // keep your logs, mute Runique's
.init();
In .external() mode, .output() destinations are ignored (your subscriber decides where logs go).
Unconditional errors (always active)
Regardless of tracing config, some events are always emitted:
- Invalid template at startup —
tracing::error!(template name + line) before startup aborts. - Critical server errors (500: database, IO, template, internal) —
tracing::error!. - Security-critical sites with a
WARNfloor — even with the domain disabled, these failures emit at least atWARN, because a silent failure there would break a guarantee: session ID rotation (anti-fixation), invalidation of other sessions (exclusive login), session persistence at login, reset-email dispatch, and error-template render failure (errors.render).