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}