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}