├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── basic.gif ├── build_console.gif ├── child_spans.gif ├── examples ├── basic.rs ├── build_console.rs ├── child_spans.rs ├── dialoguer_suspend.rs ├── filter.rs ├── multithread.rs ├── per_span_style.rs ├── progress_bar.rs └── stdout_stderr_printing.rs ├── progress_bar.gif └── src ├── filter.rs ├── lib.rs ├── pb_manager.rs ├── span_ext.rs ├── tests.rs ├── util.rs └── writer.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## 0.3.9 - 2025-01-18 3 | * fix panic when entering a grandparent span after a child span was entered (#14) 4 | * fix panic when using `with_span_field_formatter` (#15) 5 | 6 | ## 0.3.8 - 2024-12-01 7 | * improve docs 8 | 9 | ## 0.3.7 - 2024-12-01 10 | * bump dependencies 11 | * allow for customizing tick/redraw intervals 12 | * fix bug around footer not appearing after disappearing once 13 | * re-export indicatif `ProgressStyle` for ease of use 14 | * provide helper macros for printing to stdout/stderr without interfering with progress bars 15 | * disable use of `set_move_cursor` due to regression in indicatif 0.17.9 (https://github.com/console-rs/indicatif/issues/669), this may introduce new flickering unfortunately 16 | 17 | ## 0.3.6 - 2023-12-11 18 | * update dev dependencies (#8) 19 | 20 | ## 0.3.5 - 2023-08-21 21 | * add method to suspend progress bars managed by IndicatifLayer, e.g. to show dialogue confirmations (closes #4) 22 | 23 | ## 0.3.4 - 2023-04-28 24 | * add methods to fetch the `IndicatifWriter` globally if there is a default tracing subscriber and if the `IndicatifLayer` has been added 25 | 26 | ## 0.3.3 - 2023-04-27 27 | * fix a very suble race condition that could trigger a panic (#3, thanks again @Kyuuhachi!) 28 | 29 | ## 0.3.2 - 2023-04-26 30 | * fixed a race condition that could trigger a deadlock on span close (#2, thanks @Kyuuhachi!) 31 | 32 | ## 0.3.1 - 2023-04-25 33 | * `inc` is now allowed to be called before `pb_start` 34 | * added a rudimentary filter layer that allows specifying whether to show a pb or not on a per-span level 35 | 36 | ## 0.3.0 - 2023-02-18 37 | * `get_stderr_writer` replaced `get_fmt_writer` 38 | * added `get_stdout_writer` so one can print to stdout without interfering with progress bars 39 | * added `IndicatifSpanExt` to be able to set per-span progress styles, support progress bars, etc 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-indicatif" 3 | version = "0.3.9" 4 | edition = "2021" 5 | description = "Tracing layer that automatically creates and manages progress bars for active spans." 6 | license = "MIT" 7 | repository = "https://github.com/emersonford/tracing-indicatif" 8 | categories = ["command-line-interface"] 9 | keywords = ["cli", "progress", "progressbar", "progress-bar", "tracing"] 10 | documentation = "https://docs.rs/tracing-indicatif" 11 | exclude = ["*.gif"] 12 | 13 | [dependencies] 14 | indicatif = { version = "0.17.9", features = ["in_memory"] } 15 | tracing = "0.1.40" 16 | tracing-core = "0.1.32" 17 | tracing-subscriber = { version = "0.3.18" } 18 | 19 | [dev-dependencies] 20 | futures = "0.3.31" 21 | tokio = { version = "1.41.1", features = ["full"] } 22 | rand = { version = "0.8.5", features = ["std_rng"] } 23 | console = "0.15.8" 24 | dialoguer = "0.11.0" 25 | 26 | [lib] 27 | name = "tracing_indicatif" 28 | 29 | [[example]] 30 | name = "basic" 31 | 32 | [[example]] 33 | name = "child_spans" 34 | 35 | [[example]] 36 | name = "build_console" 37 | 38 | [[example]] 39 | name = "per_span_style" 40 | 41 | [[example]] 42 | name = "progress_bar" 43 | 44 | [[example]] 45 | name = "filter" 46 | 47 | [[example]] 48 | name = "multithread" 49 | 50 | [[example]] 51 | name = "stdout_stderr_printing" 52 | 53 | [[example]] 54 | name = "dialoguer_suspend" 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Emerson Ford 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tracing-indicatif 2 | [![Documentation](https://docs.rs/tracing-indicatif/badge.svg)](https://docs.rs/tracing-indicatif/) 3 | [![Crates.io](https://img.shields.io/crates/v/tracing-indicatif.svg)](https://crates.io/crates/tracing-indicatif) 4 | 5 | A [tracing](https://docs.rs/tracing/latest/tracing/) layer that automatically creates and manages [indicatif](https://docs.rs/indicatif/latest/indicatif/index.html) progress bars for active spans. 6 | 7 | Progress bars are a great way to make your CLIs feel more responsive. However, 8 | adding and managing progress bars in your libraries can be invasive, unergonomic, 9 | and difficult to keep track of. 10 | 11 | This library aims to make it easy to show progress bars for your CLI by tying 12 | progress bars to [tracing spans](https://docs.rs/tracing/latest/tracing/#spans). 13 | For CLIs/libraries already using tracing spans, this allow for a dead simple (3 14 | line) code change to enable a smooth progress bar experience for your program. 15 | This eliminates having to have code in your libraries to manually manage 16 | progress bar instances. 17 | 18 | This ends up working quite well as progress bars are fundamentally tracking the 19 | lifetime of some "span" (whether that "span" is defined explicitly or implicitly), 20 | so might as well make that relationship explicit. 21 | 22 | ## Demo 23 | See the [`examples`](https://github.com/emersonford/tracing-indicatif/tree/main/examples) 24 | folder for demo code. 25 | 26 | ### [Default Configuration](https://github.com/emersonford/tracing-indicatif/blob/main/examples/basic.rs) 27 | ![demo using basic example](basic.gif) 28 | 29 | ### [Default Configuration with Child Spans](https://github.com/emersonford/tracing-indicatif/blob/main/examples/child_spans.rs) 30 | ![demo using child_spans example](child_spans.gif) 31 | 32 | ### [Progress Bar](https://github.com/emersonford/tracing-indicatif/blob/main/examples/progress_bar.rs) 33 | ![demo using progress_bar example](progress_bar.gif) 34 | 35 | ### [Build Console Like](https://github.com/emersonford/tracing-indicatif/blob/main/examples/build_console.rs) 36 | A recreation of `buck2`'s [superconsole](https://github.com/facebookincubator/superconsole). 37 | ![demo using build_console example](build_console.gif) 38 | 39 | ## Features 40 | * Customize progress bars using the same [`ProgressStyle`](https://docs.rs/indicatif/latest/indicatif/style/struct.ProgressStyle.html#method.template) 41 | API as indicatif. 42 | * Supports displaying parent-child span relationship between progress bars. 43 | * Limit the number of progress bars visible on the terminal. 44 | * Prevents progress bars from clobbering tracing logs. 45 | -------------------------------------------------------------------------------- /basic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emersonford/tracing-indicatif/6ebfeaae14866a76084be6e758d9350c5d787e34/basic.gif -------------------------------------------------------------------------------- /build_console.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emersonford/tracing-indicatif/6ebfeaae14866a76084be6e758d9350c5d787e34/build_console.gif -------------------------------------------------------------------------------- /child_spans.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emersonford/tracing-indicatif/6ebfeaae14866a76084be6e758d9350c5d787e34/child_spans.gif -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use rand::thread_rng; 5 | use rand::Rng; 6 | use tracing::info; 7 | use tracing::instrument; 8 | use tracing_indicatif::IndicatifLayer; 9 | use tracing_subscriber::layer::SubscriberExt; 10 | use tracing_subscriber::util::SubscriberInitExt; 11 | 12 | #[instrument] 13 | async fn do_work(val: u64) -> u64 { 14 | let sleep_time = thread_rng().gen_range(Duration::from_millis(250)..Duration::from_millis(500)); 15 | tokio::time::sleep(sleep_time).await; 16 | 17 | info!("doing work for val: {}", val); 18 | 19 | let sleep_time = 20 | thread_rng().gen_range(Duration::from_millis(500)..Duration::from_millis(1000)); 21 | tokio::time::sleep(sleep_time).await; 22 | 23 | val + 1 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() { 28 | let indicatif_layer = IndicatifLayer::new(); 29 | 30 | tracing_subscriber::registry() 31 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 32 | .with(indicatif_layer) 33 | .init(); 34 | 35 | let res: u64 = stream::iter((0..20).map(|val| do_work(val))) 36 | .buffer_unordered(5) 37 | .collect::>() 38 | .await 39 | .into_iter() 40 | .sum(); 41 | 42 | println!("final result: {}", res); 43 | } 44 | -------------------------------------------------------------------------------- /examples/build_console.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use indicatif::ProgressState; 5 | use indicatif::ProgressStyle; 6 | use rand::thread_rng; 7 | use rand::Rng; 8 | use tracing::info; 9 | use tracing::info_span; 10 | use tracing::instrument; 11 | use tracing_indicatif::span_ext::IndicatifSpanExt; 12 | use tracing_indicatif::IndicatifLayer; 13 | use tracing_subscriber::layer::SubscriberExt; 14 | use tracing_subscriber::util::SubscriberInitExt; 15 | 16 | fn elapsed_subsec(state: &ProgressState, writer: &mut dyn std::fmt::Write) { 17 | let seconds = state.elapsed().as_secs(); 18 | let sub_seconds = (state.elapsed().as_millis() % 1000) / 100; 19 | let _ = writer.write_str(&format!("{}.{}s", seconds, sub_seconds)); 20 | } 21 | 22 | #[instrument] 23 | async fn build_sub_unit(sub_unit: u64) { 24 | let sleep_time = 25 | thread_rng().gen_range(Duration::from_millis(5000)..Duration::from_millis(10000)); 26 | tokio::time::sleep(sleep_time).await; 27 | 28 | if thread_rng().gen_bool(0.2) { 29 | info!("sub_unit did something!"); 30 | } 31 | } 32 | 33 | #[instrument] 34 | async fn build(unit: u64) { 35 | let sleep_time = 36 | thread_rng().gen_range(Duration::from_millis(2500)..Duration::from_millis(5000)); 37 | tokio::time::sleep(sleep_time).await; 38 | 39 | let rand_num: f64 = thread_rng().gen(); 40 | 41 | if rand_num < 0.1 { 42 | tokio::join!(build_sub_unit(0), build_sub_unit(1), build_sub_unit(2)); 43 | } else if rand_num < 0.3 { 44 | tokio::join!(build_sub_unit(0), build_sub_unit(1)); 45 | } else { 46 | build_sub_unit(0).await; 47 | } 48 | } 49 | 50 | #[tokio::main] 51 | async fn main() { 52 | let indicatif_layer = IndicatifLayer::new().with_progress_style( 53 | ProgressStyle::with_template( 54 | "{color_start}{span_child_prefix}{span_fields} -- {span_name} {wide_msg} {elapsed_subsec}{color_end}", 55 | ) 56 | .unwrap() 57 | .with_key( 58 | "elapsed_subsec", 59 | elapsed_subsec, 60 | ) 61 | .with_key( 62 | "color_start", 63 | |state: &ProgressState, writer: &mut dyn std::fmt::Write| { 64 | let elapsed = state.elapsed(); 65 | 66 | if elapsed > Duration::from_secs(8) { 67 | // Red 68 | let _ = write!(writer, "\x1b[{}m", 1 + 30); 69 | } else if elapsed > Duration::from_secs(4) { 70 | // Yellow 71 | let _ = write!(writer, "\x1b[{}m", 3 + 30); 72 | } 73 | }, 74 | ) 75 | .with_key( 76 | "color_end", 77 | |state: &ProgressState, writer: &mut dyn std::fmt::Write| { 78 | if state.elapsed() > Duration::from_secs(4) { 79 | let _ =write!(writer, "\x1b[0m"); 80 | } 81 | }, 82 | ), 83 | ).with_span_child_prefix_symbol("↳ ").with_span_child_prefix_indent(" "); 84 | 85 | tracing_subscriber::registry() 86 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 87 | .with(indicatif_layer) 88 | .init(); 89 | 90 | let header_span = info_span!("header"); 91 | header_span.pb_set_style( 92 | &ProgressStyle::with_template( 93 | "Working on tasks for command: `build`. {wide_msg} {elapsed_subsec}\n{wide_bar}", 94 | ) 95 | .unwrap() 96 | .with_key("elapsed_subsec", elapsed_subsec) 97 | .progress_chars("---"), 98 | ); 99 | header_span.pb_start(); 100 | 101 | // Bit of a hack to show a full "-----" line underneath the header. 102 | header_span.pb_set_length(1); 103 | header_span.pb_set_position(1); 104 | 105 | stream::iter((0..20).map(|val| build(val))) 106 | .buffer_unordered(7) 107 | .collect::>() 108 | .await; 109 | } 110 | -------------------------------------------------------------------------------- /examples/child_spans.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use rand::thread_rng; 5 | use rand::Rng; 6 | use tracing::instrument; 7 | use tracing_indicatif::IndicatifLayer; 8 | use tracing_subscriber::layer::SubscriberExt; 9 | use tracing_subscriber::util::SubscriberInitExt; 10 | 11 | #[instrument] 12 | async fn do_sub_work(val: u64) -> u64 { 13 | let sleep_time = 14 | thread_rng().gen_range(Duration::from_millis(1500)..Duration::from_millis(3000)); 15 | tokio::time::sleep(sleep_time).await; 16 | 17 | val + 1 18 | } 19 | 20 | #[instrument] 21 | async fn do_work(mut val: u64) -> u64 { 22 | let sleep_time = thread_rng().gen_range(Duration::from_millis(250)..Duration::from_millis(500)); 23 | tokio::time::sleep(sleep_time).await; 24 | 25 | if thread_rng().gen_bool(0.4) { 26 | let (val1, val2, val3) = tokio::join!(do_sub_work(val), do_sub_work(val), do_sub_work(val)); 27 | 28 | val = val1 + val2 + val3; 29 | } else { 30 | val = do_sub_work(val).await; 31 | } 32 | 33 | let sleep_time = 34 | thread_rng().gen_range(Duration::from_millis(500)..Duration::from_millis(1000)); 35 | tokio::time::sleep(sleep_time).await; 36 | 37 | val + 1 38 | } 39 | 40 | #[tokio::main] 41 | async fn main() { 42 | let indicatif_layer = IndicatifLayer::new(); 43 | 44 | tracing_subscriber::registry() 45 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 46 | .with(indicatif_layer) 47 | .init(); 48 | 49 | let res: u64 = stream::iter((0..20).map(|val| do_work(val))) 50 | .buffer_unordered(5) 51 | .collect::>() 52 | .await 53 | .into_iter() 54 | .sum(); 55 | 56 | println!("final result: {}", res); 57 | } 58 | -------------------------------------------------------------------------------- /examples/dialoguer_suspend.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::time::Duration; 3 | 4 | use dialoguer::Confirm; 5 | use tracing::info_span; 6 | use tracing_indicatif::span_ext::IndicatifSpanExt; 7 | use tracing_indicatif::writer::get_indicatif_stderr_writer; 8 | use tracing_indicatif::{suspend_tracing_indicatif, IndicatifLayer}; 9 | use tracing_subscriber::layer::SubscriberExt; 10 | use tracing_subscriber::util::SubscriberInitExt; 11 | 12 | fn main() { 13 | let indicatif_layer = IndicatifLayer::new(); 14 | 15 | tracing_subscriber::registry() 16 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 17 | .with(indicatif_layer) 18 | .init(); 19 | 20 | let _span = info_span!("foo"); 21 | _span.pb_start(); 22 | 23 | std::thread::sleep(Duration::from_secs(1)); 24 | 25 | suspend_tracing_indicatif(|| { 26 | if Confirm::new() 27 | .with_prompt("Do you like Rust?") 28 | .interact() 29 | .unwrap_or(false) 30 | { 31 | println!("Yay!"); 32 | } else { 33 | println!("oh... okay :("); 34 | } 35 | }); 36 | 37 | let _ = writeln!( 38 | get_indicatif_stderr_writer().unwrap(), 39 | "sleeping for some time..." 40 | ); 41 | 42 | std::thread::sleep(Duration::from_secs(1)); 43 | } 44 | -------------------------------------------------------------------------------- /examples/filter.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use rand::thread_rng; 5 | use rand::Rng; 6 | use tracing::instrument; 7 | use tracing_indicatif::filter::hide_indicatif_span_fields; 8 | use tracing_indicatif::filter::IndicatifFilter; 9 | use tracing_indicatif::IndicatifLayer; 10 | use tracing_subscriber::fmt::format::DefaultFields; 11 | use tracing_subscriber::layer::Layer; 12 | use tracing_subscriber::layer::SubscriberExt; 13 | use tracing_subscriber::util::SubscriberInitExt; 14 | 15 | #[instrument(fields(indicatif.pb_show))] 16 | async fn do_sub_work(val: u64) -> u64 { 17 | let sleep_time = 18 | thread_rng().gen_range(Duration::from_millis(1500)..Duration::from_millis(3000)); 19 | tokio::time::sleep(sleep_time).await; 20 | 21 | val + 1 22 | } 23 | 24 | #[instrument] 25 | async fn do_work(mut val: u64) -> u64 { 26 | let sleep_time = thread_rng().gen_range(Duration::from_millis(250)..Duration::from_millis(500)); 27 | tokio::time::sleep(sleep_time).await; 28 | 29 | if thread_rng().gen_bool(0.2) { 30 | let (val1, val2) = tokio::join!(do_sub_work(val), do_sub_work(val),); 31 | 32 | val = val1 + val2; 33 | } else { 34 | val = do_sub_work(val).await; 35 | } 36 | 37 | let sleep_time = 38 | thread_rng().gen_range(Duration::from_millis(500)..Duration::from_millis(1000)); 39 | tokio::time::sleep(sleep_time).await; 40 | 41 | val + 1 42 | } 43 | 44 | #[tokio::main] 45 | async fn main() { 46 | let indicatif_layer = IndicatifLayer::new() 47 | .with_span_field_formatter(hide_indicatif_span_fields(DefaultFields::new())); 48 | 49 | tracing_subscriber::registry() 50 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 51 | .with(indicatif_layer.with_filter(IndicatifFilter::new(false))) 52 | .init(); 53 | 54 | let res: u64 = stream::iter((0..20).map(|val| do_work(val))) 55 | .buffer_unordered(5) 56 | .collect::>() 57 | .await 58 | .into_iter() 59 | .sum(); 60 | 61 | println!("final result: {}", res); 62 | } 63 | -------------------------------------------------------------------------------- /examples/multithread.rs: -------------------------------------------------------------------------------- 1 | use tracing_indicatif::IndicatifLayer; 2 | use tracing_subscriber::layer::SubscriberExt; 3 | use tracing_subscriber::util::SubscriberInitExt; 4 | 5 | fn main() { 6 | let indicatif_layer = IndicatifLayer::new(); 7 | tracing_subscriber::registry() 8 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 9 | .with(indicatif_layer) 10 | .init(); 11 | 12 | let handles = (0..32) 13 | .map(|_| { 14 | std::thread::spawn(move || { 15 | let _span = tracing::info_span!("ch").entered(); 16 | 17 | let subhandles = (0..16) 18 | .map(|_| { 19 | std::thread::spawn(move || { 20 | let _span = tracing::info_span!("subch").entered(); 21 | }) 22 | }) 23 | .collect::>(); 24 | 25 | subhandles.into_iter().for_each(|i| i.join().unwrap()); 26 | }) 27 | }) 28 | .collect::>(); 29 | handles.into_iter().for_each(|i| i.join().unwrap()); 30 | } 31 | -------------------------------------------------------------------------------- /examples/per_span_style.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use indicatif::ProgressStyle; 5 | use rand::thread_rng; 6 | use rand::Rng; 7 | use tracing::instrument; 8 | use tracing::Span; 9 | use tracing_indicatif::span_ext::IndicatifSpanExt; 10 | use tracing_indicatif::IndicatifLayer; 11 | use tracing_subscriber::layer::SubscriberExt; 12 | use tracing_subscriber::util::SubscriberInitExt; 13 | 14 | #[instrument] 15 | async fn do_sub_work(val: u64) -> u64 { 16 | // Maybe we don't want a spinner here 17 | Span::current().pb_set_style( 18 | &ProgressStyle::with_template("{span_child_prefix}{span_name}{{{span_fields}}}").unwrap(), 19 | ); 20 | 21 | let sleep_time = thread_rng().gen_range(Duration::from_secs(3)..Duration::from_secs(5)); 22 | tokio::time::sleep(sleep_time).await; 23 | 24 | val + 1 25 | } 26 | 27 | #[instrument] 28 | async fn do_work(mut val: u64) -> u64 { 29 | let sleep_time = thread_rng().gen_range(Duration::from_secs(1)..Duration::from_secs(3)); 30 | tokio::time::sleep(sleep_time).await; 31 | 32 | val = do_sub_work(val).await; 33 | 34 | val + 1 35 | } 36 | 37 | #[tokio::main] 38 | async fn main() { 39 | let indicatif_layer = IndicatifLayer::new(); 40 | 41 | tracing_subscriber::registry() 42 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 43 | .with(indicatif_layer) 44 | .init(); 45 | 46 | let res: u64 = stream::iter((0..20).map(|val| do_work(val))) 47 | .buffer_unordered(5) 48 | .collect::>() 49 | .await 50 | .into_iter() 51 | .sum(); 52 | 53 | println!("final result: {}", res); 54 | } 55 | -------------------------------------------------------------------------------- /examples/progress_bar.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use indicatif::ProgressStyle; 5 | use rand::thread_rng; 6 | use rand::Rng; 7 | use tracing::info_span; 8 | use tracing::instrument; 9 | use tracing::Span; 10 | use tracing_indicatif::span_ext::IndicatifSpanExt; 11 | use tracing_indicatif::IndicatifLayer; 12 | use tracing_subscriber::layer::SubscriberExt; 13 | use tracing_subscriber::util::SubscriberInitExt; 14 | 15 | #[instrument] 16 | async fn do_sub_work(val: u64) -> u64 { 17 | let sleep_time = thread_rng().gen_range(Duration::from_secs(3)..Duration::from_secs(5)); 18 | tokio::time::sleep(sleep_time).await; 19 | 20 | val + 1 21 | } 22 | 23 | #[instrument] 24 | async fn do_work(mut val: u64) -> u64 { 25 | let sleep_time = thread_rng().gen_range(Duration::from_secs(1)..Duration::from_secs(3)); 26 | tokio::time::sleep(sleep_time).await; 27 | 28 | val = do_sub_work(val).await; 29 | 30 | val + 1 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() { 35 | let indicatif_layer = IndicatifLayer::new(); 36 | 37 | tracing_subscriber::registry() 38 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 39 | .with(indicatif_layer) 40 | .init(); 41 | 42 | let header_span = info_span!("header"); 43 | header_span.pb_set_style(&ProgressStyle::default_bar()); 44 | header_span.pb_set_length(20); 45 | 46 | let header_span_enter = header_span.enter(); 47 | 48 | let res: u64 = stream::iter((0..20).map(|val| async move { 49 | let res = do_work(val).await; 50 | Span::current().pb_inc(1); 51 | 52 | res 53 | })) 54 | .buffer_unordered(5) 55 | .collect::>() 56 | .await 57 | .into_iter() 58 | .sum(); 59 | 60 | std::mem::drop(header_span_enter); 61 | std::mem::drop(header_span); 62 | 63 | println!("final result: {}", res); 64 | } 65 | -------------------------------------------------------------------------------- /examples/stdout_stderr_printing.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use futures::stream::{self, StreamExt}; 4 | use rand::thread_rng; 5 | use rand::Rng; 6 | use tracing::instrument; 7 | use tracing_indicatif::indicatif_eprintln; 8 | use tracing_indicatif::indicatif_println; 9 | use tracing_indicatif::IndicatifLayer; 10 | use tracing_subscriber::layer::SubscriberExt; 11 | use tracing_subscriber::util::SubscriberInitExt; 12 | 13 | #[instrument] 14 | async fn do_work(val: u64) -> u64 { 15 | let sleep_time = thread_rng().gen_range(Duration::from_millis(250)..Duration::from_millis(500)); 16 | tokio::time::sleep(sleep_time).await; 17 | 18 | indicatif_eprintln!("writing val {} to stderr", val); 19 | indicatif_println!("writing val {} to stdout", val); 20 | 21 | let sleep_time = 22 | thread_rng().gen_range(Duration::from_millis(500)..Duration::from_millis(1000)); 23 | tokio::time::sleep(sleep_time).await; 24 | 25 | val + 1 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | let indicatif_layer = IndicatifLayer::new(); 31 | 32 | tracing_subscriber::registry() 33 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 34 | .with(indicatif_layer) 35 | .init(); 36 | 37 | let res: u64 = stream::iter((0..20).map(|val| do_work(val))) 38 | .buffer_unordered(5) 39 | .collect::>() 40 | .await 41 | .into_iter() 42 | .sum(); 43 | 44 | println!("final result: {}", res); 45 | } 46 | -------------------------------------------------------------------------------- /progress_bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emersonford/tracing-indicatif/6ebfeaae14866a76084be6e758d9350c5d787e34/progress_bar.gif -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | //! Provides a rudimentary filter layer that can be used to selectively enable progress bars on a 2 | //! per-span level. 3 | //! 4 | //! # Example Use 5 | //! 6 | //! ``` 7 | //! use tracing_subscriber::layer::SubscriberExt; 8 | //! use tracing_subscriber::util::SubscriberInitExt; 9 | //! use tracing_indicatif::IndicatifLayer; 10 | //! use tracing_indicatif::filter::IndicatifFilter; 11 | //! use tracing_indicatif::filter::hide_indicatif_span_fields; 12 | //! use tracing_subscriber::fmt::format::DefaultFields; 13 | //! use tracing_subscriber::layer::Layer; 14 | //! 15 | //! let indicatif_layer = IndicatifLayer::new() 16 | //! .with_span_field_formatter(hide_indicatif_span_fields(DefaultFields::new())); 17 | //! 18 | //! tracing_subscriber::registry() 19 | //! .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 20 | //! .with(indicatif_layer.with_filter(IndicatifFilter::new(false))) 21 | //! .init(); 22 | //! ``` 23 | use std::fmt; 24 | use std::marker::PhantomData; 25 | 26 | use tracing_core::{Field, Subscriber}; 27 | use tracing_subscriber::layer::Filter; 28 | use tracing_subscriber::{ 29 | field::{MakeVisitor, VisitFmt, VisitOutput}, 30 | fmt::format::Writer, 31 | }; 32 | 33 | use crate::util::FilteredFormatFields; 34 | 35 | /// A filter that filters based on the presence of a field with the name of either 36 | /// "indicatif.pb_show" or "indicatif.pb_hide" on the span. 37 | /// 38 | /// The value for this field is irrelevant and not factored in to the filtering (this is due to 39 | /// tracing not making field values available in the `on_new_span` method). To avoid confusion, it 40 | /// is recommended to set the value of this field to [`tracing::field::Empty`]. 41 | /// 42 | /// If both "indicatif.pb_show" and "indicatif.pb_hide" are present, the behavior is to show a 43 | /// progress bar. 44 | pub struct IndicatifFilter { 45 | show_progress_bars_by_default: bool, 46 | subscriber: PhantomData, 47 | } 48 | 49 | impl IndicatifFilter { 50 | /// Constructs the filter. 51 | /// 52 | /// If "indicatif.pb_show" or "indicatif.pb_hide" are not present as a field on the span, 53 | /// then the value of `show_progress_bars_by_default` is used; i.e. if 54 | /// `show_progress_bars_by_default` is `false`, then progress bars are not shown for spans by 55 | /// default. 56 | pub fn new(show_progress_bars_by_default: bool) -> Self { 57 | Self { 58 | show_progress_bars_by_default, 59 | subscriber: PhantomData, 60 | } 61 | } 62 | } 63 | 64 | impl Filter for IndicatifFilter { 65 | fn enabled( 66 | &self, 67 | meta: &tracing::Metadata<'_>, 68 | _: &tracing_subscriber::layer::Context<'_, S>, 69 | ) -> bool { 70 | if !meta.is_span() { 71 | return false; 72 | } 73 | 74 | if meta.fields().field("indicatif.pb_show").is_some() { 75 | return true; 76 | } 77 | 78 | if meta.fields().field("indicatif.pb_hide").is_some() { 79 | return false; 80 | } 81 | 82 | self.show_progress_bars_by_default 83 | } 84 | } 85 | 86 | /// Returns a [`tracing_subscriber::fmt::FormatFields`] that ignores the "indicatif.pb_show" and "indicatif.pb_hide" fields. 87 | pub fn hide_indicatif_span_fields<'writer, Format>( 88 | format: Format, 89 | ) -> FilteredFormatFields bool + Clone> 90 | where 91 | Format: MakeVisitor>, 92 | Format::Visitor: VisitFmt + VisitOutput, 93 | { 94 | FilteredFormatFields::new(format, |field: &Field| { 95 | field.name() != "indicatif.pb_show" && field.name() != "indicatif.pb_hide" 96 | }) 97 | } 98 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [tracing](https://docs.rs/tracing/latest/tracing/) layer that automatically creates and manages [indicatif](https://docs.rs/indicatif/latest/indicatif/index.html) progress bars for active spans. 2 | //! 3 | //! Progress bars are a great way to make your CLIs feel more responsive. However, 4 | //! adding and managing progress bars in your libraries can be invasive, unergonomic, 5 | //! and difficult to keep track of. 6 | //! 7 | //! This library aims to make it easy to show progress bars for your CLI by tying 8 | //! progress bars to [tracing spans](https://docs.rs/tracing/latest/tracing/#spans). 9 | //! For CLIs/libraries already using tracing spans, this allow for a dead simple (3 10 | //! line) code change to enable a smooth progress bar experience for your program. 11 | //! This eliminates having to have code in your libraries to manually manage 12 | //! progress bar instances. 13 | //! 14 | //! This ends up working quite well as progress bars are fundamentally tracking the 15 | //! lifetime of some "span" (whether that "span" is defined explicitly or implicitly), 16 | //! so might as well make that relationship explicit. 17 | //! 18 | //! An easy quick start for this crate is: 19 | //! ``` 20 | //! use tracing_subscriber::layer::SubscriberExt; 21 | //! use tracing_subscriber::util::SubscriberInitExt; 22 | //! use tracing_indicatif::IndicatifLayer; 23 | //! 24 | //! let indicatif_layer = IndicatifLayer::new(); 25 | //! 26 | //! tracing_subscriber::registry() 27 | //! .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 28 | //! .with(indicatif_layer) 29 | //! .init(); 30 | //! ``` 31 | //! See [`IndicatifLayer`] for additional documentation. 32 | //! 33 | //! See the [`examples`](https://github.com/emersonford/tracing-indicatif/tree/main/examples) folder for examples of how to customize the layer / progress bar 34 | //! appearance. 35 | //! 36 | //! Note: it is highly recommended you pass `indicatif_layer.get_stderr_writer()` or 37 | //! `indicatif_layer.get_stdout_writer()` to your `fmt::layer()` (depending on where you want to 38 | //! emit tracing logs) to prevent progress bars from clobbering any console logs. 39 | use std::any::TypeId; 40 | use std::marker::PhantomData; 41 | use std::sync::Mutex; 42 | 43 | /// Re-export of [`indicatif`]'s style module for ease of use. 44 | pub use indicatif::style; 45 | use indicatif::style::ProgressStyle; 46 | use indicatif::style::ProgressTracker; 47 | use indicatif::MultiProgress; 48 | use indicatif::ProgressBar; 49 | use tracing_core::span; 50 | use tracing_core::Subscriber; 51 | use tracing_subscriber::fmt::format::DefaultFields; 52 | use tracing_subscriber::fmt::FormatFields; 53 | use tracing_subscriber::fmt::FormattedFields; 54 | use tracing_subscriber::layer; 55 | use tracing_subscriber::registry::LookupSpan; 56 | 57 | pub mod filter; 58 | mod pb_manager; 59 | pub mod span_ext; 60 | pub mod util; 61 | pub mod writer; 62 | 63 | use pb_manager::ProgressBarManager; 64 | pub use pb_manager::TickSettings; 65 | #[doc(inline)] 66 | pub use writer::IndicatifWriter; 67 | 68 | #[derive(Clone)] 69 | struct IndicatifProgressKey { 70 | message: String, 71 | } 72 | 73 | impl ProgressTracker for IndicatifProgressKey { 74 | fn clone_box(&self) -> Box { 75 | Box::new(self.clone()) 76 | } 77 | 78 | fn tick(&mut self, _: &indicatif::ProgressState, _: std::time::Instant) {} 79 | 80 | fn reset(&mut self, _: &indicatif::ProgressState, _: std::time::Instant) {} 81 | 82 | fn write(&self, _: &indicatif::ProgressState, w: &mut dyn std::fmt::Write) { 83 | let _ = w.write_str(&self.message); 84 | } 85 | } 86 | 87 | // Suppose we have a [Span] (maybe gotten via [Span::current]) and want access to our 88 | // [IndicatifLayer] instance from it. The way to do this would be something like 89 | // ``` 90 | // span.with_subscriber(|(id, subscriber)| { 91 | // let maybe_layer = subscriber.downcast_ref::>(); 92 | // ... 93 | // }); 94 | // ``` 95 | // but this has the problem that, because `IndicatifLayer` has generic params, we need to pass 96 | // a concrete type `S` and `F` to that `downcast_ref` call. And the callsite doesn't know what 97 | // those concrete types are. 98 | // 99 | // Therefore, we use this `WithContext` struct (along with the defined `downcast_raw` method) to do 100 | // a form of indirection to something that does already know (or "remembers") what those concrete 101 | // types `S` and `F` are, so the callsite doesn't need to care about it. 102 | // 103 | // This doesn't actually return a reference to our [IndicatifLayer] instance as we only care about 104 | // the associated span data, so we just pass that to the corresponding `fn`. 105 | // 106 | // See: 107 | // * https://github.com/tokio-rs/tracing/blob/a0126b2e2d465e8e6d514acdf128fcef5b863d27/tracing-error/src/subscriber.rs#L32 108 | // * https://github.com/tokio-rs/tracing/blob/a0126b2e2d465e8e6d514acdf128fcef5b863d27/tracing-opentelemetry/src/subscriber.rs#L74 109 | #[allow(clippy::type_complexity)] 110 | pub(crate) struct WithContext( 111 | fn(&tracing::Dispatch, &span::Id, f: &mut dyn FnMut(&mut IndicatifSpanContext)), 112 | ); 113 | 114 | #[allow(clippy::type_complexity)] 115 | pub(crate) struct WithStderrWriter( 116 | fn(&tracing::Dispatch, f: &mut dyn FnMut(IndicatifWriter)), 117 | ); 118 | 119 | #[allow(clippy::type_complexity)] 120 | pub(crate) struct WithStdoutWriter( 121 | fn(&tracing::Dispatch, f: &mut dyn FnMut(IndicatifWriter)), 122 | ); 123 | 124 | #[allow(clippy::type_complexity)] 125 | pub(crate) struct WithMultiProgress(fn(&tracing::Dispatch, f: &mut dyn FnMut(MultiProgress))); 126 | 127 | impl WithContext { 128 | pub(crate) fn with_context( 129 | &self, 130 | dispatch: &tracing::Dispatch, 131 | id: &span::Id, 132 | mut f: impl FnMut(&mut IndicatifSpanContext), 133 | ) { 134 | (self.0)(dispatch, id, &mut f) 135 | } 136 | } 137 | 138 | impl WithStderrWriter { 139 | pub(crate) fn with_context( 140 | &self, 141 | dispatch: &tracing::Dispatch, 142 | mut f: impl FnMut(IndicatifWriter), 143 | ) { 144 | (self.0)(dispatch, &mut f) 145 | } 146 | } 147 | 148 | impl WithStdoutWriter { 149 | pub(crate) fn with_context( 150 | &self, 151 | dispatch: &tracing::Dispatch, 152 | mut f: impl FnMut(IndicatifWriter), 153 | ) { 154 | (self.0)(dispatch, &mut f) 155 | } 156 | } 157 | 158 | impl WithMultiProgress { 159 | pub(crate) fn with_context( 160 | &self, 161 | dispatch: &tracing::Dispatch, 162 | mut f: impl FnMut(MultiProgress), 163 | ) { 164 | (self.0)(dispatch, &mut f) 165 | } 166 | } 167 | 168 | #[derive(Default)] 169 | struct ProgressBarInitSettings { 170 | style: Option, 171 | len: Option, 172 | pos: Option, 173 | message: Option, 174 | } 175 | 176 | struct IndicatifSpanContext { 177 | // If this progress bar is `Some(pb)` and `pb.is_hidden`, it means the progress bar is queued. 178 | // We start the progress bar in hidden mode so things like `elapsed` are accurate. 179 | // 180 | // If this progress bar is `None`, it means the span has not yet been entered. 181 | progress_bar: Option, 182 | // If `Some`, the progress bar will use this style when the span is entered for the first time. 183 | pb_init_settings: ProgressBarInitSettings, 184 | // Notes: 185 | // * A parent span cannot close before its child spans, so if a parent span has a progress bar, 186 | // that parent progress bar's lifetime will be greater than this span's progress bar. 187 | // * The ProgressBar is just a wrapper around `Arc`, so cloning and tracking it here is fine. 188 | parent_progress_bar: Option, 189 | // This is only `Some` if we have some parent with a progress bar. 190 | parent_span: Option, 191 | // Fields to be passed to the progress bar as keys. 192 | span_fields_formatted: Option, 193 | span_name: String, 194 | span_child_prefix: String, 195 | // Used to quickly compute a child span's prefix without having to traverse up the entire span 196 | // scope. 197 | level: u16, 198 | } 199 | 200 | impl IndicatifSpanContext { 201 | fn add_keys_to_style(&self, style: ProgressStyle) -> ProgressStyle { 202 | style 203 | .with_key( 204 | "span_name", 205 | IndicatifProgressKey { 206 | message: self.span_name.clone(), 207 | }, 208 | ) 209 | .with_key( 210 | "span_fields", 211 | IndicatifProgressKey { 212 | message: self.span_fields_formatted.to_owned().unwrap_or_default(), 213 | }, 214 | ) 215 | .with_key( 216 | "span_child_prefix", 217 | IndicatifProgressKey { 218 | message: self.span_child_prefix.clone(), 219 | }, 220 | ) 221 | } 222 | 223 | fn make_progress_bar(&mut self, default_style: &ProgressStyle) { 224 | if self.progress_bar.is_none() { 225 | let pb = ProgressBar::hidden().with_style( 226 | self.pb_init_settings 227 | .style 228 | .take() 229 | .unwrap_or_else(|| self.add_keys_to_style(default_style.clone())), 230 | ); 231 | 232 | if let Some(len) = self.pb_init_settings.len.take() { 233 | pb.set_length(len); 234 | } 235 | 236 | if let Some(msg) = self.pb_init_settings.message.take() { 237 | pb.set_message(msg); 238 | } 239 | 240 | if let Some(pos) = self.pb_init_settings.pos.take() { 241 | pb.set_position(pos); 242 | } 243 | 244 | self.progress_bar = Some(pb); 245 | } 246 | } 247 | 248 | fn set_progress_bar_style(&mut self, style: ProgressStyle) { 249 | if let Some(ref pb) = self.progress_bar { 250 | pb.set_style(self.add_keys_to_style(style)); 251 | } else { 252 | self.pb_init_settings.style = Some(self.add_keys_to_style(style)); 253 | } 254 | } 255 | 256 | fn set_progress_bar_length(&mut self, len: u64) { 257 | if let Some(ref pb) = self.progress_bar { 258 | pb.set_length(len); 259 | } else { 260 | self.pb_init_settings.len = Some(len); 261 | } 262 | } 263 | 264 | fn set_progress_bar_position(&mut self, pos: u64) { 265 | if let Some(ref pb) = self.progress_bar { 266 | pb.set_position(pos); 267 | } else { 268 | self.pb_init_settings.pos = Some(pos); 269 | } 270 | } 271 | 272 | fn set_progress_bar_message(&mut self, msg: String) { 273 | if let Some(ref pb) = self.progress_bar { 274 | pb.set_message(msg); 275 | } else { 276 | self.pb_init_settings.message = Some(msg); 277 | } 278 | } 279 | 280 | fn inc_progress_bar_position(&mut self, pos: u64) { 281 | if let Some(ref pb) = self.progress_bar { 282 | pb.inc(pos); 283 | } else if let Some(ref mut pb_pos) = self.pb_init_settings.pos { 284 | *pb_pos += pos; 285 | } else { 286 | // indicatif defaults position to 0, so copy that behavior. 287 | self.pb_init_settings.pos = Some(pos); 288 | } 289 | } 290 | 291 | fn inc_progress_bar_length(&mut self, len: u64) { 292 | if let Some(ref pb) = self.progress_bar { 293 | pb.inc_length(len); 294 | } else if let Some(ref mut pb_len) = self.pb_init_settings.len { 295 | *pb_len += len; 296 | } 297 | } 298 | 299 | fn progress_bar_tick(&mut self) { 300 | if let Some(ref pb) = self.progress_bar { 301 | pb.tick() 302 | } 303 | } 304 | } 305 | 306 | /// The layer that handles creating and managing indicatif progress bars for active spans. This 307 | /// layer must be registered with your tracing subscriber to have any effect. 308 | /// 309 | /// This layer performs no filtering on which spans to show progress bars for. It is expected one 310 | /// attaches [filters to this 311 | /// layer](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#filtering-with-layers) 312 | /// to control which spans actually have progress bars generated for them. See 313 | /// [`filter::IndicatifFilter`] for a rudimentary filter. 314 | /// 315 | /// Progress bars will be started the very first time a span is [entered](tracing::Span::enter) 316 | /// or when one of its child spans is entered for the first time, and will finish when the span 317 | /// is [closed](tracing_subscriber::Layer::on_close) (including all child spans having closed). 318 | /// 319 | /// Progress bars are emitted to stderr. 320 | /// 321 | /// Under the hood, this just uses indicatif's [`MultiProgress`] struct to 322 | /// manage individual [`ProgressBar`] instances per span. 323 | pub struct IndicatifLayer { 324 | pb_manager: Mutex, 325 | // Allows us to fetch the `MultiProgress` without taking a lock. 326 | // Do not mutate `mp` directly, always go through `pb_manager`. 327 | mp: MultiProgress, 328 | span_field_formatter: F, 329 | progress_style: ProgressStyle, 330 | span_child_prefix_indent: &'static str, 331 | span_child_prefix_symbol: &'static str, 332 | get_context: WithContext, 333 | get_stderr_writer_context: WithStderrWriter, 334 | get_stdout_writer_context: WithStdoutWriter, 335 | get_multi_progress_context: WithMultiProgress, 336 | inner: PhantomData, 337 | } 338 | 339 | impl IndicatifLayer 340 | where 341 | S: Subscriber + for<'a> LookupSpan<'a>, 342 | { 343 | /// Spawns a progress bar for every tracing span that is received by this layer. 344 | /// 345 | /// The default settings for this layer are 7 progress bars maximum and progress bars in the 346 | /// style of: 347 | /// ```text 348 | /// ⠄ do_work{val=0} 349 | /// ⠄ do_work{val=1} 350 | /// ⠄ do_work{val=2} 351 | /// ↳ ⠴ do_sub_work{val=2} 352 | /// ↳ ⠴ do_sub_work{val=2} 353 | /// ⠄ do_work{val=3} 354 | /// ⠄ do_work{val=4} 355 | /// ...and 5 more not shown above. 356 | /// ``` 357 | pub fn new() -> Self { 358 | Self::default() 359 | } 360 | } 361 | 362 | impl Default for IndicatifLayer 363 | where 364 | S: Subscriber + for<'a> LookupSpan<'a>, 365 | { 366 | fn default() -> Self { 367 | let pb_manager = ProgressBarManager::new( 368 | 7, 369 | Some( 370 | ProgressStyle::with_template( 371 | "...and {pending_progress_bars} more not shown above.", 372 | ) 373 | .unwrap(), 374 | ), 375 | TickSettings::default(), 376 | ); 377 | let mp = pb_manager.mp.clone(); 378 | 379 | Self { 380 | pb_manager: Mutex::new(pb_manager), 381 | mp, 382 | span_field_formatter: DefaultFields::new(), 383 | progress_style: ProgressStyle::with_template( 384 | "{span_child_prefix}{spinner} {span_name}{{{span_fields}}}", 385 | ) 386 | .unwrap(), 387 | span_child_prefix_indent: " ", 388 | span_child_prefix_symbol: "↳ ", 389 | get_context: WithContext(Self::get_context), 390 | get_stderr_writer_context: WithStderrWriter(Self::get_stderr_writer_context), 391 | get_stdout_writer_context: WithStdoutWriter(Self::get_stdout_writer_context), 392 | get_multi_progress_context: WithMultiProgress(Self::get_multi_progress_context), 393 | inner: PhantomData, 394 | } 395 | } 396 | } 397 | 398 | // pub methods 399 | impl IndicatifLayer 400 | where 401 | S: Subscriber + for<'a> LookupSpan<'a>, 402 | { 403 | #[deprecated(since = "0.2.3", note = "use get_stderr_writer() instead")] 404 | pub fn get_fmt_writer(&self) -> IndicatifWriter { 405 | self.get_stderr_writer() 406 | } 407 | 408 | /// Returns the a writer for [`std::io::Stderr`] that ensures its output will not be clobbered by 409 | /// active progress bars. 410 | /// 411 | /// Instead of `eprintln!(...)` prefer `writeln!(indicatif_layer.get_stderr_writer(), ...)` 412 | /// instead to ensure your output is not clobbered by active progress bars. 413 | /// 414 | /// If one wishes tracing logs to be output to stderr, this should be passed into 415 | /// [`fmt::Layer::with_writer`](tracing_subscriber::fmt::Layer::with_writer). 416 | pub fn get_stderr_writer(&self) -> IndicatifWriter { 417 | // `MultiProgress` is merely a wrapper over an `Arc`, so we can clone here. 418 | IndicatifWriter::new(self.mp.clone()) 419 | } 420 | 421 | /// Returns the a writer for [`std::io::Stdout`] that ensures its output will not be clobbered by 422 | /// active progress bars. 423 | /// 424 | /// Instead of `println!(...)` prefer `writeln!(indicatif_layer.get_stdout_writer(), ...)` 425 | /// instead to ensure your output is not clobbered by active progress bars. 426 | /// 427 | /// If one wishes tracing logs to be output to stdout, this should be passed into 428 | /// [`fmt::Layer::with_writer`](tracing_subscriber::fmt::Layer::with_writer). 429 | pub fn get_stdout_writer(&self) -> IndicatifWriter { 430 | // `MultiProgress` is merely a wrapper over an `Arc`, so we can clone here. 431 | IndicatifWriter::new(self.mp.clone()) 432 | } 433 | 434 | /// Set the formatter for span fields, the result of which will be available as the 435 | /// progress bar template key `span_fields`. 436 | /// 437 | /// The default is the [`DefaultFields`] formatter. 438 | pub fn with_span_field_formatter(self, formatter: F2) -> IndicatifLayer 439 | where 440 | F2: for<'writer> FormatFields<'writer> + 'static, 441 | { 442 | IndicatifLayer { 443 | pb_manager: self.pb_manager, 444 | mp: self.mp, 445 | span_field_formatter: formatter, 446 | progress_style: self.progress_style, 447 | span_child_prefix_indent: self.span_child_prefix_indent, 448 | span_child_prefix_symbol: self.span_child_prefix_symbol, 449 | get_context: WithContext(IndicatifLayer::::get_context), 450 | get_stderr_writer_context: WithStderrWriter( 451 | IndicatifLayer::::get_stderr_writer_context, 452 | ), 453 | get_stdout_writer_context: WithStdoutWriter( 454 | IndicatifLayer::::get_stdout_writer_context, 455 | ), 456 | get_multi_progress_context: WithMultiProgress( 457 | IndicatifLayer::::get_multi_progress_context, 458 | ), 459 | inner: self.inner, 460 | } 461 | } 462 | 463 | /// Override the style used for displayed progress bars. 464 | /// 465 | /// Two additional keys are available for the progress bar template: 466 | /// * `span_fields` - the formatted string of this span's fields 467 | /// * `span_name` - the name of the span 468 | /// * `span_child_prefix` - a prefix that increase in size according to the number of parents 469 | /// the span has. 470 | /// 471 | /// The default template is `{span_child_prefix}{spinner} {span_name}{{{span_fields}}}`. 472 | pub fn with_progress_style(mut self, style: ProgressStyle) -> Self { 473 | self.progress_style = style; 474 | self 475 | } 476 | 477 | /// Set the indent used to mark the "level" of a given child span's progress bar. 478 | /// 479 | /// For example, if the given span is two levels deep (iow has two parent spans with progress 480 | /// bars), and this is " ", the `{span_child_prefix}` key for this span's progress bar will be 481 | /// prefixed with " ". 482 | pub fn with_span_child_prefix_indent(mut self, indent: &'static str) -> Self { 483 | self.span_child_prefix_indent = indent; 484 | self 485 | } 486 | 487 | /// Set the symbol used to denote this is a progress bar from a child span. 488 | /// 489 | /// This is ultimately concatenated with the child prefix indent to make the 490 | /// `span_child_prefix` progress bar key. 491 | pub fn with_span_child_prefix_symbol(mut self, symbol: &'static str) -> Self { 492 | self.span_child_prefix_symbol = symbol; 493 | self 494 | } 495 | 496 | /// Set the maximum number of progress bars that will be displayed, and the possible footer 497 | /// "progress bar" that displays when there are more progress bars than can be displayed. 498 | /// 499 | /// `footer_style` dictates the appearance of the footer, and the footer will only appear if 500 | /// there are more progress bars than can be displayed. If it is `None`, no footer will be 501 | /// displayed. `footer_style` has the following keys available to it: 502 | /// * `pending_progress_bars` - the number of progress bars waiting to be shown 503 | pub fn with_max_progress_bars( 504 | mut self, 505 | max_progress_bars: u64, 506 | footer_style: Option, 507 | ) -> Self { 508 | self.pb_manager 509 | .get_mut() 510 | .unwrap() 511 | .set_max_progress_bars(max_progress_bars, footer_style); 512 | 513 | self 514 | } 515 | 516 | /// Configures how often progress bars are recalcuated and redrawn to the terminal. 517 | pub fn with_tick_settings(mut self, tick_settings: TickSettings) -> Self { 518 | self.pb_manager 519 | .get_mut() 520 | .unwrap() 521 | .set_tick_settings(tick_settings); 522 | self 523 | } 524 | } 525 | 526 | impl IndicatifLayer 527 | where 528 | S: Subscriber + for<'a> LookupSpan<'a>, 529 | F: for<'writer> FormatFields<'writer> + 'static, 530 | { 531 | fn get_context( 532 | dispatch: &tracing::Dispatch, 533 | id: &span::Id, 534 | f: &mut dyn FnMut(&mut IndicatifSpanContext), 535 | ) { 536 | // The only way `get_context` can be called is if we have an `IndicatifLayer` added to the 537 | // expected subscriber, hence why we can `.expect` here. 538 | let subscriber = dispatch 539 | .downcast_ref::() 540 | .expect("subscriber should downcast to expected type; this is a bug!"); 541 | let span = subscriber 542 | .span(id) 543 | .expect("Span not found in context, this is a bug"); 544 | 545 | let mut ext = span.extensions_mut(); 546 | 547 | if let Some(indicatif_ctx) = ext.get_mut::() { 548 | f(indicatif_ctx); 549 | } 550 | } 551 | 552 | fn get_stderr_writer_context( 553 | dispatch: &tracing::Dispatch, 554 | f: &mut dyn FnMut(IndicatifWriter), 555 | ) { 556 | let layer = dispatch 557 | .downcast_ref::>() 558 | .expect("subscriber should downcast to expected type; this is a bug!"); 559 | 560 | f(layer.get_stderr_writer()) 561 | } 562 | 563 | fn get_stdout_writer_context( 564 | dispatch: &tracing::Dispatch, 565 | f: &mut dyn FnMut(IndicatifWriter), 566 | ) { 567 | let layer = dispatch 568 | .downcast_ref::>() 569 | .expect("subscriber should downcast to expected type; this is a bug!"); 570 | 571 | f(layer.get_stdout_writer()) 572 | } 573 | 574 | fn get_multi_progress_context(dispatch: &tracing::Dispatch, f: &mut dyn FnMut(MultiProgress)) { 575 | let layer = dispatch 576 | .downcast_ref::>() 577 | .expect("subscriber should downcast to expected type; this is a bug!"); 578 | 579 | f(layer.mp.clone()) 580 | } 581 | 582 | fn handle_on_enter( 583 | &self, 584 | pb_manager: &mut ProgressBarManager, 585 | id: &span::Id, 586 | ctx: &layer::Context<'_, S>, 587 | ) -> Option { 588 | let span = ctx 589 | .span(id) 590 | .expect("Span not found in context, this is a bug"); 591 | let mut ext = span.extensions_mut(); 592 | 593 | if let Some(indicatif_ctx) = ext.get_mut::() { 594 | // Start the progress bar when we enter the span for the first time. 595 | if indicatif_ctx.progress_bar.is_none() { 596 | indicatif_ctx.make_progress_bar(&self.progress_style); 597 | 598 | if let Some(ref parent_span_with_pb) = indicatif_ctx.parent_span { 599 | // Recursively start parent PBs if parent spans have not been entered yet. 600 | let parent_pb = self.handle_on_enter(pb_manager, parent_span_with_pb, ctx); 601 | 602 | indicatif_ctx.parent_progress_bar = parent_pb; 603 | } 604 | 605 | pb_manager.show_progress_bar(indicatif_ctx, id); 606 | } 607 | 608 | return indicatif_ctx.progress_bar.to_owned(); 609 | } 610 | 611 | None 612 | } 613 | } 614 | 615 | impl layer::Layer for IndicatifLayer 616 | where 617 | S: Subscriber + for<'a> LookupSpan<'a>, 618 | F: for<'writer> FormatFields<'writer> + 'static, 619 | { 620 | fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: layer::Context<'_, S>) { 621 | let span = ctx 622 | .span(id) 623 | .expect("Span not found in context, this is a bug"); 624 | let mut ext = span.extensions_mut(); 625 | 626 | let mut fields = FormattedFields::::new(String::new()); 627 | let _ = self 628 | .span_field_formatter 629 | .format_fields(fields.as_writer(), attrs); 630 | 631 | // Get the next parent span with a progress bar. 632 | let parent_span = ctx.span_scope(id).and_then(|scope| { 633 | scope.skip(1).find(|span| { 634 | let ext = span.extensions(); 635 | 636 | ext.get::().is_some() 637 | }) 638 | }); 639 | let parent_span_id = parent_span.as_ref().map(|span| span.id()); 640 | let parent_span_ext = parent_span.as_ref().map(|span| span.extensions()); 641 | let parent_indicatif_ctx = parent_span_ext 642 | .as_ref() 643 | .map(|ext| ext.get::().unwrap()); 644 | 645 | let (span_child_prefix, level) = match parent_indicatif_ctx { 646 | Some(v) => { 647 | let level = v.level + 1; 648 | 649 | ( 650 | format!( 651 | "{}{}", 652 | self.span_child_prefix_indent.repeat(level.into()), 653 | self.span_child_prefix_symbol 654 | ), 655 | level, 656 | ) 657 | } 658 | None => (String::new(), 0), 659 | }; 660 | 661 | ext.insert(IndicatifSpanContext { 662 | progress_bar: None, 663 | pb_init_settings: ProgressBarInitSettings::default(), 664 | parent_progress_bar: None, 665 | parent_span: parent_span_id, 666 | span_fields_formatted: Some(fields.fields), 667 | span_name: span.name().to_string(), 668 | span_child_prefix, 669 | level, 670 | }); 671 | } 672 | 673 | fn on_enter(&self, id: &span::Id, ctx: layer::Context<'_, S>) { 674 | let mut pb_manager_lock = self.pb_manager.lock().unwrap(); 675 | 676 | self.handle_on_enter(&mut pb_manager_lock, id, &ctx); 677 | } 678 | 679 | fn on_close(&self, id: span::Id, ctx: layer::Context<'_, S>) { 680 | let mut pb_manager_lock = self.pb_manager.lock().unwrap(); 681 | 682 | let span = ctx 683 | .span(&id) 684 | .expect("Span not found in context, this is a bug"); 685 | let mut ext = span.extensions_mut(); 686 | 687 | // Clear the progress bar only when the span has closed completely. 688 | if let Some(indicatif_ctx) = ext.get_mut::() { 689 | pb_manager_lock.finish_progress_bar(indicatif_ctx, &ctx); 690 | } 691 | } 692 | 693 | // See comments on [WithContext] for why we have this. 694 | // 695 | // SAFETY: this is safe because the `WithContext` function pointer is valid 696 | // for the lifetime of `&self`. 697 | unsafe fn downcast_raw(&self, id: TypeId) -> Option<*const ()> { 698 | match id { 699 | id if id == TypeId::of::() => Some(self as *const _ as *const ()), 700 | id if id == TypeId::of::() => { 701 | Some(&self.get_context as *const _ as *const ()) 702 | } 703 | id if id == TypeId::of::() => { 704 | Some(&self.get_stderr_writer_context as *const _ as *const ()) 705 | } 706 | id if id == TypeId::of::() => { 707 | Some(&self.get_stdout_writer_context as *const _ as *const ()) 708 | } 709 | id if id == TypeId::of::() => { 710 | Some(&self.get_multi_progress_context as *const _ as *const ()) 711 | } 712 | _ => None, 713 | } 714 | } 715 | } 716 | 717 | /// Hide all progress bars managed by [`IndicatifLayer`] (if it exists), executes `f`, then redraws 718 | /// the progress bars. Identical to [`indicatif::MultiProgress::suspend`]. 719 | /// 720 | /// Executes `f` even if there is no default tracing subscriber or if a `IndicatifLayer` has not 721 | /// been registered to that subscriber. 722 | /// 723 | /// NOTE: this does not suspend stdout/stderr prints from other threads, including things like 724 | /// `tracing::info!`. This only suspends the drawing of progress bars. 725 | /// 726 | /// WARNING: this holds an internal lock within `MultiProgress`. Calling methods like 727 | /// `writeln!(get_indicatif_stderr_writer(), "foobar")` or calling this method inside of `f` will 728 | /// result in a deadlock. 729 | pub fn suspend_tracing_indicatif R, R>(f: F) -> R { 730 | let mut mp: Option = None; 731 | 732 | tracing::dispatcher::get_default(|dispatch| { 733 | if let Some(ctx) = dispatch.downcast_ref::() { 734 | ctx.with_context(dispatch, |fetched_mp| { 735 | mp = Some(fetched_mp); 736 | }) 737 | } 738 | }); 739 | 740 | if let Some(mp) = mp { 741 | mp.suspend(f) 742 | } else { 743 | f() 744 | } 745 | } 746 | 747 | /// Helper macro that allows you to print to stdout without interfering with the progress bars 748 | /// created by tracing-indicatif. 749 | /// 750 | /// Args are directly forwarded to `writeln!`. Do not call this macro inside of 751 | /// `suspend_tracing_indicatif` or you will trigger a deadlock. 752 | #[macro_export] 753 | macro_rules! indicatif_println { 754 | ($($arg:tt)*) => { 755 | { 756 | use std::io::Write; 757 | 758 | if let Some(mut writer) = $crate::writer::get_indicatif_stdout_writer() { 759 | writeln!(writer, $($arg)*).unwrap(); 760 | } else { 761 | #[allow(clippy::explicit_write)] 762 | writeln!(std::io::stdout(), $($arg)*).unwrap(); 763 | } 764 | } 765 | } 766 | } 767 | 768 | /// Helper macro that allows you to print to stderr without interfering with the progress bars 769 | /// created by tracing-indicatif. 770 | /// 771 | /// Args are directly forwarded to `writeln!`. Do not call this macro inside of 772 | /// `suspend_tracing_indicatif` or you will trigger a deadlock. 773 | #[macro_export] 774 | macro_rules! indicatif_eprintln { 775 | ($($arg:tt)*) => { 776 | { 777 | use std::io::Write; 778 | 779 | if let Some(mut writer) = $crate::writer::get_indicatif_stderr_writer() { 780 | writeln!(writer, $($arg)*).unwrap(); 781 | } else { 782 | #[allow(clippy::explicit_write)] 783 | writeln!(std::io::stderr(), $($arg)*).unwrap(); 784 | } 785 | } 786 | } 787 | } 788 | 789 | #[cfg(test)] 790 | mod tests; 791 | -------------------------------------------------------------------------------- /src/pb_manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::sync::atomic::AtomicUsize; 3 | use std::sync::Arc; 4 | use std::time::Duration; 5 | 6 | use indicatif::style::ProgressStyle; 7 | use indicatif::MultiProgress; 8 | use indicatif::ProgressBar; 9 | use indicatif::ProgressDrawTarget; 10 | use indicatif::ProgressState; 11 | use tracing_core::span; 12 | use tracing_core::Subscriber; 13 | use tracing_subscriber::layer; 14 | use tracing_subscriber::registry::LookupSpan; 15 | 16 | use crate::IndicatifSpanContext; 17 | 18 | #[derive(Clone)] 19 | struct RequireDefault; 20 | 21 | /// Controls how often progress bars are recalculated and redrawn to the terminal. 22 | /// 23 | /// This struct must be constructed as 24 | /// ``` 25 | /// # use tracing_indicatif::TickSettings; 26 | /// TickSettings { 27 | /// term_draw_hz: 20, 28 | /// default_tick_interval: None, 29 | /// footer_tick_interval: None, 30 | /// ..Default::default() 31 | /// } 32 | /// # ; 33 | /// ``` 34 | /// as to ensure forward compatibility. 35 | #[derive(Clone)] 36 | pub struct TickSettings { 37 | /// The rate at which to draw to the terminal. 38 | /// 39 | /// A value of 20 here means indicatif will redraw the terminal state 20 times a second (i.e. 40 | /// once every 50ms). 41 | pub term_draw_hz: u8, 42 | /// The default interval to pass to `enable_steady_tick` for a new progress bar. This controls 43 | /// how often the progress bar state is recalculated. Defaults to 44 | /// `Some(Duration::from_millis(100))`. 45 | /// 46 | /// Note, this does not control how often the progress bar is actually redrawn, that is 47 | /// controlled by [`Self::term_draw_hz`]. 48 | /// 49 | /// Using `None` here will disable steady ticks for your progress bars. 50 | pub default_tick_interval: Option, 51 | /// The interval to pass to `enable_steady_tick` for the footer progress bar. This controls 52 | /// how often the footer progress bar state is recalculated. Defaults to `None`. 53 | /// 54 | /// Note, this does not control how often the footer progress bar is actually redrawn, that is 55 | /// controlled by [`Self::term_draw_hz`]. 56 | /// 57 | /// Using `None` here will disable steady ticks for the footer progress bar. Unless you have a 58 | /// spinner in your footer, you should set this to `None` as we manually redraw the footer 59 | /// whenever something changes. 60 | pub footer_tick_interval: Option, 61 | // Exists solely to require `..Default::default()` at the end of constructing this struct. 62 | #[doc(hidden)] 63 | #[allow(private_interfaces)] 64 | pub require_default: RequireDefault, 65 | } 66 | 67 | impl Default for TickSettings { 68 | fn default() -> Self { 69 | Self { 70 | term_draw_hz: 20, 71 | default_tick_interval: Some(Duration::from_millis(100)), 72 | footer_tick_interval: None, 73 | require_default: RequireDefault, 74 | } 75 | } 76 | } 77 | 78 | pub(crate) struct ProgressBarManager { 79 | pub(crate) mp: MultiProgress, 80 | active_progress_bars: u64, 81 | max_progress_bars: u64, 82 | // This is used in the footer progress bar and tracks the actual number of pending progress 83 | // bars. 84 | pending_progress_bars: Arc, 85 | // The `.len()` of this may differ from `pending_progress_bars`. If a span closes before its 86 | // progress bar is ever un-hidden, we decrement `pending_progress_bars` but won't clean the 87 | // span entry up from this `VecDeque` for performance reasons. Instead, whenever we do un-hide 88 | // a progress bar, we'll "garbage collect" closed spans from this then. 89 | pending_spans: VecDeque, 90 | // If this is `None`, a footer will never be shown. 91 | footer_pb: Option, 92 | tick_settings: TickSettings, 93 | } 94 | 95 | impl ProgressBarManager { 96 | pub(crate) fn new( 97 | max_progress_bars: u64, 98 | footer_progress_style: Option, 99 | tick_settings: TickSettings, 100 | ) -> Self { 101 | let mut s = Self { 102 | mp: { 103 | let mp = MultiProgress::new(); 104 | mp.set_draw_target(ProgressDrawTarget::stderr_with_hz( 105 | tick_settings.term_draw_hz, 106 | )); 107 | 108 | mp 109 | }, 110 | active_progress_bars: 0, 111 | max_progress_bars: 0, 112 | pending_progress_bars: Arc::new(AtomicUsize::new(0)), 113 | pending_spans: VecDeque::new(), 114 | footer_pb: None, 115 | tick_settings, 116 | }; 117 | 118 | s.set_max_progress_bars(max_progress_bars, footer_progress_style); 119 | 120 | s 121 | } 122 | 123 | pub(crate) fn set_max_progress_bars( 124 | &mut self, 125 | max_progress_bars: u64, 126 | footer_style: Option, 127 | ) { 128 | self.max_progress_bars = max_progress_bars; 129 | 130 | let pending_progress_bars = self.pending_progress_bars.clone(); 131 | self.footer_pb = footer_style.map(move |style| { 132 | ProgressBar::hidden().with_style(style.with_key( 133 | "pending_progress_bars", 134 | move |_: &ProgressState, writer: &mut dyn std::fmt::Write| { 135 | let _ = write!( 136 | writer, 137 | "{}", 138 | pending_progress_bars.load(std::sync::atomic::Ordering::Acquire) 139 | ); 140 | }, 141 | )) 142 | }); 143 | } 144 | 145 | pub(crate) fn set_tick_settings(&mut self, tick_settings: TickSettings) { 146 | self.mp.set_draw_target(ProgressDrawTarget::stderr_with_hz( 147 | tick_settings.term_draw_hz, 148 | )); 149 | self.tick_settings = tick_settings; 150 | } 151 | 152 | fn decrement_pending_pb(&mut self) { 153 | let prev_val = self 154 | .pending_progress_bars 155 | .fetch_sub(1, std::sync::atomic::Ordering::AcqRel); 156 | 157 | if let Some(footer_pb) = self.footer_pb.as_ref() { 158 | // If this span was the last one pending, clear the footer (if it was active). 159 | if prev_val == 1 { 160 | debug_assert!( 161 | !footer_pb.is_hidden(), 162 | "footer progress bar was hidden despite there being pending progress bars" 163 | ); 164 | 165 | if self.tick_settings.footer_tick_interval.is_some() { 166 | footer_pb.disable_steady_tick(); 167 | } 168 | 169 | // Appears to have broken with 170 | // https://github.com/console-rs/indicatif/pull/648 171 | // self.mp.set_move_cursor(false); 172 | footer_pb.finish_and_clear(); 173 | self.mp.remove(footer_pb); 174 | } else { 175 | footer_pb.tick(); 176 | } 177 | } 178 | } 179 | 180 | fn add_pending_pb(&mut self, span_id: &span::Id) { 181 | let prev_val = self 182 | .pending_progress_bars 183 | .fetch_add(1, std::sync::atomic::Ordering::AcqRel); 184 | self.pending_spans.push_back(span_id.clone()); 185 | 186 | // Show the footer progress bar. 187 | if let Some(footer_pb) = self.footer_pb.as_ref() { 188 | if prev_val == 0 { 189 | debug_assert!( 190 | footer_pb.is_hidden(), 191 | "footer progress bar was not hidden despite there being no pending progress bars" 192 | ); 193 | 194 | footer_pb.reset(); 195 | 196 | if let Some(tick_interval) = self.tick_settings.footer_tick_interval { 197 | footer_pb.enable_steady_tick(tick_interval); 198 | } 199 | 200 | self.mp.add(footer_pb.clone()); 201 | // Appears to have broken with 202 | // https://github.com/console-rs/indicatif/pull/648 203 | // self.mp.set_move_cursor(true); 204 | } 205 | 206 | footer_pb.tick(); 207 | } 208 | } 209 | 210 | pub(crate) fn show_progress_bar( 211 | &mut self, 212 | pb_span_ctx: &mut IndicatifSpanContext, 213 | span_id: &span::Id, 214 | ) { 215 | if self.active_progress_bars < self.max_progress_bars { 216 | let pb = match pb_span_ctx.parent_progress_bar { 217 | // TODO(emersonford): fix span ordering in progress bar, because we use 218 | // `insert_after`, we end up showing the child progress bars in reverse order. 219 | Some(ref parent_pb) => self 220 | .mp 221 | .insert_after(parent_pb, pb_span_ctx.progress_bar.take().unwrap()), 222 | None => { 223 | if self 224 | .footer_pb 225 | .as_ref() 226 | .map(|footer_pb| !footer_pb.is_hidden()) 227 | .unwrap_or(false) 228 | { 229 | self.mp 230 | .insert_from_back(1, pb_span_ctx.progress_bar.take().unwrap()) 231 | } else { 232 | self.mp.add(pb_span_ctx.progress_bar.take().unwrap()) 233 | } 234 | } 235 | }; 236 | 237 | self.active_progress_bars += 1; 238 | 239 | if let Some(tick_interval) = self.tick_settings.default_tick_interval { 240 | pb.enable_steady_tick(tick_interval); 241 | } 242 | 243 | pb.tick(); 244 | 245 | pb_span_ctx.progress_bar = Some(pb); 246 | } else { 247 | self.add_pending_pb(span_id); 248 | } 249 | } 250 | 251 | pub(crate) fn finish_progress_bar( 252 | &mut self, 253 | pb_span_ctx: &mut IndicatifSpanContext, 254 | ctx: &layer::Context<'_, S>, 255 | ) where 256 | S: Subscriber + for<'a> LookupSpan<'a>, 257 | { 258 | let Some(pb) = pb_span_ctx.progress_bar.take() else { 259 | // Span was never entered. 260 | return; 261 | }; 262 | 263 | // The span closed before we had a chance to show its progress bar. 264 | if pb.is_hidden() { 265 | self.decrement_pending_pb(); 266 | return; 267 | } 268 | 269 | // This span had an active/shown progress bar. 270 | pb.finish_and_clear(); 271 | self.mp.remove(&pb); 272 | self.active_progress_bars -= 1; 273 | 274 | loop { 275 | let Some(span_id) = self.pending_spans.pop_front() else { 276 | break; 277 | }; 278 | 279 | match ctx.span(&span_id) { 280 | Some(next_eligible_span) => { 281 | let mut ext = next_eligible_span.extensions_mut(); 282 | let indicatif_span_ctx = ext 283 | .get_mut::() 284 | .expect("No IndicatifSpanContext found; this is a bug"); 285 | 286 | // It possible `on_close` has been called on a span but it has not yet been 287 | // removed from `ctx.span` (e.g., tracing may still be iterating through each 288 | // layer's `on_close` method and cannot remove the span from the registry until 289 | // it has finished `on_close` for each layer). So we may successfully fetch the 290 | // span, despite having closed out its progress bar. 291 | if indicatif_span_ctx.progress_bar.is_none() { 292 | continue; 293 | } 294 | 295 | self.decrement_pending_pb(); 296 | self.show_progress_bar(indicatif_span_ctx, &span_id); 297 | break; 298 | } 299 | None => { 300 | // Span was closed earlier, we "garbage collect" it from the queue here. 301 | continue; 302 | } 303 | } 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/span_ext.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to modify a progress bar associated with a given span. 2 | use indicatif::ProgressStyle; 3 | use tracing::Span; 4 | 5 | use crate::IndicatifSpanContext; 6 | use crate::WithContext; 7 | 8 | // TODO(emersonford): expose stderr/stdout writers in span ext 9 | 10 | fn apply_to_indicatif_span(span: &Span, f: impl FnMut(&mut IndicatifSpanContext)) { 11 | span.with_subscriber(|(id, subscriber)| { 12 | if let Some(get_context) = subscriber.downcast_ref::() { 13 | get_context.with_context(subscriber, id, f); 14 | } 15 | }); 16 | } 17 | 18 | /// Utilities to modify the progress bar associated to tracing [`Span`]'s (if one exists). 19 | /// 20 | /// For example, you can call these on the current Span: 21 | /// ``` 22 | /// use tracing_indicatif::span_ext::IndicatifSpanExt; 23 | /// use indicatif::ProgressStyle; 24 | /// 25 | /// tracing::Span::current().pb_set_style(&ProgressStyle::default_spinner()); 26 | /// ``` 27 | /// 28 | /// NOTE: These methods will silently have no effect if a 29 | /// [`IndicatifLayer`](crate::IndicatifLayer) was not registered with the tracing subscriber, 30 | /// or if this span was filtered for the registered `IndicatifLayer`. Because of this behavior, you 31 | /// can "safely" call these methods inside of non-CLI contexts as these methods will gracefully 32 | /// do nothing if you have not enabled progress bars for your tracing spans. 33 | pub trait IndicatifSpanExt { 34 | /// Sets the [`ProgressStyle`] of the progress bar associated with this span. 35 | /// 36 | /// If this span has not yet been entered, this will be the progress style the progress bar for 37 | /// this span uses when the span is entered for the first time. If this span has been entered, 38 | /// this will update the existing progress bar's style. 39 | fn pb_set_style(&self, style: &ProgressStyle); 40 | 41 | /// Briefly enters the span, which starts the progress bar for the span. 42 | /// 43 | /// Has no effect if the span has already been entered before. 44 | fn pb_start(&self); 45 | 46 | /// Sets the length of the progress bar for this span. See 47 | /// [`set_length`](indicatif::ProgressBar::set_length). 48 | fn pb_set_length(&self, len: u64); 49 | 50 | /// Sets the position of the progress bar for this span. See 51 | /// [`set_position`](indicatif::ProgressBar::set_position). 52 | /// 53 | /// WARNING: you should call [`Self::pb_set_length`] at least once before calling this method, or you 54 | /// may see a buggy progress bar. 55 | fn pb_set_position(&self, pos: u64); 56 | 57 | /// Increments the position of the progress bar for this span. See 58 | /// [`inc`](indicatif::ProgressBar::inc). 59 | /// 60 | /// WARNING: you should call [`Self::pb_set_length`] at least once before calling this method, or you 61 | /// may see a buggy progress bar. 62 | fn pb_inc(&self, delta: u64); 63 | 64 | /// Increments the length of the progress bar for this span. See 65 | /// [`inc_length`](indicatif::ProgressBar::inc_length). 66 | /// 67 | /// Has no effect if [`Self::pb_set_length`] has not been called at least once. 68 | fn pb_inc_length(&self, delta: u64); 69 | 70 | /// Sets the message of the progress bar for this span. See 71 | /// [`set_message`](indicatif::ProgressBar::set_message). 72 | fn pb_set_message(&self, msg: &str); 73 | 74 | /// Trigger a recalculation of the progress bar state. See 75 | /// [`tick`](indicatif::ProgressBar::tick). 76 | /// 77 | /// Has no effect if the progress bar for this span is not active. 78 | fn pb_tick(&self); 79 | } 80 | 81 | impl IndicatifSpanExt for Span { 82 | fn pb_set_style(&self, style: &ProgressStyle) { 83 | apply_to_indicatif_span(self, |indicatif_ctx| { 84 | // Cloning the `ProgressStyle` is necessary to make this `FnMut` :( 85 | indicatif_ctx.set_progress_bar_style(style.clone()); 86 | }); 87 | } 88 | 89 | fn pb_start(&self) { 90 | let _ = self.enter(); 91 | } 92 | 93 | fn pb_set_length(&self, len: u64) { 94 | apply_to_indicatif_span(self, |indicatif_ctx| { 95 | indicatif_ctx.set_progress_bar_length(len); 96 | }); 97 | } 98 | 99 | fn pb_set_position(&self, pos: u64) { 100 | apply_to_indicatif_span(self, |indicatif_ctx| { 101 | indicatif_ctx.set_progress_bar_position(pos); 102 | }); 103 | } 104 | 105 | fn pb_inc(&self, pos: u64) { 106 | apply_to_indicatif_span(self, |indicatif_ctx| { 107 | indicatif_ctx.inc_progress_bar_position(pos); 108 | }); 109 | } 110 | 111 | fn pb_inc_length(&self, delta: u64) { 112 | apply_to_indicatif_span(self, |indicatif_ctx| { 113 | indicatif_ctx.inc_progress_bar_length(delta); 114 | }); 115 | } 116 | 117 | fn pb_set_message(&self, msg: &str) { 118 | apply_to_indicatif_span(self, |indicatif_ctx| { 119 | indicatif_ctx.set_progress_bar_message(msg.to_string()); 120 | }); 121 | } 122 | 123 | fn pb_tick(&self) { 124 | apply_to_indicatif_span(self, |indicatif_ctx| { 125 | indicatif_ctx.progress_bar_tick(); 126 | }); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::thread; 3 | use std::time::Duration; 4 | 5 | use indicatif::InMemoryTerm; 6 | use indicatif::MultiProgress; 7 | use indicatif::ProgressDrawTarget; 8 | use indicatif::ProgressStyle; 9 | use indicatif::TermLike; 10 | use tracing::info; 11 | use tracing::info_span; 12 | use tracing_core::Subscriber; 13 | use tracing_subscriber::fmt::format::DefaultFields; 14 | use tracing_subscriber::fmt::MakeWriter; 15 | use tracing_subscriber::layer::SubscriberExt; 16 | 17 | use crate::filter::hide_indicatif_span_fields; 18 | use crate::span_ext::IndicatifSpanExt; 19 | use crate::suspend_tracing_indicatif; 20 | use crate::IndicatifLayer; 21 | use crate::TickSettings; 22 | 23 | #[derive(Clone)] 24 | struct InMemoryTermWriter { 25 | progress_bars: Option, 26 | term: InMemoryTerm, 27 | } 28 | 29 | impl io::Write for InMemoryTermWriter { 30 | fn write(&mut self, buf: &[u8]) -> io::Result { 31 | if let Some(ref pb) = self.progress_bars { 32 | pb.suspend(|| self.term.write_str(std::str::from_utf8(buf).unwrap()))?; 33 | } else { 34 | self.term.write_str(std::str::from_utf8(buf).unwrap())?; 35 | } 36 | 37 | Ok(buf.len()) 38 | } 39 | 40 | fn flush(&mut self) -> io::Result<()> { 41 | if let Some(ref pb) = self.progress_bars { 42 | pb.suspend(|| self.term.flush()) 43 | } else { 44 | self.term.flush() 45 | } 46 | } 47 | } 48 | 49 | impl<'a> MakeWriter<'a> for InMemoryTermWriter { 50 | type Writer = InMemoryTermWriter; 51 | 52 | fn make_writer(&'a self) -> Self::Writer { 53 | self.clone() 54 | } 55 | } 56 | 57 | struct HelpersConfig { 58 | show_footer: bool, 59 | enable_steady_tick: bool, 60 | } 61 | 62 | impl Default for HelpersConfig { 63 | fn default() -> Self { 64 | Self { 65 | show_footer: true, 66 | enable_steady_tick: false, 67 | } 68 | } 69 | } 70 | 71 | fn make_helpers(config: HelpersConfig) -> (impl Subscriber, InMemoryTerm) { 72 | let indicatif_layer = IndicatifLayer::new() 73 | .with_max_progress_bars( 74 | 5, 75 | config.show_footer.then(|| { 76 | ProgressStyle::with_template("...and {pending_progress_bars} more not shown above.") 77 | .unwrap() 78 | }), 79 | ) 80 | .with_span_field_formatter(DefaultFields::new()) 81 | .with_progress_style( 82 | ProgressStyle::with_template("{span_child_prefix}{span_name}{{{span_fields}}}") 83 | .unwrap(), 84 | ) 85 | .with_span_child_prefix_indent("--") 86 | .with_span_child_prefix_symbol("> ") 87 | .with_tick_settings(TickSettings { 88 | term_draw_hz: 20, 89 | default_tick_interval: if config.enable_steady_tick { 90 | Some(Duration::from_millis(50)) 91 | } else { 92 | None 93 | }, 94 | footer_tick_interval: None, 95 | ..Default::default() 96 | }); 97 | 98 | let term = InMemoryTerm::new(10, 100); 99 | 100 | let mp = indicatif_layer.pb_manager.lock().unwrap().mp.clone(); 101 | 102 | mp.set_draw_target(ProgressDrawTarget::term_like(Box::new(term.clone()))); 103 | 104 | let writer = InMemoryTermWriter { 105 | progress_bars: Some(mp), 106 | term: term.clone(), 107 | }; 108 | 109 | ( 110 | tracing_subscriber::registry() 111 | .with( 112 | tracing_subscriber::fmt::layer() 113 | .with_ansi(false) 114 | .without_time() 115 | .with_writer(writer), 116 | ) 117 | .with(indicatif_layer), 118 | term, 119 | ) 120 | } 121 | 122 | #[test] 123 | fn test_one_basic_pb() { 124 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 125 | 126 | tracing::subscriber::with_default(subscriber, || { 127 | let _span = info_span!("foo").entered(); 128 | thread::sleep(Duration::from_millis(10)); 129 | 130 | assert_eq!( 131 | term.contents(), 132 | r#" 133 | foo{} 134 | "# 135 | .trim() 136 | ); 137 | }); 138 | } 139 | 140 | #[test] 141 | fn test_one_child_pb() { 142 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 143 | 144 | tracing::subscriber::with_default(subscriber, || { 145 | let _span = info_span!("foo").entered(); 146 | let _child_span = info_span!("child").entered(); 147 | thread::sleep(Duration::from_millis(10)); 148 | 149 | assert_eq!( 150 | term.contents(), 151 | r#" 152 | foo{} 153 | --> child{} 154 | "# 155 | .trim() 156 | ); 157 | }); 158 | } 159 | 160 | #[test] 161 | fn test_span_fields() { 162 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 163 | 164 | tracing::subscriber::with_default(subscriber, || { 165 | let _span = info_span!("foo", val = 3).entered(); 166 | 167 | thread::sleep(Duration::from_millis(10)); 168 | 169 | assert_eq!( 170 | term.contents(), 171 | r#" 172 | foo{val=3} 173 | "# 174 | .trim() 175 | ); 176 | }); 177 | } 178 | 179 | #[test] 180 | fn test_multi_child_pb() { 181 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 182 | 183 | tracing::subscriber::with_default(subscriber, || { 184 | let _span1 = info_span!("foo", blah = 1); 185 | let _span1_enter = _span1.enter(); 186 | 187 | let _child_span1 = info_span!("foo.child"); 188 | let _child_span1_enter = _child_span1.enter(); 189 | 190 | let _child_child_span1 = info_span!("foo.child.child", blah = 3, hello = "world"); 191 | let _child_child_span1_enter = _child_child_span1.enter(); 192 | 193 | std::mem::drop(_span1_enter); 194 | std::mem::drop(_child_span1_enter); 195 | std::mem::drop(_child_child_span1_enter); 196 | 197 | let _span2 = info_span!("bar"); 198 | let _span2_enter = _span2.enter(); 199 | 200 | let _child_span2 = info_span!("bar.child"); 201 | let _child_span2_enter = _child_span2.enter(); 202 | 203 | std::mem::drop(_span2_enter); 204 | std::mem::drop(_child_span2_enter); 205 | 206 | thread::sleep(Duration::from_millis(10)); 207 | 208 | assert_eq!( 209 | term.contents(), 210 | r#" 211 | foo{blah=1} 212 | --> foo.child{} 213 | ----> foo.child.child{blah=3 hello="world"} 214 | bar{} 215 | --> bar.child{} 216 | "# 217 | .trim() 218 | ); 219 | }); 220 | } 221 | 222 | #[test] 223 | fn test_max_pbs() { 224 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 225 | 226 | tracing::subscriber::with_default(subscriber, || { 227 | let _span1 = info_span!("1"); 228 | _span1.pb_start(); 229 | let _span2 = info_span!("2"); 230 | _span2.pb_start(); 231 | let _span3 = info_span!("3"); 232 | _span3.pb_start(); 233 | let _span4 = info_span!("4"); 234 | _span4.pb_start(); 235 | let _span5 = info_span!("5"); 236 | _span5.pb_start(); 237 | 238 | assert_eq!( 239 | term.contents(), 240 | r#" 241 | 1{} 242 | 2{} 243 | 3{} 244 | 4{} 245 | 5{} 246 | "# 247 | .trim() 248 | ); 249 | 250 | let _span6 = info_span!("6"); 251 | _span6.pb_start(); 252 | 253 | assert_eq!( 254 | term.contents(), 255 | r#" 256 | 1{} 257 | 2{} 258 | 3{} 259 | 4{} 260 | 5{} 261 | ...and 1 more not shown above. 262 | "# 263 | .trim() 264 | ); 265 | 266 | let _span7 = info_span!("7"); 267 | _span7.pb_start(); 268 | 269 | assert_eq!( 270 | term.contents(), 271 | r#" 272 | 1{} 273 | 2{} 274 | 3{} 275 | 4{} 276 | 5{} 277 | ...and 2 more not shown above. 278 | "# 279 | .trim() 280 | ); 281 | 282 | std::mem::drop(_span6); 283 | 284 | assert_eq!( 285 | term.contents(), 286 | r#" 287 | 1{} 288 | 2{} 289 | 3{} 290 | 4{} 291 | 5{} 292 | ...and 1 more not shown above. 293 | "# 294 | .trim() 295 | ); 296 | 297 | std::mem::drop(_span1); 298 | 299 | assert_eq!( 300 | term.contents(), 301 | r#" 302 | 2{} 303 | 3{} 304 | 4{} 305 | 5{} 306 | 7{} 307 | "# 308 | .trim() 309 | ); 310 | 311 | std::mem::drop(_span2); 312 | 313 | assert_eq!( 314 | term.contents(), 315 | r#" 316 | 3{} 317 | 4{} 318 | 5{} 319 | 7{} 320 | "# 321 | .trim() 322 | ); 323 | 324 | let _span8 = info_span!("8"); 325 | _span8.pb_start(); 326 | 327 | assert_eq!( 328 | term.contents(), 329 | r#" 330 | 3{} 331 | 4{} 332 | 5{} 333 | 7{} 334 | 8{} 335 | "# 336 | .trim() 337 | ); 338 | 339 | let _span9 = info_span!("9"); 340 | _span9.pb_start(); 341 | 342 | assert_eq!( 343 | term.contents(), 344 | r#" 345 | 3{} 346 | 4{} 347 | 5{} 348 | 7{} 349 | 8{} 350 | ...and 1 more not shown above. 351 | "# 352 | .trim() 353 | ); 354 | 355 | let _span10 = info_span!("10"); 356 | _span10.pb_start(); 357 | 358 | assert_eq!( 359 | term.contents(), 360 | r#" 361 | 3{} 362 | 4{} 363 | 5{} 364 | 7{} 365 | 8{} 366 | ...and 2 more not shown above. 367 | "# 368 | .trim() 369 | ); 370 | 371 | drop(_span3); 372 | 373 | assert_eq!( 374 | term.contents(), 375 | r#" 376 | 4{} 377 | 5{} 378 | 7{} 379 | 8{} 380 | 9{} 381 | ...and 1 more not shown above. 382 | "# 383 | .trim() 384 | ); 385 | 386 | drop(_span4); 387 | 388 | assert_eq!( 389 | term.contents(), 390 | r#" 391 | 5{} 392 | 7{} 393 | 8{} 394 | 9{} 395 | 10{} 396 | "# 397 | .trim() 398 | ); 399 | }); 400 | } 401 | 402 | #[test] 403 | fn test_max_pbs_no_footer() { 404 | let (subscriber, term) = make_helpers(HelpersConfig { 405 | show_footer: false, 406 | ..Default::default() 407 | }); 408 | 409 | tracing::subscriber::with_default(subscriber, || { 410 | let _span1 = info_span!("1"); 411 | _span1.pb_start(); 412 | let _span2 = info_span!("2"); 413 | _span2.pb_start(); 414 | let _span3 = info_span!("3"); 415 | _span3.pb_start(); 416 | let _span4 = info_span!("4"); 417 | _span4.pb_start(); 418 | let _span5 = info_span!("5"); 419 | _span5.pb_start(); 420 | let _span6 = info_span!("6"); 421 | _span6.pb_start(); 422 | 423 | thread::sleep(Duration::from_millis(10)); 424 | assert_eq!( 425 | term.contents(), 426 | r#" 427 | 1{} 428 | 2{} 429 | 3{} 430 | 4{} 431 | 5{} 432 | "# 433 | .trim() 434 | ); 435 | 436 | let _span7 = info_span!("7"); 437 | _span7.pb_start(); 438 | 439 | // Need 150ms of sleep here to trigger a refresh of the footer. 440 | thread::sleep(Duration::from_millis(150)); 441 | assert_eq!( 442 | term.contents(), 443 | r#" 444 | 1{} 445 | 2{} 446 | 3{} 447 | 4{} 448 | 5{} 449 | "# 450 | .trim() 451 | ); 452 | 453 | std::mem::drop(_span6); 454 | 455 | // Need 150ms of sleep here to trigger a refresh of the footer. 456 | thread::sleep(Duration::from_millis(150)); 457 | assert_eq!( 458 | term.contents(), 459 | r#" 460 | 1{} 461 | 2{} 462 | 3{} 463 | 4{} 464 | 5{} 465 | "# 466 | .trim() 467 | ); 468 | 469 | std::mem::drop(_span1); 470 | 471 | thread::sleep(Duration::from_millis(10)); 472 | assert_eq!( 473 | term.contents(), 474 | r#" 475 | 2{} 476 | 3{} 477 | 4{} 478 | 5{} 479 | 7{} 480 | "# 481 | .trim() 482 | ); 483 | }); 484 | } 485 | 486 | #[test] 487 | fn test_parent_no_enter_doesnt_panic() { 488 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 489 | 490 | tracing::subscriber::with_default(subscriber, || { 491 | let span = info_span!("foo"); 492 | let _child_span = info_span!(parent: &span, "child").entered(); 493 | thread::sleep(Duration::from_millis(10)); 494 | 495 | assert_eq!( 496 | term.contents(), 497 | r#" 498 | foo{} 499 | --> child{} 500 | "# 501 | .trim() 502 | ); 503 | }); 504 | } 505 | 506 | #[test] 507 | fn test_log_statements_coexist() { 508 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 509 | 510 | tracing::subscriber::with_default(subscriber, || { 511 | let _span1 = info_span!("foo"); 512 | _span1.pb_start(); 513 | 514 | thread::sleep(Duration::from_millis(10)); 515 | assert_eq!( 516 | term.contents(), 517 | r#" 518 | foo{} 519 | "# 520 | .trim() 521 | ); 522 | 523 | info!("hello world!"); 524 | assert_eq!( 525 | term.contents() 526 | .lines() 527 | .map(|line| line.trim()) 528 | .collect::>() 529 | .join("\n"), 530 | r#" 531 | INFO tracing_indicatif::tests: hello world! 532 | foo{} 533 | "# 534 | .trim() 535 | ); 536 | }); 537 | } 538 | 539 | #[test] 540 | fn test_change_style_before_show() { 541 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 542 | 543 | tracing::subscriber::with_default(subscriber, || { 544 | let span1 = info_span!("foo"); 545 | span1.pb_set_style(&ProgressStyle::with_template("hello_world").unwrap()); 546 | span1.pb_start(); 547 | 548 | thread::sleep(Duration::from_millis(10)); 549 | assert_eq!( 550 | term.contents(), 551 | r#" 552 | hello_world 553 | "# 554 | .trim() 555 | ); 556 | }); 557 | } 558 | 559 | #[test] 560 | fn test_change_style_after_show() { 561 | let (subscriber, term) = make_helpers(HelpersConfig { 562 | enable_steady_tick: true, 563 | ..Default::default() 564 | }); 565 | 566 | tracing::subscriber::with_default(subscriber, || { 567 | let span1 = info_span!("foo"); 568 | span1.pb_start(); 569 | 570 | thread::sleep(Duration::from_millis(10)); 571 | assert_eq!( 572 | term.contents(), 573 | r#" 574 | foo{} 575 | "# 576 | .trim() 577 | ); 578 | 579 | span1.pb_set_style(&ProgressStyle::with_template("hello_world").unwrap()); 580 | thread::sleep(Duration::from_millis(150)); 581 | assert_eq!( 582 | term.contents(), 583 | r#" 584 | hello_world 585 | "# 586 | .trim() 587 | ); 588 | }); 589 | } 590 | 591 | #[test] 592 | fn test_change_style_after_show_tick() { 593 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 594 | 595 | tracing::subscriber::with_default(subscriber, || { 596 | let span1 = info_span!("foo"); 597 | span1.pb_start(); 598 | 599 | assert_eq!( 600 | term.contents(), 601 | r#" 602 | foo{} 603 | "# 604 | .trim() 605 | ); 606 | 607 | span1.pb_set_style(&ProgressStyle::with_template("hello_world").unwrap()); 608 | span1.pb_tick(); 609 | assert_eq!( 610 | term.contents(), 611 | r#" 612 | hello_world 613 | "# 614 | .trim() 615 | ); 616 | }); 617 | } 618 | 619 | #[test] 620 | fn test_bar_style_progress_bar() { 621 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 622 | 623 | tracing::subscriber::with_default(subscriber, || { 624 | let span1 = info_span!("foo"); 625 | span1.pb_set_style(&ProgressStyle::default_bar()); 626 | span1.pb_set_length(10); 627 | span1.pb_start(); 628 | 629 | thread::sleep(Duration::from_millis(10)); 630 | assert_eq!( 631 | term.contents(), 632 | r#" 633 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0/10 634 | "# 635 | .trim() 636 | ); 637 | 638 | span1.pb_inc(1); 639 | 640 | thread::sleep(Duration::from_millis(150)); 641 | assert_eq!( 642 | term.contents(), 643 | r#" 644 | █████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 1/10 645 | "# 646 | .trim() 647 | ); 648 | 649 | span1.pb_inc(9); 650 | thread::sleep(Duration::from_millis(150)); 651 | assert_eq!( 652 | term.contents(), 653 | r#" 654 | ██████████████████████████████████████████████████████████████████████████████████████████████ 10/10 655 | "# 656 | .trim() 657 | ); 658 | }); 659 | } 660 | 661 | #[test] 662 | fn test_bar_style_progress_bar_inc_before_start() { 663 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 664 | 665 | tracing::subscriber::with_default(subscriber, || { 666 | let span1 = info_span!("foo"); 667 | span1.pb_set_style(&ProgressStyle::default_bar()); 668 | span1.pb_set_length(10); 669 | span1.pb_inc_length(1); 670 | span1.pb_inc(2); 671 | span1.pb_start(); 672 | 673 | thread::sleep(Duration::from_millis(10)); 674 | assert_eq!( 675 | term.contents(), 676 | r#" 677 | █████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2/11 678 | "# 679 | .trim() 680 | ); 681 | 682 | span1.pb_inc(1); 683 | 684 | thread::sleep(Duration::from_millis(150)); 685 | assert_eq!( 686 | term.contents(), 687 | r#" 688 | █████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3/11 689 | "# 690 | .trim() 691 | ); 692 | 693 | span1.pb_inc_length(1); 694 | span1.pb_inc(9); 695 | thread::sleep(Duration::from_millis(150)); 696 | assert_eq!( 697 | term.contents(), 698 | r#" 699 | ██████████████████████████████████████████████████████████████████████████████████████████████ 12/12 700 | "# 701 | .trim() 702 | ); 703 | }); 704 | } 705 | 706 | #[test] 707 | fn test_bar_style_progress_bar_inc_without_set_length() { 708 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 709 | 710 | tracing::subscriber::with_default(subscriber, || { 711 | let span1 = info_span!("foo"); 712 | span1.pb_set_style(&ProgressStyle::default_bar()); 713 | span1.pb_inc_length(5); 714 | span1.pb_inc(2); 715 | span1.pb_start(); 716 | 717 | thread::sleep(Duration::from_millis(10)); 718 | assert_eq!( 719 | term.contents(), 720 | r#" 721 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 2/2 722 | "# 723 | .trim() 724 | ); 725 | 726 | span1.pb_inc_length(5); 727 | span1.pb_inc(1); 728 | 729 | thread::sleep(Duration::from_millis(150)); 730 | assert_eq!( 731 | term.contents(), 732 | r#" 733 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3/3 734 | "# 735 | .trim() 736 | ); 737 | 738 | span1.pb_inc(8); 739 | thread::sleep(Duration::from_millis(150)); 740 | assert_eq!( 741 | term.contents(), 742 | r#" 743 | ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 11/11 744 | "# 745 | .trim() 746 | ); 747 | }); 748 | } 749 | 750 | #[test] 751 | fn test_span_ext_no_effect_when_layer_not_added() { 752 | let term = InMemoryTerm::new(10, 100); 753 | 754 | let writer = InMemoryTermWriter { 755 | progress_bars: None, 756 | term: term.clone(), 757 | }; 758 | 759 | let subscriber = tracing_subscriber::registry().with( 760 | tracing_subscriber::fmt::layer() 761 | .with_ansi(false) 762 | .without_time() 763 | .with_writer(writer), 764 | ); 765 | 766 | tracing::subscriber::with_default(subscriber, || { 767 | let span1 = info_span!("foo"); 768 | span1.pb_set_style(&ProgressStyle::default_bar()); 769 | span1.pb_set_length(10); 770 | span1.pb_start(); 771 | 772 | thread::sleep(Duration::from_millis(10)); 773 | info!("hello world!"); 774 | thread::sleep(Duration::from_millis(10)); 775 | 776 | assert_eq!( 777 | term.contents().trim(), 778 | r#" 779 | INFO tracing_indicatif::tests: hello world! 780 | "# 781 | .trim() 782 | ); 783 | }); 784 | } 785 | 786 | #[test] 787 | fn test_suspend_with_layer() { 788 | let (subscriber, term) = make_helpers(HelpersConfig::default()); 789 | 790 | tracing::subscriber::with_default(subscriber, || { 791 | let _span1 = info_span!("foo"); 792 | _span1.pb_start(); 793 | 794 | thread::sleep(Duration::from_millis(10)); 795 | assert_eq!( 796 | term.contents(), 797 | r#" 798 | foo{} 799 | "# 800 | .trim() 801 | ); 802 | 803 | let _ = suspend_tracing_indicatif(|| term.write_line("hello world")); 804 | 805 | assert_eq!( 806 | term.contents() 807 | .lines() 808 | .map(|line| line.trim()) 809 | .collect::>() 810 | .join("\n"), 811 | r#" 812 | hello world 813 | foo{} 814 | "# 815 | .trim() 816 | ); 817 | 818 | let _ = suspend_tracing_indicatif(|| term.write_line("this is a test")); 819 | 820 | assert_eq!( 821 | term.contents() 822 | .lines() 823 | .map(|line| line.trim()) 824 | .collect::>() 825 | .join("\n"), 826 | r#" 827 | hello world 828 | this is a test 829 | foo{} 830 | "# 831 | .trim() 832 | ); 833 | }); 834 | } 835 | 836 | #[test] 837 | fn test_suspend_without_layer() { 838 | let term = InMemoryTerm::new(10, 100); 839 | 840 | assert_eq!( 841 | term.contents(), 842 | r#" 843 | "# 844 | .trim() 845 | ); 846 | 847 | let _ = suspend_tracing_indicatif(|| term.write_line("hello world")); 848 | 849 | assert_eq!( 850 | term.contents() 851 | .lines() 852 | .map(|line| line.trim()) 853 | .collect::>() 854 | .join("\n"), 855 | r#" 856 | hello world 857 | "# 858 | .trim() 859 | ); 860 | } 861 | 862 | #[test] 863 | fn test_with_span_field_formatter() { 864 | let indicatif_layer = IndicatifLayer::new() 865 | .with_span_field_formatter(hide_indicatif_span_fields(DefaultFields::new())); 866 | 867 | let subscriber = tracing_subscriber::registry() 868 | .with(tracing_subscriber::fmt::layer().with_writer(indicatif_layer.get_stderr_writer())) 869 | .with(indicatif_layer); 870 | 871 | tracing::subscriber::with_default(subscriber, || { 872 | let _span = info_span!("foo"); 873 | _span.pb_start(); 874 | 875 | suspend_tracing_indicatif(|| {}); 876 | }); 877 | } 878 | 879 | #[test] 880 | fn test_parent_span_enter_ordering() { 881 | let (subscriber, _) = make_helpers(HelpersConfig::default()); 882 | 883 | tracing::subscriber::with_default(subscriber, || { 884 | let grandparent_span = info_span!("grandparent"); 885 | let parent_span = info_span!(parent: &grandparent_span, "parent"); 886 | let child_span = info_span!(parent: &parent_span, "child"); 887 | 888 | let span1 = info_span!("span1"); 889 | span1.pb_start(); 890 | let span2 = info_span!("span2"); 891 | span2.pb_start(); 892 | let span3 = info_span!("span3"); 893 | span3.pb_start(); 894 | let span4 = info_span!("span4"); 895 | span4.pb_start(); 896 | let span5 = info_span!("span5"); 897 | span5.pb_start(); 898 | 899 | child_span.pb_start(); 900 | grandparent_span.pb_start(); 901 | 902 | drop(span1); 903 | }); 904 | } 905 | 906 | // These don't actually run anything, but exist to type check macros. 907 | #[allow(dead_code)] 908 | fn type_check_indicatif_println() { 909 | indicatif_println!("{}", "hello"); 910 | } 911 | 912 | #[allow(dead_code)] 913 | fn type_check_indicatif_eprintln() { 914 | indicatif_eprintln!("{}", "hello"); 915 | } 916 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Provides some general tracing utilities that are useful for this crate, for example a utility 2 | //! to filter "indicatif.pb_show" and "indicatif.pb_hide" from printing spans. 3 | use std::fmt; 4 | 5 | use tracing::field::Visit; 6 | use tracing_core::Field; 7 | use tracing_subscriber::field::RecordFields; 8 | use tracing_subscriber::{ 9 | field::{MakeVisitor, VisitFmt, VisitOutput}, 10 | fmt::{format::Writer, FormatFields}, 11 | }; 12 | 13 | /// Wraps around an existing struct that impls [`FormatFields`], but allows for filtering specific 14 | /// fields from spans or events. 15 | pub struct FilteredFormatFields { 16 | format: Format, 17 | filter: Filter, 18 | } 19 | 20 | impl<'writer, Format, Filter> FilteredFormatFields 21 | where 22 | Format: MakeVisitor>, 23 | Format::Visitor: VisitFmt + VisitOutput, 24 | Filter: Clone + Fn(&Field) -> bool, 25 | { 26 | /// Wraps around an existing struct that impls [`FormatFields`], but filters out any fields which 27 | /// returns _false_ when passed into `filter`. 28 | pub fn new(format: Format, filter: Filter) -> Self { 29 | Self { format, filter } 30 | } 31 | } 32 | 33 | impl<'writer, Format, Filter> FormatFields<'writer> for FilteredFormatFields 34 | where 35 | Format: MakeVisitor>, 36 | Format::Visitor: VisitFmt + VisitOutput, 37 | Filter: Clone + Fn(&Field) -> bool, 38 | { 39 | fn format_fields( 40 | &self, 41 | writer: Writer<'writer>, 42 | fields: R, 43 | ) -> std::fmt::Result { 44 | let mut v = 45 | FilteredFormatFieldsVisitor::new(self.format.make_visitor(writer), self.filter.clone()); 46 | fields.record(&mut v); 47 | v.finish()?; 48 | 49 | Ok(()) 50 | } 51 | } 52 | 53 | struct FilteredFormatFieldsVisitor { 54 | visitor: Visitor, 55 | filter: Filter, 56 | } 57 | 58 | impl FilteredFormatFieldsVisitor { 59 | fn new(visitor: Visitor, filter: Filter) -> Self { 60 | Self { visitor, filter } 61 | } 62 | } 63 | 64 | impl Visit for FilteredFormatFieldsVisitor 65 | where 66 | Visitor: Visit, 67 | Filter: Fn(&Field) -> bool, 68 | { 69 | fn record_debug(&mut self, field: &tracing_core::Field, value: &dyn std::fmt::Debug) { 70 | if (self.filter)(field) { 71 | self.visitor.record_debug(field, value); 72 | } 73 | } 74 | 75 | fn record_f64(&mut self, field: &tracing_core::Field, value: f64) { 76 | if (self.filter)(field) { 77 | self.visitor.record_f64(field, value); 78 | } 79 | } 80 | 81 | fn record_i64(&mut self, field: &tracing_core::Field, value: i64) { 82 | if (self.filter)(field) { 83 | self.visitor.record_i64(field, value); 84 | } 85 | } 86 | 87 | fn record_u64(&mut self, field: &tracing_core::Field, value: u64) { 88 | if (self.filter)(field) { 89 | self.visitor.record_u64(field, value); 90 | } 91 | } 92 | 93 | fn record_str(&mut self, field: &tracing_core::Field, value: &str) { 94 | if (self.filter)(field) { 95 | self.visitor.record_str(field, value); 96 | } 97 | } 98 | 99 | fn record_i128(&mut self, field: &tracing_core::Field, value: i128) { 100 | if (self.filter)(field) { 101 | self.visitor.record_i128(field, value); 102 | } 103 | } 104 | 105 | fn record_u128(&mut self, field: &tracing_core::Field, value: u128) { 106 | if (self.filter)(field) { 107 | self.visitor.record_u128(field, value); 108 | } 109 | } 110 | 111 | fn record_bool(&mut self, field: &tracing_core::Field, value: bool) { 112 | if (self.filter)(field) { 113 | self.visitor.record_bool(field, value); 114 | } 115 | } 116 | 117 | fn record_error( 118 | &mut self, 119 | field: &tracing_core::Field, 120 | value: &(dyn std::error::Error + 'static), 121 | ) { 122 | if (self.filter)(field) { 123 | self.visitor.record_error(field, value); 124 | } 125 | } 126 | } 127 | 128 | impl VisitOutput for FilteredFormatFieldsVisitor 129 | where 130 | Visitor: VisitOutput, 131 | Filter: Fn(&Field) -> bool, 132 | { 133 | fn finish(self) -> fmt::Result { 134 | self.visitor.finish() 135 | } 136 | } 137 | 138 | impl VisitFmt for FilteredFormatFieldsVisitor 139 | where 140 | Visitor: VisitFmt, 141 | Filter: Fn(&Field) -> bool, 142 | { 143 | fn writer(&mut self) -> &mut dyn fmt::Write { 144 | self.visitor.writer() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/writer.rs: -------------------------------------------------------------------------------- 1 | //! Helpers to prevent progress bars from clobbering your console output. 2 | use std::io; 3 | use std::marker::PhantomData; 4 | 5 | use indicatif::MultiProgress; 6 | use tracing_subscriber::fmt::MakeWriter; 7 | 8 | pub trait WriterTarget: private::Sealed {} 9 | 10 | /// Marker for where the [`IndicatifWriter`] should write to. 11 | pub struct Stdout {} 12 | /// Marker for where the [`IndicatifWriter`] should write to. 13 | pub struct Stderr {} 14 | 15 | impl WriterTarget for Stdout {} 16 | impl WriterTarget for Stderr {} 17 | 18 | // TODO(emersonford): find a cleaner way to integrate this layer with fmt::Layer. 19 | /// A wrapper around [`std::io::stdout()`] or [`std::io::stderr()`] to ensure that output to either is 20 | /// not clobbered by active progress bars. This should be passed into tracing fmt's layer so 21 | /// tracing log entries are not clobbered. 22 | pub struct IndicatifWriter { 23 | progress_bars: MultiProgress, 24 | inner: PhantomData, 25 | } 26 | 27 | impl IndicatifWriter 28 | where 29 | T: WriterTarget, 30 | { 31 | pub(crate) fn new(mp: MultiProgress) -> Self { 32 | Self { 33 | progress_bars: mp, 34 | inner: PhantomData, 35 | } 36 | } 37 | } 38 | 39 | impl Clone for IndicatifWriter { 40 | fn clone(&self) -> Self { 41 | Self { 42 | progress_bars: self.progress_bars.clone(), 43 | inner: self.inner, 44 | } 45 | } 46 | } 47 | 48 | impl io::Write for IndicatifWriter { 49 | fn write(&mut self, buf: &[u8]) -> io::Result { 50 | self.progress_bars.suspend(|| io::stdout().write(buf)) 51 | } 52 | 53 | fn flush(&mut self) -> io::Result<()> { 54 | self.progress_bars.suspend(|| io::stdout().flush()) 55 | } 56 | 57 | fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { 58 | self.progress_bars 59 | .suspend(|| io::stdout().write_vectored(bufs)) 60 | } 61 | 62 | fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 63 | self.progress_bars.suspend(|| io::stdout().write_all(buf)) 64 | } 65 | 66 | fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> { 67 | self.progress_bars.suspend(|| io::stdout().write_fmt(fmt)) 68 | } 69 | } 70 | 71 | impl io::Write for IndicatifWriter { 72 | fn write(&mut self, buf: &[u8]) -> io::Result { 73 | self.progress_bars.suspend(|| io::stderr().write(buf)) 74 | } 75 | 76 | fn flush(&mut self) -> io::Result<()> { 77 | self.progress_bars.suspend(|| io::stderr().flush()) 78 | } 79 | 80 | fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { 81 | self.progress_bars 82 | .suspend(|| io::stderr().write_vectored(bufs)) 83 | } 84 | 85 | fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 86 | self.progress_bars.suspend(|| io::stderr().write_all(buf)) 87 | } 88 | 89 | fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> io::Result<()> { 90 | self.progress_bars.suspend(|| io::stderr().write_fmt(fmt)) 91 | } 92 | } 93 | 94 | impl<'a> MakeWriter<'a> for IndicatifWriter { 95 | type Writer = IndicatifWriter; 96 | 97 | fn make_writer(&'a self) -> Self::Writer { 98 | self.clone() 99 | } 100 | } 101 | 102 | impl<'a> MakeWriter<'a> for IndicatifWriter { 103 | type Writer = IndicatifWriter; 104 | 105 | fn make_writer(&'a self) -> Self::Writer { 106 | self.clone() 107 | } 108 | } 109 | 110 | mod private { 111 | pub trait Sealed {} 112 | 113 | impl Sealed for super::Stdout {} 114 | impl Sealed for super::Stderr {} 115 | } 116 | 117 | /// Returns the stderr writer (equivalent to 118 | /// [`get_stderr_writer`](crate::IndicatifLayer::get_stderr_writer)) of the registered 119 | /// [`IndicatifLayer`](crate::IndicatifLayer) for the current default tracing subscriber. 120 | /// 121 | /// Returns `None` if there is either no default tracing subscriber or if there is not a 122 | /// `IndicatifLayer` registered with that subscriber. 123 | pub fn get_indicatif_stderr_writer() -> Option> { 124 | tracing::dispatcher::get_default(|dispatch| { 125 | dispatch 126 | .downcast_ref::() 127 | .and_then(|ctx| { 128 | let mut ret: Option> = None; 129 | ctx.with_context(dispatch, |writer| { 130 | ret = Some(writer); 131 | }); 132 | 133 | ret 134 | }) 135 | }) 136 | } 137 | 138 | /// Returns the stdout writer (equivalent to 139 | /// [`get_stdout_writer`](crate::IndicatifLayer::get_stdout_writer)) of the registered 140 | /// [`IndicatifLayer`](crate::IndicatifLayer) for the current default tracing subscriber. 141 | /// 142 | /// Returns `None` if there is either no default tracing subscriber or if there is not a 143 | /// `IndicatifLayer` registered with that subscriber. 144 | pub fn get_indicatif_stdout_writer() -> Option> { 145 | tracing::dispatcher::get_default(|dispatch| { 146 | dispatch 147 | .downcast_ref::() 148 | .and_then(|ctx| { 149 | let mut ret: Option> = None; 150 | ctx.with_context(dispatch, |writer| { 151 | ret = Some(writer); 152 | }); 153 | 154 | ret 155 | }) 156 | }) 157 | } 158 | --------------------------------------------------------------------------------