mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
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:
Generated
+31
@@ -1180,6 +1180,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-socks",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tun",
|
||||
"windows-service",
|
||||
@@ -3307,6 +3308,12 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "symlink"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -3502,6 +3509,7 @@ dependencies = [
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3510,6 +3518,16 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tinystr"
|
||||
version = "0.8.4"
|
||||
@@ -3646,6 +3664,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
|
||||
@@ -39,6 +39,7 @@ hex = "0.4"
|
||||
clap = { version = "4.6", features = ["derive"] }
|
||||
tracing = "0.1"
|
||||
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"] }
|
||||
futures = "0.3"
|
||||
simple-dns = "0.11.2"
|
||||
|
||||
@@ -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.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_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.*`)
|
||||
|
||||
|
||||
@@ -116,6 +116,35 @@ done
|
||||
|
||||
# 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"
|
||||
|
||||
# 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"
|
||||
|
||||
# LaunchDaemon plist
|
||||
|
||||
@@ -21,11 +21,15 @@
|
||||
<false/>
|
||||
</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>
|
||||
<string>/usr/local/var/log/fips/fips.log</string>
|
||||
<string>/dev/null</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/usr/local/var/log/fips/fips.log</string>
|
||||
<string>/dev/null</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/usr/local/etc/fips</string>
|
||||
|
||||
@@ -39,4 +39,4 @@ echo " Removed binaries from /usr/local/bin/"
|
||||
echo ""
|
||||
echo "FIPS uninstalled."
|
||||
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
@@ -7,8 +7,9 @@ use clap::Parser;
|
||||
use fips::config::{IdentitySource, resolve_identity};
|
||||
use fips::version;
|
||||
use fips::{Config, Node};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::{debug, error, info};
|
||||
use tracing_subscriber::fmt::writer::BoxMakeWriter;
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -41,6 +42,110 @@ struct Args {
|
||||
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).
|
||||
///
|
||||
/// `config_path` overrides the default config search. `shutdown_signal`
|
||||
@@ -100,21 +205,56 @@ async fn run_daemon(
|
||||
_ => 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
|
||||
// (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
|
||||
// is to report a write failure with `eprintln!`, which itself panics when
|
||||
// stderr fails too — and the shipped supervisor configs point stdout and
|
||||
// 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.
|
||||
let ansi = log_target.is_none() && std::io::IsTerminal::is_terminal(&std::io::stdout());
|
||||
fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(true)
|
||||
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stdout()))
|
||||
.with_ansi(ansi)
|
||||
.with_writer(writer)
|
||||
.log_internal_errors(false)
|
||||
.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());
|
||||
|
||||
if loaded_paths.is_empty() {
|
||||
|
||||
+4
-3
@@ -37,9 +37,10 @@ use zeroize::{Zeroize, Zeroizing};
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto};
|
||||
pub use node::{
|
||||
BloomConfig, BuffersConfig, CacheConfig, ControlConfig, LimitsConfig, LookupConfig, MmpConfig,
|
||||
NativeApiConfig, NodeConfig, NostrRendezvousConfig, NostrRendezvousPolicy, RateLimitConfig,
|
||||
RekeyConfig, RendezvousConfig, RetryConfig, SessionConfig, SessionMmpConfig, TreeConfig,
|
||||
BloomConfig, BuffersConfig, CacheConfig, ControlConfig, LimitsConfig, LogRotation,
|
||||
LookupConfig, MmpConfig, NativeApiConfig, NodeConfig, NostrRendezvousConfig,
|
||||
NostrRendezvousPolicy, RateLimitConfig, RekeyConfig, RendezvousConfig, RetryConfig,
|
||||
SessionConfig, SessionMmpConfig, TreeConfig,
|
||||
};
|
||||
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig, TransportSpec};
|
||||
pub use transport::{
|
||||
|
||||
@@ -1336,6 +1336,57 @@ pub struct NodeConfig {
|
||||
/// Valid values: trace, debug, info, warn, error. Default: info.
|
||||
#[serde(default)]
|
||||
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 {
|
||||
@@ -1366,6 +1417,9 @@ impl Default for NodeConfig {
|
||||
ecn: EcnConfig::default(),
|
||||
rekey: RekeyConfig::default(),
|
||||
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 {
|
||||
1
|
||||
}
|
||||
@@ -1518,6 +1601,50 @@ owd_window_size: 48
|
||||
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]
|
||||
fn test_log_level_parser() {
|
||||
// Pin the observed behavior of NodeConfig::log_level():
|
||||
|
||||
Reference in New Issue
Block a user