mecomp_core/
logger.rs

1use std::time::Instant;
2use std::{io::Write, sync::LazyLock};
3
4use env_logger::fmt::style::{RgbColor, Style};
5use log::{Record, info};
6#[cfg(feature = "otel_tracing")]
7use opentelemetry::trace::TracerProvider as _;
8#[cfg(feature = "otel_tracing")]
9use opentelemetry_otlp::WithExportConfig as _;
10#[cfg(feature = "otel_tracing")]
11use opentelemetry_sdk::Resource;
12#[cfg(feature = "otel_tracing")]
13use tracing_subscriber::Layer as _;
14#[cfg(any(feature = "otel_tracing", feature = "flame", feature = "tokio_console"))]
15use tracing_subscriber::layer::SubscriberExt as _;
16
17use crate::format_duration;
18
19// This will get initialized below.
20/// Returns the init [`Instant`]
21pub static INIT_INSTANT: LazyLock<Instant> = LazyLock::new(Instant::now);
22
23/// Returns the seconds since [`INIT_INSTANT`].
24#[cfg(not(tarpaulin_include))]
25#[inline]
26pub fn uptime() -> u64 {
27    INIT_INSTANT.elapsed().as_secs()
28}
29
30#[allow(clippy::module_name_repetitions)]
31/// Initializes the logger.
32///
33/// This enables console logging on all the internals of `Mecomp`.
34///
35/// Functionality is provided by [`log`].
36///
37/// The levels are:
38/// - ERROR
39/// - WARN
40/// - INFO
41/// - DEBUG
42/// - TRACE
43///
44/// # Panics
45/// This must only be called _once_.
46#[cfg(not(tarpaulin_include))]
47#[allow(clippy::missing_inline_in_public_items)]
48pub fn init_logger(filter: log::LevelFilter, log_file_path: Option<std::path::PathBuf>) {
49    // Initialize timer.
50    let now = LazyLock::force(&INIT_INSTANT);
51
52    // create a new log file (if enabled).
53    let log_file = log_file_path.map(|path| {
54        let path = if path.is_dir() {
55            path.join("mecomp.log")
56        } else {
57            path
58        };
59
60        std::fs::OpenOptions::new()
61            .create(true)
62            .append(true)
63            .open(path)
64            .expect("Failed to create log file")
65    });
66
67    // If `RUST_LOG` isn't set, override it and disables
68    // all library crate logs except for mecomp and its sub-crates.
69    let mut env = String::new();
70    match std::env::var("RUST_LOG") {
71        Ok(e) => {
72            unsafe {
73                // SAFETY: This is safe because this code runs before we start spawning threads.
74                std::env::set_var("RUST_LOG", &e);
75            }
76            env = e;
77        }
78        // SOMEDAY:
79        // Support frontend names without *mecomp*.
80        _ => unsafe {
81            // SAFETY: This is safe because this code runs before we start spawning threads.
82            std::env::set_var(
83                "RUST_LOG",
84                format!("off,mecomp={filter},surrealqlx={filter}"),
85            );
86        },
87    }
88
89    env_logger::Builder::new()
90        .format(move |buf, record| {
91            let style = buf.default_level_style(record.level());
92            let (level_style, level) = match record.level() {
93                log::Level::Debug => (
94                    style
95                        .fg_color(Some(RgbColor::from((0, 0x80, 0x80)).into()))
96                        .bold(),
97                    "D",
98                ),
99                log::Level::Trace => (
100                    style
101                        .fg_color(Some(RgbColor::from((255, 0, 255)).into()))
102                        .bold(),
103                    "T",
104                ),
105                log::Level::Info => (
106                    style
107                        .fg_color(Some(RgbColor::from((255, 255, 255)).into()))
108                        .bold(),
109                    "I",
110                ),
111                log::Level::Warn => (
112                    style
113                        .fg_color(Some(RgbColor::from((255, 255, 0)).into()))
114                        .bold(),
115                    "W",
116                ),
117                log::Level::Error => (
118                    style
119                        .fg_color(Some(RgbColor::from((255, 0, 0)).into()))
120                        .bold(),
121                    "E",
122                ),
123            };
124
125            let dimmed_style = Style::default().dimmed();
126
127            let log_line = format!(
128                // Longest PATH in the repo: `storage/src/db/schemas/dynamic/query.rs` - `39` characters
129                // Longest file in the repo: `core/src/audio/mod.rs`                   - `4` digits
130                //
131                // Use `scripts/longest.sh` to find this.
132                //
133                //                                                                             Longest PATH ---|        |--- Longest file
134                //                                                                                             |        |
135                //                                                                                             v        v
136                "| {level_style}{level}{level_style:#} | {dimmed_style}{}{dimmed_style:#} | {dimmed_style}{: >39} @ {: <4}{dimmed_style:#} | {}",
137                format_duration(&now.elapsed()),
138                process_path_of(record),
139                record.line().unwrap_or_default(),
140                record.args(),
141            );
142            writeln!(buf, "{log_line}")?;
143
144            // Write to log file (if enabled).
145            if let Some(log_file) = &log_file {
146                let mut log_file = log_file.try_clone().expect("Failed to clone log file");
147
148                // Remove ANSI formatting from log line before writing to file.
149                let unformatted_log_line: String = log_line
150                    .replace(&level_style.render().to_string(), "")
151                    .replace(&dimmed_style.render().to_string(), "")
152                    .replace("\x1B[0m", "");
153
154                writeln!(log_file, "{unformatted_log_line}")?;
155                log_file.sync_all().expect("Failed to sync log file");
156            }
157
158            Ok(())
159        })
160        .write_style(env_logger::WriteStyle::Always)
161        .parse_default_env()
162        .init();
163
164    if env.is_empty() {
165        info!("Log Level (Flag) ... {filter}");
166    } else {
167        info!("Log Level (RUST_LOG) ... {env}");
168    }
169}
170
171/// In debug builds, we want file paths so that we can Ctrl+Click them in an IDE to open the relevant file.
172/// But in release, all we want is to be able to tell what module the log is coming from.
173///
174/// This function will behave differently depending on the build type in order to achieve this.
175///
176/// In debug builds, if we get an absolute file path for a mecomp file, we want to strip everything before the `mecomp/` part to keep things clean.
177fn process_path_of<'a>(record: &'a Record<'a>) -> &'a str {
178    #[cfg(debug_assertions)]
179    const DEBUG_BUILD: bool = true;
180    #[cfg(not(debug_assertions))]
181    const DEBUG_BUILD: bool = false;
182
183    let module_path = record.module_path();
184    let file_path = record.file();
185
186    match (DEBUG_BUILD, module_path, file_path) {
187        // In debug builds, if we get an absolute file path for a mecomp file, we want to strip everything before the `mecomp/` part to keep things clean.
188        // and in debug builds, we fall back to this if the file path is not available.
189        (true, _, Some(file)) | (false, None, Some(file)) => {
190            // if the file is an absolute path, strip everything before the `mecomp/` part
191            if file.contains("mecomp/") {
192                file.split("mecomp/").last().unwrap_or(file)
193            } else {
194                file
195            }
196        }
197        // in debug builds, we fallback to the module path if the file is not available
198        // and in release builds, we want to use the module path by default.
199        (true, Some(module), None) | (false, Some(module), _) => module,
200
201        // otherwise just use a fallback
202        (true | false, None, None) => "??",
203    }
204}
205
206/// Initializes the tracing layer.
207///
208/// # Panics
209///
210/// panics if the tracing layers cannot be initialized.
211#[must_use]
212#[allow(clippy::missing_inline_in_public_items)]
213pub fn init_tracing() -> impl tracing::Subscriber {
214    let subscriber = tracing_subscriber::registry();
215
216    #[cfg(feature = "flame")]
217    let (flame_layer, _guard) = tracing_flame::FlameLayer::with_file("tracing.folded").unwrap();
218    #[cfg(feature = "flame")]
219    let subscriber = subscriber.with(flame_layer);
220
221    #[cfg(not(feature = "verbose_tracing"))]
222    #[allow(unused_variables)]
223    let filter = tracing_subscriber::EnvFilter::builder()
224        .parse("off,mecomp=trace,surrealqlx=trace")
225        .unwrap();
226    #[cfg(feature = "verbose_tracing")]
227    #[allow(unused_variables)]
228    let filter = tracing_subscriber::EnvFilter::new("trace")
229        .add_directive("hyper=off".parse().unwrap())
230        .add_directive("opentelemetry=off".parse().unwrap())
231        .add_directive("tonic=off".parse().unwrap())
232        .add_directive("h2=off".parse().unwrap())
233        .add_directive("reqwest=off".parse().unwrap());
234
235    #[cfg(feature = "otel_tracing")]
236    unsafe {
237        // SAFETY: This is safe because this code runs before we start spawning threads.
238        std::env::set_var("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "12");
239    }
240    #[cfg(feature = "otel_tracing")]
241    let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder()
242        .with_batch_exporter(
243            opentelemetry_otlp::SpanExporter::builder()
244                .with_tonic()
245                .with_endpoint("http://localhost:4317")
246                .build()
247                .expect("Failed to build OTLP exporter"),
248        )
249        .with_id_generator(opentelemetry_sdk::trace::RandomIdGenerator::default())
250        .with_resource(Resource::builder().with_service_name("mecomp").build())
251        .build()
252        .tracer("mecomp");
253
254    #[cfg(feature = "otel_tracing")]
255    let subscriber = subscriber.with(
256        tracing_opentelemetry::layer()
257            .with_tracer(tracer)
258            .with_filter(filter),
259    );
260
261    #[cfg(feature = "tokio_console")]
262    let console_layer = console_subscriber::Builder::default()
263        .retention(std::time::Duration::from_secs(60 * 20)) // 20 minutes
264        .enable_self_trace(true)
265        .spawn();
266    #[cfg(feature = "tokio_console")]
267    let subscriber = subscriber.with(console_layer);
268
269    subscriber
270}