Skip to main content

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