feat(logging): let the daemon own and rotate its log file

The macOS package accumulated a single unbounded log file — 717 MB on a
node running at debug level. Nothing rotated it, and nothing could.

fips logs to stdout and leaves capture to the supervisor, which is right
where the platform rotates that stream: journald does, and so does syslog
under procd. launchd does not. It redirects stdout to a plain file and
appends to it forever, and the usual rename-and-signal rotators cannot
help, because launchd holds the descriptor and passes it as fd 1 — the
daemon has no way to reopen a file rotated out from under it, and
signalling it achieves nothing. Rotation therefore has to happen in the
process that writes, which means the daemon has to own the file.

`node.log_file` names it, and is unset by default so every platform whose
supervisor already rotates keeps logging to stdout exactly as before —
setting it there would only duplicate what journald and syslog hold.
`node.log_rotation` (hourly/daily/never, default daily) and
`node.log_max_files` (default 7) govern the roll. Both parse leniently in
the same way as `node.log_level`: a typo falls back to the default rather
than refusing to boot, and the resolved values are logged at startup.

Rotation is by period rather than by size, so `node.log_level` is what
actually governs volume — debug costs roughly an order of magnitude more
per day than info. Retention bounds the rest.

Writes go through a non-blocking appender, so a slow or full disk cannot
stall the tick loop behind a log write. The worker guard is held for the
lifetime of the daemon; dropping it would silently discard every line
logged afterward.

The live file carries the date the current period opened, since that is
how the appender names a rolled file. Splitting the configured path on its
extension rather than suffixing the whole name keeps `.log` on the end, so
`/var/log/fips/fips.log` is written as `fips.2026-08-31.log`. That does
mean the current file no longer has a fixed name; the packaged config
carries the `tail` incantation for it.

Opening the log is fatal on failure, matching how this binary treats an
unusable config. A daemon that silently dropped its logging because a
directory was unwritable would present as exactly the disappearing-logs
problem this exists to fix.

