Skip to main content

re_log/
lib.rs

1//! Text logging (nothing to do with rerun logging) for use in rerun libraries.
2//!
3//! Provides helpers for adding multiple loggers,
4//! and for setting up logging on native and on web.
5//!
6//! * `trace`: spammy things
7//! * `debug`: things that might be useful when debugging
8//! * `info`: things that we want to show to users
9//! * `warn`: problems that we can recover from
10//! * `error`: problems that lead to loss of functionality or data
11//!
12//! The `warn_once` etc macros are for when you want to suppress repeated
13//! logging of the exact same message.
14//!
15//! In the viewer these logs, if >= info, become notifications. See
16//! `re_ui::notifications` for more information.
17
18#[cfg(feature = "setup")]
19mod channel_logger;
20mod debug_assert;
21#[cfg(feature = "setup")]
22mod event_visitor;
23mod log_once;
24mod result_extensions;
25#[cfg(feature = "setup")]
26mod setup;
27
28#[cfg(feature = "setup")]
29pub use channel_logger::{LogMsg, Receiver, Sender, add_log_msg_receiver};
30#[cfg(feature = "setup")]
31pub use event_visitor::FieldValue;
32pub use log_once::LogOnceSet;
33pub use result_extensions::ResultExt;
34#[cfg(all(feature = "setup", not(target_arch = "wasm32")))]
35pub use setup::PanicOnWarnScope;
36#[cfg(feature = "setup")]
37pub use setup::{setup_logging, setup_logging_with_filter};
38pub use tracing::Level;
39#[cfg(feature = "setup")]
40pub use tracing_subscriber::filter::LevelFilter;
41// The tracing macros support more syntax features than the log, that's why we use them:
42pub use tracing::{debug, error, event, info, trace, warn};
43
44/// Log a warning in debug builds, or a debug message in release builds.
45///
46/// This is useful for logging messages that should be visible during development
47/// (to help catch issues), but shouldn't spam the logs in release builds.
48///
49/// In debug builds, the message is prefixed with "DEBUG: " and logged at WARN level.
50/// In release builds, the message is logged at DEBUG level without any prefix.
51///
52/// This macro never triggers panic-on-warn (`RERUN_PANIC_ON_WARN` or `PanicOnWarnScope`):
53/// that is meant to catch user-facing warnings, and this macro is never a warning
54/// in release builds.
55#[cfg(debug_assertions)]
56#[macro_export]
57macro_rules! debug_warn {
58    ($($arg:tt)+) => {
59        $crate::_with_panic_on_warn_suppressed(|| $crate::warn!("DEBUG: {}", format_args!($($arg)+)))
60    };
61}
62
63/// Log a warning in debug builds, or a debug message in release builds.
64///
65/// This is useful for logging messages that should be visible during development
66/// (to help catch issues), but shouldn't spam the logs in release builds.
67///
68/// In debug builds, the message is prefixed with "DEBUG: " and logged at WARN level.
69/// In release builds, the message is logged at DEBUG level without any prefix.
70#[cfg(not(debug_assertions))]
71#[macro_export]
72macro_rules! debug_warn {
73    ($($arg:tt)+) => {
74        $crate::debug!($($arg)+)
75    };
76}
77
78/// Like [`debug_warn!`], but only logs once per call site.
79///
80/// This is useful for logging messages that should be visible during development
81/// (to help catch issues), but shouldn't spam the logs in release builds.
82///
83/// In debug builds, the message is prefixed with "DEBUG: " and logged at WARN level.
84/// In release builds, the message is logged at DEBUG level without any prefix.
85///
86/// This macro never triggers panic-on-warn (`RERUN_PANIC_ON_WARN` or `PanicOnWarnScope`):
87/// that is meant to catch user-facing warnings, and this macro is never a warning
88/// in release builds.
89#[cfg(debug_assertions)]
90#[macro_export]
91macro_rules! debug_warn_once {
92    ($($arg:tt)+) => {
93        $crate::_with_panic_on_warn_suppressed(|| $crate::warn_once!("DEBUG: {}", format_args!($($arg)+)))
94    };
95}
96
97/// Like [`debug_warn!`], but only logs once per call site.
98///
99/// This is useful for logging messages that should be visible during development
100/// (to help catch issues), but shouldn't spam the logs in release builds.
101///
102/// In debug builds, the message is prefixed with "DEBUG: " and logged at WARN level.
103/// In release builds, the message is logged at DEBUG level without any prefix.
104#[cfg(not(debug_assertions))]
105#[macro_export]
106macro_rules! debug_warn_once {
107    ($($arg:tt)+) => {
108        $crate::debug_once!($($arg)+)
109    };
110}
111
112/// Re-exports of other crates.
113pub mod external {
114    pub use log;
115}
116
117/// Never log anything less serious than a `ERROR` from these crates.
118#[cfg(any(feature = "setup", not(target_arch = "wasm32")))]
119const CRATES_AT_ERROR_LEVEL: &[&str] = &[
120    // silence rustls in release mode: https://github.com/rerun-io/rerun/issues/3104
121    #[cfg(not(debug_assertions))]
122    "rustls",
123];
124
125/// Never log anything less serious than a `WARN` from these crates.
126#[cfg(any(feature = "setup", not(target_arch = "wasm32")))]
127const CRATES_AT_WARN_LEVEL: &[&str] = &[
128    // wgpu crates spam a lot on info level, which is really annoying
129    // TODO(emilk): remove once https://github.com/gfx-rs/wgpu/issues/3206 is fixed
130    "naga",
131    "tracing",
132    "wgpu_core",
133    "wgpu_hal",
134    "zbus",
135];
136
137/// Never log anything less serious than a `INFO` from these crates.
138///
139/// These creates are quite spammy on debug, drowning out what we care about:
140#[cfg(any(feature = "setup", not(target_arch = "wasm32")))]
141const CRATES_AT_INFO_LEVEL: &[&str] = &[
142    "datafusion_optimizer",
143    "datafusion",
144    "h2",
145    "hyper",
146    "opentelemetry", // Spams about NoopMeterProvider
147    "prost_build",
148    "reqwest", // Spams "starting new connection: …"
149    "sqlparser",
150    "tonic_web",
151    "tower",
152    "ureq",
153    // only let rustls log in debug mode: https://github.com/rerun-io/rerun/issues/3104
154    #[cfg(debug_assertions)]
155    "rustls",
156    // walkers generates noise around tile download, see https://github.com/podusowski/walkers/issues/199
157    "walkers",
158    // winit 0.30.5 spams about `set_cursor_visible` calls. It's gone on winit master, so hopefully gone in next winit release.
159    "winit",
160];
161
162/// Determines the default log filter.
163///
164/// Native: Get `RUST_LOG` environment variable or `info`, if not set.
165/// Also sets some other log levels on crates that are too loud.
166///
167/// Web: `debug` since web console allows arbitrary filtering.
168#[cfg(not(target_arch = "wasm32"))]
169pub fn default_log_filter() -> String {
170    let base_log_filter = if cfg!(debug_assertions) {
171        // We want the DEBUG level to be useful yet not too spammy.
172        // This is a good way to enforce that.
173        "debug"
174    } else {
175        // Important to keep the default at (at least) "info",
176        // as we print crucial information at INFO,
177        // e.g. the ip:port when hosting a server with `rerun-cli`.
178        "info"
179    };
180    log_filter_from_env_or_default(base_log_filter)
181}
182
183/// Determines the default log filter.
184///
185/// Native: Get `RUST_LOG` environment variable or `info`, if not set.
186/// Also sets some other log levels on crates that are too loud.
187///
188/// Web: `debug` since web console allows arbitrary filtering.
189#[cfg(target_arch = "wasm32")]
190pub fn default_log_filter() -> String {
191    "debug".to_owned()
192}
193
194/// Determines the log filter from the `RUST_LOG` environment variable or an explicit default.
195///
196/// Always adds builtin filters as well.
197#[cfg(not(target_arch = "wasm32"))]
198pub fn log_filter_from_env_or_default(default_base_log_filter: &str) -> String {
199    let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| default_base_log_filter.to_owned());
200    add_builtin_log_filter(&rust_log)
201}
202
203/// Adds builtin log level filters for crates that are too verbose.
204#[cfg(not(target_arch = "wasm32"))]
205fn add_builtin_log_filter(base_log_filter: &str) -> String {
206    use std::fmt::Write as _;
207
208    let mut rust_log = base_log_filter.to_lowercase();
209
210    if base_log_filter != "off" {
211        // If base level is `off`, don't opt-in to anything.
212
213        for crate_name in crate::CRATES_AT_ERROR_LEVEL {
214            if !rust_log.contains(&format!("{crate_name}=")) {
215                write!(rust_log, ",{crate_name}=error").ok();
216            }
217        }
218
219        if base_log_filter != "error" {
220            // If base level is `error`, don't opt-in to `warn` or `info`.
221
222            for crate_name in crate::CRATES_AT_WARN_LEVEL {
223                if !rust_log.contains(&format!("{crate_name}=")) {
224                    write!(rust_log, ",{crate_name}=warn").ok();
225                }
226            }
227
228            if base_log_filter != "warn" {
229                // If base level is not `error`/`warn`, don't opt-in to `info`.
230
231                for crate_name in crate::CRATES_AT_INFO_LEVEL {
232                    if !rust_log.contains(&format!("{crate_name}=")) {
233                        write!(rust_log, ",{crate_name}=info").ok();
234                    }
235                }
236            }
237        }
238    }
239
240    //TODO(#8077): should be removed as soon as the upstream issue is resolved
241    rust_log += ",walkers::download=off";
242
243    rust_log
244}
245
246/// Should we log this message given the filter?
247#[cfg(feature = "setup")]
248fn is_log_enabled(
249    filter: tracing_subscriber::filter::LevelFilter,
250    target: &str,
251    level: &tracing::Level,
252) -> bool {
253    if CRATES_AT_ERROR_LEVEL
254        .iter()
255        .any(|crate_name| target.starts_with(crate_name))
256    {
257        *level <= tracing_subscriber::filter::LevelFilter::ERROR
258    } else if CRATES_AT_WARN_LEVEL
259        .iter()
260        .any(|crate_name| target.starts_with(crate_name))
261    {
262        *level <= tracing_subscriber::filter::LevelFilter::WARN
263    } else if CRATES_AT_INFO_LEVEL
264        .iter()
265        .any(|crate_name| target.starts_with(crate_name))
266    {
267        *level <= tracing_subscriber::filter::LevelFilter::INFO
268    } else {
269        *level <= filter
270    }
271}
272
273/// Check if an environment variable is set to a truthy value.
274///
275/// Returns `true` if the environment variable is set to "1/true/yes/on" (case-insensitive).
276/// Returns `false` if the environment variable is set to "0/false/no/off" (case-insensitive).
277/// Otherwise returns `None`.
278///
279/// # Example
280///
281/// ```ignore
282/// if env_var_flag("TELEMETRY_ENABLED") == Some(true) {
283///     // enable telemetry
284/// }
285/// ```
286pub fn env_var_flag(var_name: &str) -> Option<bool> {
287    match std::env::var(var_name)
288        .ok()?
289        .trim()
290        .to_ascii_lowercase()
291        .as_str()
292    {
293        "" => None,
294        "0" | "false" | "no" | "off" => Some(false),
295        "1" | "true" | "yes" | "on" => Some(true),
296        value => {
297            crate::warn_once!(
298                "Ignoring unrecognized value {value:?} for environment variable {var_name:?} \
299                    (expected one of: 1/true/yes/on, 0/false/no/off); falling back to the default."
300            );
301            None
302        }
303    }
304}
305
306/// Check if an environment variable is set to a truthy value.
307///
308/// Returns `true` if the environment variable is set to "1/true/yes/on" (case-insensitive).
309/// Otherwise returns `false`.
310///
311/// # Example
312///
313/// ```ignore
314/// if env_var_is_truthy("TELEMETRY_ENABLED") {
315///     // enable telemetry
316/// }
317/// ```
318pub fn env_var_is_truthy(var_name: &str) -> bool {
319    env_var_flag(var_name).unwrap_or(false)
320}
321
322/// Is `RERUN_VERY_STRICT` set to a truthy value?
323///
324/// In very strict mode, Rerun may panic anywhere, at any time, for any reason whenever it
325/// detects something it does not like — e.g. out-of-order chunks, unsorted timelines,
326/// or other invariant violations. Very strict mode is meant for development, testing and
327/// CI, never for production: enable it to catch silent corruption early.
328///
329/// The result is cached on the first call, so subsequent calls are very cheap and
330/// changing the environment variable at runtime has no effect.
331pub fn is_rerun_very_strict() -> bool {
332    static VERY_STRICT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
333    *VERY_STRICT.get_or_init(|| env_var_is_truthy("RERUN_VERY_STRICT"))
334}
335
336/// Is `RERUN_PANIC_ON_WARN` set to a truthy value?
337///
338/// When enabled, any user-facing warning or error log message causes a panic
339/// (see `setup_logging`). This is meant for tests and CI, to catch warnings early.
340///
341/// The result is cached on the first call, so subsequent calls are very cheap and
342/// changing the environment variable at runtime has no effect.
343pub fn is_panic_on_warn() -> bool {
344    static PANIC_ON_WARN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
345    *PANIC_ON_WARN.get_or_init(|| env_var_is_truthy("RERUN_PANIC_ON_WARN"))
346}
347
348thread_local! {
349    static SUPPRESS_PANIC_ON_WARN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
350}
351
352/// Runs `f` with panic-on-warn suppressed on the current thread.
353///
354/// Used by [`debug_warn!`] & co, which are warnings only in debug builds
355/// and thus shouldn't trip `RERUN_PANIC_ON_WARN` or `PanicOnWarnScope`.
356///
357/// This relies on `tracing` dispatching events synchronously on the emitting thread.
358#[doc(hidden)] // implementation detail of the `debug_warn!` family
359pub fn _with_panic_on_warn_suppressed<R>(f: impl FnOnce() -> R) -> R {
360    // RAII-restore, so a panic during `f` (e.g. while formatting) doesn't leak the flag.
361    struct Guard(bool);
362
363    impl Drop for Guard {
364        fn drop(&mut self) {
365            SUPPRESS_PANIC_ON_WARN.with(|suppress| suppress.set(self.0));
366        }
367    }
368
369    let _guard = Guard(SUPPRESS_PANIC_ON_WARN.with(|suppress| suppress.replace(true)));
370    f()
371}
372
373/// Is panic-on-warn currently suppressed on this thread (see [`_with_panic_on_warn_suppressed`])?
374#[cfg(all(feature = "setup", not(target_arch = "wasm32")))] // only used by the `PanicOnWarn` layer
375pub(crate) fn is_panic_on_warn_suppressed() -> bool {
376    SUPPRESS_PANIC_ON_WARN.with(|suppress| suppress.get())
377}
378
379/// Shorten a path to a Rust source file.
380///
381/// Example input:
382/// * `/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs`
383/// * `crates/rerun/src/main.rs`
384/// * `/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs`
385///
386/// Example output:
387/// * `tokio-1.24.1/src/runtime/runtime.rs`
388/// * `rerun/src/main.rs`
389/// * `core/src/ops/function.rs`
390#[allow(clippy::allow_attributes, dead_code)] // only used on web and in tests
391fn shorten_file_path(file_path: &str) -> &str {
392    if let Some(i) = file_path.rfind("/src/") {
393        if let Some(prev_slash) = file_path[..i].rfind('/') {
394            &file_path[prev_slash + 1..]
395        } else {
396            file_path
397        }
398    } else {
399        file_path
400    }
401}
402
403#[test]
404fn test_shorten_file_path() {
405    for (before, after) in [
406        (
407            "/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
408            "tokio-1.24.1/src/runtime/runtime.rs",
409        ),
410        ("crates/rerun/src/main.rs", "rerun/src/main.rs"),
411        (
412            "/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
413            "core/src/ops/function.rs",
414        ),
415        ("/weird/path/file.rs", "/weird/path/file.rs"),
416    ] {
417        assert_eq!(shorten_file_path(before), after);
418    }
419}