1use std::{
3 borrow::Cow,
4 collections::HashMap,
5 future::Future,
6 io,
7 str::FromStr,
8 sync::{Arc, Mutex},
9 time::{Duration, Instant},
10};
11
12use clap_verbosity_flag::{InfoLevel, Verbosity};
13use console::style;
14use indicatif::{
15 HumanBytes, HumanDuration, MultiProgress, ProgressBar, ProgressState, ProgressStyle,
16};
17use tracing::{field, Level};
18use tracing_core::{span::Id, Event, Field, Subscriber};
19use tracing_subscriber::{
20 filter::{Directive, ParseError},
21 fmt::{
22 self,
23 format::{self, Format},
24 FmtContext, FormatEvent, FormatFields, MakeWriter,
25 },
26 layer::{Context, SubscriberExt},
27 registry::LookupSpan,
28 util::SubscriberInitExt,
29 EnvFilter, Layer,
30};
31
32pub struct TracingFormatter;
34
35impl<S, N> FormatEvent<S, N> for TracingFormatter
36where
37 S: Subscriber + for<'a> LookupSpan<'a>,
38 N: for<'a> FormatFields<'a> + 'static,
39{
40 fn format_event(
41 &self,
42 ctx: &FmtContext<'_, S, N>,
43 mut writer: format::Writer<'_>,
44 event: &Event<'_>,
45 ) -> std::fmt::Result {
46 let metadata = event.metadata();
47 if *metadata.level() == tracing_core::metadata::Level::INFO
48 && metadata.target().starts_with("rattler_build")
49 {
50 ctx.format_fields(writer.by_ref(), event)?;
51 writeln!(writer)
52 } else {
53 let default_format = Format::default();
54 default_format.format_event(ctx, writer, event)
55 }
56 }
57}
58
59#[derive(Debug, Default)]
60struct SharedState {
61 indentation_level: usize,
62 timestamps: HashMap<Id, Instant>,
63 formatted_spans: HashMap<Id, String>,
64 warnings: Vec<String>,
65}
66
67struct CustomVisitor<'a> {
68 writer: &'a mut dyn io::Write,
69 result: io::Result<()>,
70}
71
72impl<'a> CustomVisitor<'a> {
73 fn new(writer: &'a mut dyn io::Write) -> Self {
74 Self {
75 writer,
76 result: Ok(()),
77 }
78 }
79}
80
81impl<'a> field::Visit for CustomVisitor<'a> {
82 fn record_str(&mut self, field: &Field, value: &str) {
83 if self.result.is_err() {
84 return;
85 }
86
87 self.record_debug(field, &format_args!("{}", value))
88 }
89
90 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
91 if self.result.is_err() {
92 return;
93 }
94
95 self.result = match field.name() {
96 "message" => write!(self.writer, "{:?}", value),
97 "recipe" => write!(self.writer, " recipe: {:?}", value),
98 "package" => write!(self.writer, " package: {:?}", value),
99 _ => Ok(()),
100 };
101 }
102}
103
104fn chunk_string_without_ansi(input: &str, max_chunk_length: usize) -> Vec<String> {
105 let mut chunks: Vec<String> = vec![];
106 let mut current_chunk = String::new();
107 let mut current_length = 0;
108 let mut chars = input.chars().peekable();
109
110 while let Some(c) = chars.next() {
111 if c == '\x1B' {
112 current_chunk.push(c);
114 while let Some(&next_char) = chars.peek() {
115 current_chunk.push(chars.next().unwrap());
117 if next_char.is_ascii_alphabetic() {
118 break;
120 }
121 }
122 } else {
123 current_length += 1;
125 current_chunk.push(c);
126 if current_length == max_chunk_length {
127 chunks.push(current_chunk);
129 current_chunk = String::new();
130 current_length = 0;
131 }
132 }
133 }
134
135 if !current_chunk.is_empty() {
137 chunks.push(current_chunk);
138 }
139
140 chunks
141}
142
143fn indent_levels(indent: usize) -> String {
144 let mut s = String::new();
145 for _ in 0..indent {
146 s.push_str(" │");
147 }
148 format!("{}", style(s).cyan())
149}
150
151impl<S> Layer<S> for LoggingOutputHandler
152where
153 S: Subscriber + for<'a> LookupSpan<'a>,
154{
155 fn on_new_span(
156 &self,
157 attrs: &tracing_core::span::Attributes<'_>,
158 id: &tracing_core::span::Id,
159 ctx: Context<'_, S>,
160 ) {
161 let mut state = self.state.lock().unwrap();
162 state.timestamps.insert(id.clone(), Instant::now());
163 let span = ctx.span(id);
164
165 if let Some(span) = span {
166 let mut s = Vec::new();
167 let mut w = io::Cursor::new(&mut s);
168 attrs.record(&mut CustomVisitor::new(&mut w));
169 let s = String::from_utf8_lossy(w.get_ref());
170
171 if !s.is_empty() {
172 state
173 .formatted_spans
174 .insert(id.clone(), format!("{}{}", span.name(), s));
175 } else {
176 state
177 .formatted_spans
178 .insert(id.clone(), span.name().to_string());
179 }
180 }
181 }
182
183 fn on_enter(&self, id: &Id, _ctx: Context<'_, S>) {
184 let mut state = self.state.lock().unwrap();
185 let ind = indent_levels(state.indentation_level);
186 if let Some(txt) = state.formatted_spans.get(id) {
187 eprintln!("{ind}\n{ind} {} {}", style("╭─").cyan(), txt);
188 }
189
190 state.indentation_level += 1;
191 }
192
193 fn on_exit(&self, id: &Id, _ctx: Context<'_, S>) {
194 let mut state = self.state.lock().unwrap();
195
196 let prev_ind = indent_levels(state.indentation_level);
197
198 if state.indentation_level > 0 {
199 state.indentation_level -= 1;
200 }
201
202 let ind = indent_levels(state.indentation_level);
203
204 let elapsed_time = state
205 .timestamps
206 .remove(id)
207 .map(|t| t.elapsed())
208 .unwrap_or_default();
209
210 let human_duration = HumanDuration(elapsed_time);
211
212 eprintln!(
213 "{prev_ind}\n{ind} {} (took {})",
214 style("╰───────────────────").cyan(),
215 human_duration
216 );
217 }
218
219 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
220 let mut state = self.state.lock().unwrap();
221 let indent_str = indent_levels(state.indentation_level);
222
223 let mut s = Vec::new();
224 event.record(&mut CustomVisitor::new(&mut s));
225 let s = String::from_utf8_lossy(&s);
226
227 let (prefix, prefix_len) =
228 if event.metadata().level() <= &tracing_core::metadata::Level::WARN {
229 state.warnings.push(s.to_string());
230 if event.metadata().level() == &tracing_core::metadata::Level::ERROR {
231 (style("× error ").red().bold(), 7)
232 } else {
233 (style("⚠ warning ").yellow().bold(), 9)
234 }
235 } else {
236 (style(""), 0)
237 };
238
239 let width: usize = terminal_size::terminal_size()
240 .map(|(w, _)| w.0)
241 .unwrap_or(160) as usize;
242
243 let max_width = width - (state.indentation_level * 2) - 1 - prefix_len;
244
245 self.progress_bars.suspend(|| {
246 for line in s.lines() {
247 if line.len() <= max_width {
249 eprintln!("{} {}{}", indent_str, prefix, line);
250 } else {
251 chunk_string_without_ansi(line, max_width)
252 .iter()
253 .for_each(|chunk| {
254 eprintln!("{} {}{}", indent_str, prefix, chunk);
255 });
256 }
257 }
258 });
259 }
260}
261
262#[derive(Debug)]
264pub struct LoggingOutputHandler {
265 state: Arc<Mutex<SharedState>>,
266 progress_bars: MultiProgress,
267 writer: io::Stderr,
268}
269
270impl Clone for LoggingOutputHandler {
271 fn clone(&self) -> Self {
272 Self {
273 state: self.state.clone(),
274 progress_bars: self.progress_bars.clone(),
275 writer: io::stderr(),
276 }
277 }
278}
279
280impl Default for LoggingOutputHandler {
281 fn default() -> Self {
283 Self {
284 state: Arc::new(Mutex::new(SharedState::default())),
285 progress_bars: MultiProgress::new(),
286 writer: io::stderr(),
287 }
288 }
289}
290
291impl LoggingOutputHandler {
292 pub fn from_multi_progress(multi_progress: MultiProgress) -> LoggingOutputHandler {
294 Self {
295 state: Arc::new(Mutex::new(SharedState::default())),
296 progress_bars: multi_progress,
297 writer: io::stderr(),
298 }
299 }
300
301 pub fn with_indent_levels(&self, template: &str) -> String {
304 let state = self.state.lock().unwrap();
305 let indent_str = indent_levels(state.indentation_level);
306 format!("{} {}", indent_str, template)
307 }
308
309 pub fn multi_progress(&self) -> &MultiProgress {
311 &self.progress_bars
312 }
313
314 pub fn default_bytes_style(&self) -> indicatif::ProgressStyle {
317 let template_str = self.with_indent_levels(
318 "{spinner:.green} {prefix:20!} [{elapsed_precise}] [{bar:40!.bright.yellow/dim.white}] {bytes:>8} @ {smoothed_bytes_per_sec:8}"
319 );
320
321 indicatif::ProgressStyle::default_bar()
322 .template(&template_str)
323 .unwrap()
324 .progress_chars("━━╾─")
325 .with_key(
326 "smoothed_bytes_per_sec",
327 |s: &ProgressState, w: &mut dyn std::fmt::Write| match (
328 s.pos(),
329 s.elapsed().as_millis(),
330 ) {
331 (pos, elapsed_ms) if elapsed_ms > 0 => {
332 _ = write!(
334 w,
335 "{}/s",
336 HumanBytes((pos as f64 * 1000_f64 / elapsed_ms as f64) as u64)
337 );
338 }
339 _ => {
340 _ = write!(w, "-");
341 }
342 },
343 )
344 }
345
346 pub fn default_progress_style(&self) -> indicatif::ProgressStyle {
349 let template_str = self.with_indent_levels(
350 "{spinner:.green} {prefix:20!} [{elapsed_precise}] [{bar:40!.bright.yellow/dim.white}] {pos:>7}/{len:7}"
351 );
352 indicatif::ProgressStyle::default_bar()
353 .template(&template_str)
354 .unwrap()
355 .progress_chars("━━╾─")
356 }
357
358 pub fn deserializing_progress_style(&self) -> indicatif::ProgressStyle {
361 let template_str =
362 self.with_indent_levels("{spinner:.green} {prefix:20!} [{elapsed_precise}] {wide_msg}");
363 indicatif::ProgressStyle::default_bar()
364 .template(&template_str)
365 .unwrap()
366 .progress_chars("━━╾─")
367 }
368
369 pub fn finished_progress_style(&self) -> indicatif::ProgressStyle {
371 let template_str = self.with_indent_levels(&format!(
372 "{} {{prefix:20!}} [{{elapsed_precise}}] {{msg:.bold.green}}",
373 console::style(console::Emoji("✔", " ")).green()
374 ));
375
376 indicatif::ProgressStyle::default_bar()
377 .template(&template_str)
378 .unwrap()
379 .progress_chars("━━╾─")
380 }
381
382 pub fn errored_progress_style(&self) -> indicatif::ProgressStyle {
384 let template_str = self.with_indent_levels(&format!(
385 "{} {{prefix:20!}} [{{elapsed_precise}}] {{msg:.bold.red}}",
386 console::style(console::Emoji("×", " ")).red()
387 ));
388
389 indicatif::ProgressStyle::default_bar()
390 .template(&template_str)
391 .unwrap()
392 .progress_chars("━━╾─")
393 }
394
395 pub fn long_running_progress_style(&self) -> indicatif::ProgressStyle {
398 let template_str = self.with_indent_levels("{spinner:.green} {msg}");
399 ProgressStyle::with_template(&template_str).unwrap()
400 }
401
402 pub fn add_progress_bar(&self, progress_bar: indicatif::ProgressBar) -> indicatif::ProgressBar {
404 self.progress_bars.add(progress_bar)
405 }
406
407 pub fn set_progress_bars_hidden(&self, hidden: bool) {
409 self.progress_bars.set_draw_target(if hidden {
410 indicatif::ProgressDrawTarget::hidden()
411 } else {
412 indicatif::ProgressDrawTarget::stderr()
413 });
414 }
415
416 pub fn wrap_in_progress<T, F: FnOnce() -> T>(
419 &self,
420 msg: impl Into<Cow<'static, str>>,
421 func: F,
422 ) -> T {
423 let pb = self.add_progress_bar(
424 ProgressBar::hidden()
425 .with_style(self.long_running_progress_style())
426 .with_message(msg),
427 );
428 pb.enable_steady_tick(Duration::from_millis(100));
429 let result = func();
430 pb.finish_and_clear();
431 result
432 }
433
434 pub async fn wrap_in_progress_async<T, Fut: Future<Output = T>>(
437 &self,
438 msg: impl Into<Cow<'static, str>>,
439 future: Fut,
440 ) -> T {
441 self.wrap_in_progress_async_with_progress(msg, |_pb| future)
442 .await
443 }
444
445 pub async fn wrap_in_progress_async_with_progress<
448 T,
449 Fut: Future<Output = T>,
450 F: FnOnce(ProgressBar) -> Fut,
451 >(
452 &self,
453 msg: impl Into<Cow<'static, str>>,
454 f: F,
455 ) -> T {
456 let pb = self.add_progress_bar(
457 ProgressBar::hidden()
458 .with_style(self.long_running_progress_style())
459 .with_message(msg),
460 );
461 pb.enable_steady_tick(Duration::from_millis(100));
462 let result = f(pb.clone()).await;
463 pb.finish_and_clear();
464 result
465 }
466}
467
468impl io::Write for LoggingOutputHandler {
469 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
470 self.progress_bars.suspend(|| self.writer.write(buf))
471 }
472
473 fn flush(&mut self) -> io::Result<()> {
474 self.progress_bars.suspend(|| self.writer.flush())
475 }
476}
477
478impl<'a> MakeWriter<'a> for LoggingOutputHandler {
479 type Writer = LoggingOutputHandler;
480
481 fn make_writer(&'a self) -> Self::Writer {
482 self.clone()
483 }
484}
485#[derive(clap::ValueEnum, Clone, Eq, PartialEq, Debug, Copy)]
491pub enum LogStyle {
492 Fancy,
494 Json,
496 Plain,
498}
499
500pub fn get_default_env_filter(
503 verbose: clap_verbosity_flag::LevelFilter,
504) -> Result<EnvFilter, ParseError> {
505 let mut result = EnvFilter::new(format!("rattler_build={verbose}"));
506
507 if verbose >= clap_verbosity_flag::LevelFilter::Trace {
508 result = result.add_directive(Directive::from_str("resolvo=info")?);
509 result = result.add_directive(Directive::from_str("rattler=info")?);
510 result = result.add_directive(Directive::from_str(
511 "rattler_networking::authentication_storage=info",
512 )?);
513 } else {
514 result = result.add_directive(Directive::from_str("resolvo=warn")?);
515 result = result.add_directive(Directive::from_str("rattler=warn")?);
516 result = result.add_directive(Directive::from_str("rattler_repodata_gateway::fetch=off")?);
517 result = result.add_directive(Directive::from_str(
518 "rattler_networking::authentication_storage=off",
519 )?);
520 }
521
522 Ok(result)
523}
524
525struct GitHubActionsLayer(bool);
526
527impl<S: Subscriber> Layer<S> for GitHubActionsLayer {
528 fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
529 if !self.0 {
530 return;
531 }
532 let metadata = event.metadata();
533
534 let mut message = Vec::new();
535 event.record(&mut CustomVisitor::new(&mut message));
536 let message = String::from_utf8_lossy(&message);
537
538 match *metadata.level() {
539 Level::ERROR => println!("::error ::{}", message),
540 Level::WARN => println!("::warning ::{}", message),
541 _ => {}
542 }
543 }
544}
545
546#[derive(clap::ValueEnum, Clone, Eq, PartialEq, Debug, Copy, Default)]
548pub enum Color {
549 Always,
551 Never,
553 #[default]
555 Auto,
556}
557
558pub fn init_logging(
560 log_style: &LogStyle,
561 verbosity: &Verbosity<InfoLevel>,
562 color: &Color,
563 #[cfg(feature = "tui")] tui_log_sender: Option<
564 tokio::sync::mpsc::UnboundedSender<crate::tui::event::Event>,
565 >,
566) -> Result<LoggingOutputHandler, ParseError> {
567 let log_handler = LoggingOutputHandler::default();
568
569 let use_colors = match color {
570 Color::Always => Some(true),
571 Color::Never => Some(false),
572 Color::Auto => None,
573 };
574
575 if let Some(use_colors) = use_colors {
577 console::set_colors_enabled(use_colors);
578 console::set_colors_enabled_stderr(use_colors);
579 }
580
581 let registry =
583 tracing_subscriber::registry().with(get_default_env_filter(verbosity.log_level_filter())?);
584
585 let log_style = if verbosity.log_level_filter() >= clap_verbosity_flag::LevelFilter::Debug {
586 LogStyle::Plain
587 } else {
588 *log_style
589 };
590
591 let registry = registry.with(GitHubActionsLayer(github_integration_enabled()));
592
593 #[cfg(feature = "tui")]
594 {
595 if let Some(tui_log_sender) = tui_log_sender {
596 log_handler.set_progress_bars_hidden(true);
597 let writer = crate::tui::logger::TuiOutputHandler {
598 log_sender: tui_log_sender,
599 };
600 registry
601 .with(
602 fmt::layer()
603 .with_writer(writer)
604 .without_time()
605 .with_level(false)
606 .with_target(false),
607 )
608 .init();
609 return Ok(log_handler);
610 }
611 }
612
613 match log_style {
614 LogStyle::Fancy => {
615 registry.with(log_handler.clone()).init();
616 }
617 LogStyle::Plain => {
618 registry
619 .with(
620 fmt::layer()
621 .with_writer(log_handler.clone())
622 .event_format(TracingFormatter),
623 )
624 .init();
625 }
626 LogStyle::Json => {
627 log_handler.set_progress_bars_hidden(true);
628 registry
629 .with(fmt::layer().json().with_writer(io::stderr))
630 .init();
631 }
632 }
633
634 Ok(log_handler)
635}
636
637pub fn github_integration_enabled() -> bool {
639 std::env::var("GITHUB_ACTIONS").is_ok()
640 && std::env::var("RATTLER_BUILD_ENABLE_GITHUB_INTEGRATION") == Ok("true".to_string())
641}