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}