logging.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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::DiagnosticLevel, CompilerMessage};
  18. use clap::Parser;
  19. use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
  20. use once_cell::sync::OnceCell;
  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: OnceCell<UnboundedSender<TraceMsg>> = OnceCell::new();
  48. pub static VERBOSITY: OnceCell<Verbosity> = OnceCell::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("error,dx=trace,dioxus-cli=trace,manganis-cli-support=trace")
  65. } else {
  66. EnvFilter::new(format!(
  67. "error,dx={our_level},dioxus-cli={our_level},manganis-cli-support={our_level}",
  68. our_level = if args.verbosity.verbose {
  69. "debug"
  70. } else {
  71. "info"
  72. }
  73. ))
  74. };
  75. let json_filter = tracing_subscriber::filter::filter_fn(move |meta| {
  76. if meta.fields().len() == 1 && meta.fields().iter().next().unwrap().name() == "json" {
  77. return args.verbosity.json_output;
  78. }
  79. true
  80. });
  81. let fmt_layer = tracing_subscriber::fmt::layer()
  82. .with_target(args.verbosity.verbose)
  83. .fmt_fields(
  84. format::debug_fn(move |writer, field, value| {
  85. if field.name() == "json" && !args.verbosity.json_output {
  86. return Ok(());
  87. }
  88. write!(writer, "{}", format_field(field.name(), value))
  89. })
  90. .delimited(" "),
  91. )
  92. .with_timer(PrettyUptime::default());
  93. let fmt_layer = if args.verbosity.json_output {
  94. fmt_layer.json().flatten_event(true).boxed()
  95. } else {
  96. fmt_layer.boxed()
  97. };
  98. // When running in interactive mode (of which serve is the only one), we don't want to log to console directly
  99. let print_fmts_filter =
  100. tracing_subscriber::filter::filter_fn(|_| !TUI_ACTIVE.load(Ordering::Relaxed));
  101. let sub = tracing_subscriber::registry()
  102. .with(filter)
  103. .with(json_filter)
  104. .with(FileAppendLayer::new())
  105. .with(CLILayer {})
  106. .with(fmt_layer.with_filter(print_fmts_filter));
  107. #[cfg(feature = "tokio-console")]
  108. let sub = sub.with(console_subscriber::spawn());
  109. sub.init();
  110. args
  111. }
  112. /// Get a handle to the trace controller.
  113. pub fn redirect() -> Self {
  114. let (tui_tx, tui_rx) = unbounded();
  115. TUI_ACTIVE.store(true, Ordering::Relaxed);
  116. TUI_TX.set(tui_tx.clone()).unwrap();
  117. Self { tui_rx }
  118. }
  119. /// Wait for the internal logger to send a message
  120. pub(crate) async fn wait(&mut self) -> ServeUpdate {
  121. use futures_util::StreamExt;
  122. let log = self.tui_rx.next().await.expect("tracer should never die");
  123. ServeUpdate::TracingLog { log }
  124. }
  125. }
  126. impl Drop for TraceController {
  127. fn drop(&mut self) {
  128. TUI_ACTIVE.store(false, Ordering::Relaxed);
  129. // re-emit any remaining messages
  130. while let Ok(Some(msg)) = self.tui_rx.try_next() {
  131. let contents = match msg.content {
  132. TraceContent::Text(text) => text,
  133. TraceContent::Cargo(msg) => msg.message.to_string(),
  134. };
  135. tracing::error!("{}", contents);
  136. }
  137. }
  138. }
  139. /// A logging layer that appends to a file.
  140. ///
  141. /// This layer returns on any error allowing the cli to continue work
  142. /// despite failing to log to a file. This helps in case of permission errors and similar.
  143. pub(crate) struct FileAppendLayer {
  144. file_path: PathBuf,
  145. buffer: Mutex<String>,
  146. }
  147. impl FileAppendLayer {
  148. fn new() -> Self {
  149. let file_path = Self::log_path();
  150. if !file_path.exists() {
  151. _ = std::fs::write(&file_path, "");
  152. }
  153. Self {
  154. file_path,
  155. buffer: Mutex::new(String::new()),
  156. }
  157. }
  158. pub(crate) fn log_path() -> PathBuf {
  159. std::env::temp_dir().join(LOG_FILE_NAME)
  160. }
  161. }
  162. impl<S> Layer<S> for FileAppendLayer
  163. where
  164. S: Subscriber + for<'a> LookupSpan<'a>,
  165. {
  166. fn on_event(
  167. &self,
  168. event: &tracing::Event<'_>,
  169. _ctx: tracing_subscriber::layer::Context<'_, S>,
  170. ) {
  171. let mut visitor = CollectVisitor::new();
  172. event.record(&mut visitor);
  173. let new_line = if visitor.source == TraceSrc::Cargo {
  174. visitor.message
  175. } else {
  176. let meta = event.metadata();
  177. let level = meta.level();
  178. let mut final_msg = String::new();
  179. _ = write!(
  180. final_msg,
  181. "[{level}] {}: {} ",
  182. meta.module_path().unwrap_or("dx"),
  183. visitor.message
  184. );
  185. for (field, value) in visitor.fields.iter() {
  186. _ = write!(final_msg, "{} ", format_field(field, value));
  187. }
  188. _ = writeln!(final_msg);
  189. final_msg
  190. };
  191. // Append logs
  192. let new_data = console::strip_ansi_codes(&new_line).to_string();
  193. if let Ok(mut buf) = self.buffer.lock() {
  194. *buf += &new_data;
  195. // TODO: Make this efficient.
  196. _ = fs::write(&self.file_path, buf.as_bytes());
  197. }
  198. }
  199. }
  200. /// This is our "subscriber" (layer) that records structured data for the tui output.
  201. struct CLILayer;
  202. impl<S> Layer<S> for CLILayer
  203. where
  204. S: Subscriber + for<'a> LookupSpan<'a>,
  205. {
  206. // Subscribe to user
  207. fn on_event(
  208. &self,
  209. event: &tracing::Event<'_>,
  210. _ctx: tracing_subscriber::layer::Context<'_, S>,
  211. ) {
  212. if !TUI_ACTIVE.load(Ordering::Relaxed) {
  213. return;
  214. }
  215. let mut visitor = CollectVisitor::new();
  216. event.record(&mut visitor);
  217. let meta = event.metadata();
  218. let level = meta.level();
  219. let mut final_msg = String::new();
  220. write!(final_msg, "{} ", visitor.message).unwrap();
  221. for (field, value) in visitor.fields.iter() {
  222. write!(final_msg, "{} ", format_field(field, value)).unwrap();
  223. }
  224. if visitor.source == TraceSrc::Unknown {
  225. visitor.source = TraceSrc::Dev;
  226. }
  227. _ = TUI_TX
  228. .get()
  229. .unwrap()
  230. .unbounded_send(TraceMsg::text(visitor.source, *level, final_msg));
  231. }
  232. }
  233. /// A record visitor that collects dx-specific info and user-provided fields for logging consumption.
  234. struct CollectVisitor {
  235. message: String,
  236. source: TraceSrc,
  237. fields: HashMap<String, String>,
  238. }
  239. impl CollectVisitor {
  240. pub fn new() -> Self {
  241. Self {
  242. message: String::new(),
  243. source: TraceSrc::Unknown,
  244. fields: HashMap::new(),
  245. }
  246. }
  247. }
  248. impl Visit for CollectVisitor {
  249. fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
  250. let name = field.name();
  251. let mut value_string = String::new();
  252. write!(value_string, "{:?}", value).unwrap();
  253. if name == "message" {
  254. self.message = value_string;
  255. return;
  256. }
  257. if name == DX_SRC_FLAG {
  258. self.source = TraceSrc::from(value_string);
  259. return;
  260. }
  261. self.fields.insert(name.to_string(), value_string);
  262. }
  263. }
  264. /// Formats a tracing field and value, removing any internal fields from the final output.
  265. fn format_field(field_name: &str, value: &dyn Debug) -> String {
  266. let mut out = String::new();
  267. match field_name {
  268. "message" => write!(out, "{:?}", value),
  269. _ => write!(out, "{}={:?}", field_name, value),
  270. }
  271. .unwrap();
  272. out
  273. }
  274. #[derive(Clone, PartialEq)]
  275. pub struct TraceMsg {
  276. pub source: TraceSrc,
  277. pub level: Level,
  278. pub content: TraceContent,
  279. pub timestamp: chrono::DateTime<chrono::Local>,
  280. }
  281. #[derive(Clone, PartialEq)]
  282. #[allow(clippy::large_enum_variant)]
  283. pub enum TraceContent {
  284. Cargo(CompilerMessage),
  285. Text(String),
  286. }
  287. impl TraceMsg {
  288. pub fn text(source: TraceSrc, level: Level, content: String) -> Self {
  289. Self {
  290. source,
  291. level,
  292. content: TraceContent::Text(content),
  293. timestamp: chrono::Local::now(),
  294. }
  295. }
  296. /// Create a new trace message from a cargo compiler message
  297. ///
  298. /// All `cargo` messages are logged at the `TRACE` level since they get *very* noisy during development
  299. pub fn cargo(content: CompilerMessage) -> Self {
  300. Self {
  301. level: match content.message.level {
  302. DiagnosticLevel::Ice => Level::ERROR,
  303. DiagnosticLevel::Error => Level::ERROR,
  304. DiagnosticLevel::FailureNote => Level::ERROR,
  305. DiagnosticLevel::Warning => Level::TRACE,
  306. DiagnosticLevel::Note => Level::TRACE,
  307. DiagnosticLevel::Help => Level::TRACE,
  308. _ => Level::TRACE,
  309. },
  310. timestamp: chrono::Local::now(),
  311. source: TraceSrc::Cargo,
  312. content: TraceContent::Cargo(content),
  313. }
  314. }
  315. }
  316. #[derive(Clone, PartialEq)]
  317. pub enum TraceSrc {
  318. App(TargetPlatform),
  319. Dev,
  320. Build,
  321. Bundle,
  322. Cargo,
  323. Unknown,
  324. }
  325. impl std::fmt::Debug for TraceSrc {
  326. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  327. let as_string = self.to_string();
  328. write!(f, "{as_string}")
  329. }
  330. }
  331. impl From<String> for TraceSrc {
  332. fn from(value: String) -> Self {
  333. match value.as_str() {
  334. "dev" => Self::Dev,
  335. "bld" => Self::Build,
  336. "cargo" => Self::Cargo,
  337. "app" => Self::App(TargetPlatform::Web),
  338. "windows" => Self::App(TargetPlatform::Windows),
  339. "macos" => Self::App(TargetPlatform::MacOS),
  340. "linux" => Self::App(TargetPlatform::Linux),
  341. "server" => Self::App(TargetPlatform::Server),
  342. _ => Self::Unknown,
  343. }
  344. }
  345. }
  346. impl Display for TraceSrc {
  347. fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  348. match self {
  349. Self::App(platform) => match platform {
  350. TargetPlatform::Web => write!(f, "web"),
  351. TargetPlatform::MacOS => write!(f, "macos"),
  352. TargetPlatform::Windows => write!(f, "windows"),
  353. TargetPlatform::Linux => write!(f, "linux"),
  354. TargetPlatform::Server => write!(f, "server"),
  355. TargetPlatform::Ios => write!(f, "ios"),
  356. TargetPlatform::Android => write!(f, "android"),
  357. TargetPlatform::Liveview => write!(f, "liveview"),
  358. },
  359. Self::Dev => write!(f, "dev"),
  360. Self::Build => write!(f, "build"),
  361. Self::Cargo => write!(f, "cargo"),
  362. Self::Unknown => write!(f, "n/a"),
  363. Self::Bundle => write!(f, "bundle"),
  364. }
  365. }
  366. }
  367. /// Retrieve and print the relative elapsed wall-clock time since an epoch.
  368. ///
  369. /// The `Default` implementation for `Uptime` makes the epoch the current time.
  370. #[derive(Debug, Clone, Copy, Eq, PartialEq)]
  371. pub struct PrettyUptime {
  372. epoch: Instant,
  373. }
  374. impl Default for PrettyUptime {
  375. fn default() -> Self {
  376. Self {
  377. epoch: Instant::now(),
  378. }
  379. }
  380. }
  381. impl FormatTime for PrettyUptime {
  382. fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
  383. let e = self.epoch.elapsed();
  384. write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
  385. }
  386. }