Skip to main content

lavende_core/
common.rs

1pub mod errors {
2use serde::{Deserialize, Serialize};
3use crate::common::utils::now_ms;
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub enum Severity {
7    Common,
8    Suspicious,
9    Fault,
10}
11#[derive(Debug, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct RustalinkError {
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}
22impl RustalinkError {
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 {
47use std::{ops::Deref, sync::Arc};
48use rand::{Rng, distributions::Alphanumeric};
49use tokio::sync::{Mutex, RwLock};
50pub type Shared<T> = Arc<Mutex<T>>;
51pub type SharedRw<T> = Arc<RwLock<T>>;
52pub type AnyError = Box<dyn std::error::Error + Send + Sync>;
53pub type AnyResult<T> = std::result::Result<T, AnyError>;
54macro_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}
97define_id!(GuildId, String);
98define_id!(SessionId, String);
99define_id!(UserId, u64, copy);
100define_id!(ChannelId, u64, copy);
101impl Deref for GuildId {
102    type Target = str;
103    fn deref(&self) -> &Self::Target {
104        &self.0
105    }
106}
107impl Deref for SessionId {
108    type Target = str;
109    fn deref(&self) -> &Self::Target {
110        &self.0
111    }
112}
113impl 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)]
126pub enum AudioFormat {
127    Aac,
128    Opus,
129    Webm,
130    Mp4,
131    Mp3,
132    Ogg,
133    Flac,
134    Wav,
135    Unknown,
136}
137impl 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}
212fn 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 {
221use std::{sync::Arc, time::Duration};
222use dashmap::DashMap;
223use reqwest::{Client, Proxy};
224use tracing::warn;
225use crate::{common::utils::default_user_agent, config::HttpProxyConfig};
226pub struct HttpClientPool {
227    clients: DashMap<Option<HttpProxyConfig>, Arc<Client>>,
228}
229impl 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}
282impl Default for HttpClientPool {
283    fn default() -> Self {
284        Self::new()
285    }
286}
287}
288pub mod utils {
289use std::time::{SystemTime, UNIX_EPOCH};
290pub const RESET: &str = "\x1b[0m";
291pub const BOLD: &str = "\x1b[1m";
292pub const DIM: &str = "\x1b[2m";
293pub const ORANGE: &str = "\x1b[38;5;208m";
294pub const GREEN: &str = "\x1b[32m";
295pub const CYAN: &str = "\x1b[36m";
296pub const YELLOW: &str = "\x1b[33m";
297pub const BLUE: &str = "\x1b[34m";
298pub const MAGENTA: &str = "\x1b[35m";
299pub const RED: &str = "\x1b[31m";
300pub const COLOR_ERROR: &str = RED;
301pub const COLOR_WARN: &str = YELLOW;
302pub const COLOR_INFO: &str = GREEN;
303pub const COLOR_DEBUG: &str = BLUE;
304pub const COLOR_TRACE: &str = MAGENTA;
305pub fn now_ms() -> u64 {
306    SystemTime::now()
307        .duration_since(UNIX_EPOCH)
308        .unwrap_or_default()
309        .as_millis() as u64
310}
311pub fn now_nanos() -> u64 {
312    SystemTime::now()
313        .duration_since(UNIX_EPOCH)
314        .unwrap_or_default()
315        .as_nanos() as u64
316}
317pub 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}
337pub fn memory_usage_report() -> String {
338    "0 B".to_string()
339}
340pub 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";
341pub fn default_user_agent() -> String {
342    DEFAULT_USER_AGENT.to_owned()
343}
344pub 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 {
367pub mod formatter {
368use core::fmt::{self as core_fmt};
369use tracing::{Event, Level, Subscriber};
370use tracing_subscriber::{
371    fmt::{
372        self, FmtContext,
373        format::{FormatEvent, FormatFields},
374    },
375    registry::LookupSpan,
376};
377use crate::common::utils::{
378    BOLD, COLOR_DEBUG, COLOR_ERROR, COLOR_INFO, COLOR_TRACE, COLOR_WARN, DIM, RESET,
379    memory_usage_report,
380};
381pub struct CustomFormatter {
382    use_ansi: bool,
383}
384impl 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 =
393            time::OffsetDateTime::now_local().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(&self, writer: &mut fmt::format::Writer<'_>, level: &Level) -> core_fmt::Result {
404        let level_str = format!("{: <5}", level);
405        if self.use_ansi {
406            let color = match *level {
407                Level::ERROR => COLOR_ERROR,
408                Level::WARN => COLOR_WARN,
409                Level::INFO => COLOR_INFO,
410                Level::DEBUG => COLOR_DEBUG,
411                Level::TRACE => COLOR_TRACE,
412            };
413            write!(writer, "{color}{BOLD}{level_str}{RESET} ")
414        } else {
415            write!(writer, "{level_str} ")
416        }
417    }
418}
419impl<S, N> FormatEvent<S, N> for CustomFormatter
420where
421    S: Subscriber + for<'a> LookupSpan<'a>,
422    N: for<'a> FormatFields<'a> + 'static,
423{
424    fn format_event(
425        &self,
426        ctx: &FmtContext<'_, S, N>,
427        mut writer: fmt::format::Writer<'_>,
428        event: &Event<'_>,
429    ) -> core_fmt::Result {
430        let (reset, dim) = if self.use_ansi {
431            (RESET, DIM)
432        } else {
433            ("", "")
434        };
435        write!(writer, "{dim}[{}]{reset} ", memory_usage_report())?;
436        self.write_timestamp(&mut writer)?;
437        let metadata = event.metadata();
438        self.write_level(&mut writer, metadata.level())?;
439        let target = metadata.target();
440        let line = metadata
441            .line()
442            .map(|l| l.to_string())
443            .unwrap_or_else(|| "??".to_string());
444        write!(writer, "{dim}{target}: {line}{reset} > ")?;
445        ctx.format_fields(writer.by_ref(), event)?;
446        write!(writer, "{reset}")?;
447        writeln!(writer)
448    }
449}
450}
451pub mod writer {
452use std::{
453    fs::{File, OpenOptions},
454    io::{self, BufRead, BufReader, Write},
455    path::{Path, PathBuf},
456    sync::Arc,
457};
458use parking_lot::Mutex;
459fn today_date() -> String {
460    let secs = std::time::SystemTime::now()
461        .duration_since(std::time::UNIX_EPOCH)
462        .unwrap_or_default()
463        .as_secs();
464    let days = (secs / 86400) as u32;
465    let (y, m, d) = days_to_ymd(days);
466    format!("{y:04}-{m:02}-{d:02}")
467}
468fn days_to_ymd(mut days: u32) -> (u32, u32, u32) {
469    days += 719468;
470    let era = days / 146097;
471    let doe = days - era * 146097;
472    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
473    let y = yoe + era * 400;
474    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
475    let mp = (5 * doy + 2) / 153;
476    let d = doy - (153 * mp + 2) / 5 + 1;
477    let m = if mp < 10 { mp + 3 } else { mp - 9 };
478    let y = if m <= 2 { y + 1 } else { y };
479    (y, m, d)
480}
481fn resolve_path(base_path: &str, rotate_daily: bool, date: &str) -> String {
482    if !rotate_daily {
483        return base_path.to_string();
484    }
485    let base = Path::new(base_path);
486    let stem = base
487        .file_stem()
488        .and_then(|s| s.to_str())
489        .unwrap_or("rustalink");
490    let dir: PathBuf = base.parent().unwrap_or(Path::new(".")).into();
491    dir.join(format!("{stem}-{date}.log"))
492        .to_string_lossy()
493        .into_owned()
494}
495#[derive(Clone)]
496pub struct CircularFileWriter {
497    base_path: String,
498    max_lines: u32,
499    max_files: u32,
500    rotate_daily: bool,
501    state: Arc<Mutex<WriterState>>,
502}
503struct WriterState {
504    file: Option<File>,
505    current_date: Option<String>,
506    lines_since_prune: u32,
507    is_pruning: bool,
508}
509impl CircularFileWriter {
510    pub fn new(path: String, max_lines: u32, max_files: u32, rotate_daily: bool) -> Self {
511        Self {
512            base_path: path,
513            max_lines,
514            max_files,
515            rotate_daily,
516            state: Arc::new(Mutex::new(WriterState {
517                file: None,
518                current_date: None,
519                lines_since_prune: 0,
520                is_pruning: false,
521            })),
522        }
523    }
524    fn current_path(&self) -> String {
525        if self.rotate_daily {
526            resolve_path(&self.base_path, true, &today_date())
527        } else {
528            self.base_path.clone()
529        }
530    }
531    fn ensure_file_open<'a>(&self, state: &'a mut WriterState) -> io::Result<&'a mut File> {
532        let today = if self.rotate_daily {
533            Some(today_date())
534        } else {
535            None
536        };
537        let need_rotate = state.file.is_none()
538            || match (&state.current_date, &today) {
539                (Some(curr), Some(new)) => curr != new,
540                _ => false,
541            };
542        if need_rotate {
543            state.file = None;
544            let path = if self.rotate_daily {
545                let d = today.as_deref().unwrap_or("");
546                resolve_path(&self.base_path, true, d)
547            } else {
548                self.base_path.clone()
549            };
550            if let Some(parent) = Path::new(&path).parent() {
551                let _ = std::fs::create_dir_all(parent);
552            }
553            state.file = Some(OpenOptions::new().create(true).append(true).open(&path)?);
554            state.current_date = today;
555            if self.rotate_daily && self.max_files > 0 {
556                self.cleanup_old_files();
557            }
558        }
559        Ok(state.file.as_mut().expect("file was just opened"))
560    }
561    fn spawn_prune(&self) {
562        let path = self.current_path();
563        let max_lines = self.max_lines;
564        let state_arc = self.state.clone();
565        std::thread::spawn(move || {
566            if let Err(e) = Self::do_prune(&path, max_lines) {
567                eprintln!("Failed to prune log file '{}': {}", path, e);
568            }
569            let mut state = state_arc.lock();
570            state.is_pruning = false;
571        });
572    }
573    fn cleanup_old_files(&self) {
574        let base = Path::new(&self.base_path);
575        let dir = base.parent().unwrap_or(Path::new("."));
576        let stem = base
577            .file_stem()
578            .and_then(|s| s.to_str())
579            .unwrap_or("rustalink");
580        let max_files = self.max_files as usize;
581        let mut log_files: Vec<std::path::PathBuf> = match std::fs::read_dir(dir) {
582            Ok(entries) => entries
583                .filter_map(|e| e.ok())
584                .map(|e| e.path())
585                .filter(|p| {
586                    p.extension().and_then(|e| e.to_str()) == Some("log")
587                        && p.file_stem()
588                            .and_then(|s| s.to_str())
589                            .map(|s| s.starts_with(stem) && s != stem)
590                            .unwrap_or(false)
591                })
592                .collect(),
593            Err(_) => return,
594        };
595        if log_files.len() <= max_files {
596            return;
597        }
598        log_files.sort();
599        let to_delete = log_files.len() - max_files;
600        for path in log_files.iter().take(to_delete) {
601            if let Err(e) = std::fs::remove_file(path) {
602                eprintln!("Failed to delete old log file '{}': {}", path.display(), e);
603            }
604        }
605    }
606    fn do_prune(path: &str, max_lines: u32) -> io::Result<()> {
607        if !Path::new(path).exists() {
608            return Ok(());
609        }
610        let lines: Vec<String> = {
611            let file = File::open(path)?;
612            let reader = BufReader::new(file);
613            reader.lines().collect::<Result<_, _>>()?
614        };
615        if lines.len() > max_lines as usize {
616            let start = lines.len() - max_lines as usize;
617            let tmp_path = format!("{}.tmp", path);
618            {
619                let mut file = File::create(&tmp_path)?;
620                for line in &lines[start..] {
621                    writeln!(file, "{}", line)?;
622                }
623            }
624            std::fs::rename(tmp_path, path)?;
625        }
626        Ok(())
627    }
628}
629impl io::Write for CircularFileWriter {
630    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
631        let mut state = self.state.lock();
632        let file = self.ensure_file_open(&mut state)?;
633        file.write_all(buf)?;
634        let new_lines = buf.iter().filter(|&&b| b == b'\n').count() as u32;
635        state.lines_since_prune += new_lines;
636        let prune_threshold = (self.max_lines / 10).max(50);
637        if state.lines_since_prune >= prune_threshold && !state.is_pruning {
638            state.is_pruning = true;
639            state.lines_since_prune = 0;
640            state.file = None; 
641            self.spawn_prune();
642        }
643        Ok(buf.len())
644    }
645    fn flush(&mut self) -> io::Result<()> {
646        let mut state = self.state.lock();
647        if let Some(file) = &mut state.file {
648            file.flush()?;
649        }
650        Ok(())
651    }
652}
653impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for CircularFileWriter {
654    type Writer = Self;
655    fn make_writer(&'a self) -> Self::Writer {
656        self.clone()
657    }
658}
659#[cfg(test)]
660mod tests {
661    use std::fs;
662    use tracing_subscriber::fmt::MakeWriter;
663    use super::*;
664    fn cleanup_test_file(path: &str) {
665        let _ = fs::remove_file(path);
666        let _ = fs::remove_file(format!("{}.tmp", path));
667    }
668    #[test]
669    fn test_circular_file_writer_new() {
670        let writer = CircularFileWriter::new("test_new.log".to_string(), 100, 0, false);
671        let state = writer.state.lock();
672        assert_eq!(state.lines_since_prune, 0);
673        assert!(!state.is_pruning);
674        assert!(state.file.is_none());
675        cleanup_test_file("test_new.log");
676    }
677    #[test]
678    fn test_write_creates_file() {
679        let path = "test_create.log";
680        cleanup_test_file(path);
681        let mut writer = CircularFileWriter::new(path.to_string(), 100, 0, false);
682        let data = b"test line\n";
683        let result = writer.write(data);
684        assert!(result.is_ok());
685        assert_eq!(result.unwrap(), data.len());
686        assert!(Path::new(path).exists());
687        cleanup_test_file(path);
688    }
689    #[test]
690    fn test_write_counts_newlines() {
691        let path = "test_newlines.log";
692        cleanup_test_file(path);
693        let mut writer = CircularFileWriter::new(path.to_string(), 1000, 0, false);
694        writer.write(b"line1\nline2\nline3\n").unwrap();
695        let state = writer.state.lock();
696        assert_eq!(state.lines_since_prune, 3);
697        cleanup_test_file(path);
698    }
699    #[test]
700    fn test_write_no_newlines() {
701        let path = "test_no_newlines.log";
702        cleanup_test_file(path);
703        let mut writer = CircularFileWriter::new(path.to_string(), 1000, 0, false);
704        writer.write(b"no newline here").unwrap();
705        let state = writer.state.lock();
706        assert_eq!(state.lines_since_prune, 0);
707        cleanup_test_file(path);
708    }
709    #[test]
710    fn test_flush() {
711        let path = "test_flush.log";
712        cleanup_test_file(path);
713        let mut writer = CircularFileWriter::new(path.to_string(), 100, 0, false);
714        writer.write(b"test\n").unwrap();
715        let result = writer.flush();
716        assert!(result.is_ok());
717        cleanup_test_file(path);
718    }
719    #[test]
720    fn test_flush_without_file() {
721        let mut writer =
722            CircularFileWriter::new("test_flush_no_file.log".to_string(), 100, 0, false);
723        let result = writer.flush();
724        assert!(result.is_ok());
725        cleanup_test_file("test_flush_no_file.log");
726    }
727    #[test]
728    fn test_clone() {
729        let writer = CircularFileWriter::new("test_clone.log".to_string(), 100, 0, false);
730        let cloned = writer.clone();
731        assert!(Arc::ptr_eq(&writer.state, &cloned.state));
732        cleanup_test_file("test_clone.log");
733    }
734    #[test]
735    fn test_make_writer() {
736        let writer = CircularFileWriter::new("test_make_writer.log".to_string(), 100, 0, false);
737        let made = writer.make_writer();
738        assert!(Arc::ptr_eq(&writer.state, &made.state));
739        cleanup_test_file("test_make_writer.log");
740    }
741    #[test]
742    fn test_do_prune_nonexistent_file() {
743        let result = CircularFileWriter::do_prune("nonexistent_prune.log", 10);
744        assert!(result.is_ok());
745    }
746    #[test]
747    fn test_do_prune_small_file() {
748        let path = "test_prune_small.log";
749        cleanup_test_file(path);
750        fs::write(path, "line1\nline2\nline3\n").unwrap();
751        let result = CircularFileWriter::do_prune(path, 10);
752        assert!(result.is_ok());
753        let content = fs::read_to_string(path).unwrap();
754        assert_eq!(content.lines().count(), 3);
755        cleanup_test_file(path);
756    }
757    #[test]
758    fn test_do_prune_large_file() {
759        let path = "test_prune_large.log";
760        cleanup_test_file(path);
761        let mut content = String::new();
762        for i in 1..=20 {
763            content.push_str(&format!("line{}\n", i));
764        }
765        fs::write(path, content).unwrap();
766        let result = CircularFileWriter::do_prune(path, 10);
767        assert!(result.is_ok());
768        let pruned = fs::read_to_string(path).unwrap();
769        let lines: Vec<&str> = pruned.lines().collect();
770        assert_eq!(lines.len(), 10);
771        assert_eq!(lines[0], "line11");
772        assert_eq!(lines[9], "line20");
773        cleanup_test_file(path);
774    }
775    #[test]
776    fn test_resolve_path_no_rotate() {
777        let p = resolve_path("./logs/rustalink.log", false, "2026-03-13");
778        assert_eq!(p, "./logs/rustalink.log");
779    }
780    #[test]
781    fn test_resolve_path_rotate() {
782        let p = resolve_path("./logs/rustalink.log", true, "2026-03-13");
783        assert!(p.contains("2026-03-13"));
784        assert!(p.ends_with(".log"));
785    }
786    #[test]
787    fn test_today_date_format() {
788        let d = today_date();
789        let parts: Vec<&str> = d.split('-').collect();
790        assert_eq!(parts.len(), 3);
791        assert_eq!(parts[0].len(), 4); 
792        assert_eq!(parts[1].len(), 2); 
793        assert_eq!(parts[2].len(), 2); 
794    }
795    #[test]
796    fn test_prune_threshold_calculation() {
797        let _writer = CircularFileWriter::new("test.log".to_string(), 1000, 0, false);
798        let threshold = (1000 / 10).max(50);
799        assert_eq!(threshold, 100);
800        let _writer = CircularFileWriter::new("test.log".to_string(), 100, 0, false);
801        let threshold = (100 / 10).max(50);
802        assert_eq!(threshold, 50);
803        let _writer = CircularFileWriter::new("test.log".to_string(), 10, 0, false);
804        let threshold = (10 / 10).max(50);
805        assert_eq!(threshold, 50);
806        cleanup_test_file("test.log");
807    }
808}
809}
810use std::{fs, path::Path, sync::OnceLock};
811use tracing_subscriber::{EnvFilter, fmt, prelude::*};
812use crate::{common::utils::strip_ansi_escapes, config::LoggingConfig};
813pub use formatter::CustomFormatter;
814pub use writer::CircularFileWriter;
815pub(crate) static GLOBAL_FILE_WRITER: OnceLock<CircularFileWriter> = OnceLock::new();
816#[macro_export]
817macro_rules! log_print {
818    ($($arg:tt)*) => {{
819        let msg = format!($($arg)*);
820        std::print!("{}", msg);
821        $crate::common::logger::append_to_file_raw(&msg);
822    }};
823}
824#[macro_export]
825macro_rules! log_println {
826    () => {{
827        std::println!();
828        $crate::common::logger::append_to_file_raw("\n");
829    }};
830    ($($arg:tt)*) => {{
831        let msg = format!($($arg)*);
832        std::println!("{}", msg);
833        $crate::common::logger::append_to_file_raw(&format!("{}\n", msg));
834    }};
835}
836pub fn append_to_file_raw(msg: &str) {
837    if let Some(mut writer) = GLOBAL_FILE_WRITER.get().cloned() {
838        use std::io::Write;
839        let clean_msg = strip_ansi_escapes(msg);
840        let _ = writer.write_all(clean_msg.as_bytes());
841    }
842}
843pub fn init(config: &LoggingConfig) {
844    let _ = tracing_log::LogTracer::init();
845    let log_level = config.level.as_deref().unwrap_or("info");
846    let filter_str = match config.filters.as_deref() {
847        Some(f) if !f.is_empty() => format!("{log_level},{f}"),
848        _ => log_level.to_string(),
849    };
850    let env_filter =
851        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter_str));
852    let stdout_layer = fmt::layer()
853        .event_format(CustomFormatter::new(true))
854        .with_ansi(true);
855    let file_layer = config.file.as_ref().map(|file_config| {
856        if let Some(parent) = Path::new(&file_config.path).parent() {
857            let _ = fs::create_dir_all(parent);
858        }
859        let writer = CircularFileWriter::new(
860            file_config.path.clone(),
861            file_config.max_lines,
862            file_config.max_files,
863            file_config.rotate_daily,
864        );
865        let _ = GLOBAL_FILE_WRITER.set(writer.clone());
866        fmt::layer()
867            .with_writer(writer)
868            .event_format(CustomFormatter::new(false))
869            .with_ansi(false)
870    });
871    let _ = tracing_subscriber::registry()
872        .with(env_filter)
873        .with(stdout_layer)
874        .with(file_layer)
875        .try_init();
876}
877}
878pub use errors::*;
879pub use http::*;
880pub use logger::*;
881pub use types::*;
882pub use utils::*;