summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-23 11:26:01 +0200
committertv <tv@krebsco.de>2026-10-23 11:26:01 +0200
commit1acd998742b9b15ce754650915ccefd7f6e64939 (patch)
tree49baf7d6db62179b839eebe4aa82a6f3eeeb3e6b
parentdd4454a163d2a2afbffdd62ee6008ac8ee86dc09 (diff)
allow changing log filtering while node runs
This also changes the default to tracing-subscriber.
-rw-r--r--src/api/log.rs32
-rw-r--r--src/api/mod.rs5
-rw-r--r--src/logging.rs58
-rw-r--r--src/main.rs3
4 files changed, 97 insertions, 1 deletions
diff --git a/src/api/log.rs b/src/api/log.rs
new file mode 100644
index 0000000..5d44926
--- /dev/null
+++ b/src/api/log.rs
@@ -0,0 +1,32 @@
+//! /admin/log: read and replace the log filter of the running node.
+
+use axum::{body::Bytes, http::StatusCode};
+use tracing::{info, warn};
+
+use crate::logging;
+
+/// `GET /admin/log`: the filter in effect, as plain text in `RUST_LOG` syntax.
+pub(crate) async fn get_log_filter() -> Result<String, StatusCode> {
+ logging::filter().map_err(|error| {
+ warn!(error = %format!("{error:#}"), "Cannot read the log filter");
+ StatusCode::INTERNAL_SERVER_ERROR
+ })
+}
+
+/// `PUT /admin/log`: replace the filter; the body is plain text in `RUST_LOG` syntax,
+/// e.g. `info,continuum::swarm::sync=trace`. Answers with the filter now in effect;
+/// 400 (with the reason) if the body doesn't parse, and the old filter stays.
+pub(crate) async fn put_log_filter(body: Bytes) -> Result<String, (StatusCode, String)> {
+ let directives = std::str::from_utf8(&body)
+ .map_err(|_| {
+ (
+ StatusCode::BAD_REQUEST,
+ "The filter must be UTF-8\n".to_owned(),
+ )
+ })?
+ .trim();
+ let applied = logging::set_filter(directives)
+ .map_err(|error| (StatusCode::BAD_REQUEST, format!("{error:#}\n")))?;
+ info!(filter = %applied, "Log filter replaced");
+ Ok(applied + "\n")
+}
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 2399ec8..a2bd7e0 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -2,6 +2,7 @@
pub(crate) mod auth;
mod debug;
+mod log;
mod peers;
mod store;
@@ -37,6 +38,10 @@ pub(crate) fn router(state: AppState, auth_state: AuthState, debug_api: bool) ->
.route("/stream/{*key}", get(store::stream_store_entry))
.route("/peers", get(peers::list_peers))
.route("/peers/{peer}", delete(peers::delete_peer))
+ .route(
+ "/admin/log",
+ get(log::get_log_filter).put(log::put_log_filter),
+ )
.merge(crate::remake::router::<AppState>());
if debug_api {
diff --git a/src/logging.rs b/src/logging.rs
new file mode 100644
index 0000000..4496125
--- /dev/null
+++ b/src/logging.rs
@@ -0,0 +1,58 @@
+//! Logging: a compact fmt logger whose filter can be replaced while the node runs
+//! (`/admin/log`), and, with the `tokio-console` feature, a tokio-console layer.
+//!
+//! The filter starts from `RUST_LOG`, or `info` without it. It applies to the fmt
+//! logger only, so the console layer keeps getting the runtime events it needs.
+
+use std::sync::OnceLock;
+
+use anyhow::{Result, anyhow};
+use tracing_subscriber::{EnvFilter, Registry, prelude::*, reload};
+
+type FilterHandle = reload::Handle<EnvFilter, Registry>;
+
+static FILTER: OnceLock<FilterHandle> = OnceLock::new();
+
+/// Installs the global subscriber. Call once, first thing in main.
+pub(crate) fn init() {
+ let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
+ let (filter, handle) = reload::Layer::new(filter);
+
+ let registry = tracing_subscriber::registry().with(
+ tracing_subscriber::fmt::layer()
+ .compact()
+ .with_filter(filter),
+ );
+
+ #[cfg(feature = "tokio-console")]
+ let registry = registry
+ .with(console_subscriber::spawn().with_filter(EnvFilter::new("tokio=trace,runtime=trace")));
+
+ registry.init();
+ let _ = FILTER.set(handle);
+}
+
+/// The filter in effect, in `RUST_LOG` syntax.
+pub(crate) fn filter() -> Result<String> {
+ handle()?
+ .with_current(|filter| filter.to_string())
+ .map_err(|error| anyhow!("Reading the log filter: {error}"))
+}
+
+/// Replaces the filter. `directives` uses `RUST_LOG` syntax, e.g.
+/// `info,continuum::swarm::sync=trace`. An invalid one leaves the old filter in place.
+pub(crate) fn set_filter(directives: &str) -> Result<String> {
+ let filter = EnvFilter::try_new(directives)
+ .map_err(|error| anyhow!("Invalid log filter {directives:?}: {error}"))?;
+ let applied = filter.to_string();
+ handle()?
+ .reload(filter)
+ .map_err(|error| anyhow!("Replacing the log filter: {error}"))?;
+ Ok(applied)
+}
+
+fn handle() -> Result<&'static FilterHandle> {
+ FILTER
+ .get()
+ .ok_or_else(|| anyhow!("Logging is not initialised"))
+}
diff --git a/src/main.rs b/src/main.rs
index 1a9d68e..5abc0e2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,6 +20,7 @@ use url::Url;
mod api;
mod document;
+mod logging;
mod remake;
mod state;
mod swarm;
@@ -97,7 +98,7 @@ struct Args {
#[tokio::main]
async fn main() -> Result<()> {
- console_subscriber::init();
+ logging::init();
let args = Args::parse();