Skip to main content

lavende_core/
common.rs

1pub mod errors {
2    use crate::common::utils::now_ms;
3    use serde::{Deserialize, Serialize};
4    #[derive(Debug, Clone, Serialize, Deserialize)]
5    #[serde(rename_all = "camelCase")]
6    pub enum Severity {
7        Common,
8        Suspicious,
9        Fault,
10    }
11    #[derive(Debug, Serialize)]
12    #[serde(rename_all = "camelCase")]
13    pub struct LavendeError {
14        pub timestamp: u64,
15        pub status: u16,
16        pub error: String,
17        pub message: String,
18        pub path: String,
19        #[serde(skip_serializing_if = "Option::is_none")]
20        pub trace: Option<String>,
21    }
22    impl LavendeError {
23        pub fn bad_request(message: impl Into<String>, path: impl Into<String>) -> Self {
24            Self::new(400, "Bad Request", message, path)
25        }
26        pub fn not_found(message: impl Into<String>, path: impl Into<String>) -> Self {
27            Self::new(404, "Not Found", message, path)
28        }
29        pub fn new(
30            status: u16,
31            error: impl Into<String>,
32            message: impl Into<String>,
33            path: impl Into<String>,
34        ) -> Self {
35            Self {
36                timestamp: now_ms(),
37                status,
38                error: error.into(),
39                message: message.into(),
40                path: path.into(),
41                trace: None,
42            }
43        }
44    }
45}
46pub mod types {
47    use rand::{Rng, distributions::Alphanumeric};
48    use std::{ops::Deref, sync::Arc};
49    use tokio::sync::{Mutex, RwLock};
50    pub type Shared<T> = Arc<Mutex<T>>;
51    pub type SharedRw<T> = Arc<RwLock<T>>;
52    pub type AnyError = Box<dyn std::error::Error + Send + Sync>;
53    pub type AnyResult<T> = std::result::Result<T, AnyError>;
54    macro_rules! define_id {
55        ($name:ident, $type:ty) => {
56            #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
57            #[serde(transparent)]
58            pub struct $name(pub $type);
59            impl From<$type> for $name {
60                fn from(val: $type) -> Self {
61                    Self(val)
62                }
63            }
64            impl std::fmt::Display for $name {
65                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66                    write!(f, "{}", self.0)
67                }
68            }
69        };
70        ($name:ident, $type:ty, copy) => {
71            #[derive(
72                Debug,
73                Clone,
74                Copy,
75                PartialEq,
76                Eq,
77                PartialOrd,
78                Ord,
79                Hash,
80                serde::Serialize,
81                serde::Deserialize,
82            )]
83            #[serde(transparent)]
84            pub struct $name(pub $type);
85            impl From<$type> for $name {
86                fn from(val: $type) -> Self {
87                    Self(val)
88                }
89            }
90            impl std::fmt::Display for $name {
91                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92                    write!(f, "{}", self.0)
93                }
94            }
95        };
96    }
97    define_id!(GuildId, String);
98    define_id!(SessionId, String);
99    define_id!(UserId, u64, copy);
100    define_id!(ChannelId, u64, copy);
101    impl Deref for GuildId {
102        type Target = str;
103        fn deref(&self) -> &Self::Target {
104            &self.0
105        }
106    }
107    impl Deref for SessionId {
108        type Target = str;
109        fn deref(&self) -> &Self::Target {
110            &self.0
111        }
112    }
113    impl SessionId {
114        pub fn generate() -> Self {
115            let rng = rand::thread_rng();
116            let s: String = rng
117                .sample_iter(&Alphanumeric)
118                .filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
119                .take(16)
120                .map(char::from)
121                .collect();
122            Self(s)
123        }
124    }
125    #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
126    pub enum AudioFormat {
127        Aac,
128        Opus,
129        Webm,
130        Mp4,
131        Mp3,
132        Ogg,
133        Flac,
134        Wav,
135        Unknown,
136    }
137    impl AudioFormat {
138        pub fn as_ext(&self) -> &'static str {
139            match self {
140                Self::Aac => "aac",
141                Self::Opus => "opus",
142                Self::Webm => "webm",
143                Self::Mp4 => "mp4",
144                Self::Mp3 => "mp3",
145                Self::Ogg => "ogg",
146                Self::Flac => "flac",
147                Self::Wav => "wav",
148                Self::Unknown => "",
149            }
150        }
151        pub fn from_ext(ext: &str) -> Self {
152            match ext.to_lowercase().as_str() {
153                "aac" => Self::Aac,
154                "opus" => Self::Opus,
155                "webm" => Self::Webm,
156                "mp4" | "m4a" => Self::Mp4,
157                "mp3" => Self::Mp3,
158                "ogg" => Self::Ogg,
159                "flac" => Self::Flac,
160                "wav" => Self::Wav,
161                _ => Self::Unknown,
162            }
163        }
164        pub fn from_url(url: &str) -> Self {
165            if url.contains(".m3u8") || url.contains("/playlist") {
166                return Self::Aac;
167            }
168            if let Some(itag) = extract_youtube_itag(url) {
169                match itag {
170                    249..=251 => return Self::Webm,
171                    139..=141 => return Self::Mp4,
172                    _ => {}
173                }
174            }
175            if url.contains("mime=audio%2Fwebm") || url.contains("mime=audio/webm") {
176                return Self::Webm;
177            }
178            if url.contains("mime=audio%2Fmp4") || url.contains("mime=audio/mp4") {
179                return Self::Mp4;
180            }
181            let from_path = url
182                .split('?')
183                .next()
184                .and_then(|path| std::path::Path::new(path).extension())
185                .and_then(|ext| ext.to_str())
186                .map(Self::from_ext)
187                .unwrap_or(Self::Unknown);
188            if from_path != Self::Unknown {
189                return from_path;
190            }
191            if url.contains(".mp4") || url.contains(".m4a") {
192                return Self::Mp4;
193            }
194            if url.contains(".flac") {
195                return Self::Flac;
196            }
197            if url.contains(".mp3") {
198                return Self::Mp3;
199            }
200            if url.contains(".ogg") {
201                return Self::Ogg;
202            }
203            if url.contains(".webm") {
204                return Self::Webm;
205            }
206            Self::Unknown
207        }
208        pub fn is_opus_passthrough(&self) -> bool {
209            matches!(self, Self::Webm | Self::Ogg | Self::Opus)
210        }
211    }
212    fn extract_youtube_itag(url: &str) -> Option<u32> {
213        url.split('?').nth(1)?.split('&').find_map(|pair| {
214            let (k, v) = pair.split_once('=')?;
215            if k == "itag" { v.parse().ok() } else { None }
216        })
217    }
218}
219
220pub mod http {
221    use crate::{common::utils::default_user_agent, config::HttpProxyConfig};
222    use dashmap::DashMap;
223    use reqwest::{Client, Proxy};
224    use std::{sync::Arc, time::Duration};
225    use tracing::warn;
226    pub struct HttpClientPool {
227        clients: DashMap<Option<HttpProxyConfig>, Arc<Client>>,
228    }
229    impl HttpClientPool {
230        pub fn new() -> Self {
231            Self {
232                clients: DashMap::new(),
233            }
234        }
235        pub fn get(&self, proxy: Option<HttpProxyConfig>) -> Arc<Client> {
236            self.clients
237                .entry(proxy.clone())
238                .or_insert_with(|| Arc::new(self.create_client(proxy)))
239                .clone()
240        }
241        fn create_client(&self, proxy: Option<HttpProxyConfig>) -> Client {
242            let mut builder = Client::builder()
243                .user_agent(default_user_agent())
244                .gzip(true)
245                .deflate(true)
246                .timeout(Duration::from_secs(15))
247                .connect_timeout(Duration::from_secs(5))
248                .tcp_nodelay(true)
249                .pool_max_idle_per_host(10)
250                .pool_idle_timeout(Duration::from_secs(70));
251            if let Some(url) = proxy.as_ref().and_then(|config| config.url.as_ref()) {
252                match Proxy::all(url) {
253                    Ok(mut proxy_obj) => {
254                        if let Some((u, p)) = proxy
255                            .as_ref()
256                            .and_then(|c| c.username.as_ref().zip(c.password.as_ref()))
257                        {
258                            proxy_obj = proxy_obj.basic_auth(u, p);
259                        }
260                        builder = builder.proxy(proxy_obj);
261                    }
262                    Err(e) => {
263                        warn!(
264                            "HttpClientPool: failed to parse proxy URL '{}': {} — proxy will be ignored",
265                            url, e
266                        );
267                    }
268                }
269            }
270            match builder.build() {
271                Ok(client) => client,
272                Err(e) => {
273                    warn!(
274                        "HttpClientPool: failed to build client ({}), falling back to default",
275                        e
276                    );
277                    Client::new()
278                }
279            }
280        }
281    }
282    impl Default for HttpClientPool {
283        fn default() -> Self {
284            Self::new()
285        }
286    }
287}
288pub mod utils {
289    use std::time::{SystemTime, UNIX_EPOCH};
290    pub const RESET: &str = "\x1b[0m";
291    pub const BOLD: &str = "\x1b[1m";
292    pub const DIM: &str = "\x1b[2m";
293    pub const ORANGE: &str = "\x1b[38;5;208m";
294    pub const GREEN: &str = "\x1b[32m";
295    pub const CYAN: &str = "\x1b[36m";
296    pub const YELLOW: &str = "\x1b[33m";
297    pub const BLUE: &str = "\x1b[34m";
298    pub const MAGENTA: &str = "\x1b[35m";
299    pub const RED: &str = "\x1b[31m";
300    pub const COLOR_ERROR: &str = RED;
301    pub const COLOR_WARN: &str = YELLOW;
302    pub const COLOR_INFO: &str = GREEN;
303    pub const COLOR_DEBUG: &str = BLUE;
304    pub const COLOR_TRACE: &str = MAGENTA;
305    pub fn now_ms() -> u64 {
306        SystemTime::now()
307            .duration_since(UNIX_EPOCH)
308            .unwrap_or_default()
309            .as_millis() as u64
310    }
311    pub fn now_nanos() -> u64 {
312        SystemTime::now()
313            .duration_since(UNIX_EPOCH)
314            .unwrap_or_default()
315            .as_nanos() as u64
316    }
317    pub fn strip_ansi_escapes(s: &str) -> String {
318        let mut result = String::with_capacity(s.len());
319        let mut chars = s.chars().peekable();
320        while let Some(c) = chars.next() {
321            if c == '\x1b' {
322                if let Some('[') = chars.peek() {
323                    chars.next();
324                    while let Some(&nc) = chars.peek() {
325                        chars.next();
326                        if nc.is_ascii_alphabetic() {
327                            break;
328                        }
329                    }
330                }
331            } else {
332                result.push(c);
333            }
334        }
335        result
336    }
337    pub fn memory_usage_report() -> String {
338        "0 B".to_string()
339    }
340    pub const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
341    pub fn default_user_agent() -> String {
342        DEFAULT_USER_AGENT.to_owned()
343    }
344    pub fn shorten_error_cause(err: &str) -> String {
345        let mut scrubbed = err;
346        if let Some(idx) = scrubbed.find(" for https://") {
347            scrubbed = &scrubbed[..idx];
348        } else if let Some(idx) = scrubbed.find(" for http://") {
349            scrubbed = &scrubbed[..idx];
350        } else if let Some(idx) = scrubbed.find(" (https://") {
351            scrubbed = &scrubbed[..idx];
352        } else if let Some(idx) = scrubbed.find(" (http://") {
353            scrubbed = &scrubbed[..idx];
354        } else if scrubbed.contains("error sending request for url") {
355            return "error sending request for url".to_string();
356        }
357        if let Some(line) = scrubbed.lines().next() {
358            if line.len() > 100 {
359                return format!("{}...", &line[..97]);
360            }
361            return line.to_string();
362        }
363        scrubbed.to_string()
364    }
365}
366pub mod logger {
367    pub mod formatter {
368        use crate::common::utils::{
369            BOLD, COLOR_DEBUG, COLOR_ERROR, COLOR_INFO, COLOR_TRACE, COLOR_WARN, DIM, RESET,
370            memory_usage_report,
371        };
372        use core::fmt::{self as core_fmt};
373        use tracing::{Event, Level, Subscriber};
374        use tracing_subscriber::{
375            fmt::{
376                self, FmtContext,
377                format::{FormatEvent, FormatFields},
378            },
379            registry::LookupSpan,
380        };
381        pub struct CustomFormatter {
382            use_ansi: bool,
383        }
384        impl CustomFormatter {
385            pub fn new(use_ansi: bool) -> Self {
386                Self { use_ansi }
387            }
388            fn write_timestamp(&self, writer: &mut fmt::format::Writer<'_>) -> core_fmt::Result {
389                let format = time::macros::format_description!(
390                    "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"
391                );
392                let now = time::OffsetDateTime::now_local()
393                    .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
394                let timestamp = now
395                    .format(&format)
396                    .unwrap_or_else(|_| "Unknown Time".to_string());
397                if self.use_ansi {
398                    write!(writer, "{DIM}[{timestamp}]{RESET} ")
399                } else {
400                    write!(writer, "[{timestamp}] ")
401                }
402            }
403            fn write_level(
404                &self,
405                writer: &mut fmt::format::Writer<'_>,
406                level: &Level,
407            ) -> core_fmt::Result {
408                let level_str = format!("{: <5}", level);
409                if self.use_ansi {
410                    let color = match *level {
411                        Level::ERROR => COLOR_ERROR,
412                        Level::WARN => COLOR_WARN,
413                        Level::INFO => COLOR_INFO,
414                        Level::DEBUG => COLOR_DEBUG,
415                        Level::TRACE => COLOR_TRACE,
416                    };
417                    write!(writer, "{color}{BOLD}{level_str}{RESET} ")
418                } else {
419                    write!(writer, "{level_str} ")
420                }
421            }
422        }
423        impl<S, N> FormatEvent<S, N> for CustomFormatter
424        where
425            S: Subscriber + for<'a> LookupSpan<'a>,
426            N: for<'a> FormatFields<'a> + 'static,
427        {
428            fn format_event(
429                &self,
430                ctx: &FmtContext<'_, S, N>,
431                mut writer: fmt::format::Writer<'_>,
432                event: &Event<'_>,
433            ) -> core_fmt::Result {
434                let (reset, dim) = if self.use_ansi {
435                    (RESET, DIM)
436                } else {
437                    ("", "")
438                };
439                write!(writer, "{dim}[{}]{reset} ", memory_usage_report())?;
440                self.write_timestamp(&mut writer)?;
441                let metadata = event.metadata();
442                self.write_level(&mut writer, metadata.level())?;
443                let target = metadata.target();
444                let line = metadata
445                    .line()
446                    .map(|l| l.to_string())
447                    .unwrap_or_else(|| "??".to_string());
448                write!(writer, "{dim}{target}: {line}{reset} > ")?;
449                ctx.format_fields(writer.by_ref(), event)?;
450                write!(writer, "{reset}")?;
451                writeln!(writer)
452            }
453        }
454    }
455    pub mod writer {
456        use parking_lot::Mutex;
457        use std::{
458            fs::{File, OpenOptions},
459            io::{self, BufRead, BufReader, Write},
460            path::{Path, PathBuf},
461            sync::Arc,
462        };
463        fn today_date() -> String {
464            let secs = std::time::SystemTime::now()
465                .duration_since(std::time::UNIX_EPOCH)
466                .unwrap_or_default()
467                .as_secs();
468            let days = (secs / 86400) as u32;
469            let (y, m, d) = days_to_ymd(days);
470            format!("{y:04}-{m:02}-{d:02}")
471        }
472        fn days_to_ymd(mut days: u32) -> (u32, u32, u32) {
473            days += 719468;
474            let era = days / 146097;
475            let doe = days - era * 146097;
476            let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
477            let y = yoe + era * 400;
478            let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
479            let mp = (5 * doy + 2) / 153;
480            let d = doy - (153 * mp + 2) / 5 + 1;
481            let m = if mp < 10 { mp + 3 } else { mp - 9 };
482            let y = if m <= 2 { y + 1 } else { y };
483            (y, m, d)
484        }
485        fn resolve_path(base_path: &str, rotate_daily: bool, date: &str) -> String {
486            if !rotate_daily {
487                return base_path.to_string();
488            }
489            let base = Path::new(base_path);
490            let stem = base
491                .file_stem()
492                .and_then(|s| s.to_str())
493                .unwrap_or("lavende");
494            let dir: PathBuf = base.parent().unwrap_or(Path::new(".")).into();
495            dir.join(format!("{stem}-{date}.log"))
496                .to_string_lossy()
497                .into_owned()
498        }
499        #[derive(Clone)]
500        pub struct CircularFileWriter {
501            base_path: String,
502            max_lines: u32,
503            max_files: u32,
504            rotate_daily: bool,
505            state: Arc<Mutex<WriterState>>,
506        }
507        struct WriterState {
508            file: Option<File>,
509            current_date: Option<String>,
510            lines_since_prune: u32,
511            is_pruning: bool,
512        }
513        impl CircularFileWriter {
514            pub fn new(path: String, max_lines: u32, max_files: u32, rotate_daily: bool) -> Self {
515                Self {
516                    base_path: path,
517                    max_lines,
518                    max_files,
519                    rotate_daily,
520                    state: Arc::new(Mutex::new(WriterState {
521                        file: None,
522                        current_date: None,
523                        lines_since_prune: 0,
524                        is_pruning: false,
525                    })),
526                }
527            }
528            fn current_path(&self) -> String {
529                if self.rotate_daily {
530                    resolve_path(&self.base_path, true, &today_date())
531                } else {
532                    self.base_path.clone()
533                }
534            }
535            fn ensure_file_open<'a>(&self, state: &'a mut WriterState) -> io::Result<&'a mut File> {
536                let today = if self.rotate_daily {
537                    Some(today_date())
538                } else {
539                    None
540                };
541                let need_rotate = state.file.is_none()
542                    || match (&state.current_date, &today) {
543                        (Some(curr), Some(new)) => curr != new,
544                        _ => false,
545                    };
546                if need_rotate {
547                    state.file = None;
548                    let path = if self.rotate_daily {
549                        let d = today.as_deref().unwrap_or("");
550                        resolve_path(&self.base_path, true, d)
551                    } else {
552                        self.base_path.clone()
553                    };
554                    if let Some(parent) = Path::new(&path).parent() {
555                        let _ = std::fs::create_dir_all(parent);
556                    }
557                    state.file = Some(OpenOptions::new().create(true).append(true).open(&path)?);
558                    state.current_date = today;
559                    if self.rotate_daily && self.max_files > 0 {
560                        self.cleanup_old_files();
561                    }
562                }
563                Ok(state.file.as_mut().expect("file was just opened"))
564            }
565            fn spawn_prune(&self) {
566                let path = self.current_path();
567                let max_lines = self.max_lines;
568                let state_arc = self.state.clone();
569                std::thread::spawn(move || {
570                    if let Err(e) = Self::do_prune(&path, max_lines) {
571                        eprintln!("Failed to prune log file '{}': {}", path, e);
572                    }
573                    let mut state = state_arc.lock();
574                    state.is_pruning = false;
575                });
576            }
577            fn cleanup_old_files(&self) {
578                let base = Path::new(&self.base_path);
579                let dir = base.parent().unwrap_or(Path::new("."));
580                let stem = base
581                    .file_stem()
582                    .and_then(|s| s.to_str())
583                    .unwrap_or("lavende");
584                let max_files = self.max_files as usize;
585                let mut log_files: Vec<std::path::PathBuf> = match std::fs::read_dir(dir) {
586                    Ok(entries) => entries
587                        .filter_map(|e| e.ok())
588                        .map(|e| e.path())
589                        .filter(|p| {
590                            p.extension().and_then(|e| e.to_str()) == Some("log")
591                                && p.file_stem()
592                                    .and_then(|s| s.to_str())
593                                    .map(|s| s.starts_with(stem) && s != stem)
594                                    .unwrap_or(false)
595                        })
596                        .collect(),
597                    Err(_) => return,
598                };
599                if log_files.len() <= max_files {
600                    return;
601                }
602                log_files.sort();
603                let to_delete = log_files.len() - max_files;
604                for path in log_files.iter().take(to_delete) {
605                    if let Err(e) = std::fs::remove_file(path) {
606                        eprintln!("Failed to delete old log file '{}': {}", path.display(), e);
607                    }
608                }
609            }
610            fn do_prune(path: &str, max_lines: u32) -> io::Result<()> {
611                if !Path::new(path).exists() {
612                    return Ok(());
613                }
614                let lines: Vec<String> = {
615                    let file = File::open(path)?;
616                    let reader = BufReader::new(file);
617                    reader.lines().collect::<Result<_, _>>()?
618                };
619                if lines.len() > max_lines as usize {
620                    let start = lines.len() - max_lines as usize;
621                    let tmp_path = format!("{}.tmp", path);
622                    {
623                        let mut file = File::create(&tmp_path)?;
624                        for line in &lines[start..] {
625                            writeln!(file, "{}", line)?;
626                        }
627                    }
628                    std::fs::rename(tmp_path, path)?;
629                }
630                Ok(())
631            }
632        }
633        impl io::Write for CircularFileWriter {
634            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
635                let mut state = self.state.lock();
636                let file = self.ensure_file_open(&mut state)?;
637                file.write_all(buf)?;
638                let new_lines = buf.iter().filter(|&&b| b == b'\n').count() as u32;
639                state.lines_since_prune += new_lines;
640                let prune_threshold = (self.max_lines / 10).max(50);
641                if state.lines_since_prune >= prune_threshold && !state.is_pruning {
642                    state.is_pruning = true;
643                    state.lines_since_prune = 0;
644                    state.file = None;
645                    self.spawn_prune();
646                }
647                Ok(buf.len())
648            }
649            fn flush(&mut self) -> io::Result<()> {
650                let mut state = self.state.lock();
651                if let Some(file) = &mut state.file {
652                    file.flush()?;
653                }
654                Ok(())
655            }
656        }
657        impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CircularFileWriter {
658            type Writer = Self;
659            fn make_writer(&'a self) -> Self::Writer {
660                self.clone()
661            }
662        }
663        #[cfg(test)]
664        mod tests {
665            use super::*;
666            use std::fs;
667            use tracing_subscriber::fmt::MakeWriter;
668            fn cleanup_test_file(path: &str) {
669                let _ = fs::remove_file(path);
670                let _ = fs::remove_file(format!("{}.tmp", path));
671            }
672            #[test]
673            fn test_circular_file_writer_new() {
674                let writer = CircularFileWriter::new("test_new.log".to_string(), 100, 0, false);
675                let state = writer.state.lock();
676                assert_eq!(state.lines_since_prune, 0);
677                assert!(!state.is_pruning);
678                assert!(state.file.is_none());
679                cleanup_test_file("test_new.log");
680            }
681            #[test]
682            fn test_write_creates_file() {
683                let path = "test_create.log";
684                cleanup_test_file(path);
685                let mut writer = CircularFileWriter::new(path.to_string(), 100, 0, false);
686                let data = b"test line\n";
687                let result = writer.write(data);
688                assert!(result.is_ok());
689                assert_eq!(result.unwrap(), data.len());
690                assert!(Path::new(path).exists());
691                cleanup_test_file(path);
692            }
693            #[test]
694            fn test_write_counts_newlines() {
695                let path = "test_newlines.log";
696                cleanup_test_file(path);
697                let mut writer = CircularFileWriter::new(path.to_string(), 1000, 0, false);
698                writer.write(b"line1\nline2\nline3\n").unwrap();
699                let state = writer.state.lock();
700                assert_eq!(state.lines_since_prune, 3);
701                cleanup_test_file(path);
702            }
703            #[test]
704            fn test_write_no_newlines() {
705                let path = "test_no_newlines.log";
706                cleanup_test_file(path);
707                let mut writer = CircularFileWriter::new(path.to_string(), 1000, 0, false);
708                writer.write(b"no newline here").unwrap();
709                let state = writer.state.lock();
710                assert_eq!(state.lines_since_prune, 0);
711                cleanup_test_file(path);
712            }
713            #[test]
714            fn test_flush() {
715                let path = "test_flush.log";
716                cleanup_test_file(path);
717                let mut writer = CircularFileWriter::new(path.to_string(), 100, 0, false);
718                writer.write(b"test\n").unwrap();
719                let result = writer.flush();
720                assert!(result.is_ok());
721                cleanup_test_file(path);
722            }
723            #[test]
724            fn test_flush_without_file() {
725                let mut writer =
726                    CircularFileWriter::new("test_flush_no_file.log".to_string(), 100, 0, false);
727                let result = writer.flush();
728                assert!(result.is_ok());
729                cleanup_test_file("test_flush_no_file.log");
730            }
731            #[test]
732            fn test_clone() {
733                let writer = CircularFileWriter::new("test_clone.log".to_string(), 100, 0, false);
734                let cloned = writer.clone();
735                assert!(Arc::ptr_eq(&writer.state, &cloned.state));
736                cleanup_test_file("test_clone.log");
737            }
738            #[test]
739            fn test_make_writer() {
740                let writer =
741                    CircularFileWriter::new("test_make_writer.log".to_string(), 100, 0, false);
742                let made = writer.make_writer();
743                assert!(Arc::ptr_eq(&writer.state, &made.state));
744                cleanup_test_file("test_make_writer.log");
745            }
746            #[test]
747            fn test_do_prune_nonexistent_file() {
748                let result = CircularFileWriter::do_prune("nonexistent_prune.log", 10);
749                assert!(result.is_ok());
750            }
751            #[test]
752            fn test_do_prune_small_file() {
753                let path = "test_prune_small.log";
754                cleanup_test_file(path);
755                fs::write(path, "line1\nline2\nline3\n").unwrap();
756                let result = CircularFileWriter::do_prune(path, 10);
757                assert!(result.is_ok());
758                let content = fs::read_to_string(path).unwrap();
759                assert_eq!(content.lines().count(), 3);
760                cleanup_test_file(path);
761            }
762            #[test]
763            fn test_do_prune_large_file() {
764                let path = "test_prune_large.log";
765                cleanup_test_file(path);
766                let mut content = String::new();
767                for i in 1..=20 {
768                    content.push_str(&format!("line{}\n", i));
769                }
770                fs::write(path, content).unwrap();
771                let result = CircularFileWriter::do_prune(path, 10);
772                assert!(result.is_ok());
773                let pruned = fs::read_to_string(path).unwrap();
774                let lines: Vec<&str> = pruned.lines().collect();
775                assert_eq!(lines.len(), 10);
776                assert_eq!(lines[0], "line11");
777                assert_eq!(lines[9], "line20");
778                cleanup_test_file(path);
779            }
780            #[test]
781            fn test_resolve_path_no_rotate() {
782                let p = resolve_path("./logs/lavende.log", false, "2026-03-13");
783                assert_eq!(p, "./logs/lavende.log");
784            }
785            #[test]
786            fn test_resolve_path_rotate() {
787                let p = resolve_path("./logs/lavende.log", true, "2026-03-13");
788                assert!(p.contains("2026-03-13"));
789                assert!(p.ends_with(".log"));
790            }
791            #[test]
792            fn test_today_date_format() {
793                let d = today_date();
794                let parts: Vec<&str> = d.split('-').collect();
795                assert_eq!(parts.len(), 3);
796                assert_eq!(parts[0].len(), 4);
797                assert_eq!(parts[1].len(), 2);
798                assert_eq!(parts[2].len(), 2);
799            }
800            #[test]
801            fn test_prune_threshold_calculation() {
802                let _writer = CircularFileWriter::new("test.log".to_string(), 1000, 0, false);
803                let threshold = (1000 / 10).max(50);
804                assert_eq!(threshold, 100);
805                let _writer = CircularFileWriter::new("test.log".to_string(), 100, 0, false);
806                let threshold = (100 / 10).max(50);
807                assert_eq!(threshold, 50);
808                let _writer = CircularFileWriter::new("test.log".to_string(), 10, 0, false);
809                let threshold = (10 / 10).max(50);
810                assert_eq!(threshold, 50);
811                cleanup_test_file("test.log");
812            }
813        }
814    }
815    use crate::{common::utils::strip_ansi_escapes, config::LoggingConfig};
816    pub use formatter::CustomFormatter;
817    use std::{fs, path::Path, sync::OnceLock};
818    use tracing_subscriber::{EnvFilter, fmt, prelude::*};
819    pub use writer::CircularFileWriter;
820    pub(crate) static GLOBAL_FILE_WRITER: OnceLock<CircularFileWriter> = OnceLock::new();
821    #[macro_export]
822    macro_rules! log_print {
823    ($($arg:tt)*) => {{
824        let msg = format!($($arg)*);
825        std::print!("{}", msg);
826        $crate::common::logger::append_to_file_raw(&msg);
827    }};
828}
829    #[macro_export]
830    macro_rules! log_println {
831    () => {{
832        std::println!();
833        $crate::common::logger::append_to_file_raw("\n");
834    }};
835    ($($arg:tt)*) => {{
836        let msg = format!($($arg)*);
837        std::println!("{}", msg);
838        $crate::common::logger::append_to_file_raw(&format!("{}\n", msg));
839    }};
840}
841    pub fn append_to_file_raw(msg: &str) {
842        if let Some(mut writer) = GLOBAL_FILE_WRITER.get().cloned() {
843            use std::io::Write;
844            let clean_msg = strip_ansi_escapes(msg);
845            let _ = writer.write_all(clean_msg.as_bytes());
846        }
847    }
848    pub fn init(config: &LoggingConfig) {
849        let _ = tracing_log::LogTracer::init();
850        let log_level = config.level.as_deref().unwrap_or("info");
851        let filter_str = match config.filters.as_deref() {
852            Some(f) if !f.is_empty() => format!("{log_level},{f}"),
853            _ => log_level.to_string(),
854        };
855        let env_filter =
856            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter_str));
857        let stdout_layer = fmt::layer()
858            .event_format(CustomFormatter::new(true))
859            .with_ansi(true);
860        let file_layer = config.file.as_ref().map(|file_config| {
861            if let Some(parent) = Path::new(&file_config.path).parent() {
862                let _ = fs::create_dir_all(parent);
863            }
864            let writer = CircularFileWriter::new(
865                file_config.path.clone(),
866                file_config.max_lines,
867                file_config.max_files,
868                file_config.rotate_daily,
869            );
870            let _ = GLOBAL_FILE_WRITER.set(writer.clone());
871            fmt::layer()
872                .with_writer(writer)
873                .event_format(CustomFormatter::new(false))
874                .with_ansi(false)
875        });
876        let _ = tracing_subscriber::registry()
877            .with(env_filter)
878            .with(stdout_layer)
879            .with(file_layer)
880            .try_init();
881    }
882}
883pub use errors::*;
884pub use http::*;
885pub use logger::*;
886pub use types::*;
887pub use utils::*;