use crate::{serve::ServeUpdate, Cli, Commands, Platform as TargetPlatform, Verbosity};
use cargo_metadata::{diagnostic::DiagnosticLevel, CompilerMessage};
use clap::Parser;
use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use once_cell::sync::OnceCell;
use std::{
collections::HashMap,
env,
fmt::{Debug, Display, Write as _},
fs,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
},
time::Instant,
};
use tracing::{field::Visit, Level, Subscriber};
use tracing_subscriber::{
fmt::{
format::{self, Writer},
time::FormatTime,
},
prelude::*,
registry::LookupSpan,
EnvFilter, Layer,
};
const LOG_ENV: &str = "DIOXUS_LOG";
const LOG_FILE_NAME: &str = "dx.log";
const DX_SRC_FLAG: &str = "dx_src";
static TUI_ACTIVE: AtomicBool = AtomicBool::new(false);
static TUI_TX: OnceCell<UnboundedSender<TraceMsg>> = OnceCell::new();
pub static VERBOSITY: OnceCell<Verbosity> = OnceCell::new();
pub(crate) struct TraceController {
pub(crate) tui_rx: UnboundedReceiver<TraceMsg>,
}
impl TraceController {
pub fn initialize() -> Cli {
let args = Cli::parse();
VERBOSITY
.set(args.verbosity)
.expect("verbosity should only be set once");
let filter = if env::var(LOG_ENV).is_ok() {
EnvFilter::from_env(LOG_ENV)
} else if matches!(args.action, Commands::Serve(_)) {
EnvFilter::new("error,dx=trace,dioxus-cli=trace,manganis-cli-support=trace")
} else {
EnvFilter::new(format!(
"error,dx={our_level},dioxus-cli={our_level},manganis-cli-support={our_level}",
our_level = if args.verbosity.verbose {
"debug"
} else {
"info"
}
))
};
let json_filter = tracing_subscriber::filter::filter_fn(move |meta| {
if meta.fields().len() == 1 && meta.fields().iter().next().unwrap().name() == "json" {
return args.verbosity.json_output;
}
true
});
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(args.verbosity.verbose)
.fmt_fields(
format::debug_fn(move |writer, field, value| {
if field.name() == "json" && !args.verbosity.json_output {
return Ok(());
}
if field.name() == "dx_src" && !args.verbosity.verbose {
return Ok(());
}
write!(writer, "{}", format_field(field.name(), value))
})
.delimited(" "),
)
.with_timer(PrettyUptime::default());
let fmt_layer = if args.verbosity.json_output {
fmt_layer.json().flatten_event(true).boxed()
} else {
fmt_layer.boxed()
};
let print_fmts_filter =
tracing_subscriber::filter::filter_fn(|_| !TUI_ACTIVE.load(Ordering::Relaxed));
let sub = tracing_subscriber::registry()
.with(filter)
.with(json_filter)
.with(FileAppendLayer::new())
.with(CLILayer {})
.with(fmt_layer.with_filter(print_fmts_filter));
#[cfg(feature = "tokio-console")]
let sub = sub.with(console_subscriber::spawn());
sub.init();
args
}
pub fn redirect() -> Self {
let (tui_tx, tui_rx) = unbounded();
TUI_ACTIVE.store(true, Ordering::Relaxed);
TUI_TX.set(tui_tx.clone()).unwrap();
Self { tui_rx }
}
pub(crate) async fn wait(&mut self) -> ServeUpdate {
use futures_util::StreamExt;
let log = self.tui_rx.next().await.expect("tracer should never die");
ServeUpdate::TracingLog { log }
}
}
impl Drop for TraceController {
fn drop(&mut self) {
TUI_ACTIVE.store(false, Ordering::Relaxed);
while let Ok(Some(msg)) = self.tui_rx.try_next() {
let contents = match msg.content {
TraceContent::Text(text) => text,
TraceContent::Cargo(msg) => msg.message.to_string(),
};
tracing::error!("{}", contents);
}
}
}
pub(crate) struct FileAppendLayer {
file_path: PathBuf,
buffer: Mutex<String>,
}
impl FileAppendLayer {
fn new() -> Self {
let file_path = Self::log_path();
if !file_path.exists() {
_ = std::fs::write(&file_path, "");
}
Self {
file_path,
buffer: Mutex::new(String::new()),
}
}
pub(crate) fn log_path() -> PathBuf {
std::env::temp_dir().join(LOG_FILE_NAME)
}
}
impl<S> Layer<S> for FileAppendLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = CollectVisitor::new();
event.record(&mut visitor);
let new_line = if visitor.source == TraceSrc::Cargo {
visitor.message
} else {
let meta = event.metadata();
let level = meta.level();
let mut final_msg = String::new();
_ = write!(
final_msg,
"[{level}] {}: {} ",
meta.module_path().unwrap_or("dx"),
visitor.message
);
for (field, value) in visitor.fields.iter() {
_ = write!(final_msg, "{} ", format_field(field, value));
}
_ = writeln!(final_msg);
final_msg
};
let new_data = console::strip_ansi_codes(&new_line).to_string();
if let Ok(mut buf) = self.buffer.lock() {
*buf += &new_data;
_ = fs::write(&self.file_path, buf.as_bytes());
}
}
}
struct CLILayer;
impl<S> Layer<S> for CLILayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
if !TUI_ACTIVE.load(Ordering::Relaxed) {
return;
}
let mut visitor = CollectVisitor::new();
event.record(&mut visitor);
let meta = event.metadata();
let level = meta.level();
let mut final_msg = String::new();
write!(final_msg, "{} ", visitor.message).unwrap();
for (field, value) in visitor.fields.iter() {
write!(final_msg, "{} ", format_field(field, value)).unwrap();
}
if visitor.source == TraceSrc::Unknown {
visitor.source = TraceSrc::Dev;
}
_ = TUI_TX
.get()
.unwrap()
.unbounded_send(TraceMsg::text(visitor.source, *level, final_msg));
}
}
struct CollectVisitor {
message: String,
source: TraceSrc,
fields: HashMap<String, String>,
}
impl CollectVisitor {
pub fn new() -> Self {
Self {
message: String::new(),
source: TraceSrc::Unknown,
fields: HashMap::new(),
}
}
}
impl Visit for CollectVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
let name = field.name();
let mut value_string = String::new();
write!(value_string, "{:?}", value).unwrap();
if name == "message" {
self.message = value_string;
return;
}
if name == DX_SRC_FLAG {
self.source = TraceSrc::from(value_string);
return;
}
self.fields.insert(name.to_string(), value_string);
}
}
fn format_field(field_name: &str, value: &dyn Debug) -> String {
let mut out = String::new();
match field_name {
"message" => write!(out, "{:?}", value),
_ => write!(out, "{}={:?}", field_name, value),
}
.unwrap();
out
}
#[derive(Clone, PartialEq)]
pub struct TraceMsg {
pub source: TraceSrc,
pub level: Level,
pub content: TraceContent,
pub timestamp: chrono::DateTime<chrono::Local>,
}
#[derive(Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum TraceContent {
Cargo(CompilerMessage),
Text(String),
}
impl TraceMsg {
pub fn text(source: TraceSrc, level: Level, content: String) -> Self {
Self {
source,
level,
content: TraceContent::Text(content),
timestamp: chrono::Local::now(),
}
}
pub fn cargo(content: CompilerMessage) -> Self {
Self {
level: match content.message.level {
DiagnosticLevel::Ice => Level::ERROR,
DiagnosticLevel::Error => Level::ERROR,
DiagnosticLevel::FailureNote => Level::ERROR,
DiagnosticLevel::Warning => Level::TRACE,
DiagnosticLevel::Note => Level::TRACE,
DiagnosticLevel::Help => Level::TRACE,
_ => Level::TRACE,
},
timestamp: chrono::Local::now(),
source: TraceSrc::Cargo,
content: TraceContent::Cargo(content),
}
}
}
#[derive(Clone, PartialEq)]
pub enum TraceSrc {
App(TargetPlatform),
Dev,
Build,
Bundle,
Cargo,
Unknown,
}
impl std::fmt::Debug for TraceSrc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let as_string = self.to_string();
write!(f, "{as_string}")
}
}
impl From<String> for TraceSrc {
fn from(value: String) -> Self {
match value.as_str() {
"dev" => Self::Dev,
"bld" => Self::Build,
"cargo" => Self::Cargo,
"app" => Self::App(TargetPlatform::Web),
"windows" => Self::App(TargetPlatform::Windows),
"macos" => Self::App(TargetPlatform::MacOS),
"linux" => Self::App(TargetPlatform::Linux),
"server" => Self::App(TargetPlatform::Server),
_ => Self::Unknown,
}
}
}
impl Display for TraceSrc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::App(platform) => match platform {
TargetPlatform::Web => write!(f, "web"),
TargetPlatform::MacOS => write!(f, "macos"),
TargetPlatform::Windows => write!(f, "windows"),
TargetPlatform::Linux => write!(f, "linux"),
TargetPlatform::Server => write!(f, "server"),
TargetPlatform::Ios => write!(f, "ios"),
TargetPlatform::Android => write!(f, "android"),
TargetPlatform::Liveview => write!(f, "liveview"),
},
Self::Dev => write!(f, "dev"),
Self::Build => write!(f, "build"),
Self::Cargo => write!(f, "cargo"),
Self::Unknown => write!(f, "n/a"),
Self::Bundle => write!(f, "bundle"),
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct PrettyUptime {
epoch: Instant,
}
impl Default for PrettyUptime {
fn default() -> Self {
Self {
epoch: Instant::now(),
}
}
}
impl FormatTime for PrettyUptime {
fn format_time(&self, w: &mut Writer<'_>) -> std::fmt::Result {
let e = self.epoch.elapsed();
write!(w, "{:4}.{:2}s", e.as_secs(), e.subsec_millis())
}
}