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