term_guard/
logging.rs

1//! Logging utilities and configuration for Term.
2//!
3//! This module provides utilities for performance-sensitive logging configuration
4//! and best practices for structured logging with OpenTelemetry integration.
5
6use tracing::Level;
7
8/// Logging configuration for Term.
9///
10/// This configuration allows fine-grained control over logging behavior
11/// to ensure minimal performance impact in production environments.
12#[derive(Debug, Clone)]
13pub struct LogConfig {
14    /// Base log level for Term components
15    pub base_level: Level,
16    /// Whether to log constraint evaluation details
17    pub log_constraint_details: bool,
18    /// Whether to log data source operations
19    pub log_data_operations: bool,
20    /// Whether to include metrics in log output
21    pub log_metrics: bool,
22    /// Maximum length for logged field values (to prevent huge logs)
23    pub max_field_length: usize,
24}
25
26impl Default for LogConfig {
27    fn default() -> Self {
28        Self {
29            base_level: Level::INFO,
30            log_constraint_details: false,
31            log_data_operations: true,
32            log_metrics: true,
33            max_field_length: 256,
34        }
35    }
36}
37
38impl LogConfig {
39    /// Creates a verbose configuration suitable for debugging.
40    pub fn verbose() -> Self {
41        Self {
42            base_level: Level::DEBUG,
43            log_constraint_details: true,
44            log_data_operations: true,
45            log_metrics: true,
46            max_field_length: 1024,
47        }
48    }
49
50    /// Creates a minimal configuration for production with lowest overhead.
51    pub fn production() -> Self {
52        Self {
53            base_level: Level::WARN,
54            log_constraint_details: false,
55            log_data_operations: false,
56            log_metrics: false,
57            max_field_length: 128,
58        }
59    }
60
61    /// Creates a balanced configuration suitable for most use cases.
62    pub fn balanced() -> Self {
63        Self::default()
64    }
65}
66
67/// Macro for performance-sensitive debug logging.
68///
69/// This macro only evaluates its arguments if debug logging is enabled,
70/// avoiding the overhead of formatting when logs won't be emitted.
71#[macro_export]
72macro_rules! perf_debug {
73    ($config:expr, $($arg:tt)*) => {
74        if $config.base_level <= tracing::Level::DEBUG {
75            tracing::debug!($($arg)*);
76        }
77    };
78}
79
80/// Macro for conditional constraint logging.
81#[macro_export]
82macro_rules! log_constraint {
83    ($config:expr, $($arg:tt)*) => {
84        if $config.log_constraint_details {
85            tracing::debug!($($arg)*);
86        }
87    };
88}
89
90/// Macro for conditional data operation logging.
91#[macro_export]
92macro_rules! log_data_op {
93    ($config:expr, $($arg:tt)*) => {
94        if $config.log_data_operations {
95            tracing::info!($($arg)*);
96        }
97    };
98}
99
100/// Truncates a string to the maximum field length if needed.
101pub fn truncate_field(value: &str, max_length: usize) -> String {
102    if value.len() <= max_length {
103        value.to_string()
104    } else {
105        let truncated = &value[..max_length];
106        format!("{truncated}...(truncated)")
107    }
108}
109
110/// Utilities for setting up structured logging with OpenTelemetry integration.
111pub mod setup {
112    use tracing::Level;
113
114    /// Configuration for Term's logging setup.
115    #[derive(Debug, Clone)]
116    pub struct LoggingConfig {
117        /// Log level for the application
118        pub level: Level,
119        /// Log level for Term components specifically
120        pub term_level: Level,
121        /// Whether to use JSON output format
122        pub json_format: bool,
123        /// Whether to include trace correlation (requires telemetry feature)
124        pub trace_correlation: bool,
125        /// Environment filter override
126        pub env_filter: Option<String>,
127    }
128
129    impl Default for LoggingConfig {
130        fn default() -> Self {
131            Self {
132                level: Level::INFO,
133                term_level: Level::DEBUG,
134                json_format: false,
135                trace_correlation: false,
136                env_filter: None,
137            }
138        }
139    }
140
141    impl LoggingConfig {
142        /// Creates a configuration for production use.
143        pub fn production() -> Self {
144            Self {
145                level: Level::WARN,
146                term_level: Level::INFO,
147                json_format: true,
148                trace_correlation: true,
149                env_filter: None,
150            }
151        }
152
153        /// Creates a configuration for development use.
154        pub fn development() -> Self {
155            Self {
156                level: Level::DEBUG,
157                term_level: Level::DEBUG,
158                json_format: false,
159                trace_correlation: false,
160                env_filter: None,
161            }
162        }
163
164        /// Creates a configuration for structured logging with trace correlation.
165        pub fn structured() -> Self {
166            Self {
167                level: Level::INFO,
168                term_level: Level::DEBUG,
169                json_format: true,
170                trace_correlation: true,
171                env_filter: None,
172            }
173        }
174
175        /// Sets the log level for the application.
176        pub fn with_level(mut self, level: Level) -> Self {
177            self.level = level;
178            self
179        }
180
181        /// Sets the log level for Term components.
182        pub fn with_term_level(mut self, level: Level) -> Self {
183            self.term_level = level;
184            self
185        }
186
187        /// Sets whether to use JSON output format.
188        pub fn with_json_format(mut self, enabled: bool) -> Self {
189            self.json_format = enabled;
190            self
191        }
192
193        /// Sets whether to include trace correlation.
194        pub fn with_trace_correlation(mut self, enabled: bool) -> Self {
195            self.trace_correlation = enabled;
196            self
197        }
198
199        /// Sets a custom environment filter.
200        pub fn with_env_filter(mut self, filter: impl Into<String>) -> Self {
201            self.env_filter = Some(filter.into());
202            self
203        }
204
205        /// Builds the environment filter string.
206        pub fn env_filter(&self) -> String {
207            if let Some(ref filter) = self.env_filter {
208                filter.clone()
209            } else {
210                format!(
211                    "{}={},term_guard={}",
212                    self.level.as_str().to_lowercase(),
213                    self.level.as_str().to_lowercase(),
214                    self.term_level.as_str().to_lowercase()
215                )
216            }
217        }
218    }
219
220    /// Initializes basic logging without OpenTelemetry.
221    ///
222    /// This is suitable for applications that don't need trace correlation.
223    ///
224    /// # Examples
225    ///
226    /// ```rust,no_run
227    /// use term_guard::logging::setup::{LoggingConfig, init_logging};
228    ///
229    /// // Initialize with default configuration
230    /// init_logging(LoggingConfig::default()).unwrap();
231    ///
232    /// // Initialize with custom configuration
233    /// let config = LoggingConfig::development()
234    ///     .with_json_format(true);
235    /// init_logging(config).unwrap();
236    /// ```
237    pub fn init_logging(config: LoggingConfig) -> Result<(), Box<dyn std::error::Error>> {
238        use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer};
239
240        let env_filter = EnvFilter::try_from_default_env()
241            .unwrap_or_else(|_| EnvFilter::new(config.env_filter()));
242
243        let fmt_layer = if config.json_format {
244            tracing_subscriber::fmt::layer().json().boxed()
245        } else {
246            tracing_subscriber::fmt::layer().boxed()
247        };
248
249        let subscriber = tracing_subscriber::registry()
250            .with(env_filter)
251            .with(fmt_layer);
252
253        subscriber.init();
254
255        Ok(())
256    }
257
258    /// Initializes logging with OpenTelemetry integration for trace correlation.
259    ///
260    /// This requires the `telemetry` feature to be enabled. When telemetry is disabled,
261    /// this function falls back to basic logging.
262    ///
263    /// # Examples
264    ///
265    /// ```rust,ignore
266    /// use term_guard::logging::setup::{LoggingConfig, init_logging_with_telemetry};
267    /// use opentelemetry_sdk::trace::{TracerProvider, Tracer};
268    ///
269    /// // Note: This function is temporarily disabled due to dependency conflicts
270    /// // Create a concrete tracer (not BoxedTracer)
271    /// let provider = TracerProvider::default();
272    /// let tracer = provider.tracer("my-service");
273    ///
274    /// // Initialize structured logging with trace correlation
275    /// let config = LoggingConfig::structured();
276    /// init_logging_with_telemetry(config, tracer).unwrap();
277    /// ```
278    #[cfg(feature = "telemetry")]
279    #[allow(dead_code)] // TODO: Fix compatibility with newer tracing-opentelemetry version
280    pub fn init_logging_with_telemetry<T>(
281        _config: LoggingConfig,
282        _tracer: T,
283    ) -> Result<(), Box<dyn std::error::Error>>
284    where
285        T: tracing_opentelemetry::PreSampledTracer + opentelemetry::trace::Tracer + 'static,
286    {
287        // TODO: This function needs to be updated for compatibility with tracing-opentelemetry 0.26
288        // The layer composition is causing type inference issues. For now, users should
289        // initialize telemetry separately from logging.
290        Err("init_logging_with_telemetry is temporarily disabled due to dependency version conflicts. Please initialize telemetry and logging separately.".into())
291    }
292
293    /// Fallback when telemetry feature is not enabled.
294    #[cfg(not(feature = "telemetry"))]
295    pub fn init_logging_with_telemetry(
296        config: LoggingConfig,
297        _tracer: (),
298    ) -> Result<(), Box<dyn std::error::Error>> {
299        tracing::warn!("Telemetry feature not enabled, falling back to basic logging");
300        init_logging(config)
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_log_config_defaults() {
310        let config = LogConfig::default();
311        assert_eq!(config.base_level, Level::INFO);
312        assert!(!config.log_constraint_details);
313        assert!(config.log_data_operations);
314        assert!(config.log_metrics);
315        assert_eq!(config.max_field_length, 256);
316    }
317
318    #[test]
319    fn test_log_config_verbose() {
320        let config = LogConfig::verbose();
321        assert_eq!(config.base_level, Level::DEBUG);
322        assert!(config.log_constraint_details);
323        assert!(config.log_data_operations);
324        assert!(config.log_metrics);
325        assert_eq!(config.max_field_length, 1024);
326    }
327
328    #[test]
329    fn test_log_config_production() {
330        let config = LogConfig::production();
331        assert_eq!(config.base_level, Level::WARN);
332        assert!(!config.log_constraint_details);
333        assert!(!config.log_data_operations);
334        assert!(!config.log_metrics);
335        assert_eq!(config.max_field_length, 128);
336    }
337
338    #[test]
339    fn test_truncate_field() {
340        let short_text = "hello";
341        assert_eq!(truncate_field(short_text, 10), "hello");
342
343        let long_text = "this is a very long text that should be truncated";
344        assert_eq!(truncate_field(long_text, 10), "this is a ...(truncated)");
345    }
346}