url_preview/
logging.rs

1use crate::utils::truncate_str;
2use crate::Preview;
3use std::fmt::Display;
4use std::path::PathBuf;
5use tracing::{debug, error, info};
6use tracing_appender::rolling::{RollingFileAppender, Rotation};
7use tracing_subscriber::{
8    fmt as subscriber_fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
9};
10
11#[derive(Debug)]
12pub struct LogConfig {
13    pub log_dir: PathBuf,
14    pub log_level: String,
15    pub console_output: bool,
16    pub file_output: bool,
17}
18
19impl Default for LogConfig {
20    fn default() -> Self {
21        Self {
22            log_dir: "logs".into(),
23            log_level: "info".into(),
24            console_output: true,
25            file_output: true,
26        }
27    }
28}
29
30fn create_separator(width: usize, ch: char) -> String {
31    std::iter::repeat(ch).take(width).collect()
32}
33
34pub fn log_preview_card(preview: &Preview, url: &str) {
35    const CARD_WIDTH: usize = 80;
36    const CONTENT_WIDTH: usize = CARD_WIDTH - 2;
37
38    fn wrap_text(text: &str, width: usize) -> String {
39        let mut wrapped = String::new();
40        let mut line_length = 0;
41
42        for word in text.split_whitespace() {
43            if line_length + word.len() + 1 > width {
44                wrapped.push('\n');
45                wrapped.push_str("  ");
46                wrapped.push_str(word);
47                line_length = word.len() + 2;
48            } else {
49                if line_length > 0 {
50                    wrapped.push(' ');
51                    line_length += 1;
52                }
53                wrapped.push_str(word);
54                line_length += word.len();
55            }
56        }
57        wrapped
58    }
59
60    let url_wrapped = wrap_text(url, CONTENT_WIDTH - 5);
61    let title_wrapped = wrap_text(preview.title.as_deref().unwrap_or("N/A"), CONTENT_WIDTH - 7);
62    let desc_wrapped = wrap_text(
63        preview.description.as_deref().unwrap_or("N/A"),
64        CONTENT_WIDTH - 6,
65    );
66    let image_wrapped = wrap_text(
67        preview.image_url.as_deref().unwrap_or("N/A"),
68        CONTENT_WIDTH - 7,
69    );
70    let site_wrapped = wrap_text(
71        preview.site_name.as_deref().unwrap_or("N/A"),
72        CONTENT_WIDTH - 6,
73    );
74
75    let horizontal_line = "═".repeat(CARD_WIDTH - 2);
76
77    info!(
78        "\n╔{}╗\n\
79         URL: {}\n\
80         Title: {}\n\
81         Desc: {}\n\
82         Image: {}\n\
83         Site: {}\n\
84         ╚{}╝",
85        horizontal_line,
86        url_wrapped,
87        title_wrapped,
88        desc_wrapped,
89        image_wrapped,
90        site_wrapped,
91        horizontal_line,
92    );
93}
94
95pub fn log_error_card<E: Display + std::error::Error>(url: &str, error: &E) {
96    const CARD_WIDTH: usize = 70;
97    const CONTENT_WIDTH: usize = CARD_WIDTH - 8;
98
99    let top_bottom = create_separator(CARD_WIDTH - 2, '═');
100    let middle = create_separator(CARD_WIDTH - 2, '─');
101
102    let mut error_details = error.to_string();
103    if let Some(source) = error.source() {
104        error_details = format!("{} (原因: {})", error_details, source);
105    }
106
107    error!(
108        "\n╔═{}═╗\n\
109         ║ URL: {:<width$} ║\n\
110         ║{}║\n\
111         ║ 错误: {:<width$} ║\n\
112         ╚═{}═╝",
113        top_bottom,
114        truncate_str(url, CONTENT_WIDTH),
115        middle,
116        truncate_str(&error_details, CONTENT_WIDTH),
117        top_bottom,
118        width = CONTENT_WIDTH
119    );
120}
121
122pub fn setup_logging(config: LogConfig) {
123    let env_filter =
124        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
125
126    let mut layers = Vec::new();
127
128    if config.console_output {
129        let console_layer = subscriber_fmt::layer()
130            .with_target(true)
131            .with_thread_ids(true)
132            .with_line_number(true)
133            .with_file(true)
134            .with_span_events(subscriber_fmt::format::FmtSpan::FULL)
135            .pretty();
136        layers.push(console_layer.boxed());
137    }
138
139    if config.file_output {
140        std::fs::create_dir_all(&config.log_dir).expect("Failed to create log directory");
141
142        let file_appender =
143            RollingFileAppender::new(Rotation::DAILY, &config.log_dir, "url-preview.log");
144
145        let file_layer = subscriber_fmt::layer()
146            .with_ansi(false)
147            .with_target(true)
148            .with_thread_ids(true)
149            .with_line_number(true)
150            .with_file(true)
151            .with_writer(file_appender);
152
153        layers.push(file_layer.boxed());
154    }
155
156    tracing_subscriber::registry()
157        .with(env_filter)
158        .with(layers)
159        .try_init()
160        .expect("Failed to set global default subscriber");
161
162    debug!("Logging system initialized with config: {:?}", config);
163}
164
165pub struct LogLevelGuard {
166    _guard: tracing::dispatcher::DefaultGuard,
167}
168
169impl LogLevelGuard {
170    pub fn set_level(level: &str) -> Self {
171        let filter = EnvFilter::new(level);
172        let subscriber = tracing_subscriber::registry()
173            .with(subscriber_fmt::layer())
174            .with(filter);
175
176        LogLevelGuard {
177            _guard: tracing::subscriber::set_default(subscriber),
178        }
179    }
180}