1use tracing_appender::rolling::{RollingFileAppender, Rotation};
8use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
9
10#[macro_export]
12macro_rules! log_request {
13 ($method:expr, $path:expr, $status:expr, $duration:expr) => {
14 tracing::info!(
15 method = $method,
16 path = $path,
17 status = $status,
18 duration_ms = $duration,
19 "HTTP request completed"
20 );
21 };
22 ($method:expr, $path:expr, $status:expr, $duration:expr, $($key:ident = $value:expr),+) => {
23 tracing::info!(
24 method = $method,
25 path = $path,
26 status = $status,
27 duration_ms = $duration,
28 $($key = $value),+,
29 "HTTP request completed"
30 );
31 };
32}
33
34#[macro_export]
36macro_rules! log_error {
37 ($error_code:expr, $message:expr) => {
38 tracing::error!(
39 error_code = $error_code,
40 $message
41 );
42 };
43 ($error_code:expr, $message:expr, $($key:ident = $value:expr),+) => {
44 tracing::error!(
45 error_code = $error_code,
46 $($key = $value),+,
47 $message
48 );
49 };
50}
51
52pub fn init() {
76 let env_filter = match std::env::var("RUST_LOG") {
78 Ok(val) => EnvFilter::new(val.trim()),
79 Err(_) => EnvFilter::new("info"),
80 };
81
82 let log_file_dir = std::env::var("LOG_FILE_DIR").ok();
84 let log_file_prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
85 let file_only = std::env::var("LOG_FILE_ONLY").unwrap_or_default() == "true";
86 let enable_spans =
87 std::env::var("LOG_ENABLE_SPANS").unwrap_or_else(|_| "true".to_string()) == "true";
88
89 let registry = tracing_subscriber::registry().with(env_filter);
90
91 match (log_file_dir, file_only) {
92 (Some(log_dir), false) => {
94 let mut console_layer = fmt::layer()
95 .json()
96 .with_current_span(enable_spans)
97 .with_span_list(false);
98
99 if enable_spans {
100 console_layer = console_layer
101 .with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
102 }
103
104 let file_appender =
105 RollingFileAppender::new(Rotation::DAILY, &log_dir, &log_file_prefix);
106 let mut file_layer = fmt::layer()
107 .json()
108 .with_current_span(enable_spans)
109 .with_span_list(false)
110 .with_writer(file_appender);
111
112 if enable_spans {
113 file_layer = file_layer
114 .with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
115 }
116
117 let _ = registry.with(console_layer).with(file_layer).try_init();
118 }
119 (Some(log_dir), true) => {
121 let file_appender =
122 RollingFileAppender::new(Rotation::DAILY, &log_dir, &log_file_prefix);
123 let mut file_layer = fmt::layer()
124 .json()
125 .with_current_span(enable_spans)
126 .with_span_list(false)
127 .with_writer(file_appender);
128
129 if enable_spans {
130 file_layer = file_layer
131 .with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
132 }
133
134 let _ = registry.with(file_layer).try_init();
135 }
136 (None, _) => {
138 let mut console_layer = fmt::layer()
139 .json()
140 .with_current_span(enable_spans)
141 .with_span_list(false);
142
143 if enable_spans {
144 console_layer = console_layer
145 .with_span_events(fmt::format::FmtSpan::ENTER | fmt::format::FmtSpan::EXIT);
146 }
147
148 let _ = registry.with(console_layer).try_init();
149 }
150 }
151}
152
153pub fn validate_config() -> Result<String, String> {
155 let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
156 let log_file_dir = std::env::var("LOG_FILE_DIR").ok();
157 let log_file_prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
158 let file_only = std::env::var("LOG_FILE_ONLY").unwrap_or_default() == "true";
159 let enable_spans =
160 std::env::var("LOG_ENABLE_SPANS").unwrap_or_else(|_| "true".to_string()) == "true";
161
162 if let Err(e) = EnvFilter::try_new(rust_log.trim()) {
164 return Err(format!("Invalid RUST_LOG format: {}", e));
165 }
166
167 if let Some(ref dir) = log_file_dir {
169 if let Err(e) = std::fs::create_dir_all(dir) {
170 return Err(format!("Cannot create log directory '{}': {}", dir, e));
171 }
172 }
173
174 let config = match (log_file_dir.as_ref(), file_only) {
175 (Some(dir), false) => format!(
176 "Console + File logging to {}/{}.YYYY-MM-DD",
177 dir, log_file_prefix
178 ),
179 (Some(dir), true) => format!(
180 "File-only logging to {}/{}.YYYY-MM-DD",
181 dir, log_file_prefix
182 ),
183 (None, _) => "Console-only logging".to_string(),
184 };
185
186 let spans_status = if enable_spans { "enabled" } else { "disabled" };
187
188 Ok(format!(
189 "ā RUST_LOG: {}\nā Mode: {}\nā Spans: {}",
190 rust_log, config, spans_status
191 ))
192}
193
194pub fn print_config() {
196 match validate_config() {
197 Ok(config) => println!("Logging Configuration:\n{}", config),
198 Err(error) => eprintln!("Logging Configuration Error: {}", error),
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_init_does_not_panic() {
208 let _ = std::panic::catch_unwind(|| {
209 init();
210 });
211 }
212
213 #[test]
214 fn test_env_var_parsing() {
215 std::env::set_var("LOG_FILE_PREFIX", "test");
217 let prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
218 assert_eq!(prefix, "test");
219 std::env::remove_var("LOG_FILE_PREFIX");
220 }
221}
222
223pub mod structured {
225 use tracing::{error, info};
226
227 pub fn http_request(method: &str, path: &str, status: u16, duration_ms: u64) {
229 info!(
230 method = method,
231 path = path,
232 status = status,
233 duration_ms = duration_ms,
234 "HTTP request completed"
235 );
236 }
237
238 pub fn database_op(operation: &str, table: &str, duration_ms: u64, rows_affected: Option<u64>) {
240 info!(
241 operation = operation,
242 table = table,
243 duration_ms = duration_ms,
244 rows_affected = rows_affected,
245 "Database operation completed"
246 );
247 }
248
249 pub fn user_action(user_id: u64, action: &str, resource: Option<&str>) {
251 info!(
252 user_id = user_id,
253 action = action,
254 resource = resource,
255 "User action performed"
256 );
257 }
258
259 pub fn error_with_context(error_code: &str, message: &str) {
261 error!(error_code = error_code, "{}" = message);
262 }
263}