watchexec-cli 2.5.1

Executes commands in response to file modifications
Documentation
use std::{env::var, io::stderr, path::PathBuf};

use clap::{ArgAction, Parser, ValueHint};
use miette::{bail, Result};
use tokio::fs::metadata;
use tracing::{info, warn};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
use tracing_subscriber::{EnvFilter, FmtSubscriber};

use super::OPTSET_DEBUGGING;

#[derive(Debug, Clone, Parser)]
pub struct LoggingArgs {
	/// Set diagnostic log level
	///
	/// This enables diagnostic logging, which is useful for investigating bugs or gaining more
	/// insight into faulty filters or "missing" events. Use multiple times to increase verbosity.
	///
	/// Goes up to '-vvvv'. When submitting bug reports, default to a '-vvv' log level.
	///
	/// You may want to use with '--log-file' to avoid polluting your terminal.
	///
	/// Setting $WATCHEXEC_LOG also works, and takes precedence, but is not recommended. However, using
	/// $WATCHEXEC_LOG is the only way to get logs from before these options are parsed.
	#[arg(
		long,
		short,
		help_heading = OPTSET_DEBUGGING,
		action = ArgAction::Count,
		default_value = "0",
		num_args = 0,
		display_order = 220,
	)]
	pub verbose: u8,

	/// Write diagnostic logs to a file
	///
	/// This writes diagnostic logs to a file, instead of the terminal, in JSON format. If a log
	/// level was not already specified, this will set it to '-vvv'.
	///
	/// If a path is not provided, the default is the working directory. Note that with
	/// '--ignore-nothing', the write events to the log will likely get picked up by Watchexec,
	/// causing a loop; prefer setting a path outside of the watched directory.
	///
	/// If the path provided is a directory, a file will be created in that directory. The file name
	/// will be the current date and time, in the format 'watchexec.YYYY-MM-DDTHH-MM-SSZ.log'.
	#[arg(
		long,
		help_heading = OPTSET_DEBUGGING,
		num_args = 0..=1,
		default_missing_value = ".",
		value_hint = ValueHint::AnyPath,
		value_name = "PATH",
		display_order = 120,
	)]
	pub log_file: Option<PathBuf>,

	/// Print events that trigger actions
	///
	/// This prints the events that triggered the action when handling it (after debouncing), in a
	/// human readable form. This is useful for debugging filters.
	///
	/// Use '-vvv' instead when you need more diagnostic information.
	#[arg(
		long,
		help_heading = OPTSET_DEBUGGING,
		display_order = 160,
	)]
	pub print_events: bool,
}

pub fn preargs() -> bool {
	let mut log_on = false;

	#[cfg(feature = "dev-console")]
	match console_subscriber::try_init() {
		Ok(_) => {
			warn!("dev-console enabled");
			log_on = true;
		}
		Err(e) => {
			eprintln!("Failed to initialise tokio console, falling back to normal logging\n{e}")
		}
	}

	if !log_on && var("WATCHEXEC_LOG").is_ok() {
		let subscriber =
			FmtSubscriber::builder().with_env_filter(EnvFilter::from_env("WATCHEXEC_LOG"));
		match subscriber.try_init() {
			Ok(()) => {
				warn!(WATCHEXEC_LOG=%var("WATCHEXEC_LOG").unwrap(), "logging configured from WATCHEXEC_LOG");
				log_on = true;
			}
			Err(e) => {
				eprintln!("Failed to initialise logging with WATCHEXEC_LOG, falling back\n{e}");
			}
		}
	}

	log_on
}

pub async fn postargs(args: &LoggingArgs) -> Result<Option<WorkerGuard>> {
	if args.verbose == 0 {
		return Ok(None);
	}

	let (log_writer, guard) = if let Some(file) = &args.log_file {
		let is_dir = metadata(&file).await.map_or(false, |info| info.is_dir());
		let (dir, filename) = if is_dir {
			(
				file.to_owned(),
				PathBuf::from(format!(
					"watchexec.{}.log",
					chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ")
				)),
			)
		} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
			(parent.into(), PathBuf::from(file_name))
		} else {
			bail!("Failed to determine log file name");
		};

		non_blocking(rolling::never(dir, filename))
	} else {
		non_blocking(stderr())
	};

	let mut builder = tracing_subscriber::fmt().with_env_filter(match args.verbose {
		0 => unreachable!("checked by if earlier"),
		1 => "warn",
		2 => "info",
		3 => "debug",
		_ => "trace",
	});

	if args.verbose > 2 {
		use tracing_subscriber::fmt::format::FmtSpan;
		builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
	}

	match if args.log_file.is_some() {
		builder.json().with_writer(log_writer).try_init()
	} else if args.verbose > 3 {
		builder.pretty().with_writer(log_writer).try_init()
	} else {
		builder.with_writer(log_writer).try_init()
	} {
		Ok(()) => info!("logging initialised"),
		Err(e) => eprintln!("Failed to initialise logging, continuing with none\n{e}"),
	}

	Ok(Some(guard))
}