iota/iota-logger/src/lib.rs
2026-07-27 20:37:33 +02:00

442 lines
11 KiB
Rust

use std::{
fs::{self, OpenOptions},
io::Write,
sync::{OnceLock, atomic::Ordering, mpsc},
thread,
time::{SystemTime, UNIX_EPOCH},
};
use mtp::codec::{CommunicationValue, DataTypeId, DataValue, Version};
use ratatui::style::Color;
use iota_state::{UNIQUE, UiLogEntry};
use tokio::sync::broadcast;
pub mod language_creator;
pub mod language_manager;
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
static LOG_BROADCASTER: OnceLock<broadcast::Sender<UiLogEntry>> = OnceLock::new();
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[allow(unused)]
pub enum PrintType {
Call,
Client,
Iota,
Omikron,
Omega,
General,
Command,
}
impl PrintType {
pub fn prefix_color(self) -> Color {
match self {
PrintType::Call => Color::Magenta,
PrintType::Client => Color::Green,
PrintType::Iota => Color::Yellow,
PrintType::Omikron => Color::Blue,
PrintType::Omega => Color::Cyan,
PrintType::General => Color::LightCyan,
PrintType::Command => Color::LightGreen,
}
}
}
struct LogMessage {
timestamp_ms: u128,
prefix: String,
kind: PrintType,
is_error: bool,
translation_key: Option<String>,
format_args: Vec<String>,
message: Option<String>,
}
/* The logger owns file persistence while consumers receive rendered entries
* through a process-local broadcast subscription. */
pub fn startup() {
startup_with_log_dir(Some(
iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.expect("resolve Iota user paths")
.log_dir,
));
}
/// `None` keeps logging on stderr only (the systemd default).
pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
let (tx, rx) = mpsc::channel::<LogMessage>();
if LOGGER.set(tx).is_err() {
return;
}
let (broadcast_tx, _) = broadcast::channel(512);
let _ = LOG_BROADCASTER.set(broadcast_tx.clone());
thread::spawn(move || {
let mut file = log_dir.and_then(|log_dir| {
fs::create_dir_all(&log_dir).ok()?;
let start_ts = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
OpenOptions::new()
.create(true)
.append(true)
.open(log_dir.join(format!("log_{start_ts}.txt")))
.ok()
});
for msg in rx {
let resolved_message = if let Some(key) = msg.translation_key {
let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect();
language_manager::format(&key, &args)
} else {
msg.message.unwrap_or_default()
};
let timestamp = format_timestamp_inline(msg.timestamp_ms);
let prefix = if msg.prefix.is_empty() {
String::new()
} else {
format!("{} ", msg.prefix)
};
let line = format!(
"{} {} {}{}",
fixed_box(&msg.timestamp_ms.to_string(), 13),
timestamp,
prefix,
resolved_message
);
if let Some(file) = file.as_mut() {
let _ = writeln!(file, "{}", line);
}
let _ = writeln!(std::io::stderr(), "{}", line);
let entry = UiLogEntry {
timestamp_ms: msg.timestamp_ms,
sender: format!("{:?}", msg.kind),
message: resolved_message,
is_error: msg.is_error,
};
let _ = broadcast_tx.send(entry);
}
});
}
pub fn subscribe() -> Option<broadcast::Receiver<UiLogEntry>> {
LOG_BROADCASTER.get().map(broadcast::Sender::subscribe)
}
fn format_timestamp_inline(timestamp_ms: u128) -> String {
let secs = (timestamp_ms / 1000) as i64;
let hours = (secs / 3600) % 24;
let minutes = (secs / 60) % 60;
let seconds = secs % 60;
format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds)
}
fn fixed_box(content: &str, width: usize) -> String {
let s: String = content.chars().take(width).collect();
let len = s.chars().count();
if len < width {
format!("[{}{}]", " ".repeat(width - len), s)
} else {
s
}
}
pub fn log_internal_translated(
kind: PrintType,
prefix: String,
is_error: bool,
key: &str,
args: Vec<String>,
) {
if let Some(tx) = LOGGER.get() {
UNIQUE.store(true, Ordering::Relaxed);
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
prefix,
kind,
is_error,
translation_key: Some(key.to_string()),
format_args: args,
message: None,
});
}
}
pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) {
if let Some(tx) = LOGGER.get() {
UNIQUE.store(true, Ordering::Relaxed);
let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
prefix,
kind,
is_error,
translation_key: None,
format_args: Vec::new(),
message: Some(message),
});
}
}
#[macro_export]
macro_rules! log_t {
($key:expr) => {
$crate::log_internal_translated(
$crate::PrintType::General,
"".to_string(),
false,
$key,
vec![]
)
};
($key:expr, $($arg:expr),+) => {
$crate::log_internal_translated(
$crate::PrintType::General,
"".to_string(),
false,
$key,
vec![$($arg),+]
)
};
}
#[macro_export]
macro_rules! log_t_err {
($key:expr) => {
$crate::log_internal_translated(
$crate::PrintType::General,
"".to_string(),
true,
$key,
vec![]
)
};
($key:expr, $($arg:expr),+) => {
$crate::log_internal_translated(
$crate::PrintType::General,
"".to_string(),
true,
$key,
vec![$($arg.to_string()),+]
)
};
}
/// Log a command message.
#[macro_export]
macro_rules! log_command {
($($arg:tt)*) => {
$crate::log_internal(
$crate::PrintType::Command,
"".to_string(),
false,
format!($($arg)*)
)
};
}
/// Log a general informational message.
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
$crate::log_internal(
$crate::PrintType::General,
"".to_string(),
false,
format!($($arg)*)
)
};
}
/// Log an inbound message (`>`).
#[macro_export]
macro_rules! log_in {
($($arg:tt)*) => {
$crate::log_internal(
$crate::PrintType::General,
">".to_string(),
false,
format!($($arg)*)
)
};
}
/// Log an outbound message (`<`).
#[macro_export]
macro_rules! log_out {
($($arg:tt)*) => {
$crate::log_internal(
$crate::PrintType::General,
"<".to_string(),
false,
format!($($arg)*)
)
};
}
/// Log an error message (`>>`).
#[macro_export]
macro_rules! log_err {
($($arg:tt)*) => {
$crate::log_internal(
$crate::PrintType::General,
">>".to_string(),
true,
format!($($arg)*)
)
};
}
// ******** COMMUNICATION VALUES ********
pub fn log_cv_internal(
prefix: &'static str,
cv: &CommunicationValue,
print_type: Option<PrintType>,
) {
let formatted = format_cv(cv);
log_internal(
print_type.unwrap_or(PrintType::General),
prefix.to_string(),
false,
formatted,
);
}
pub fn format_cv(cv: &CommunicationValue) -> String {
let mut parts = Vec::new();
let sender = cv.get_sender();
let receiver = cv.get_receiver();
if sender > 0 && receiver > 0 {
parts.push(format!("{} > {}", sender, receiver));
} else if sender > 0 {
parts.push(format!("{}", sender));
} else if receiver > 0 {
parts.push(format!("> {}", receiver));
}
let comm_type = cv
.get_comm_type_enum()
.map(|kind| kind.to_string())
.unwrap_or_else(|| cv.get_type().to_string());
parts.push(format!("{} (id={})", comm_type, cv.get_id()));
let data = cv.data();
let formated_data = format_data_container(
data.iter().map(|(k, v)| (*k, v.clone())).collect(),
Version(1, 0),
);
parts.push(format!("{}", formated_data));
parts.join(": ")
}
fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -> String {
let parts: Vec<String> = data
.into_iter()
.map(|(key, value)| {
let key_str = key.to_string();
match value {
DataValue::Str(s) => format!("{}=\"{}\"", key_str, s),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
format!("{}={{ {} }}", key_str, inner_formatted)
}
DataValue::Array(arr) => {
let arr_formatted = format_array(arr, version.clone());
format!("{}=[{}]", key_str, arr_formatted)
}
DataValue::Bool(b) => format!("{}={}", key_str, b),
DataValue::BoolTrue => format!("{}=true", key_str),
DataValue::BoolFalse => format!("{}=false", key_str),
DataValue::SignedNumber(num) => format!("{}={}", key_str, num),
_ => "".to_string(),
}
})
.collect();
parts.join(", ")
}
fn format_array(arr: Vec<DataValue>, version: Version) -> String {
let parts: Vec<String> = arr
.into_iter()
.map(|value| match value {
DataValue::Str(s) => format!("\"{}\"", s),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
format!("{{ {} }}", inner_formatted)
}
DataValue::Array(inner_arr) => {
let formatted = format_array(inner_arr, version.clone());
format!("[{}]", formatted)
}
DataValue::Bool(b) => b.to_string(),
DataValue::BoolTrue => "true".to_string(),
DataValue::BoolFalse => "false".to_string(),
DataValue::SignedNumber(num) => num.to_string(),
_ => String::new(),
})
.collect();
parts.join(", ")
}
#[macro_export]
macro_rules! log_cv {
($kind:expr, $cv:expr) => {
$crate::log_cv_internal("", &$cv, Some($kind))
};
($cv:expr) => {
$crate::log_cv_internal("", &$cv, None)
};
}
#[macro_export]
macro_rules! log_cv_in {
($kind:expr, $cv:expr) => {
$crate::log_cv_internal("> ", &$cv, Some($kind))
};
($cv:expr) => {
$crate::log_cv_internal("> ", &$cv, None)
};
}
#[macro_export]
macro_rules! log_cv_out {
($kind:expr, $cv:expr) => {
$crate::log_cv_internal("< ", &$cv, Some($kind))
};
($cv:expr) => {
$crate::log_cv_internal("< ", &$cv, None)
};
}