macOS packaging is the only one that turns this on. The plist stops
redirecting stdout and stderr, which would otherwise reintroduce the
unbounded file alongside the rotated one; the cost is that a failure
before logging initialises, and a panic, now go nowhere rather than to
fips.log. The setting is inserted after the `node:` key rather than
appended to the shared config, which ends at a top-level `peers:` key —
an appended block would land under the wrong mapping, and a second
top-level `node:` would collide with the first.
This commit is contained in:
Arjen
2026-08-31 09:36:50 +01:00
parent 66f0de8ad8
commit 98786e527a
9 changed files with 345 additions and 9 deletions
Generated
+31
View File
@@ -1180,6 +1180,7 @@ dependencies = [
"tokio", "tokio",
"tokio-socks", "tokio-socks",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"tun", "tun",
"windows-service", "windows-service",
@@ -3307,6 +3308,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@@ -3502,6 +3509,7 @@ dependencies = [
"powerfmt", "powerfmt",
"serde_core", "serde_core",
"time-core", "time-core",
"time-macros",
] ]
[[package]] [[package]]
@@ -3510,6 +3518,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.4" version = "0.8.4"
@@ -3646,6 +3664,19 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.20",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.31" version = "0.1.31"
+1
View File
@@ -39,6 +39,7 @@ hex = "0.4"
clap = { version = "4.6", features = ["derive"] } clap = { version = "4.6", features = ["derive"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2.3"
tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "net", "time", "process", "io-util"] } tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "net", "time", "process", "io-util"] }
futures = "0.3" futures = "0.3"
simple-dns = "0.11.2" simple-dns = "0.11.2"
+3
View File
@@ -119,6 +119,9 @@ to the highest-priority config file for operator visibility, even in ephemeral m
| `node.link_dead_timeout_secs` | u64 | `30` | No-traffic timeout before a peer is declared dead and removed | | `node.link_dead_timeout_secs` | u64 | `30` | No-traffic timeout before a peer is declared dead and removed |
| `node.drain_timeout_secs` | u64 | `2` | Upper bound in seconds on the `Draining` shutdown phase. On shutdown the node broadcasts Disconnect to its peers and then waits up to this long for the links to clear, exiting as soon as the last peer is gone. `0` skips the wait. The key is absent from a default config file rather than written with its default value, so an unset key and the 2-second default are the same thing | | `node.drain_timeout_secs` | u64 | `2` | Upper bound in seconds on the `Draining` shutdown phase. On shutdown the node broadcasts Disconnect to its peers and then waits up to this long for the links to clear, exiting as soon as the last peer is gone. `0` skips the wait. The key is absent from a default config file rather than written with its default value, so an unset key and the 2-second default are the same thing |
| `node.log_level` | string | `"info"` | Tracing filter default. Case-insensitive; one of `trace`, `debug`, `info`, `warn`, `error`. Overridden by the `RUST_LOG` environment variable when set | | `node.log_level` | string | `"info"` | Tracing filter default. Case-insensitive; one of `trace`, `debug`, `info`, `warn`, `error`. Overridden by the `RUST_LOG` environment variable when set |
| `node.log_file` | string | unset | Log to this file, rotated by the daemon, instead of to stdout. Leave unset wherever the platform already rotates the captured stream — journald and syslog both do, and logging to a file there would only duplicate what they hold. Set it where nothing rotates: launchd redirects stdout to a plain file and never truncates it, and gives the daemon no way to reopen one rotated out from under it, so the daemon has to own the file. The macOS package sets this; no other packaging does. The live file carries the date the current period opened, so `/var/log/fips/fips.log` is written as `fips.2026-08-31.log` |
| `node.log_rotation` | string | `"daily"` | Rotation period, consulted only when `node.log_file` is set. Case-insensitive; one of `hourly`, `daily`, `never`. Rotation is by period and not by size, so `node.log_level` is what governs volume — `debug` costs roughly an order of magnitude more per day than `info`. `never` keeps one unbounded file and exists for an operator who has arranged rotation another way. An unrecognised value falls back to `daily` rather than refusing to start |
| `node.log_max_files` | usize | `7` | How many log files to keep, consulted only when `node.log_file` is set. Counts the live file, so 7 daily files is a week. The oldest is deleted as a new one is opened. `0` is clamped to `1`, since the appender deletes on open and a retention of zero would delete the file it is about to write |
### Resource Limits (`node.limits.*`) ### Resource Limits (`node.limits.*`)
+29
View File
@@ -116,6 +116,35 @@ done
# Config (marked as conf file via postinstall logic — won't overwrite on upgrade) # Config (marked as conf file via postinstall logic — won't overwrite on upgrade)
cp "${PACKAGING_DIR}/common/fips.yaml" "${STAGING_DIR}/usr/local/etc/fips/fips.yaml.default" cp "${PACKAGING_DIR}/common/fips.yaml" "${STAGING_DIR}/usr/local/etc/fips/fips.yaml.default"
# Turn on file logging, which is a macOS-only default. Everywhere else the
# daemon logs to stdout and the platform rotates it (journald, syslog);
# launchd has no rotation at all, so the daemon has to own the file itself.
# Set here rather than in packaging/common/fips.yaml, which is shared with
# the platforms that must keep logging to stdout.
#
# Inserted directly after the `node:` line rather than appended: the shared
# config ends at a top-level `peers:` key, so an appended block would land
# under the wrong mapping, and a second top-level `node:` would collide with
# the first.
LOG_SETTINGS=' # Log rotation. launchd does not rotate what it captures, so the\
# daemon owns this file and rolls it. The live file carries the date the\
# period opened -- with the settings below, fips.<YYYY-MM-DD>.log -- and\
# the oldest is deleted once log_max_files exist. Follow the current one\
# with: tail -f "$(ls -t /usr/local/var/log/fips/fips.*.log | head -1)"\
log_file: /usr/local/var/log/fips/fips.log\
log_rotation: daily\
log_max_files: 7'
CONF_DEFAULT="${STAGING_DIR}/usr/local/etc/fips/fips.yaml.default"
if ! grep -q '^node:' "${CONF_DEFAULT}"; then
echo "packaging/common/fips.yaml has no top-level 'node:' key to insert log settings under" >&2
exit 1
fi
awk -v settings="${LOG_SETTINGS}" '
{ print }
!done && /^node:$/ { print settings; done = 1 }
' "${CONF_DEFAULT}" > "${CONF_DEFAULT}.tmp"
mv "${CONF_DEFAULT}.tmp" "${CONF_DEFAULT}"
cp "${PACKAGING_DIR}/common/hosts" "${STAGING_DIR}/usr/local/etc/fips/hosts.default" cp "${PACKAGING_DIR}/common/hosts" "${STAGING_DIR}/usr/local/etc/fips/hosts.default"
# LaunchDaemon plist # LaunchDaemon plist
+6 -2
View File
@@ -21,11 +21,15 @@
<false/> <false/>
</dict> </dict>
<!-- The daemon owns and rotates its own log (node.log_file), because
launchd holds this descriptor and never truncates or rotates it,
and gives the daemon no way to reopen one rotated out from under
it. Redirecting here too would reintroduce the unbounded file. -->
<key>StandardOutPath</key> <key>StandardOutPath</key>
<string>/usr/local/var/log/fips/fips.log</string> <string>/dev/null</string>
<key>StandardErrorPath</key> <key>StandardErrorPath</key>
<string>/usr/local/var/log/fips/fips.log</string> <string>/dev/null</string>
<key>WorkingDirectory</key> <key>WorkingDirectory</key>
<string>/usr/local/etc/fips</string> <string>/usr/local/etc/fips</string>
+1 -1
View File
@@ -39,4 +39,4 @@ echo " Removed binaries from /usr/local/bin/"
echo "" echo ""
echo "FIPS uninstalled." echo "FIPS uninstalled."
echo "Config preserved at /usr/local/etc/fips/ (remove manually if desired)" echo "Config preserved at /usr/local/etc/fips/ (remove manually if desired)"
echo "Logs preserved at /usr/local/var/log/fips/ (remove manually if desired)" echo "Logs preserved at /usr/local/var/log/fips/fips.*.log (remove manually if desired)"
+143 -3
View File
@@ -7,8 +7,9 @@ use clap::Parser;
use fips::config::{IdentitySource, resolve_identity}; use fips::config::{IdentitySource, resolve_identity};
use fips::version; use fips::version;
use fips::{Config, Node}; use fips::{Config, Node};
use std::path::PathBuf; use std::path::{Path, PathBuf};
use tracing::{debug, error, info}; use tracing::{debug, error, info};
use tracing_subscriber::fmt::writer::BoxMakeWriter;
use tracing_subscriber::{EnvFilter, fmt}; use tracing_subscriber::{EnvFilter, fmt};
use zeroize::Zeroize; use zeroize::Zeroize;
@@ -41,6 +42,110 @@ struct Args {
uninstall_service: bool, uninstall_service: bool,
} }
/// Where a file-backed log is being written, for the startup log line.
///
/// The operator configured one path and the appender writes another (the
/// rolled name carries the period), so reporting both is what makes the log
/// findable.
struct LogTarget {
directory: String,
live_example: String,
}
/// Build the rolling-file writer for `node.log_file`.
///
/// The appender takes a directory plus a prefix and suffix rather than a
/// filename, and interposes the rotation period between them, so
/// `/var/log/fips/fips.log` is written as `fips.2026-08-31.log`. Splitting on
/// the extension rather than appending the date to the whole name is what
/// keeps the `.log` extension on the live file.
///
/// Fatal on failure, matching how this binary treats an unusable config: a
/// daemon that silently discarded its logging because a directory was not
/// writable would present exactly as the disappearing-logs problem the file
/// exists to solve.
fn build_file_writer(
path: &str,
config: &fips::Config,
) -> (
BoxMakeWriter,
tracing_appender::non_blocking::WorkerGuard,
LogTarget,
) {
use fips::config::LogRotation;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
let path = Path::new(path);
let directory = match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent,
_ => Path::new("."),
};
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
eprintln!("Invalid node.log_file (no filename): {}", path.display());
std::process::exit(1);
};
let extension = path.extension().and_then(|s| s.to_str());
// The packaged layouts create this, but a hand-written config naming a
// fresh directory should not have to.
if let Err(e) = std::fs::create_dir_all(directory) {
eprintln!("Cannot create log directory {}: {e}", directory.display());
std::process::exit(1);
}
let rotation = match config.node.log_rotation() {
LogRotation::Hourly => Rotation::HOURLY,
LogRotation::Daily => Rotation::DAILY,
LogRotation::Never => Rotation::NEVER,
};
let mut builder = RollingFileAppender::builder()
.rotation(rotation.clone())
.filename_prefix(stem)
.max_log_files(config.node.log_max_files());
if let Some(extension) = extension {
builder = builder.filename_suffix(extension);
}
let appender = match builder.build(directory) {
Ok(appender) => appender,
Err(e) => {
eprintln!("Cannot open log file {}: {e}", path.display());
std::process::exit(1);
}
};
// Non-blocking: a slow or full disk must not stall the node's tick loop
// behind a write. The guard returned here keeps the writer thread alive.
let (writer, guard) = tracing_appender::non_blocking(appender);
let live_example = match extension {
Some(extension) => format!("{stem}.<{}>.{extension}", rotation_label(&rotation)),
None => format!("{stem}.<{}>", rotation_label(&rotation)),
};
(
BoxMakeWriter::new(writer),
guard,
LogTarget {
directory: directory.display().to_string(),
live_example,
},
)
}
/// The shape of the date stamp the appender interposes, for the startup line.
fn rotation_label(rotation: &tracing_appender::rolling::Rotation) -> &'static str {
use tracing_appender::rolling::Rotation;
if *rotation == Rotation::HOURLY {
"YYYY-MM-DD-HH"
} else if *rotation == Rotation::DAILY {
"YYYY-MM-DD"
} else {
"no date stamp"
}
}
/// Run the FIPS daemon (shared between foreground and service modes). /// Run the FIPS daemon (shared between foreground and service modes).
/// ///
/// `config_path` overrides the default config search. `shutdown_signal` /// `config_path` overrides the default config search. `shutdown_signal`
@@ -100,21 +205,56 @@ async fn run_daemon(
_ => filter, _ => filter,
}; };
// Where the log goes. Unset `node.log_file` keeps the stream on stdout
// for a supervisor to capture, which is what journald and syslog want and
// what every platform but macOS gets. Naming a file moves ownership of it
// to us so we can roll it, because launchd holds the descriptor it
// redirects and gives the daemon no way to reopen one rotated out from
// under it.
//
// `_log_guard` must outlive the daemon: dropping it flushes and stops the
// writer thread, and a log line emitted after that is discarded. It is
// held until `main` returns.
let (writer, _log_guard, log_target) = match config.node.log_file.as_deref() {
Some(path) => {
let (writer, guard, target) = build_file_writer(path, &config);
(writer, Some(guard), Some(target))
}
None => (BoxMakeWriter::new(std::io::stdout), None, None),
};
// ANSI color only when stdout is a terminal — under a supervisor // ANSI color only when stdout is a terminal — under a supervisor
// (daemon(8), systemd) escape codes would litter the log file. // (daemon(8), systemd) escape codes would litter the log file. A log file
// we own is never a terminal.
// //
// Never let a failed log write panic the thread that logged. The default // Never let a failed log write panic the thread that logged. The default
// is to report a write failure with `eprintln!`, which itself panics when // is to report a write failure with `eprintln!`, which itself panics when
// stderr fails too — and the shipped supervisor configs point stdout and // stderr fails too — and the shipped supervisor configs point stdout and
// stderr at the same place, so one full disk satisfies both. A worker // stderr at the same place, so one full disk satisfies both. A worker
// thread killed that way takes its share of the peer space with it. // thread killed that way takes its share of the peer space with it.
let ansi = log_target.is_none() && std::io::IsTerminal::is_terminal(&std::io::stdout());
fmt() fmt()
.with_env_filter(filter) .with_env_filter(filter)
.with_target(true) .with_target(true)
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout())) .with_ansi(ansi)
.with_writer(writer)
.log_internal_errors(false) .log_internal_errors(false)
.init(); .init();
// Logged first, and only when we own the file: the operator who goes
// looking for output that is no longer on stdout needs the resolved path
// and the retention that governs it, and the rolled name is not the one
// they configured.
if let Some(target) = &log_target {
info!(
directory = %target.directory,
live_file = %target.live_example,
rotation = ?config.node.log_rotation(),
max_files = config.node.log_max_files(),
"Logging to file"
);
}
info!("FIPS {} starting", version::short_version()); info!("FIPS {} starting", version::short_version());
if loaded_paths.is_empty() { if loaded_paths.is_empty() {
+4 -3
View File
@@ -37,9 +37,10 @@ use zeroize::{Zeroize, Zeroizing};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto}; pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto};
pub use node::{ pub use node::{
BloomConfig, BuffersConfig, CacheConfig, ControlConfig, LimitsConfig, LookupConfig, MmpConfig, BloomConfig, BuffersConfig, CacheConfig, ControlConfig, LimitsConfig, LogRotation,
NativeApiConfig, NodeConfig, NostrRendezvousConfig, NostrRendezvousPolicy, RateLimitConfig, LookupConfig, MmpConfig, NativeApiConfig, NodeConfig, NostrRendezvousConfig,
RekeyConfig, RendezvousConfig, RetryConfig, SessionConfig, SessionMmpConfig, TreeConfig, NostrRendezvousPolicy, RateLimitConfig, RekeyConfig, RendezvousConfig, RetryConfig,
SessionConfig, SessionMmpConfig, TreeConfig,
}; };
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig, TransportSpec}; pub use peer::{ConnectPolicy, PeerAddress, PeerConfig, TransportSpec};
pub use transport::{ pub use transport::{
+127
View File
@@ -1336,6 +1336,57 @@ pub struct NodeConfig {
/// Valid values: trace, debug, info, warn, error. Default: info. /// Valid values: trace, debug, info, warn, error. Default: info.
#[serde(default)] #[serde(default)]
pub log_level: Option<String>, pub log_level: Option<String>,
/// Log file (`node.log_file`). Unset by default, which keeps logging on
/// stdout for a supervisor to capture — the right arrangement wherever
/// the platform already rotates that stream (journald, syslog).
///
/// Set it only where nothing else rotates: launchd redirects stdout to a
/// plain file and never truncates it, and the classic rename-and-signal
/// rotators cannot help there, because the supervisor holds the
/// descriptor and the daemon has no way to reopen it. Naming a file here
/// makes the daemon own it instead, so it can roll it itself.
///
/// The live file carries a date suffix (`fips.log.2026-08-31`); see
/// [`NodeConfig::log_rotation`].
#[serde(default)]
pub log_file: Option<String>,
/// Log rotation period (`node.log_rotation`). Case-insensitive.
/// Valid values: daily, hourly, never. Default: daily.
///
/// Only consulted when [`NodeConfig::log_file`] is set. Rotation is by
/// period rather than by size, so a level change is what bounds the
/// volume: `debug` costs roughly an order of magnitude more per day than
/// `info`. Retention is [`NodeConfig::log_max_files`] periods.
///
/// `never` keeps a single file that grows without limit, which is the
/// defect this exists to fix — it is here for an operator who has
/// arranged rotation some other way, not as a setting to reach for.
#[serde(default)]
pub log_rotation: Option<String>,
/// How many rotated log files to keep (`node.log_max_files`). Default: 7.
///
/// Counts the live file, so 7 daily files is a week of history. Older
/// files are deleted as new ones are opened. Only consulted when
/// [`NodeConfig::log_file`] is set.
#[serde(default)]
pub log_max_files: Option<usize>,
}
/// How often the daemon rolls its log file over.
///
/// Parsed from [`NodeConfig::log_rotation`]; see there for why the choice is
/// a period rather than a size.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogRotation {
/// Roll at the top of each hour.
Hourly,
/// Roll at midnight.
Daily,
/// Never roll: one file, unbounded.
Never,
} }
impl Default for NodeConfig { impl Default for NodeConfig {
@@ -1366,6 +1417,9 @@ impl Default for NodeConfig {
ecn: EcnConfig::default(), ecn: EcnConfig::default(),
rekey: RekeyConfig::default(), rekey: RekeyConfig::default(),
log_level: None, log_level: None,
log_file: None,
log_rotation: None,
log_max_files: None,
} }
} }
} }
@@ -1387,6 +1441,35 @@ impl NodeConfig {
} }
} }
/// Get the log rotation period. Default: daily.
///
/// Lenient in the same way as [`NodeConfig::log_level`]: an unrecognised
/// value falls back to the default rather than refusing to start. A
/// daemon that will not boot over a typo in a log setting is worse than
/// one that rolls daily when the operator asked for something else, and
/// the resolved value is logged at startup either way.
pub fn log_rotation(&self) -> LogRotation {
match self
.log_rotation
.as_deref()
.map(|s| s.to_lowercase())
.as_deref()
{
Some("hourly") => LogRotation::Hourly,
Some("never") => LogRotation::Never,
_ => LogRotation::Daily,
}
}
/// How many log files to keep. Default: 7, and never zero.
///
/// Zero is clamped to one: the appender deletes on open, so a retention
/// of zero would delete the file it is about to write and lose the log
/// entirely.
pub fn log_max_files(&self) -> usize {
self.log_max_files.unwrap_or(7).max(1)
}
fn default_tick_interval_secs() -> u64 { fn default_tick_interval_secs() -> u64 {
1 1
} }
@@ -1518,6 +1601,50 @@ owd_window_size: 48
assert_eq!(c.startup_sweep_max_age_secs, 3_600); assert_eq!(c.startup_sweep_max_age_secs, 3_600);
} }
#[test]
fn test_log_rotation_parser() {
// Same leniency as log_level: unknown and unset both fall back to the
// default rather than refusing to start.
let rotation = |input: Option<&str>| {
NodeConfig {
log_rotation: input.map(|s| s.to_string()),
..NodeConfig::default()
}
.log_rotation()
};
assert_eq!(rotation(Some("hourly")), LogRotation::Hourly);
assert_eq!(rotation(Some("HOURLY")), LogRotation::Hourly);
assert_eq!(rotation(Some("never")), LogRotation::Never);
assert_eq!(rotation(Some("daily")), LogRotation::Daily);
assert_eq!(rotation(None), LogRotation::Daily);
assert_eq!(rotation(Some("weekly")), LogRotation::Daily);
assert_eq!(rotation(Some("")), LogRotation::Daily);
}
#[test]
fn test_log_max_files_never_zero() {
// The appender deletes on open, so a retention of zero would delete
// the file it is about to write.
let max = |input: Option<usize>| {
NodeConfig {
log_max_files: input,
..NodeConfig::default()
}
.log_max_files()
};
assert_eq!(max(None), 7);
assert_eq!(max(Some(0)), 1);
assert_eq!(max(Some(3)), 3);
}
#[test]
fn test_logging_defaults_to_stdout() {
// Unset log_file is what keeps every platform whose supervisor
// already rotates (journald, syslog) on stdout. Regressing this to a
// file default would double-log there.
assert!(NodeConfig::default().log_file.is_none());
}
#[test] #[test]
fn test_log_level_parser() { fn test_log_level_parser() {
// Pin the observed behavior of NodeConfig::log_level(): // Pin the observed behavior of NodeConfig::log_level():