1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Integrates the Rerun SDK with the [`log`] crate.

use log::Log;
use re_types::{archetypes::TextLog, components::TextLogLevel};

use crate::RecordingStream;

// ---

/// Implements a [`log::Log`] that forwards all events to the Rerun SDK.
///
/// ```
/// let rec = rerun::RecordingStreamBuilder::new("rerun_example_app").buffered()?;
///
/// rerun::Logger::new(rec.clone()) // recording streams are ref-counted
///     .with_path_prefix("logs")
///     .with_filter(rerun::default_log_filter())
///     .init()?;
///
/// log::info!("This INFO log got added through the standard logging interface");
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
#[derive(Debug)]
pub struct Logger {
    rec: RecordingStream,
    filter: Option<env_logger::filter::Filter>,
    path_prefix: Option<String>,
}

impl Drop for Logger {
    fn drop(&mut self) {
        self.flush();
    }
}

impl Logger {
    /// Returns a new [`Logger`] that forwards all events to the specified [`RecordingStream`].
    pub fn new(rec: RecordingStream) -> Self {
        Self {
            rec,
            filter: None,
            path_prefix: None,
        }
    }

    /// Configures the [`Logger`] to prefix the specified `path_prefix` to all events.
    #[inline]
    pub fn with_path_prefix(mut self, path_prefix: impl Into<String>) -> Self {
        self.path_prefix = Some(path_prefix.into());
        self
    }

    /// Configures the [`Logger`] to filter events.
    ///
    /// This uses the familiar [env_logger syntax].
    ///
    /// If you don't call this, the [`Logger`] will parse the `RUST_LOG` environment variable
    /// instead when you [`Logger::init`] it.
    ///
    /// [env_logger syntax]: https://docs.rs/env_logger/latest/env_logger/index.html#enabling-logging
    #[inline]
    pub fn with_filter(mut self, filter: impl AsRef<str>) -> Self {
        use env_logger::filter::Builder;
        self.filter = Some(Builder::new().parse(filter.as_ref()).build());
        self
    }

    /// Sets the [`Logger`] as global logger.
    ///
    /// All calls to [`log`] macros will go through this [`Logger`] from this point on.
    pub fn init(mut self) -> Result<(), log::SetLoggerError> {
        if self.filter.is_none() {
            use env_logger::filter::Builder;
            self.filter = Some(Builder::new().parse(&re_log::default_log_filter()).build());
        }

        // NOTE: We will have to make filtering decisions on a per-crate/module basis, therefore
        // there is no global filtering ceiling.
        log::set_max_level(log::LevelFilter::max());
        log::set_boxed_logger(Box::new(self))
    }
}

impl log::Log for Logger {
    #[inline]
    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
        self.filter
            .as_ref()
            .map_or(true, |filter| filter.enabled(metadata))
    }

    #[inline]
    fn log(&self, record: &log::Record<'_>) {
        if !self
            .filter
            .as_ref()
            .map_or(true, |filter| filter.matches(record))
        {
            return;
        }

        let target = record.metadata().target().replace("::", "/");
        let ent_path = if let Some(path_prefix) = self.path_prefix.as_ref() {
            format!("{path_prefix}/{target}")
        } else {
            target
        };

        let level = log_level_to_rerun_level(record.metadata().level());

        let body = format!("{}", record.args());

        self.rec
            .log(ent_path, &TextLog::new(body).with_level(level))
            .ok(); // ignore error
    }

    #[inline]
    fn flush(&self) {
        self.rec.flush_blocking();
    }
}

// ---

fn log_level_to_rerun_level(lvl: log::Level) -> TextLogLevel {
    match lvl {
        log::Level::Error => TextLogLevel::ERROR,
        log::Level::Warn => TextLogLevel::WARN,
        log::Level::Info => TextLogLevel::INFO,
        log::Level::Debug => TextLogLevel::DEBUG,
        log::Level::Trace => TextLogLevel::TRACE,
    }
    .into()
}