mecomp_core/
logger.rs

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