···1919> [!TIP]
2020>
2121> This page is also used for snapshot testing! To see how all the links would look like
2222-> in Markdown after they have been processed, see
2323-> [supported-syntax.snap](/crates/mdbook-rustdoc-links/src/tests/snaps/supported-syntax.snap)
2424-> and
2525-> [supported-syntax.stderr.snap](/crates/mdbook-rustdoc-links/src/tests/snaps/supported-syntax.stderr.snap).
2222+> in Markdown after they have been processed, see [supported-syntax.snap] and
2323+> [supported-syntax.stderr.snap].
2424+>
2525+> [supported-syntax.snap]:
2626+> /crates/mdbook-rustdoc-links/src/tests/snaps/docs/src/rustdoc-links/supported-syntax.md.snap
2727+> [supported-syntax.stderr.snap]:
2828+> /crates/mdbook-rustdoc-links/src/tests/snaps/docs/src/rustdoc-links/supported-syntax.md.stderr.snap
26292730## Types, modules, associated items
2831
···11use anyhow::{Result, anyhow};
22use serde::Deserialize;
33use tap::Pipe;
44+use tracing::Level;
4555-pub fn is_ci() -> Option<String> {
66- let ci = std::env::var("CI").unwrap_or("".into());
77- if matches!(ci.as_str(), "" | "0" | "false") {
88- None
99- } else {
1010- Some(ci)
1111- }
1212-}
66+use crate::env::is_ci;
137148/// Flag indicating how the program should proceed when there are warnings.
159///
···3125}
32263327impl OnWarning {
3434- pub fn check(&self, level: log::Level) -> Result<()> {
2828+ pub fn check(&self, level: Level) -> Result<()> {
3529 match level {
3636- log::Level::Error => Err(anyhow!("preprocessor has errors")),
3737- log::Level::Warn => match self {
3030+ Level::ERROR => Err(anyhow!("preprocessor has errors")),
3131+ Level::WARN => match self {
3832 Self::AlwaysFail => {
3933 anyhow!("treating warnings as errors because the `fail-on-warnings` option is set to \"always\"")
4034 .context("preprocessor has errors")
4135 .pipe(Err)
4236 }
4337 Self::FailInCi => {
4444- let Some(ci) = Self::warning_as_error() else {
3838+ let Some(ci) = is_ci() else {
4539 return Ok(());
4640 };
4741 anyhow!("treating warnings as errors because the `fail-on-warnings` option is set to \"ci\" and CI={ci}")
···55495650 pub fn adjusted<T, E>(&self, result: Result<Result<T, E>, E>) -> Result<Result<T, E>, E> {
5751 match result {
5858- Ok(Err(error)) if Self::warning_as_error().is_some() => Err(error),
5252+ Ok(Err(error)) if is_ci().is_some() => Err(error),
5953 result => result,
6054 }
6161- }
6262-6363- fn warning_as_error() -> Option<String> {
6464- is_ci()
6555 }
6656}
+1
crates/mdbookkit/src/lib.rs
···1212pub mod diagnostics;
1313#[cfg(feature = "_testing")]
1414pub mod docs;
1515+pub mod env;
1516pub mod error;
1617pub mod logging;
1718pub mod markdown;
+396-354
crates/mdbookkit/src/logging.rs
···11-//! Progress reporting and logging for preprocessors.
22-31use std::{
42 collections::BTreeSet,
55- fmt, io,
66- sync::{OnceLock, mpsc},
33+ fmt::Debug,
44+ sync::{Arc, LazyLock, mpsc},
75 thread,
86 time::{Duration, Instant},
97};
1081111-use anyhow::{Context, Result};
1212-use console::{StyledObject, Term, colors_enabled_stderr, set_colors_enabled};
1313-use env_logger::Logger;
99+use console::{StyledObject, Term};
1410use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle};
1515-use log::{Level, LevelFilter, Log};
1611use tap::{Pipe, Tap};
1212+use tracing::{
1313+ Event, Subscriber,
1414+ field::{Field, Visit},
1515+ span::{Attributes, Id},
1616+ warn,
1717+};
1818+use tracing_subscriber::{
1919+ EnvFilter, Layer,
2020+ filter::{LevelFilter, filter_fn},
2121+ layer::{Context, SubscriberExt},
2222+ registry::{LookupSpan, SpanRef},
2323+ util::SubscriberInitExt,
2424+};
17251818-pub fn spinner() -> SpinnerHandle {
1919- SpinnerHandle
2626+use crate::env::{MDBOOK_LOG, is_colored, is_logging, set_colored, set_logging};
2727+2828+#[doc(hidden)]
2929+#[macro_export]
3030+macro_rules! branded {
3131+ // cannot use env!("CARGO_PKG_NAME") because that would be
3232+ // the package name at callsite
3333+ ( $suffix:literal ) => {{ concat!("mdbookkit.", $suffix) }};
2034}
21352222-pub struct SpinnerHandle;
3636+macro_rules! is_branded {
3737+ ( $metadata:expr, $suffix:literal ) => {{ $metadata.fields().field(branded!($suffix)).is_some() }};
3838+ ( $metadata:expr ) => {{
3939+ $metadata
4040+ .fields()
4141+ .iter()
4242+ .any(|f| f.name().starts_with("mdbookkit."))
4343+ }};
4444+}
23452424-macro_rules! spinner_log {
2525- ( $level:ident ! ( $($args:tt)* ) ) => {
2626- log::$level!(target: env!("CARGO_CRATE_NAME"), $($args)*);
2727- };
4646+pub struct Logging {
4747+ pub logging: Option<bool>,
4848+ pub colored: Option<bool>,
4949+ pub level: LevelFilter,
2850}
29513030-impl SpinnerHandle {
3131- pub fn create(&self, prefix: &str, total: Option<u64>) -> &Self {
3232- let prefix = prefix.into();
3333- let msg = Message::Create { prefix, total };
5252+impl Logging {
5353+ pub fn init(self) {
5454+ init_logging(self);
5555+ }
5656+}
34573535- if let Some(Spinner { tx, .. }) = SPINNER.get() {
3636- tx.send(msg).ok();
3737- } else {
3838- spinner_log!(info!("{msg}"));
5858+impl Default for Logging {
5959+ fn default() -> Self {
6060+ Self {
6161+ logging: None,
6262+ colored: None,
6363+ level: LevelFilter::INFO,
3964 }
6565+ }
6666+}
40674141- self
6868+fn init_logging(options: Logging) {
6969+ if let Some(logging) = options.logging {
7070+ set_logging(logging);
7171+ }
7272+ if let Some(colored) = options.colored {
7373+ set_colored(colored);
4274 }
43754444- pub fn update<D: fmt::Display>(&self, prefix: &str, update: D) -> &Self {
4545- let key = prefix.into();
4646- let update = update.to_string();
4747- let msg = Message::Update { key, update };
7676+ // https://github.com/rust-lang/mdBook/blob/v0.5.2/src/main.rs#L93
48774949- if let Some(Spinner { tx, .. }) = SPINNER.get() {
5050- tx.send(msg).ok();
5151- } else {
5252- spinner_log!(info!("{msg}"));
5353- }
7878+ let filter = EnvFilter::builder()
7979+ .with_default_directive(options.level.into())
8080+ .parse_lossy(MDBOOK_LOG.as_deref().unwrap_or_default());
54815555- self
5656- }
8282+ let logger = tracing_subscriber::fmt::layer()
8383+ .without_time()
8484+ .with_target(filter.max_level_hint().unwrap_or(options.level) < LevelFilter::INFO)
8585+ .with_ansi(is_colored())
8686+ .with_writer(|| WRITER.clone())
8787+ .with_filter(if TICKER.is_some() {
8888+ Some(filter_fn(|metadata| !is_branded!(metadata)))
8989+ } else {
9090+ None
9191+ });
57925858- pub fn task<D: fmt::Display>(&self, prefix: &str, task: D) -> TaskHandle {
5959- let key = String::from(prefix);
6060- let task = task.to_string();
9393+ let ticker = ConsoleLayer.with_filter(if TICKER.is_none() {
9494+ Some(filter_fn(|metadata| !metadata.is_event()))
9595+ } else {
9696+ None
9797+ });
61986262- let open = Message::Task {
6363- key: key.clone(),
6464- task: task.clone(),
6565- };
6666- let done = Some(Message::Done { key, task });
9999+ tracing_subscriber::registry()
100100+ .with(filter)
101101+ .with(logger)
102102+ .with(ticker)
103103+ .init();
104104+}
671056868- if let Some(Spinner { tx, .. }) = SPINNER.get() {
6969- tx.send(open).ok();
7070- let spin = Some(tx.clone());
7171- return TaskHandle { spin, done };
7272- }
106106+macro_rules! derive_event {
107107+ ( $id:expr, $metadata:expr, $($field:literal = $value:expr),* ) => {{
108108+ let metadata = $metadata;
109109+ let fields = ::tracing::field::FieldSet::new(&[$($field),*], metadata.callsite());
110110+ #[allow(unused)]
111111+ let mut iter = fields.iter();
112112+ let values = [$(
113113+ (&iter.next().unwrap(), ::core::option::Option::Some(&$value as &dyn tracing::field::Value)),
114114+ )*];
115115+ Event::child_of($id, metadata, &fields.value_set(&values));
116116+ }};
117117+}
731187474- spinner_log!(info!("{open}"));
7575- let spin = None;
7676- TaskHandle { spin, done }
119119+#[macro_export]
120120+macro_rules! timer {
121121+ ( $level:expr, $key:literal, $( $span:tt )* ) => {
122122+ ::tracing::span!(
123123+ $level,
124124+ $key,
125125+ { $crate::branded!("timer") } = ::tracing::field::Empty,
126126+ $($span)*
127127+ )
128128+ };
129129+ ( $level:expr, $key:literal ) => {
130130+ $crate::timer!($level, $key,)
77131 }
132132+}
781337979- pub fn finish<D: fmt::Display>(&self, prefix: &str, update: D) {
8080- let key = prefix.into();
8181- let update = update.to_string();
8282- let msg = Message::Finish { key, update };
134134+#[macro_export]
135135+macro_rules! timer_event {
136136+ ( $parent:expr, $level:expr, $( $span:tt )* ) => {
137137+ ::tracing::event!(
138138+ parent: $parent,
139139+ $level,
140140+ { $crate::branded!("timer.event") } = ::tracing::field::Empty,
141141+ $($span)*
142142+ )
143143+ };
144144+ ( $parent:expr, $level:expr ) => {
145145+ $crate::timer_event!($parent, $level,)
146146+ }
147147+}
831488484- if let Some(Spinner { tx, .. }) = SPINNER.get() {
8585- tx.send(msg).ok();
8686- } else {
8787- spinner_log!(info!("{msg}"));
8888- }
149149+#[macro_export]
150150+macro_rules! timer_item {
151151+ ( $parent:expr, $level:expr, $key:literal, $( $span:tt )* ) => {
152152+ ::tracing::span!(
153153+ parent: $parent,
154154+ $level,
155155+ $key,
156156+ { $crate::branded!("timer.item") } = ::tracing::field::Empty,
157157+ $($span)*
158158+ )
159159+ };
160160+ ( $parent:expr, $level:expr, $key:literal ) => {
161161+ $crate::timer_item!($parent, $level, $key,)
89162 }
90163}
911649292-#[must_use]
9393-pub struct TaskHandle {
9494- spin: Option<mpsc::Sender<Message>>,
9595- done: Option<Message>,
9696-}
165165+struct ConsoleLayer;
971669898-impl Drop for TaskHandle {
9999- fn drop(&mut self) {
100100- let Some(done) = self.done.take() else { return };
101101- if let Some(ref tx) = self.spin {
102102- tx.send(done).ok();
103103- } else {
104104- spinner_log!(info!("{done}"));
105105- }
106106- }
107107-}
167167+impl<S> Layer<S> for ConsoleLayer
168168+where
169169+ S: Subscriber + for<'a> LookupSpan<'a>,
170170+{
171171+ fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
172172+ let Some(span) = ctx.span(id) else { return };
108173109109-#[derive(Debug)]
110110-enum Message {
111111- Create { prefix: String, total: Option<u64> },
112112- Update { key: String, update: String },
113113- Task { key: String, task: String },
114114- Done { key: String, task: String },
115115- Finish { key: String, update: String },
116116-}
174174+ if is_branded!(span, "timer") {
175175+ let TimerVisitor { count, .. } = TimerVisitor::from_attrs(attrs);
117176118118-impl fmt::Display for Message {
119119- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120120- match self {
121121- Self::Create { prefix, .. } => fmt::Display::fmt(prefix, f),
122122- Self::Update { key, update } => {
123123- fmt::Display::fmt(key, f)?;
124124- fmt::Display::fmt(" ", f)?;
125125- fmt::Display::fmt(update, f)?;
126126- Ok(())
127127- }
128128- Self::Task { key, task } => {
129129- fmt::Display::fmt(key, f)?;
130130- fmt::Display::fmt(" ", f)?;
131131- fmt::Display::fmt(task, f)?;
132132- Ok(())
177177+ let timer = Timer {
178178+ prefix: SpanPath::to_string(&span),
179179+ key: span.name(),
180180+ count,
181181+ };
182182+183183+ span.extensions_mut().insert(timer.clone());
184184+185185+ if let Some(ticker) = &*TICKER {
186186+ (ticker.tx).send(ProgressTick::TimerCreate(timer)).ok();
187187+ } else {
188188+ derive_event!(id, span.metadata(), "message" = "started");
133189 }
134134- Self::Done { key, task } => {
135135- fmt::Display::fmt(key, f)?;
136136- fmt::Display::fmt(" ", f)?;
137137- fmt::Display::fmt(task, f)?;
138138- fmt::Display::fmt(" .. done", f)?;
139139- Ok(())
140140- }
141141- Self::Finish { key, update } => {
142142- fmt::Display::fmt(key, f)?;
143143- fmt::Display::fmt(" .. ", f)?;
144144- fmt::Display::fmt(update, f)?;
145145- Ok(())
190190+ } else if is_branded!(span, "timer.item")
191191+ && let TimerVisitor {
192192+ item: Some(item), ..
193193+ } = TimerVisitor::from_attrs(attrs)
194194+ && let Some(parent) = span.parent()
195195+ && let Some(Timer { key, .. }) = parent.extensions().get::<Timer>()
196196+ {
197197+ span.extensions_mut().insert(TimerItem(item.clone()));
198198+199199+ if let Some(ticker) = &*TICKER {
200200+ (ticker.tx).send(ProgressTick::ItemOpen { key, item }).ok();
201201+ } else {
202202+ derive_event!(id, span.metadata(), "message" = "started");
146203 }
147204 }
148205 }
149149-}
150206151151-pub fn styled<D>(val: D) -> console::StyledObject<D> {
152152- if let Some(Spinner { term, .. }) = SPINNER.get() {
153153- term.style()
154154- } else {
155155- console::Style::new().for_stderr()
156156- }
157157- .apply_to(val)
158158-}
207207+ fn on_close(&self, id: Id, ctx: Context<'_, S>) {
208208+ let Some(span) = ctx.span(&id) else { return };
159209160160-#[macro_export]
161161-macro_rules! styled {
162162- ( ( $($display:tt)+ ) . $($style:tt)+ ) => {{
163163- $crate::logging::styled( $($display)* ) . $($style)*
164164- }};
165165-}
210210+ // n.b. using extensions_mut involves a RwLock write, which will cause
211211+ // derive_event to deadlock when it tries to read from the same span
166212167167-pub fn is_logging() -> bool {
168168- SPINNER.get().is_none()
169169-}
213213+ if let Some(Timer { key, .. }) = span.extensions().get::<Timer>() {
214214+ if let Some(ticker) = &*TICKER {
215215+ ticker.tx.send(ProgressTick::TimerFinish { key }).ok();
216216+ } else {
217217+ derive_event!(id, span.metadata(), "message" = "finished");
218218+ }
219219+ } else if let Some(parent) = span.parent()
220220+ && let Some(Timer { key, .. }) = parent.extensions().get::<Timer>()
221221+ && let Some(TimerItem(item)) = span.extensions().get::<TimerItem>()
222222+ {
223223+ if let Some(ticker) = &*TICKER {
224224+ let item = item.clone();
225225+ (ticker.tx).send(ProgressTick::ItemDone { key, item }).ok();
226226+ } else {
227227+ derive_event!(id, span.metadata(), "message" = "finished");
228228+ }
229229+ }
230230+ }
170231171171-#[macro_export]
172172-macro_rules! log_debug {
173173- () => {
174174- |err| log::debug!("{err:?}")
175175- };
176176-}
232232+ fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
233233+ if !is_branded!(event.metadata(), "timer.event") {
234234+ return;
235235+ }
177236178178-#[macro_export]
179179-macro_rules! log_trace {
180180- () => {
181181- |err| log::trace!("{err:?}")
182182- };
183183-}
184184-185185-#[macro_export]
186186-macro_rules! log_warning {
187187- () => {
188188- |err| {
189189- if log::log_enabled!(log::Level::Debug) {
190190- log::warn!("{err:?}")
191191- } else {
192192- log::warn!("{err}")
237237+ if let TimerVisitor {
238238+ message: Some(msg), ..
239239+ } = TimerVisitor::from_event(event)
240240+ && let Some(span) = (event.parent().cloned())
241241+ .or_else(|| ctx.current_span().id().cloned())
242242+ .and_then(|id| ctx.span(&id))
243243+ && let Some(Timer { key, .. }) = span.extensions().get::<Timer>()
244244+ {
245245+ if let Some(ticker) = &*TICKER {
246246+ (ticker.tx)
247247+ .send(ProgressTick::TimerUpdate { key, msg })
248248+ .ok();
193249 }
194250 }
195195- };
196196- (detailed) => {
197197- |err| log::warn!("{err:?}")
198198- };
251251+ }
199252}
200253201201-/// Either a [`console::Term`] or an [`env_logger::Logger`].
202202-///
203203-/// This is automatically detected upon installation as the global logger. The logic is:
204204-///
205205-/// - If the `RUST_LOG` env var is set, this will use [`env_logger`].
206206-/// - If stderr is not "user-attended", as determined by [`console::user_attended_stderr()`],
207207-/// like if stderr is piped to a file, this will use [`env_logger`].
208208-/// - Otherwise, this will use [`console`].
209209-///
210210-/// When this is a [`console::Term`], logs are handled by the global [`indicatif`] spinner.
211211-///
212212-/// When this is an [`env_logger::Logger`], there will not be a spinner, and progress
213213-/// reports are printed as logs instead.
214214-pub enum ConsoleLogger {
215215- Console(Term),
216216- Logger(Logger),
254254+#[derive(Debug, Default)]
255255+struct TimerVisitor {
256256+ message: Option<String>,
257257+ item: Option<Arc<str>>,
258258+ count: Option<u64>,
217259}
218260219219-impl ConsoleLogger {
220220- /// Install a [`ConsoleLogger`] as the global [`log`] logger.
221221- pub fn install(name: &str) {
222222- Self::try_install(name).expect("logger should not have been set");
261261+impl Visit for TimerVisitor {
262262+ fn record_str(&mut self, field: &Field, value: &str) {
263263+ match field.name() {
264264+ "message" => self.message = Some(value.into()),
265265+ "item" => self.item = Some(value.into()),
266266+ _ => {}
267267+ }
223268 }
224269225225- pub fn try_install(name: &str) -> Result<()> {
226226- log::set_boxed_logger(Box::new(Self::new(name)))?;
227227- log::set_max_level(LevelFilter::max());
228228- Ok(())
270270+ fn record_u64(&mut self, field: &Field, value: u64) {
271271+ if field.name() == "count" {
272272+ self.count = Some(value)
273273+ }
229274 }
230275231231- fn new(name: &str) -> Self {
232232- match maybe_logging() {
233233- Some(LevelFilter::Off) => SPINNER
234234- .get_or_init(|| spawn_spinner(name))
235235- .term
236236- .clone()
237237- .pipe(Self::Console),
238238- level => env_logger::Builder::new()
239239- .format(log_format)
240240- .parse_default_env()
241241- .tap_mut(|builder| {
242242- if let Some(level) = level {
243243- builder.filter_level(level);
244244- }
245245- })
246246- .build()
247247- .pipe(Self::Logger),
248248- }
276276+ fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
277277+ self.record_str(field, &format!("{value:?}"));
249278 }
250279}
251280252252-fn maybe_logging() -> Option<LevelFilter> {
253253- if std::env::var("RUST_LOG")
254254- .map(|v| !v.is_empty())
255255- .unwrap_or(false)
256256- {
257257- // RUST_LOG to be parsed by env_logger
258258- None
259259- } else if !console::user_attended_stderr() {
260260- // RUST_LOG not set but stderr isn't a terminal
261261- // log info and above
262262- Some(LevelFilter::Info)
263263- } else {
264264- // use spinner instead
265265- Some(LevelFilter::Off)
281281+impl TimerVisitor {
282282+ #[inline]
283283+ fn from_attrs(attrs: &Attributes<'_>) -> Self {
284284+ Self::default().tap_mut(|v| attrs.values().record(v))
285285+ }
286286+287287+ #[inline]
288288+ fn from_event(event: &Event<'_>) -> Self {
289289+ Self::default().tap_mut(|v| event.record(v))
266290 }
267291}
268292269269-impl Log for ConsoleLogger {
270270- fn enabled(&self, metadata: &log::Metadata) -> bool {
271271- match self {
272272- ConsoleLogger::Logger(logger) => logger.enabled(metadata),
273273- ConsoleLogger::Console(_) => {
274274- if metadata.target().starts_with(env!("CARGO_CRATE_NAME")) {
275275- metadata.level() <= Level::Info
276276- } else {
277277- metadata.level() <= Level::Warn
278278- }
279279- }
280280- }
293293+struct SpanPath<'a, R: LookupSpan<'a>>(Option<SpanRef<'a, R>>);
294294+295295+impl<'a, R: LookupSpan<'a>> Iterator for SpanPath<'a, R> {
296296+ type Item = &'static str;
297297+298298+ fn next(&mut self) -> Option<Self::Item> {
299299+ let span = self.0.take()?;
300300+ self.0 = span.parent();
301301+ Some(span.name())
281302 }
303303+}
282304283283- fn log(&self, record: &log::Record) {
284284- match self {
285285- ConsoleLogger::Logger(logger) => logger.log(record),
286286- ConsoleLogger::Console(term) => {
287287- if !self.enabled(record.metadata()) {
288288- return;
289289- }
290290- let Ok(message) = Vec::<u8>::new()
291291- .pipe(|mut buf| log_format(&mut buf, record).and(Ok(buf)))
292292- .context("failed to emit log message")
293293- .and_then(|buf| Ok(String::from_utf8(buf)?))
294294- else {
295295- return;
296296- };
297297- let message = styled_log(message.trim_end(), record);
298298- term.write_line(&message.to_string()).ok();
299299- }
305305+impl<'a, R: LookupSpan<'a>> SpanPath<'a, R> {
306306+ fn new(span: &SpanRef<'a, R>) -> Self {
307307+ Self(span.parent())
308308+ }
309309+310310+ fn to_string(span: &SpanRef<'a, R>) -> Option<Arc<str>> {
311311+ let mut items = Self::new(span).collect::<Vec<_>>();
312312+ if items.is_empty() {
313313+ None
314314+ } else {
315315+ items.reverse();
316316+ Some(items.join(":").into())
300317 }
301318 }
319319+}
302320303303- fn flush(&self) {
304304- match self {
305305- ConsoleLogger::Console(term) => {
306306- term.flush().ok();
307307- }
308308- ConsoleLogger::Logger(logger) => {
309309- logger.flush();
310310- }
321321+#[derive(Debug, Clone)]
322322+struct Timer {
323323+ prefix: Option<Arc<str>>,
324324+ key: &'static str,
325325+ count: Option<u64>,
326326+}
327327+328328+impl std::fmt::Display for Timer {
329329+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330330+ if let Some(ref prefix) = self.prefix {
331331+ write!(f, "[{prefix}] {}", self.key)
332332+ } else {
333333+ write!(f, "{}", self.key)
311334 }
312335 }
313336}
314337315315-pub static SPINNER: OnceLock<Spinner> = OnceLock::new();
338338+#[derive(Debug, Clone)]
339339+struct TimerItem(Arc<str>);
316340317317-pub struct Spinner {
318318- tx: mpsc::Sender<Message>,
319319- term: Term,
341341+#[derive(Debug)]
342342+enum ProgressTick {
343343+ TimerCreate(Timer),
344344+ TimerUpdate { key: &'static str, msg: String },
345345+ ItemOpen { key: &'static str, item: Arc<str> },
346346+ ItemDone { key: &'static str, item: Arc<str> },
347347+ TimerFinish { key: &'static str },
320348}
321349322322-fn spawn_spinner(name: &str) -> Spinner {
323323- // https://github.com/console-rs/indicatif/issues/698
324324- set_colors_enabled(colors_enabled_stderr());
350350+#[derive(Clone)]
351351+struct ProgressTicker {
352352+ tx: mpsc::Sender<ProgressTick>,
353353+}
325354326326- let (tx, rx) = mpsc::channel();
355355+static WRITER: LazyLock<Term> = LazyLock::new(Term::stderr);
327356328328- let term = Term::stderr();
357357+static TICKER: LazyLock<Option<ProgressTicker>> = LazyLock::new(|| {
358358+ if is_logging() {
359359+ None
360360+ } else {
361361+ Some(spawn_ticker())
362362+ }
363363+});
329364330330- let target = term.clone();
331331- let template = format!("{{spinner:.cyan}} [{name}] {{prefix}} ... {{msg}}",);
365365+fn spawn_ticker() -> ProgressTicker {
366366+ let (tx, rx) = mpsc::channel();
332367333333- // this thread is detached. this is okay in usage because SPINNER.get_or_init
334334- // guarantees this function is called at most once
368368+ let style = ProgressStyle::with_template("{spinner:.cyan} {prefix} ... {msg}")
369369+ .unwrap()
370370+ .tick_chars("⠇⠋⠙⠸⠴⠦⠿");
335371336372 thread::spawn(move || {
337373 struct Bar {
338338- prefix: String,
374374+ timer: Timer,
339375 bar: ProgressBar,
340376 }
341377378378+ impl Bar {
379379+ fn count(&self) {
380380+ let Self { timer, bar } = self;
381381+ if let Some(length) = bar.length() {
382382+ let counter = styled(format!("({}/{length})", bar.position())).dim();
383383+ bar.set_prefix(format!("{timer} {counter}"))
384384+ }
385385+ }
386386+ }
387387+342388 let mut current: Option<Bar> = None;
343389344344- let mut tasks = BTreeSet::<String>::new();
390390+ let mut tasks = BTreeSet::new();
345391 let mut task_idx = 0;
346392 let mut interval = Instant::now();
347393348394 loop {
395395+ let current_ref = |key: &str| {
396396+ let bar = current.as_ref()?;
397397+ if bar.timer.key == key {
398398+ Some(bar)
399399+ } else {
400400+ None
401401+ }
402402+ };
403403+349404 match rx.recv_timeout(Duration::from_millis(100)) {
350405 Err(mpsc::RecvTimeoutError::Timeout) => {}
351406352407 Err(mpsc::RecvTimeoutError::Disconnected) => break,
353408354354- Ok(Message::Create { prefix, total }) => {
409409+ Ok(ProgressTick::TimerCreate(timer)) => {
355410 if let Some(bar) = current {
356411 bar.bar.abandon()
357412 }
358413359359- let style = ProgressStyle::with_template(&template)
360360- .unwrap()
361361- .tick_chars("⠇⠋⠙⠸⠴⠦⠿");
362362-363363- let bar = ProgressDrawTarget::term(target.clone(), 20)
364364- .pipe(|target| ProgressBar::with_draw_target(total, target))
365365- .with_prefix(prefix.clone())
366366- .with_style(style);
414414+ let bar = ProgressDrawTarget::term(WRITER.clone(), 20)
415415+ .pipe(|target| ProgressBar::with_draw_target(timer.count, target))
416416+ .with_prefix(timer.to_string())
417417+ .with_style(style.clone());
367418368419 bar.enable_steady_tick(Duration::from_millis(100));
369420370370- current = Some(Bar { prefix, bar });
421421+ current = Some(Bar { timer, bar });
371422 }
372423373373- Ok(Message::Update { key, update }) => {
374374- let Some(Bar {
375375- ref bar,
376376- ref prefix,
377377- }) = current
378378- else {
424424+ Ok(ProgressTick::TimerUpdate { key, msg }) => {
425425+ let Some(Bar { bar, .. }) = current_ref(key) else {
379426 continue;
380427 };
381428382382- if &key != prefix {
383383- continue;
384384- }
385385-386386- bar.set_message(update);
429429+ bar.set_message(msg);
387430 bar.tick();
388431 }
389432390390- Ok(Message::Finish { key, update }) => {
391391- let Some(Bar {
392392- ref bar,
393393- ref prefix,
394394- }) = current
395395- else {
433433+ Ok(ProgressTick::TimerFinish { key }) => {
434434+ let Some(Bar { bar, .. }) = current_ref(key) else {
396435 continue;
397436 };
398437399399- if &key != prefix {
400400- continue;
401401- }
402402-403403- bar.finish_with_message(update);
438438+ bar.finish_with_message(styled("done").green().to_string());
404439 current = None;
405440 }
406441407407- Ok(Message::Task { key, task }) => {
408408- let Some(Bar {
409409- ref bar,
410410- ref prefix,
411411- }) = current
412412- else {
442442+ Ok(ProgressTick::ItemOpen { key, item }) => {
443443+ let Some(current) = current_ref(key) else {
413444 continue;
414445 };
415446416416- if &key != prefix {
417417- continue;
418418- }
419419-420420- if let Some(length) = bar.length() {
421421- let counter = styled(format!("({}/{length})", bar.position())).dim();
422422- bar.set_prefix(format!("{prefix} {counter}"))
423423- }
447447+ current.count();
448448+ current.bar.set_message(styled(&item).magenta().to_string());
449449+ current.bar.tick();
424450425425- bar.set_message(styled(&task).magenta().to_string());
426426- bar.tick();
427427-428428- tasks.insert(task);
451451+ tasks.insert(item);
429452 interval = Instant::now();
430453 }
431454432432- Ok(Message::Done { key, task }) => {
433433- let Some(Bar {
434434- ref bar,
435435- ref prefix,
436436- }) = current
437437- else {
455455+ Ok(ProgressTick::ItemDone { key, item }) => {
456456+ let Some(current) = current_ref(key) else {
438457 continue;
439458 };
440459441441- if &key != prefix {
442442- continue;
443443- }
460460+ current.bar.inc(1);
444461445445- bar.inc(1);
446446-447447- if let Some(length) = bar.length() {
448448- let counter = styled(format!("({}/{length})", bar.position())).dim();
449449- bar.set_prefix(format!("{prefix} {counter}"))
450450- }
451451-452452- bar.set_message(styled(&task).green().to_string());
453453- bar.tick();
462462+ current.count();
463463+ current.bar.set_message(styled(&item).green().to_string());
464464+ current.bar.tick();
454465455455- tasks.insert(task);
466466+ tasks.remove(&item);
456467 interval = Instant::now();
457468 }
458469 }
459470460471 if let Some(Bar {
461461- ref prefix,
472472+ timer: Timer { key, .. },
462473 ref bar,
474474+ ..
463475 }) = current
464476 {
465477 let now = Instant::now();
466478467479 if now - interval > Duration::from_secs(10) {
468480 interval = now;
481481+469482 if task_idx >= tasks.len() {
470483 task_idx = 0
471484 }
485485+472486 if let Some(task) = tasks.iter().nth(task_idx) {
473473- spinner_log!(warn!(
474474- "task {prefix} - {task} has been running for more than {}",
475475- HumanDuration(bar.elapsed())
476476- ));
487487+ let elapsed = HumanDuration(bar.elapsed());
488488+ // TODO: how to attach to current span
489489+ // TODO: how to clear line for tracing when ticker is running
490490+ warn!("task {key} - {task} has been running for more than {elapsed}");
477491 bar.set_message(styled(task).magenta().to_string());
478492 task_idx += 1;
479493 }
···482496 }
483497 });
484498485485- Spinner { tx, term }
499499+ ProgressTicker { tx }
500500+}
501501+502502+#[inline]
503503+pub fn stderr() -> impl std::io::Write {
504504+ WRITER.clone()
505505+}
506506+507507+// FIXME: ensure colors do not appear in tracing output
508508+// https://github.com/tokio-rs/tracing/issues/3378
509509+#[inline]
510510+pub fn styled<D>(val: D) -> StyledObject<D> {
511511+ WRITER.style().apply_to(val)
512512+}
513513+514514+#[macro_export]
515515+macro_rules! emit_trace {
516516+ () => {
517517+ |err| ::tracing::trace!("{err:?}")
518518+ };
519519+ ($fmt:expr) => {
520520+ |e| ::tracing::trace!($fmt, e)
521521+ };
486522}
487523488488-/// <https://github.com/rust-lang/mdBook/blob/07b25cdb643899aeca2307fbab7690fa7eeec36b/src/main.rs#L100-L109>
489489-fn log_format<W: io::Write>(formatter: &mut W, record: &log::Record) -> io::Result<()> {
490490- let message = format!(
491491- "{} [{}] ({}): {}",
492492- chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
493493- record.level(),
494494- record.target(),
495495- record.args()
496496- );
497497- let message = styled_log(message, record);
498498- writeln!(formatter, "{message}",)
524524+#[macro_export]
525525+macro_rules! emit_debug {
526526+ () => {
527527+ |err| ::tracing::debug!("{err:?}")
528528+ };
529529+ ($fmt:expr) => {
530530+ |e| ::tracing::debug!($fmt, e)
531531+ };
499532}
500533501501-fn styled_log<D>(message: D, record: &log::Record) -> StyledObject<D> {
502502- match record.level() {
503503- Level::Warn => styled(message).yellow(),
504504- Level::Error => styled(message).red(),
505505- Level::Info => styled(message),
506506- _ => styled(message).dim(),
507507- }
534534+#[macro_export]
535535+macro_rules! emit_warning {
536536+ () => {
537537+ |e| {
538538+ if ::tracing::enabled!(::tracing::Level::DEBUG) {
539539+ ::tracing::warn!("{:?}", e)
540540+ } else {
541541+ ::tracing::warn!("{}", e)
542542+ }
543543+ }
544544+ };
545545+ ($fmt:expr) => {
546546+ |e| ::tracing::warn!($fmt, e)
547547+ };
508548}
549549+550550+// TODO: clean up logging messages & make use of spans/instrument
···2233pub use mdbookkit::Diagnostics;
4455-pub mod error {
66- pub use mdbookkit::error::is_ci;
55+pub mod env {
66+ pub use mdbookkit::env::is_ci;
77}
+6-6
docs/src/rustdoc-links/name-resolution.md
···4747you may write `crate::*`, although this is not required:
48484949> ```md
5050-> The [`is_ci`][crate::error::is_ci] function detects whether the preprocessor is
5151-> running in a [continuous integration](continuous-integration.md) environment, such
5252-> that warnings may be promoted to errors.
5050+> The [`is_ci`][crate::env::is_ci] function detects whether the preprocessor is running
5151+> in a [continuous integration](continuous-integration.md) environment, such that
5252+> warnings may be promoted to errors.
5353> ```
5454>
5555-> The [`is_ci`][crate::error::is_ci] function detects whether the preprocessor is
5656-> running in a [continuous integration](continuous-integration.md) environment, such
5757-> that warnings may be promoted to errors.
5555+> The [`is_ci`][crate::env::is_ci] function detects whether the preprocessor is running
5656+> in a [continuous integration](continuous-integration.md) environment, such that
5757+> warnings may be promoted to errors.
58585959For everything else, provide its full path, as if you were writing a `use` declaration:
6060
+1-1
docs/src/snippets/detecting-ci.md
···991010[^ci-true]:
1111 Specifically, when `CI` is anything other than `""`, `"0"`, or `"false"`. The logic
1212- is encapsulated in the [`is_ci`][crate::error::is_ci] function.
1212+ is encapsulated in the [`is_ci`][crate::env::is_ci] function.
13131414<!-- prettier-ignore-start -->
1515
-17
docs/src/snippets/logging.md
···11<!-- TODO: -->
22-33-By default, the preprocessor shows a progress spinner when it is running.
44-55-When running in CI, progress is instead printed as logs (using [log] and
66-[env_logger])[^stderr].
77-88-You can control logging levels using the [`RUST_LOG`] environment variable.
99-1010-[^stderr]:
1111- Specifically, when stderr is redirected to something that isn't a terminal, such as
1212- a file.
1313-1414-<!-- prettier-ignore-start -->
1515-1616-[`RUST_LOG`]: https://docs.rs/env_logger/latest/env_logger/#enabling-logging
1717-1818-<!-- prettier-ignore-end -->