1#[cfg(feature = "logging")]
2use tracing_subscriber::{fmt, prelude::*, EnvFilter, Layer};
3
4#[cfg(feature = "logging")]
5use crate::config::CommonConfig;
6
7#[derive(Debug, Clone)]
9pub enum LogFormat {
10 Pretty,
12 Compact,
14 Json,
16 Full,
18}
19
20#[derive(Debug, Clone)]
22pub enum LogOutput {
23 Stdout,
25 Stderr,
27 File(String),
29}
30
31#[derive(Debug, Clone)]
33pub enum RequestIdStrategy {
34 Uuid,
36 Short,
38 Timestamp,
40 PrefixedUuid(String),
42}
43
44#[derive(Debug, Clone)]
46pub struct LoggingConfig {
47 pub level: String,
48 pub format: LogFormat,
49 pub output: LogOutput,
50 pub show_target: bool,
51 pub show_thread_ids: bool,
52 pub show_file_line: bool,
53 pub show_timestamps: bool,
54 pub service_name: Option<String>,
55 pub auto_request_id: bool,
56 pub request_id_strategy: RequestIdStrategy,
57 pub request_id_field: String,
58}
59
60impl Default for LoggingConfig {
61 fn default() -> Self {
62 Self {
63 level: "info".to_string(),
64 format: LogFormat::Pretty,
65 output: LogOutput::Stdout,
66 show_target: true,
67 show_thread_ids: false,
68 show_file_line: false,
69 show_timestamps: true,
70 service_name: None,
71 auto_request_id: false,
72 request_id_strategy: RequestIdStrategy::Uuid,
73 request_id_field: "request_id".to_string(),
74 }
75 }
76}
77
78impl LoggingConfig {
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn with_level(mut self, level: impl Into<String>) -> Self {
84 self.level = level.into();
85 self
86 }
87
88 pub fn with_format(mut self, format: LogFormat) -> Self {
89 self.format = format;
90 self
91 }
92
93 pub fn with_output(mut self, output: LogOutput) -> Self {
94 self.output = output;
95 self
96 }
97
98 pub fn with_service_name(mut self, name: impl Into<String>) -> Self {
99 self.service_name = Some(name.into());
100 self
101 }
102
103 pub fn show_target(mut self, show: bool) -> Self {
104 self.show_target = show;
105 self
106 }
107
108 pub fn show_thread_ids(mut self, show: bool) -> Self {
109 self.show_thread_ids = show;
110 self
111 }
112
113 pub fn show_file_line(mut self, show: bool) -> Self {
114 self.show_file_line = show;
115 self
116 }
117
118 pub fn show_timestamps(mut self, show: bool) -> Self {
119 self.show_timestamps = show;
120 self
121 }
122
123 pub fn with_auto_request_id(mut self, enabled: bool) -> Self {
125 self.auto_request_id = enabled;
126 self
127 }
128
129 pub fn with_request_id_strategy(mut self, strategy: RequestIdStrategy) -> Self {
131 self.request_id_strategy = strategy;
132 self
133 }
134
135 pub fn with_request_id_field(mut self, field_name: impl Into<String>) -> Self {
137 self.request_id_field = field_name.into();
138 self
139 }
140}
141
142#[cfg(feature = "logging")]
144pub fn generate_request_id(strategy: &RequestIdStrategy) -> String {
145 match strategy {
146 RequestIdStrategy::Uuid => {
147 use std::time::{SystemTime, UNIX_EPOCH};
149 let timestamp = SystemTime::now()
150 .duration_since(UNIX_EPOCH)
151 .unwrap()
152 .as_nanos();
153 let random = (timestamp % 1000000) as u32;
154 format!(
155 "{:08x}-{:04x}-{:04x}",
156 timestamp as u32,
157 random,
158 random >> 16
159 )
160 }
161 RequestIdStrategy::Short => {
162 use std::time::{SystemTime, UNIX_EPOCH};
163 let timestamp = SystemTime::now()
164 .duration_since(UNIX_EPOCH)
165 .unwrap()
166 .as_nanos();
167 format!("{:08x}", (timestamp % 0xffffffff) as u32)
168 }
169 RequestIdStrategy::Timestamp => {
170 use std::time::{SystemTime, UNIX_EPOCH};
171 let timestamp = SystemTime::now()
172 .duration_since(UNIX_EPOCH)
173 .unwrap()
174 .as_millis();
175 format!("req_{}", timestamp)
176 }
177 RequestIdStrategy::PrefixedUuid(prefix) => {
178 let uuid_part = generate_request_id(&RequestIdStrategy::Uuid);
179 format!("{}_{}", prefix, uuid_part)
180 }
181 }
182}
183
184#[cfg(feature = "logging")]
186pub fn create_request_span(strategy: &RequestIdStrategy) -> (String, tracing::Span) {
187 let request_id = generate_request_id(strategy);
188 let span = tracing::info_span!("request", request_id = %request_id);
189 (request_id, span)
190}
191
192#[cfg(feature = "logging")]
194pub fn create_request_span_with_id(request_id: &str) -> tracing::Span {
195 tracing::info_span!("request", request_id = %request_id)
196}
197
198#[cfg(feature = "logging")]
200#[macro_export]
201macro_rules! request_span {
202 () => {{
203 let (request_id, span) =
204 $crate::logging::create_request_span(&$crate::logging::RequestIdStrategy::Uuid);
205 span
206 }};
207 ($strategy:expr) => {{
208 let (request_id, span) = $crate::logging::create_request_span(&$strategy);
209 span
210 }};
211}
212
213#[cfg(feature = "logging")]
215#[macro_export]
216macro_rules! named_request_span {
217 ($name:literal) => {{
218 let request_id = $crate::logging::generate_request_id(&$crate::logging::RequestIdStrategy::Uuid);
219 tracing::info_span!($name, request_id = %request_id)
220 }};
221 ($name:literal, $strategy:expr) => {{
222 let request_id = $crate::logging::generate_request_id(&$strategy);
223 tracing::info_span!($name, request_id = %request_id)
224 }};
225}
226
227#[cfg(feature = "logging")]
229#[macro_export]
230macro_rules! request_span_with_id {
231 ($id:expr) => {
232 $crate::logging::create_request_span_with_id($id)
233 };
234}
235
236#[cfg(feature = "logging")]
240pub fn init() {
241 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
242 fmt().with_env_filter(filter).init();
243}
244
245#[cfg(feature = "logging")]
247pub fn init_with_config(config: &CommonConfig) {
248 let filter =
249 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
250
251 fmt()
252 .with_env_filter(filter)
253 .with_target(false)
254 .with_thread_ids(true)
255 .with_file(true)
256 .with_line_number(true)
257 .init();
258}
259
260#[cfg(feature = "logging")]
262pub fn init_structured(service_name: &str) {
263 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
264
265 fmt()
266 .with_env_filter(filter)
267 .with_target(false)
268 .with_thread_ids(true)
269 .init();
270
271 tracing::info!(service = service_name, "Logging initialized");
272}
273
274#[cfg(feature = "logging")]
276pub fn init_advanced(config: LoggingConfig) {
277 let filter =
278 EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.level));
279
280 let fmt_layer = match config.format {
281 LogFormat::Pretty => fmt::layer()
282 .pretty()
283 .with_target(config.show_target)
284 .with_thread_ids(config.show_thread_ids)
285 .with_file(config.show_file_line)
286 .with_line_number(config.show_file_line)
287 .with_ansi(true)
288 .boxed(),
289
290 LogFormat::Compact => fmt::layer()
291 .compact()
292 .with_target(config.show_target)
293 .with_thread_ids(config.show_thread_ids)
294 .with_file(config.show_file_line)
295 .with_line_number(config.show_file_line)
296 .boxed(),
297
298 LogFormat::Json => fmt::layer()
299 .json()
300 .with_target(config.show_target)
301 .with_thread_ids(config.show_thread_ids)
302 .with_file(config.show_file_line)
303 .with_line_number(config.show_file_line)
304 .boxed(),
305
306 LogFormat::Full => fmt::layer()
307 .with_target(config.show_target)
308 .with_thread_ids(config.show_thread_ids)
309 .with_file(config.show_file_line)
310 .with_line_number(config.show_file_line)
311 .with_ansi(true)
312 .boxed(),
313 };
314
315 tracing_subscriber::registry()
316 .with(filter)
317 .with(fmt_layer)
318 .init();
319
320 if let Some(service_name) = config.service_name {
321 tracing::info!(
322 service = service_name,
323 format = ?config.format,
324 level = config.level,
325 auto_request_id = config.auto_request_id,
326 request_id_strategy = ?config.request_id_strategy,
327 "Advanced logging initialized"
328 );
329 }
330}
331
332#[cfg(feature = "logging")]
334pub fn init_json_production(service_name: &str, level: Option<&str>) {
335 let log_level = level.unwrap_or("info");
336 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(log_level));
337
338 fmt()
339 .json()
340 .with_env_filter(filter)
341 .with_target(false)
342 .with_thread_ids(true)
343 .with_file(false)
344 .with_line_number(false)
345 .with_current_span(true)
346 .with_span_list(false)
347 .init();
348
349 tracing::info!(
350 service = service_name,
351 environment = "production",
352 log_level = log_level,
353 "Production JSON logging initialized"
354 );
355}
356
357#[cfg(feature = "logging")]
359pub fn init_development(service_name: &str) {
360 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug"));
361
362 fmt()
363 .pretty()
364 .with_env_filter(filter)
365 .with_target(true)
366 .with_thread_ids(true)
367 .with_file(true)
368 .with_line_number(true)
369 .with_ansi(true)
370 .init();
371
372 tracing::info!(
373 service = service_name,
374 environment = "development",
375 "Development logging initialized with pretty formatting"
376 );
377}
378
379#[cfg(feature = "logging")]
381pub fn init_test() {
382 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));
383
384 fmt()
385 .compact()
386 .with_env_filter(filter)
387 .with_target(false)
388 .with_thread_ids(false)
389 .with_file(false)
390 .with_line_number(false)
391 .with_ansi(false)
392 .init();
393}
394
395#[cfg(feature = "logging")]
397pub fn from_common_config(config: &CommonConfig) -> LoggingConfig {
398 let format = match config.custom.get("log_format").map(|s| s.as_str()) {
399 Some("json") => LogFormat::Json,
400 Some("compact") => LogFormat::Compact,
401 Some("full") => LogFormat::Full,
402 _ => LogFormat::Pretty,
403 };
404
405 let output = match config.custom.get("log_output").map(|s| s.as_str()) {
406 Some("stderr") => LogOutput::Stderr,
407 Some(path) if path.starts_with("/") || path.contains(".log") => {
408 LogOutput::File(path.to_string())
409 }
410 _ => LogOutput::Stdout,
411 };
412
413 LoggingConfig::new()
414 .with_level(&config.log_level)
415 .with_format(format)
416 .with_output(output)
417 .with_service_name(&config.service_name)
418 .show_target(
419 config
420 .custom
421 .get("log_show_target")
422 .map(|s| s == "true")
423 .unwrap_or(true),
424 )
425 .show_thread_ids(
426 config
427 .custom
428 .get("log_show_threads")
429 .map(|s| s == "true")
430 .unwrap_or(false),
431 )
432 .show_file_line(
433 config
434 .custom
435 .get("log_show_location")
436 .map(|s| s == "true")
437 .unwrap_or(false),
438 )
439}