navree

Logging

Structured logging on tracing: levels per target, pretty or JSON output, redaction of secrets, rotating files and the per-request log line.

On this page

Navree logs with tracing. LoggerModule (crate navree-log, feature log) installs one subscriber for the process with readable lines in development, JSON lines in production, redaction of fields that look like secrets, levels per target that you can change while the app runs, and an optional rotating log file. Every request writes one summary line, and every log line written during a request carries the request's id and user.

Getting started

Apps created with navree new already include the module, right after ConfigModule:

src/app.rs

LoggerModule::from_config(), or a plain .module(LoggerModule), reads the log section of your configuration. Keep it right after ConfigModule: shutdown hooks run in reverse order, so the log file is flushed after every other module has logged its shutdown.

To log from your own code, add tracing to your dependencies (the navree facade doesn't re-export it):

Cargo.toml

Configuration

Every key of the log section is optional:

config/app.toml
KeyDefaultWhat it does
log.formatpretty in debug builds, json in release buildsthe line format on stdout and in the file
log.levelinfothe level of every target without its own entry
log.levelsnonelevels per target; a target covers its submodules
log.redactpassword, token, authorization, cookie, secretfield-name fragments whose values are written as [redacted]; a list you set replaces the defaults
log.filenonea rotating log file, written in addition to stdout
log.file.pathstorage/logs/navree.logthe file; the date goes before the extension
log.file.rotationdailywhen a new file starts (UTC)
log.file.keep_days14how many days of files are kept (at least 1)
log.slow_query_ms200the slow-query threshold, used when database.slow_query_ms isn't set

Level, format and rotation names are case-insensitive. Invalid values (an unknown level, a target with spaces, keep_days = 0) stop the boot with a message naming the key.

Environment variables are strings, so the lists have a string form. A new app's config/app.toml sets log.level, and .env doesn't override the TOML file, so set APP_LOG__LEVEL as a real environment variable (or change config/app.toml); the other keys work from .env too:

.env

You can also configure the module in code:

Rust

Writing log messages

Use tracing's macros. Put values in fields rather than in the message, so JSON output keeps them as data:

src/billing/service.rs
Text
  • %value records with Display, ?value with Debug, and plain numbers, booleans and strings keep their type in JSON.
  • An error field holding a &dyn std::error::Error is written with its whole source chain in JSON output.
  • The target is your module path (my_app::billing::service above). Set one yourself with target: "billing" to group lines from several modules.
  • Records from crates that use the log crate are routed into tracing too.

Log levels

log.level sets the level for everything; log.levels overrides it per target. A target covers everything below it, so navree_db = "debug" also covers navree_db::query. These are the main targets Navree itself writes to:

TargetWhat
navree::requestthe request log line; app-wide request timeouts
navree::error5xx responses and panics
navree::securityrate limits hit, CSRF failures and other security events
navree::authfailed authentication (DEBUG), and problems such as a missing JWT secret or an unsent reset mail
navree_db::queryslow queries (WARN), and every query (DEBUG) with database.log_queries = true
navree_db::migrate, navree_db::uowmigrations, rollbacks that fail
navree::httpthe server starting and stopping, and maintenance-file problems
navree::job, navree::schedule, navree::mail, navree::notify, navree::realtime, navree::audit, navree::events, navree::session, navree::access, navree::storage, navree::apithe module of the same name

To quiet the per-request lines in production, set "navree::request" = "warn": timeouts are still logged, completed requests are not.

Changing levels at run time

LoggerModule exports LogControl, which changes levels without a restart. Inject it anywhere, for example in an admin endpoint:

src/admin/log_level.rs
  • set_level(target, level) sets one target; set_default_level(level) sets everything else.
  • directives() returns the current filter, such as info,navree_db=debug.
  • Bad input is an error, never a broken filter: an unknown level gives invalid log level with the list of valid ones. Targets may only hold letters, digits, _, -, . and :, so a target can't smuggle in other filter directives.

Changes apply to the running process only; the next start reads the configuration again.

Output formats

Pretty

One readable line per event: the time, the level, the spans the event happened in, the target, the message and the fields. Lines are coloured when stdout is a terminal, unless NO_COLOR is set. A request in development looks like this:

Text

JSON

One JSON object per line, for log collectors:

One line, formatted
  • timestamp is UTC, RFC 3339. level is upper case.
  • fields holds the event's fields, the message included.
  • span is the innermost span with its fields and name; spans lists every span from the outermost. Both are absent for events outside any span.

The request log line

Every request writes one line at INFO on navree::request when it completes, with the message request completed:

fields of the request log line
FieldValue
methodthe HTTP method
routethe route template (/api/v1/deals/{id}), or unmatched when no route matched, so ids never end up in it
statusthe response status
latency_msthe time from the request arriving to the response being ready, in milliseconds
request_idthe X-Request-Id of the response
user_idthe authenticated user; absent for guests
ipthe client address (see proxies)

Navree runs each request in a request span with the fields method, path, request_id and, once the user is known, user_id. Every line logged while the request runs, in your handlers and services too, carries these fields, so you can find every line of one request by its id.

The request id is the client's X-Request-Id when it is 1 to 64 characters of letters, digits, _ and -; anything else is replaced by a new UUID v7 in simple form (32 hex characters), so a client can't forge log lines through it. Either way it is echoed in the response's X-Request-Id header.

Redaction

Before a line is written, any event or span field whose name contains one of the log.redact fragments, ignoring ASCII case, is written as [redacted]. With the defaults, password, api_token, Authorization, session_cookie and client_secret are all redacted, whatever their type. Redaction applies to every format and to the log file.

Rust

Setting log.redact replaces the default list, so include the defaults you still want:

config/app.toml

Log files

With log.file set, every line also goes to a file, rotated with tracing-appender:

rotationFile namesOld files
dailystorage/logs/navree.2026-10-26.logthe newest keep_days files are kept
hourlyone file per hourkeep_days × 24 files are kept
neverstorage/logs/navree.lognever rotated or deleted
  • Relative paths are relative to the working directory; the directory is created when missing.
  • Lines in the file use log.format too, without colours.
  • A background thread writes the file, so logging never waits for the disk. If the disk falls behind by 128,000 lines, new file lines are dropped (stdout still gets them); LogControl::dropped_file_lines() counts them.
  • When the app shuts down, the module waits up to 5 seconds for queued lines to be written.

Other logs

  • Server errors and panics go to navree::error; see errors.
  • Slow queries go to navree_db::query at WARN with the SQL (never the bound values) and elapsed_ms. The threshold is database.slow_query_ms, or log.slow_query_ms; 0 turns it off. Set database.log_queries = true to log every query at DEBUG.
  • Security events (rate limits, CSRF failures) go to navree::security at WARN; with the audit module they're also written to the audit log.

Testing

Call navree::log::init_for_tests() at the start of a test to see log lines in the output of failing tests. It installs a pretty, colourless subscriber that writes through the test harness and takes its levels from RUST_LOG (info by default). Repeated calls do nothing.

To assert on log lines, build a subscriber without installing it and capture what it writes:

tests/logging.rs

In an async test, keep the subscriber active across .awaits with let _guard = tracing::dispatcher::set_default(&dispatch); in a single-threaded #[tokio::test].

Limits

  • The subscriber is installed once per process. A second app booted in the same process (tests do this) reuses it: its level and levels apply, but its format, file and redact settings don't, and a warning says so.
  • If another global tracing subscriber was installed first, Navree leaves it in place, LoggerModule settings don't apply, and LogControl can't change levels.
  • There is no OpenTelemetry or Sentry export yet; see the roadmap.

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks