Skip to content

Commit b80b338

Browse files
feat(iroh-cli): add file logging by default for start commands (#2175)
## Description Adds file logging by default to commands that start a node. Parameters are: - the "RUST_LOG" directive, in the form of and env var `IROH_FILE_RUST_LOG` or in the configuration file. - rotation: how often a new file is created. Either daily, hourly or never. - max files: Maximum number of files to keep File logging can be disabled by either setting the max_files to 0 in the configuration file or by setting the rust log statement to off, in either the env var or the config file. (example config file) ```toml [file_logs] rust_log = "iroh=trace" rotation = "daily" max_files = 1 ``` The files created live the `IROH_DATA_DIR/logs` dir and are named `iroh-2024-03-02.log` (for a daily rotation, or with the hour, for an hourly rotation) By default, we log everything in debug, rotating hourly, keeping at most 4 files (so total 4 hours of logs). This is short default since debug logs, even in an idle node, are a lot ## Notes & open questions n/a ## Change checklist - [x] Self-review. - [x] Documentation updates if relevant. - [ ] Tests if relevant. --------- Co-authored-by: Floris Bruynooghe <flub@devork.be>
1 parent 7986394 commit b80b338

File tree

8 files changed

+216
-52
lines changed

8 files changed

+216
-52
lines changed

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

iroh-cli/Cargo.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,32 @@ console = { version = "0.15.5" }
3333
derive_more = { version = "1.0.0-beta.1", features = ["display"] }
3434
dialoguer = { version = "0.11.0", default-features = false }
3535
dirs-next = { version = "2.0.0" }
36+
flume = "0.11.0"
3637
futures = "0.3.30"
3738
hex = "0.4.3"
3839
human-time = { version = "0.1.6" }
3940
indicatif = { version = "0.17", features = ["tokio"] }
4041
iroh = { version = "0.13.0", path = "../iroh", features = ["metrics"] }
4142
iroh-metrics = { version = "0.13.0", path = "../iroh-metrics" }
4243
parking_lot = "0.12.1"
43-
postcard = "1.0.8"
4444
portable-atomic = "1"
45+
postcard = "1.0.8"
4546
quic-rpc = { version = "0.7.0", features = ["flume-transport", "quinn-transport"] }
4647
quinn = "0.10.2"
4748
rand = "0.8.5"
4849
rustyline = { version = "12.0.0" }
50+
serde = { version = "1.0.197", features = ["derive"] }
51+
serde_with = "3.7.0"
4952
shell-words = { version = "1.1.0" }
5053
shellexpand = { version = "3.1.0" }
51-
serde = { version = "1.0.197", features = ["derive"] }
5254
strum = { version = "0.26.2", features = ["derive"] }
55+
tempfile = "3.10.1"
5356
thiserror = "1.0.58"
5457
time = { version = "0.3", features = ["formatting"] }
58+
tokio = { version = "1.36.0", features = ["full"] }
5559
tracing = "0.1.40"
60+
tracing-appender = "0.2.3"
5661
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
57-
tokio = { version = "1.36.0", features = ["full"] }
58-
tempfile = "3.10.1"
59-
flume = "0.11.0"
6062

6163
[dev-dependencies]
6264
duct = "0.13.6"

iroh-cli/src/commands.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ pub(crate) struct Cli {
3737
#[clap(long, global = true)]
3838
start: bool,
3939

40-
/// Send log output to specified file descriptor.
41-
#[cfg(unix)]
42-
#[clap(long)]
43-
pub(crate) log_fd: Option<i32>,
44-
4540
/// Port to serve metrics on. -1 to disable.
4641
#[clap(long)]
4742
pub(crate) metrics_port: Option<MetricsPort>,
@@ -131,6 +126,7 @@ impl Cli {
131126
)
132127
.await
133128
} else {
129+
crate::logging::init_terminal_logging()?;
134130
let iroh = IrohRpc::connect(data_dir).await.context("rpc connect")?;
135131
console::run(&iroh, &env).await
136132
}
@@ -147,6 +143,7 @@ impl Cli {
147143
)
148144
.await
149145
} else {
146+
crate::logging::init_terminal_logging()?;
150147
let iroh = IrohRpc::connect(data_dir).await.context("rpc connect")?;
151148
command.run(&iroh, &env).await
152149
}

iroh-cli/src/commands/doctor.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,8 @@ fn inspect_ticket(ticket: &str) -> anyhow::Result<()> {
932932
}
933933

934934
pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> {
935+
let data_dir = iroh_data_root()?;
936+
let _guard = crate::logging::init_terminal_and_file_logging(&config.file_logs, &data_dir)?;
935937
match command {
936938
Commands::Report {
937939
stun_host,

iroh-cli/src/commands/start.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ where
3737
F: FnOnce(iroh::client::mem::Iroh) -> T + Send + 'static,
3838
T: Future<Output = Result<()>> + 'static,
3939
{
40+
let _guard = crate::logging::init_terminal_and_file_logging(&config.file_logs, iroh_data_root)?;
4041
let metrics_fut = start_metrics_server(config.metrics_addr);
4142

4243
let res = run_with_command_inner(config, iroh_data_root, run_type, command).await;

iroh-cli/src/config.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub(crate) const ENV_PREFIX: &str = "IROH";
3131

3232
const ENV_AUTHOR: &str = "AUTHOR";
3333
const ENV_DOC: &str = "DOC";
34+
const ENV_FILE_RUST_LOG: &str = "IROH_FILE_RUST_LOG";
3435

3536
/// Fetches the environment variable `IROH_<key>` from the current process.
3637
pub(crate) fn env_var(key: &str) -> std::result::Result<String, env::VarError> {
@@ -91,6 +92,7 @@ pub(crate) struct NodeConfig {
9192
pub(crate) gc_policy: GcPolicy,
9293
/// Bind address on which to serve Prometheus metrics
9394
pub(crate) metrics_addr: Option<SocketAddr>,
95+
pub(crate) file_logs: super::logging::FileLogging,
9496
}
9597

9698
impl Default for NodeConfig {
@@ -100,6 +102,7 @@ impl Default for NodeConfig {
100102
relay_nodes: [default_na_relay_node(), default_eu_relay_node()].into(),
101103
gc_policy: GcPolicy::Disabled,
102104
metrics_addr: Some(([127, 0, 0, 1], 9090).into()),
105+
file_logs: Default::default(),
103106
}
104107
}
105108
}
@@ -174,7 +177,13 @@ impl NodeConfig {
174177

175178
let cfg = builder.build()?;
176179
debug!("make_config:\n{:#?}\n", cfg);
177-
let cfg = cfg.try_deserialize()?;
180+
let mut cfg: NodeConfig = cfg.try_deserialize()?;
181+
182+
// override from env var
183+
// NOTE: explicitely doing this since `dep:config` will be removed.
184+
if let Some(env_filter) = env_file_rust_log().transpose()? {
185+
cfg.file_logs.rust_log = env_filter;
186+
}
178187
Ok(cfg)
179188
}
180189

@@ -332,6 +341,18 @@ fn env_doc() -> Result<Option<NamespaceId>> {
332341
}
333342
}
334343

344+
/// Parse [`ENV_FILE_RUST_LOG`] as [`tracing_subscriber::EnvFilter`]. Returns `None` if not
345+
/// present.
346+
fn env_file_rust_log() -> Option<Result<crate::logging::EnvFilter>> {
347+
match env::var(ENV_FILE_RUST_LOG) {
348+
Ok(s) => Some(crate::logging::EnvFilter::from_str(&s).map_err(Into::into)),
349+
Err(e) => match e {
350+
env::VarError::NotPresent => None,
351+
e @ env::VarError::NotUnicode(_) => Some(Err(e.into())),
352+
},
353+
}
354+
}
355+
335356
/// Name of directory that wraps all iroh files in a given application directory
336357
const IROH_DIR: &str = "iroh";
337358

iroh-cli/src/logging.rs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
use std::path::Path;
2+
3+
use derive_more::FromStr;
4+
use serde::{Deserialize, Serialize};
5+
use serde_with::{DeserializeFromStr, SerializeDisplay};
6+
use tracing_appender::{non_blocking, rolling};
7+
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, Layer};
8+
9+
/// `RUST_LOG` statement used by default in file logging.
10+
// rustyline is annoying
11+
pub(crate) const DEFAULT_FILE_RUST_LOG: &str = "rustyline=warn,debug";
12+
13+
/// Initialize logging both in the terminal and file based.
14+
///
15+
/// The terminal based logging layer will:
16+
/// - use the default [`fmt::format::Format`].
17+
/// - log to [`std::io::Stderr`]
18+
///
19+
/// The file base logging layer will:
20+
/// - use the default [`fmt::format::Format`] save for:
21+
/// - including line numbers.
22+
/// - not using ansi colors.
23+
/// - create log files in the `logs` dir inside the given `iroh_data_root`.
24+
/// - rotate files every [`Self::rotation`].
25+
/// - keep at most [`Self::max_files`] log files.
26+
/// - use the filtering defined by [`Self::rust_log`]. When not provided, the default
27+
/// [`DEFAULT_FILE_RUST_LOG`] is used.
28+
/// - create log files with the name `iroh-<ROTATION_BASED_NAME>.log` (ex: iroh-2024-02-02.log)
29+
pub(crate) fn init_terminal_and_file_logging(
30+
file_log_config: &FileLogging,
31+
logs_dir: &Path,
32+
) -> anyhow::Result<non_blocking::WorkerGuard> {
33+
let terminal_layer = fmt::layer()
34+
.with_writer(std::io::stderr)
35+
.with_filter(tracing_subscriber::EnvFilter::from_default_env());
36+
let (file_layer, guard) = {
37+
let FileLogging {
38+
rust_log,
39+
max_files,
40+
rotation,
41+
} = file_log_config;
42+
43+
let filter = rust_log.layer();
44+
45+
let (file_logger, guard) = {
46+
let file_appender = if *max_files == 0 || &filter.to_string() == "off" {
47+
fmt::writer::OptionalWriter::none()
48+
} else {
49+
let rotation = match rotation {
50+
Rotation::Hourly => rolling::Rotation::HOURLY,
51+
Rotation::Daily => rolling::Rotation::DAILY,
52+
Rotation::Never => rolling::Rotation::NEVER,
53+
};
54+
let logs_path = logs_dir.join("logs");
55+
56+
let file_appender = rolling::Builder::new()
57+
.rotation(rotation)
58+
.max_log_files(*max_files)
59+
.filename_prefix("iroh")
60+
.filename_suffix("log")
61+
.build(logs_path)?;
62+
fmt::writer::OptionalWriter::some(file_appender)
63+
};
64+
non_blocking(file_appender)
65+
};
66+
67+
let layer = fmt::Layer::new()
68+
.with_ansi(false)
69+
.with_line_number(true)
70+
.with_writer(file_logger)
71+
.with_filter(filter);
72+
(layer, guard)
73+
};
74+
tracing_subscriber::registry()
75+
.with(file_layer)
76+
.with(terminal_layer)
77+
.try_init()?;
78+
Ok(guard)
79+
}
80+
81+
/// Initialize logging in the terminal.
82+
///
83+
/// This will:
84+
/// - use the default [`fmt::format::Format`].
85+
/// - log to [`std::io::Stderr`]
86+
pub(crate) fn init_terminal_logging() -> anyhow::Result<()> {
87+
let terminal_layer = fmt::layer()
88+
.with_writer(std::io::stderr)
89+
.with_filter(tracing_subscriber::EnvFilter::from_default_env());
90+
tracing_subscriber::registry()
91+
.with(terminal_layer)
92+
.try_init()?;
93+
Ok(())
94+
}
95+
96+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
97+
#[serde(default)]
98+
pub(crate) struct FileLogging {
99+
/// RUST_LOG directive to filter file logs.
100+
pub(crate) rust_log: EnvFilter,
101+
/// Maximum number of files to keep.
102+
pub(crate) max_files: usize,
103+
/// How often should a new log file be produced.
104+
pub(crate) rotation: Rotation,
105+
}
106+
107+
impl Default for FileLogging {
108+
fn default() -> Self {
109+
Self {
110+
rust_log: EnvFilter::default(),
111+
max_files: 4,
112+
rotation: Rotation::default(),
113+
}
114+
}
115+
}
116+
117+
/// Wrapper to obtain a [`tracing_subscriber::EnvFilter`] that satisfies required bounds.
118+
#[derive(
119+
Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, derive_more::Display,
120+
)]
121+
#[display("{_0}")]
122+
pub(crate) struct EnvFilter(String);
123+
124+
impl FromStr for EnvFilter {
125+
type Err = <tracing_subscriber::EnvFilter as FromStr>::Err;
126+
127+
fn from_str(s: &str) -> Result<Self, Self::Err> {
128+
// validate the RUST_LOG statement
129+
let _valid_env = tracing_subscriber::EnvFilter::from_str(s)?;
130+
Ok(EnvFilter(s.into()))
131+
}
132+
}
133+
134+
impl Default for EnvFilter {
135+
fn default() -> Self {
136+
Self(DEFAULT_FILE_RUST_LOG.into())
137+
}
138+
}
139+
140+
impl EnvFilter {
141+
pub(crate) fn layer(&self) -> tracing_subscriber::EnvFilter {
142+
tracing_subscriber::EnvFilter::from_str(&self.0).expect("validated RUST_LOG statement")
143+
}
144+
}
145+
146+
/// How often should a new file be created for file logs.
147+
///
148+
/// Akin to [`tracing_appender::rolling::Rotation`].
149+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
150+
#[serde(rename_all = "lowercase")]
151+
pub(crate) enum Rotation {
152+
#[default]
153+
Hourly,
154+
Daily,
155+
Never,
156+
}
157+
158+
#[cfg(test)]
159+
mod tests {
160+
use super::*;
161+
162+
/// Tests that the default file logging `RUST_LOG` statement produces a valid layer.
163+
#[test]
164+
fn test_default_file_rust_log() {
165+
let _ = EnvFilter::default().layer();
166+
}
167+
}

0 commit comments

Comments
 (0)