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(
62        preview.title.as_deref().unwrap_or("N/A"),
63        CONTENT_WIDTH - 7,
64    );
65    let desc_wrapped = wrap_text(
66        preview.description.as_deref().unwrap_or("N/A"),
67        CONTENT_WIDTH - 6,
68    );
69    let image_wrapped = wrap_text(
70        preview.image_url.as_deref().unwrap_or("N/A"),
71        CONTENT_WIDTH - 7,
72    );
73    let site_wrapped = wrap_text(
74        preview.site_name.as_deref().unwrap_or("N/A"),
75        CONTENT_WIDTH - 6,
76    );
77
78    let horizontal_line = "═".repeat(CARD_WIDTH - 2);
79
80    info!(
81        "\n╔{}╗\n\
82         URL: {}\n\
83         Title: {}\n\
84         Desc: {}\n\
85         Image: {}\n\
86         Site: {}\n\
87         ╚{}╝",
88        horizontal_line,
89        url_wrapped,
90        title_wrapped,
91        desc_wrapped,
92        image_wrapped,
93        site_wrapped,
94        horizontal_line,
95    );
96}
97
98pub fn log_error_card<E: Display + std::error::Error>(url: &str, error: &E) {
99    const CARD_WIDTH: usize = 70;
100    const CONTENT_WIDTH: usize = CARD_WIDTH - 8;
101
102    let top_bottom = create_separator(CARD_WIDTH - 2, '═');
103    let middle = create_separator(CARD_WIDTH - 2, '─');
104
105    let mut error_details = error.to_string();
106    if let Some(source) = error.source() {
107        error_details = format!("{} (原因: {})", error_details, source);
108    }
109
110    error!(
111        "\n╔═{}═╗\n\
112         ║ URL: {:<width$} ║\n\
113         ║{}║\n\
114         ║ 错误: {:<width$} ║\n\
115         ╚═{}═╝",
116        top_bottom,
117        truncate_str(url, CONTENT_WIDTH),
118        middle,
119        truncate_str(&error_details, CONTENT_WIDTH),
120        top_bottom,
121        width = CONTENT_WIDTH
122    );
123}
124
125pub fn setup_logging(config: LogConfig) {
126    let env_filter =
127        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
128
129    let mut layers = Vec::new();
130
131    if config.console_output {
132        let console_layer = subscriber_fmt::layer()
133            .with_target(true)
134            .with_thread_ids(true)
135            .with_line_number(true)
136            .with_file(true)
137            .with_span_events(subscriber_fmt::format::FmtSpan::FULL)
138            .pretty();
139        layers.push(console_layer.boxed());
140    }
141
142    if config.file_output {
143        std::fs::create_dir_all(&config.log_dir).expect("Failed to create log directory");
144
145        let file_appender =
146            RollingFileAppender::new(Rotation::DAILY, &config.log_dir, "url-preview.log");
147
148        let file_layer = subscriber_fmt::layer()
149            .with_ansi(false)
150            .with_target(true)
151            .with_thread_ids(true)
152            .with_line_number(true)
153            .with_file(true)
154            .with_writer(file_appender);
155
156        layers.push(file_layer.boxed());
157    }
158
159    tracing_subscriber::registry()
160        .with(env_filter)
161        .with(layers)
162        .try_init()
163        .expect("Failed to set global default subscriber");
164
165    debug!("Logging system initialized with config: {:?}", config);
166}
167
168pub struct LogLevelGuard {
169    _guard: tracing::dispatcher::DefaultGuard,
170}
171
172impl LogLevelGuard {
173    pub fn set_level(level: &str) -> Self {
174        let filter = EnvFilter::new(level);
175        let subscriber = tracing_subscriber::registry()
176            .with(subscriber_fmt::layer())
177            .with(filter);
178
179        LogLevelGuard {
180            _guard: tracing::subscriber::set_default(subscriber),
181        }
182    }
183}