1use ansiterm::Colour;
4use std::str;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::{env, io};
7use tracing::{Level, Metadata};
8pub use tracing_subscriber::{
9 self,
10 filter::{filter_fn, EnvFilter, LevelFilter},
11 fmt::{format::FmtSpan, MakeWriter},
12 layer::SubscriberExt,
13};
14
15const ACTION_COLUMN_WIDTH: usize = 12;
16
17static JSON_MODE_ACTIVE: AtomicBool = AtomicBool::new(false);
19
20fn is_json_mode_active() -> bool {
22 JSON_MODE_ACTIVE.load(Ordering::SeqCst)
23}
24
25fn get_action_indentation(action: &str) -> String {
27 if action.len() < ACTION_COLUMN_WIDTH {
28 " ".repeat(ACTION_COLUMN_WIDTH - action.len())
29 } else {
30 String::new()
31 }
32}
33
34enum TextStyle {
35 Plain,
36 Bold,
37 Label(String),
38 Action(String),
39}
40
41enum LogLevel {
42 #[allow(dead_code)]
43 Trace,
44 Debug,
45 Info,
46 Warn,
47 Error,
48}
49
50fn print_message(text: &str, color: Colour, style: TextStyle, level: LogLevel) {
52 let log_msg = match (is_json_mode_active(), style) {
53 (true, TextStyle::Plain | TextStyle::Bold) => text.to_string(),
55 (true, TextStyle::Label(label)) => format!("{label}: {text}"),
56 (true, TextStyle::Action(action)) => {
57 let indent = get_action_indentation(&action);
58 format!("{indent}{action} {text}")
59 }
60
61 (false, TextStyle::Plain) => format!("{}", color.paint(text)),
63 (false, TextStyle::Bold) => format!("{}", color.bold().paint(text)),
64 (false, TextStyle::Label(label)) => format!("{} {}", color.bold().paint(label), text),
65 (false, TextStyle::Action(action)) => {
66 let indent = get_action_indentation(&action);
67 format!("{}{} {}", indent, color.bold().paint(action), text)
68 }
69 };
70
71 match level {
72 LogLevel::Trace => tracing::trace!("{}", log_msg),
73 LogLevel::Debug => tracing::debug!("{}", log_msg),
74 LogLevel::Info => tracing::info!("{}", log_msg),
75 LogLevel::Warn => tracing::warn!("{}", log_msg),
76 LogLevel::Error => tracing::error!("{}", log_msg),
77 }
78}
79
80pub fn println_label_green(label: &str, txt: &str) {
82 print_message(
83 txt,
84 Colour::Green,
85 TextStyle::Label(label.to_string()),
86 LogLevel::Info,
87 );
88}
89
90pub fn println_action_green(action: &str, txt: &str) {
92 println_action(action, txt, Colour::Green);
93}
94
95pub fn println_label_red(label: &str, txt: &str) {
97 print_message(
98 txt,
99 Colour::Red,
100 TextStyle::Label(label.to_string()),
101 LogLevel::Info,
102 );
103}
104
105pub fn println_action_red(action: &str, txt: &str) {
107 println_action(action, txt, Colour::Red);
108}
109
110pub fn println_action_yellow(action: &str, txt: &str) {
112 println_action(action, txt, Colour::Yellow);
113}
114
115fn println_action(action: &str, txt: &str, color: Colour) {
116 print_message(
117 txt,
118 color,
119 TextStyle::Action(action.to_string()),
120 LogLevel::Info,
121 );
122}
123
124pub fn println_warning(txt: &str) {
126 print_message(
127 txt,
128 Colour::Yellow,
129 TextStyle::Label("warning:".to_string()),
130 LogLevel::Warn,
131 );
132}
133
134pub fn println_warning_verbose(txt: &str) {
136 print_message(
137 txt,
138 Colour::Yellow,
139 TextStyle::Label("warning:".to_string()),
140 LogLevel::Debug,
141 );
142}
143
144pub fn println_error(txt: &str) {
146 print_message(
147 txt,
148 Colour::Red,
149 TextStyle::Label("error:".to_string()),
150 LogLevel::Error,
151 );
152}
153
154pub fn println_red(txt: &str) {
155 print_message(txt, Colour::Red, TextStyle::Plain, LogLevel::Info);
156}
157
158pub fn println_green(txt: &str) {
159 print_message(txt, Colour::Green, TextStyle::Plain, LogLevel::Info);
160}
161
162pub fn println_yellow(txt: &str) {
163 print_message(txt, Colour::Yellow, TextStyle::Plain, LogLevel::Info);
164}
165
166pub fn println_green_bold(txt: &str) {
167 print_message(txt, Colour::Green, TextStyle::Bold, LogLevel::Info);
168}
169
170pub fn println_yellow_bold(txt: &str) {
171 print_message(txt, Colour::Yellow, TextStyle::Bold, LogLevel::Info);
172}
173
174pub fn println_yellow_err(txt: &str) {
175 print_message(txt, Colour::Yellow, TextStyle::Plain, LogLevel::Error);
176}
177
178pub fn println_red_err(txt: &str) {
179 print_message(txt, Colour::Red, TextStyle::Plain, LogLevel::Error);
180}
181
182const LOG_FILTER: &str = "RUST_LOG";
183
184#[derive(PartialEq, Eq)]
185pub enum TracingWriter {
186 Stdio,
188 Stdout,
190 Stderr,
192 Json,
194}
195
196#[derive(Default)]
197pub struct TracingSubscriberOptions {
198 pub verbosity: Option<u8>,
199 pub silent: Option<bool>,
200 pub log_level: Option<LevelFilter>,
201 pub writer_mode: Option<TracingWriter>,
202 pub regex_filter: Option<String>,
203}
204
205impl<'a> MakeWriter<'a> for TracingWriter {
208 type Writer = Box<dyn io::Write>;
209
210 fn make_writer(&'a self) -> Self::Writer {
211 match self {
212 TracingWriter::Stderr => Box::new(io::stderr()),
213 _ => Box::new(io::stdout()),
217 }
218 }
219
220 fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
221 if *self == TracingWriter::Stderr
225 || (*self == TracingWriter::Stdio && meta.level() <= &Level::WARN)
226 {
227 return Box::new(io::stderr());
228 }
229
230 Box::new(io::stdout())
232 }
233}
234
235pub fn init_tracing_subscriber(options: TracingSubscriberOptions) {
239 let env_filter = match env::var_os(LOG_FILTER) {
240 Some(_) => EnvFilter::try_from_default_env().expect("Invalid `RUST_LOG` provided"),
241 None => EnvFilter::new("info"),
242 };
243
244 let level_filter = options
245 .log_level
246 .or_else(|| {
247 options.verbosity.and_then(|verbosity| match verbosity {
248 1 => Some(LevelFilter::DEBUG), 2 => Some(LevelFilter::TRACE), _ => None,
251 })
252 })
253 .or_else(|| {
254 options
255 .silent
256 .and_then(|silent| silent.then_some(LevelFilter::OFF))
257 });
258
259 let writer_mode = match options.writer_mode {
260 Some(TracingWriter::Json) => {
261 JSON_MODE_ACTIVE.store(true, Ordering::SeqCst);
262 TracingWriter::Json
263 }
264 Some(TracingWriter::Stderr) => TracingWriter::Stderr,
265 _ => TracingWriter::Stdio,
266 };
267
268 let builder = tracing_subscriber::fmt::Subscriber::builder()
269 .with_env_filter(env_filter)
270 .with_ansi(true)
271 .with_level(false)
272 .with_file(false)
273 .with_line_number(false)
274 .without_time()
275 .with_target(false)
276 .with_writer(writer_mode);
277
278 let filter = filter_fn(move |metadata| {
280 if let Some(ref regex_filter) = options.regex_filter {
281 let regex = regex::Regex::new(regex_filter).unwrap();
282 regex.is_match(metadata.target())
283 } else {
284 true
285 }
286 });
287
288 match (is_json_mode_active(), level_filter) {
289 (true, Some(level)) => {
290 let subscriber = builder.json().with_max_level(level).finish().with(filter);
291 tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
292 }
293 (true, None) => {
294 let subscriber = builder.json().finish().with(filter);
295 tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
296 }
297 (false, Some(level)) => {
298 let subscriber = builder.with_max_level(level).finish().with(filter);
299 tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
300 }
301 (false, None) => {
302 let subscriber = builder.finish().with(filter);
303 tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use tracing_test::traced_test;
312
313 fn setup_test() {
315 JSON_MODE_ACTIVE.store(false, Ordering::SeqCst);
316 }
317
318 #[traced_test]
319 #[test]
320 fn test_println_label_green() {
321 setup_test();
322
323 let txt = "main.sw";
324 println_label_green("Compiling", txt);
325
326 let expected_action = "\x1b[1;32mCompiling\x1b[0m";
327 assert!(logs_contain(&format!("{expected_action} {txt}")));
328 }
329
330 #[traced_test]
331 #[test]
332 fn test_println_label_red() {
333 setup_test();
334
335 let txt = "main.sw";
336 println_label_red("Error", txt);
337
338 let expected_action = "\x1b[1;31mError\x1b[0m";
339 assert!(logs_contain(&format!("{expected_action} {txt}")));
340 }
341
342 #[traced_test]
343 #[test]
344 fn test_println_action_green() {
345 setup_test();
346
347 let txt = "main.sw";
348 println_action_green("Compiling", txt);
349
350 let expected_action = "\x1b[1;32mCompiling\x1b[0m";
351 assert!(logs_contain(&format!(" {expected_action} {txt}")));
352 }
353
354 #[traced_test]
355 #[test]
356 fn test_println_action_green_long() {
357 setup_test();
358
359 let txt = "main.sw";
360 println_action_green("Supercalifragilistic", txt);
361
362 let expected_action = "\x1b[1;32mSupercalifragilistic\x1b[0m";
363 assert!(logs_contain(&format!("{expected_action} {txt}")));
364 }
365
366 #[traced_test]
367 #[test]
368 fn test_println_action_red() {
369 setup_test();
370
371 let txt = "main";
372 println_action_red("Removing", txt);
373
374 let expected_action = "\x1b[1;31mRemoving\x1b[0m";
375 assert!(logs_contain(&format!(" {expected_action} {txt}")));
376 }
377
378 #[traced_test]
379 #[test]
380 fn test_json_mode_println_functions() {
381 setup_test();
382
383 JSON_MODE_ACTIVE.store(true, Ordering::SeqCst);
384
385 println_label_green("Label", "Value");
387 assert!(logs_contain("Label: Value"));
388
389 println_action_green("Action", "Target");
390 assert!(logs_contain("Action"));
391 assert!(logs_contain("Target"));
392
393 println_green("Green text");
394 assert!(logs_contain("Green text"));
395
396 println_warning("This is a warning");
397 assert!(logs_contain("This is a warning"));
398
399 println_error("This is an error");
400 assert!(logs_contain("This is an error"));
401
402 JSON_MODE_ACTIVE.store(false, Ordering::SeqCst);
403 }
404}