Skip to main content

blinq_common/
logging.rs

1#[cfg(feature = "logging")]
2use tracing_subscriber::{fmt, prelude::*, EnvFilter, Layer};
3
4#[cfg(feature = "logging")]
5use crate::config::CommonConfig;
6
7/// Logging format options
8#[derive(Debug, Clone)]
9pub enum LogFormat {
10    /// Human-readable format for development
11    Pretty,
12    /// Compact format for production
13    Compact,
14    /// JSON format for structured logging
15    Json,
16    /// Full format with all details
17    Full,
18}
19
20/// Logging output destination
21#[derive(Debug, Clone)]
22pub enum LogOutput {
23    /// Output to stdout
24    Stdout,
25    /// Output to stderr
26    Stderr,
27    /// Output to a file (path)
28    File(String),
29}
30
31/// Request ID generation strategy
32#[derive(Debug, Clone)]
33pub enum RequestIdStrategy {
34    /// Generate UUID v4
35    Uuid,
36    /// Generate short alphanumeric ID (8 chars)
37    Short,
38    /// Generate timestamp-based ID
39    Timestamp,
40    /// Custom prefix + UUID
41    PrefixedUuid(String),
42}
43
44/// Advanced logging configuration
45#[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    /// Enable automatic request ID injection
124    pub fn with_auto_request_id(mut self, enabled: bool) -> Self {
125        self.auto_request_id = enabled;
126        self
127    }
128
129    /// Set request ID generation strategy
130    pub fn with_request_id_strategy(mut self, strategy: RequestIdStrategy) -> Self {
131        self.request_id_strategy = strategy;
132        self
133    }
134
135    /// Set custom field name for request ID (default: "request_id")
136    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/// Generate a request ID based on the strategy
143#[cfg(feature = "logging")]
144pub fn generate_request_id(strategy: &RequestIdStrategy) -> String {
145    match strategy {
146        RequestIdStrategy::Uuid => {
147            // Simple UUID-like generation without external deps
148            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/// Create a request span with automatic request ID injection
185#[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/// Create a request span with custom request ID
193#[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/// Macro to create a request span with automatic ID generation
199#[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/// Macro to create a named request span with automatic ID generation
214#[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/// Macro to create a request span with custom ID
228#[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/// Initialize global tracing subscriber for logging with env filter.
237///
238/// Reads RUST_LOG from env, defaults to "info" if not set.
239#[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/// Initialize logging with custom configuration
246#[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/// Initialize structured logging with service name
261#[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/// Initialize logging with advanced configuration
275#[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/// Initialize JSON logging for production environments
333#[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/// Initialize development logging with pretty formatting
358#[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/// Initialize minimal logging for testing
380#[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/// Create a logging configuration from CommonConfig
396#[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}