logging.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. //! CLI Tracing
  2. //!
  3. //! The CLI's tracing has internal and user-facing logs. User-facing logs are directly routed to the user in some form.
  4. //! Internal logs are stored in a log file for consumption in bug reports and debugging.
  5. //! We use tracing fields to determine whether a log is internal or external and additionally if the log should be
  6. //! formatted or not.
  7. //!
  8. //! These two fields are
  9. //! `dx_src` which tells the logger that this is a user-facing message and should be routed as so.
  10. //! `dx_no_fmt`which tells the logger to avoid formatting the log and to print it as-is.
  11. //!
  12. //! 1. Build general filter
  13. //! 2. Build file append layer for logging to a file. This file is reset on every CLI-run.
  14. //! 3. Build CLI layer for routing tracing logs to the TUI.
  15. //! 4. Build fmt layer for non-interactive logging with a custom writer that prevents output during interactive mode.
  16. use crate::{serve::ServeUpdate, Cli, Commands, Platform as TargetPlatform, Verbosity};
  17. use cargo_metadata::diagnostic::{Diagnostic, DiagnosticLevel};
  18. use clap::Parser;
  19. use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
  20. use std::sync::OnceLock;
  21. use std::{
  22. collections::HashMap,
  23. env,
  24. fmt::{Debug, Display, Write as _},
  25. fs,
  26. path::PathBuf,
  27. sync::{
  28. atomic::{AtomicBool, Ordering},
  29. Mutex,
  30. },
  31. time::Instant,
  32. };
  33. use tracing::{field::Visit, Level, Subscriber};
  34. use tracing_subscriber::{
  35. fmt::{
  36. format::{self, Writer},
  37. time::FormatTime,
  38. },
  39. prelude::*,
  40. registry::LookupSpan,
  41. EnvFilter, Layer,
  42. };
  43. const LOG_ENV: &str = "DIOXUS_LOG";
  44. const LOG_FILE_NAME: &str = "dx.log";
  45. const DX_SRC_FLAG: &str = "dx_src";
  46. static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
  47. static TUI_TX: OnceLock<UnboundedSender<TraceMsg>> = OnceLock::new();
  48. pub static VERBOSITY: OnceLock<Verbosity> = OnceLock::new();
  49. pub(crate) struct TraceController {
  50. pub(crate) tui_rx: UnboundedReceiver<TraceMsg>,
  51. }
  52. impl TraceController {
  53. /// Initialize the CLI and set up the tracing infrastructure
  54. pub fn initialize() -> Cli {
  55. let args = Cli::parse();
  56. VERBOSITY
  57. .set(args.verbosity)
  58. .expect("verbosity should only be set once");
  59. // By default we capture ourselves at a higher tracing level when serving
  60. // This ensures we're tracing ourselves even if we end up tossing the logs
  61. let filter = if env::var(LOG_ENV).is_ok() {
  62. EnvFilter::from_env(LOG_ENV)
  63. } else if matches!(args.action, Commands::Serve(_)) {
  64. EnvFilter::new(
  65. "error,dx=trace,dioxus_cli=trace,manganis_cli_support=trace,wasm_split_cli=trace,subsecond_cli_support=trace",
  66. )
  67. } else {
  68. EnvFilter::new(format!(
  69. "error,dx={our_level},dioxus_cli={our_level},manganis_cli_support={our_level},wasm_split_cli={our_level},subsecond_cli_support={our_level}",
  70. our_level = if args.verbosity.verbose {
  71. "debug"
  72. } else {
  73. "info"
  74. }
  75. ))
  76. };
  77. let json_filter = tracing_subscriber::filter::filter_fn(move |meta| {
  78. if meta.fields().len() == 1 && meta.fields().iter().next().unwrap().name() == "json" {
  79. return args.verbosity.json_output;
  80. }
  81. true
  82. });
  83. let fmt_layer = tracing_subscriber::fmt::layer()
  84. .with_target(args.verbosity.verbose)
  85. .fmt_fields(
  86. format::debug_fn(move |writer, field, value| {
  87. if field.name() == "json" && !args.verbosity.json_output {
  88. return Ok(());
  89. }
  90. if field.name() == "dx_src" && !args.verbosity.verbose {
  91. return Ok(());
  92. }
  93. write!(writer, "{}", format_field(field.name(), value))
  94. })
  95. .delimited(" "),
  96. )
  97. .with_timer(PrettyUptime::default());
  98. let fmt_layer = if args.verbosity.json_output {
  99. fmt_layer.json().flatten_event(true).boxed()
  100. } else {
  101. fmt_layer.boxed()
  102. };
  103. // When running in interactive mode (of which serve is the only one), we don't want to log to console directly
  104. let print_fmts_filter =
  105. tracing_subscriber::filter::filter_fn(|_| !TUI_ACTIVE.load(Ordering::Relaxed));
  106. let sub = tracing_subscriber::registry()
  107. .with(filter)
  108. .with(json_filter)
  109. .with(FileAppendLayer::new())
  110. .with(CLILayer {})
  111. .with(fmt_layer.with_filter(print_fmts_filter));
  112. #[cfg(feature = "tokio-console")]
  113. let sub = sub.with(console_subscriber::spawn());
  114. sub.init();
  115. args
  116. }
  117. /// Get a handle to the trace controller.
  118. pub fn redirect(interactive: bool) -> Self {
  119. let (tui_tx, tui_rx) = unbounded();
  120. if interactive {
  121. TUI_ACTIVE.store(true, Ordering::Relaxed);
  122. TUI_TX.set(tui_tx.clone()).unwrap();
  123. }
  124. Self { tui_rx }
  125. }
  126. /// Wait for the internal logger to send a message
  127. pub(crate) async fn wait(&mut self) -> ServeUpdate {
  128. use futures_util::StreamExt;
  129. let Some(log) = self.tui_rx.next().await else {
  130. return std::future::pending().await;
  131. };
  132. ServeUpdate::TracingLog { log }
  133. }
  134. pub(crate) fn shutdown_panic(&mut self) {
  135. TUI_ACTIVE.store(false, Ordering::Relaxed);
  136. // re-emit any remaining messages
  137. while let Ok(Some(msg)) = self.tui_rx.try_next() {
  138. let content = match msg.content {
  139. TraceContent::Text(text) => text,
  140. TraceContent::Cargo(msg) => msg.message.to_string(),
  141. };
  142. match msg.level {
  143. Level::ERROR => tracing::error!("{content}"),
  144. Level::WARN => tracing::warn!("{content}"),
  145. Level::INFO => tracing::info!("{content}"),
  146. Level::DEBUG => tracing::debug!("{content}"),
  147. Level::TRACE => tracing::trace!("{content}"),
  148. }
  149. }
  150. }
  151. }
  152. /// A logging layer that appends to a file.
  153. ///
  154. /// This layer returns on any error allowing the cli to continue work
  155. /// despite failing to log to a file. This helps in case of permission errors and similar.
  156. pub(crate) struct FileAppendLayer {
  157. file_path: PathBuf,
  158. buffer: Mutex<String>,
  159. }
  160. impl FileAppendLayer {
  161. fn new() -> Self {
  162. let file_path = Self::log_path();
  163. if !file_path.exists() {
  164. _ = std::fs::write(&file_path, "");
  165. }
  166. Self {
  167. file_path,
  168. buffer: Mutex::new(String::new()),
  169. }
  170. }
  171. pub(crate) fn log_path() -> PathBuf {
  172. std::env::temp_dir().join(LOG_FILE_NAME)
  173. }
  174. }
  175. impl<S> Layer<S> for FileAppendLayer
  176. where
  177. S: Subscriber + for<'a> LookupSpan<'a>,
  178. {
  179. fn on_event(
  180. &self,
  181. event: &tracing::Event<'_>,
  182. _ctx: tracing_subscriber::layer::Context<'_, S>,
  183. ) {
  184. let mut visitor = CollectVisitor::new();
  185. event.record(&mut visitor);
  186. let new_line = if visitor.source == TraceSrc::Cargo {
  187. visitor.message
  188. } else {
  189. let meta = event.metadata();
  190. let level = meta.level();
  191. let mut final_msg = String::new();
  192. _ = write!(
  193. final_msg,
  194. "[{level}] {}: {} ",
  195. meta.module_path().unwrap_or("dx"),
  196. visitor.message
  197. );
  198. for (field, value) in visitor.fields.iter() {
  199. _ = write!(final_msg, "{} ", format_field(field, value));
  200. }
  201. _ = writeln!(final_msg);
  202. final_msg
  203. };
  204. // Append logs
  205. let new_data = console::strip_ansi_codes(&new_line).to_string();
  206. if let Ok(mut buf) = self.buffer.lock() {
  207. *buf += &new_data;
  208. // TODO: Make this efficient.
  209. _ = fs::write(&self.file_path, buf.as_bytes());
  210. }
  211. }
  212. }
  213. /// This is our "subscriber" (layer) that records structured data for the tui output.
  214. struct CLILayer;
  215. impl<S> Layer<S> for CLILayer
  216. where
  217. S: Subscriber + for<'a> LookupSpan<'a>,
  218. {
  219. // Subscribe to user
  220. fn on_event(
  221. &self,
  222. event: &tracing::Event<'_>,
  223. _ctx: tracing_subscriber::layer::Context<'_, S>,
  224. ) {
  225. if !TUI_ACTIVE.load(Ordering::Relaxed) {
  226. return;
  227. }
  228. let mut visitor = CollectVisitor::new();
  229. event.record(&mut visitor);
  230. let meta = event.metadata();
  231. let level = meta.level();
  232. let mut final_msg = String::new();
  233. write!(final_msg, "{} ", visitor.message).unwrap();
  234. for (field, value) in visitor.fields.iter() {
  235. write!(final_msg, "{} ", format_field(field, value)).unwrap();
  236. }
  237. if visitor.source == TraceSrc::Unknown {
  238. visitor.source = TraceSrc::Dev;
  239. }
  240. _ = TUI_TX
  241. .get()
  242. .unwrap()
  243. .unbounded_send(TraceMsg::text(visitor.source, *level, final_msg));
  244. }
  245. }
  246. /// A record visitor that collects dx-specific info and user-provided fields for logging consumption.
  247. struct CollectVisitor {
  248. message: String,
  249. source: TraceSrc,
  250. fields: HashMap<String, String>,
  251. }
  252. impl CollectVisitor {
  253. pub fn new() -> Self {
  254. Self {
  255. message: String::new(),
  256. source: TraceSrc::Unknown,
  257. fields: HashMap::new(),
  258. }
  259. }
  260. }
  261. impl Visit for CollectVisitor {
  262. fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
  263. let name = field.name();
  264. let mut value_string = String::new();
  265. write!(value_string, "{:?}", value).unwrap();
  266. if name == "message" {
  267. self.message = value_string;
  268. return;
  269. }
  270. if name == DX_SRC_FLAG {
  271. self.source = TraceSrc::from(value_string);
  272. return;
  273. }
  274. self.fields.insert(name.to_string(), value_string);
  275. }
  276. }
  277. /// Formats a tracing field and value, removing any internal fields from the final output.
  278. fn format_field(field_name: &str, value: &dyn Debug) -> String {
  279. let mut out = String::new();
  280. match field_name {
  281. "message" => write!(out, "{:?}", value),
  282. _ => write!(out, "{}={:?}", field_name, value),
  283. }
  284. .unwrap();
  285. out
  286. }
  287. #[derive(Clone, PartialEq)]
  288. pub struct TraceMsg {
  289. pub source: TraceSrc,
  290. pub level: Level,
  291. pub content: TraceContent,
  292. pub timestamp: chrono::DateTime<chrono::Local>,
  293. }
  294. #[derive(Clone, PartialEq)]
  295. #[allow(clippy::large_enum_variant)]
  296. pub enum TraceContent {
  297. Cargo(Diagnostic),
  298. Text(String),
  299. }
  300. impl TraceMsg {
  301. pub fn text(source: TraceSrc, level: Level, content: String) -> Self {
  302. Self {
  303. source,
  304. level,
  305. content: TraceContent::Text(content),
  306. timestamp: chrono::Local::now(),
  307. }
  308. }
  309. /// Create a new trace message from a cargo compiler message
  310. ///
  311. /// All `cargo` messages are logged at the `TRACE` level since they get *very* noisy during development
  312. pub fn cargo(content: Diagnostic) -> Self {
  313. Self {
  314. level: match content.level {
  315. DiagnosticLevel::Ice => Level::ERROR,
  316. DiagnosticLevel::Error => Level::ERROR,
  317. DiagnosticLevel::FailureNote => Level::ERROR,
  318. DiagnosticLevel::Warning => Level::TRACE,
  319. DiagnosticLevel::Note => Level::TRACE,
  320. DiagnosticLevel::Help => Level::TRACE,
  321. _ => Level::TRACE,
  322. },
  323. timestamp: chrono::Local::now(),
  324. source: TraceSrc::Cargo,
  325. content: TraceContent::Cargo(content),
  326. }
  327. }
  328. }
  329. #[derive(Clone, PartialEq)]
  330. pub enum TraceSrc {
  331. App(TargetPlatform),
  332. Dev,
  333. Build,
  334. Bundle,
  335. Cargo,
  336. Unknown,
  337. }
  338. impl std::fmt::Debug for TraceSrc {
  339. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  340. let as_string = self.to_string();
  341. write!(f, "{as_string}")
  342. }
  343. }
  344. impl From<String> for TraceSrc {
  345. fn from(value: String) -> Self {
  346. match value.as_str() {
  347. "dev" => Self::Dev,
  348. "bld" => Self::Build,
  349. "cargo" => Self::Cargo,
  350. "app" => Self::App(TargetPlatform::Web),
  351. "windows" => Self::App(TargetPlatform::Windows),
  352. "macos" => Self::App(TargetPlatform::MacOS),
  353. "linux" => Self::App(TargetPlatform::Linux),
  354. "server" => Self::App(TargetPlatform::Server),
  355. _ => Self::Unknown,
  356. }
  357. }
  358. }
  359. impl Display for TraceSrc {
  360. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  361. match self {
  362. Self::App(platform) => match platform {
  363. TargetPlatform::Web => write!(f, "web"),
  364. TargetPlatform::MacOS => write!(f, "macos"),
  365. TargetPlatform::Windows => write!(f, "windows"),
  366. TargetPlatform::Linux => write!(f, "linux"),
  367. TargetPlatform::Server => write!(f, "server"),
  368. TargetPlatform::Ios => write!(f, "ios"),
  369. TargetPlatform::Android => write!(f, "android"),
  370. TargetPlatform::Liveview => write!(f, "liveview"),
  371. },
  372. Self::Dev => write!(f, "dev"),
  373. Self::Build => write!(f, "build"),
  374. Self::Cargo => write!(f, "cargo"),
  375. Self::Unknown => write!(f, "n/a"),
  376. Self::Bundle => write!(f, "bundle"),
  377. }
  378. }
  379. }
  380. /// Retrieve and print the relative elapsed wall-clock time since an epoch.
  381. ///
  382. /// The `Default` implementation for `Uptime` makes the epoch the current time.
  383. #[derive(Debug, Clone, Copy, Eq, PartialEq)]
  384. pub struct PrettyUptime {
  385. epoch: Instant,
  386. }
  387. impl Default for PrettyUptime {
  388. fn default() -> Self {
  389. Self {
  390. epoch: Instant::now(),
  391. }
  392. }
  393. }
  394. impl FormatTime for PrettyUptime {
  395. fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
  396. let e = self.epoch.elapsed();
  397. write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
  398. }
  399. }