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