use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LogLevel {
Info,
Warning,
Error,
}
#[derive(Clone)]
pub struct LogEntry {
pub message: String,
pub level: LogLevel,
pub timestamp: String,
pub file: &'static str,
pub line: u32,
}
const MAX_LOG_ENTRIES: usize = 2048;
static MIN_LOG_LEVEL: Mutex<LogLevel> = Mutex::new(LogLevel::Info);
static GLOBAL_LOGS: Mutex<Vec<LogEntry>> = Mutex::new(Vec::new());
static LOG_VERSION: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn get_log_version() -> usize {
LOG_VERSION.load(std::sync::atomic::Ordering::Relaxed)
}
fn lock_logs() -> std::sync::MutexGuard<'static, Vec<LogEntry>> {
match GLOBAL_LOGS.lock() {
Ok(guard) => guard,
Err(poisoned) => {
eprintln!("[Logger] Mutex poisoned — veri kurtarılıyor");
poisoned.into_inner()
}
}
}
fn lock_min_level() -> std::sync::MutexGuard<'static, LogLevel> {
match MIN_LOG_LEVEL.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
#[doc(hidden)]
pub fn log_message(level: LogLevel, msg: String, file: &'static str, line: u32) {
let min_level = *lock_min_level();
if (level as u8) < (min_level as u8) {
return;
}
let mut logs = lock_logs();
if logs.len() >= MAX_LOG_ENTRIES {
logs.remove(0);
}
#[cfg(target_arch = "wasm32")]
let timestamp = {
let now = web_time::SystemTime::now();
let duration = now.duration_since(web_time::SystemTime::UNIX_EPOCH).unwrap_or_default();
let secs = duration.as_secs();
let mins = (secs / 60) % 60;
let hours = (secs / 3600) % 24;
let secs_of_min = secs % 60;
format!("{:02}:{:02}:{:02}", hours, mins, secs_of_min)
};
#[cfg(not(target_arch = "wasm32"))]
let timestamp = chrono::Local::now().format("%H:%M:%S").to_string();
logs.push(LogEntry {
message: msg.clone(),
level,
timestamp: timestamp.clone(),
file,
line,
});
LOG_VERSION.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
match level {
LogLevel::Info => println!("[{}] [INFO] {}:{} — {}", timestamp, file, line, msg),
LogLevel::Warning => eprintln!("[{}] [WARN] {}:{} — {}", timestamp, file, line, msg),
LogLevel::Error => eprintln!("[{}] [ERROR] {}:{} — {}", timestamp, file, line, msg),
}
}
pub fn get_logs<F, R>(f: F) -> R
where
F: FnOnce(&[LogEntry]) -> R,
{
let logs = lock_logs();
f(&logs)
}
pub fn clear_logs() {
lock_logs().clear();
LOG_VERSION.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
pub fn drain_logs() -> Vec<LogEntry> {
let drained = lock_logs().drain(..).collect();
LOG_VERSION.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
drained
}
pub fn log_count() -> usize {
lock_logs().len()
}
pub fn set_min_log_level(level: LogLevel) {
*lock_min_level() = level;
}
#[macro_export]
macro_rules! gizmo_log {
(Info, $($arg:tt)*) => {
$crate::logger::log_message(
$crate::logger::LogLevel::Info,
format!($($arg)*),
file!(), line!()
)
};
(Warning, $($arg:tt)*) => {
$crate::logger::log_message(
$crate::logger::LogLevel::Warning,
format!($($arg)*),
file!(), line!()
)
};
(Error, $($arg:tt)*) => {
$crate::logger::log_message(
$crate::logger::LogLevel::Error,
format!($($arg)*),
file!(), line!()
)
};
}
use tracing_subscriber::Layer;
use tracing::Subscriber;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
pub struct GizmoTracingLayer;
impl<S> Layer<S> for GizmoTracingLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let meta = event.metadata();
let level = match *meta.level() {
tracing::Level::ERROR => LogLevel::Error,
tracing::Level::WARN => LogLevel::Warning,
_ => LogLevel::Info, };
struct EventVisitor(String);
impl tracing::field::Visit for EventVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0 = format!("{:?}", value);
} else {
self.0.push_str(&format!(" {}={:?}", field.name(), value));
}
}
}
let mut visitor = EventVisitor(String::new());
event.record(&mut visitor);
let mut msg = visitor.0;
if msg.starts_with('"') && msg.ends_with('"') {
msg = msg[1..msg.len()-1].to_string();
}
log_message(level, msg, meta.file().unwrap_or("unknown"), meta.line().unwrap_or(0));
}
}
pub fn init_tracing() {
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,wgpu=warn,naga=warn,gizmo_core=debug"));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.without_time();
let subscriber = tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.with(GizmoTracingLayer);
let _ = tracing::subscriber::set_global_default(subscriber);
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Mutex, MutexGuard};
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn setup() -> MutexGuard<'static, ()> {
let guard = TEST_LOCK.lock().expect("logger test lock poisoned");
clear_logs();
set_min_log_level(LogLevel::Info);
guard
}
#[test]
fn test_log_and_read() {
let _guard = setup();
log_message(LogLevel::Info, "test mesajı".into(), "test.rs", 1);
get_logs(|logs| {
assert_eq!(logs.len(), 1);
assert_eq!(logs[0].message, "test mesajı");
assert_eq!(logs[0].level, LogLevel::Info);
assert_eq!(logs[0].file, "test.rs");
assert_eq!(logs[0].line, 1);
});
}
#[test]
fn test_drain_clears() {
let _guard = setup();
log_message(LogLevel::Warning, "w1".into(), "test.rs", 10);
log_message(LogLevel::Error, "e1".into(), "test.rs", 20);
let drained = drain_logs();
assert_eq!(drained.len(), 2);
assert_eq!(log_count(), 0);
}
#[test]
fn test_clear_logs() {
let _guard = setup();
log_message(LogLevel::Info, "clear me".into(), "test.rs", 1);
assert_eq!(log_count(), 1);
clear_logs();
assert_eq!(log_count(), 0);
}
#[test]
fn test_ring_buffer_capacity() {
let _guard = setup();
for i in 0..MAX_LOG_ENTRIES + 500 {
log_message(
LogLevel::Info,
format!("cap_test_{}", i),
"test.rs",
i as u32,
);
}
let count = log_count();
assert!(
count <= MAX_LOG_ENTRIES,
"ring buffer kapasitesi aşıldı: {} > {}",
count,
MAX_LOG_ENTRIES
);
}
#[test]
fn test_min_level_filter() {
let _guard = setup();
set_min_log_level(LogLevel::Warning);
log_message(LogLevel::Info, "filtered".into(), "test.rs", 1);
assert_eq!(log_count(), 0, "Info filtrelenmeli");
log_message(LogLevel::Warning, "kept".into(), "test.rs", 2);
assert_eq!(log_count(), 1, "Warning geçmeli");
log_message(LogLevel::Error, "also kept".into(), "test.rs", 3);
assert_eq!(log_count(), 2, "Error geçmeli");
}
#[test]
fn test_log_level_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(LogLevel::Info);
set.insert(LogLevel::Warning);
set.insert(LogLevel::Error);
assert_eq!(set.len(), 3);
}
}