Skip to main content

composio_sdk/utils/
logging.rs

1//! Logging utilities for Composio SDK
2//!
3//! This module provides logging configuration with verbosity control,
4//! message truncation, and environment-based setup.
5//!
6//! # Features
7//!
8//! - Environment-based log level configuration
9//! - Verbosity levels (0-3) with automatic message truncation
10//! - Global logger access via `get_logger()`
11//! - Optional `local-debug` feature for detailed tracing
12//!
13//! # Examples
14//!
15//! ```no_run
16//! use composio_sdk::logging::{setup, LogLevel};
17//!
18//! // Setup logging with INFO level
19//! setup(LogLevel::Info);
20//!
21//! // Log with automatic truncation based on verbosity
22//! log::info!("Processing large payload...");
23//! ```
24
25use std::sync::OnceLock;
26
27/// Environment variable for log level configuration
28pub const ENV_COMPOSIO_LOGGING_LEVEL: &str = "COMPOSIO_LOGGING_LEVEL";
29
30/// Environment variable for log verbosity (0-3)
31pub const ENV_COMPOSIO_LOG_VERBOSITY: &str = "COMPOSIO_LOG_VERBOSITY";
32
33/// Default logger name
34const DEFAULT_LOGGER_NAME: &str = "composio";
35
36/// Global verbosity level
37static VERBOSITY: OnceLock<u8> = OnceLock::new();
38
39/// Log verbosity levels
40///
41/// Controls how much detail is included in log messages:
42/// - Level 0: Minimal (256 chars)
43/// - Level 1: Normal (512 chars)
44/// - Level 2: Verbose (1024 chars)
45/// - Level 3: Full (unlimited)
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Verbosity {
48    /// Minimal verbosity (256 chars max)
49    Minimal = 0,
50    /// Normal verbosity (512 chars max)
51    Normal = 1,
52    /// Verbose (1024 chars max)
53    Verbose = 2,
54    /// Full verbosity (no truncation)
55    Full = 3,
56}
57
58impl Verbosity {
59    /// Get max line size for this verbosity level
60    pub fn max_line_size(self) -> Option<usize> {
61        match self {
62            Verbosity::Minimal => Some(256),
63            Verbosity::Normal => Some(512),
64            Verbosity::Verbose => Some(1024),
65            Verbosity::Full => None,
66        }
67    }
68
69    /// Parse verbosity from environment variable
70    fn from_env() -> Self {
71        std::env::var(ENV_COMPOSIO_LOG_VERBOSITY)
72            .ok()
73            .and_then(|v| v.parse::<u8>().ok())
74            .and_then(Self::from_u8)
75            .unwrap_or(Verbosity::Minimal)
76    }
77
78    /// Convert u8 to Verbosity
79    fn from_u8(value: u8) -> Option<Self> {
80        match value {
81            0 => Some(Verbosity::Minimal),
82            1 => Some(Verbosity::Normal),
83            2 => Some(Verbosity::Verbose),
84            3 => Some(Verbosity::Full),
85            _ => None,
86        }
87    }
88}
89
90/// Log levels supported by Composio SDK
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum LogLevel {
93    /// Critical errors
94    Critical,
95    /// Fatal errors (alias for Critical)
96    Fatal,
97    /// Errors
98    Error,
99    /// Warnings
100    Warning,
101    /// Warnings (alias)
102    Warn,
103    /// Informational messages
104    Info,
105    /// Debug messages
106    Debug,
107    /// Not set (inherit from parent)
108    NotSet,
109}
110
111impl LogLevel {
112    /// Parse log level from environment variable
113    pub fn from_env() -> Option<Self> {
114        std::env::var(ENV_COMPOSIO_LOGGING_LEVEL)
115            .ok()
116            .and_then(|s| Self::from_str(&s))
117    }
118
119    /// Parse log level from string
120    pub fn from_str(s: &str) -> Option<Self> {
121        match s.to_lowercase().as_str() {
122            "critical" => Some(LogLevel::Critical),
123            "fatal" => Some(LogLevel::Fatal),
124            "error" => Some(LogLevel::Error),
125            "warning" => Some(LogLevel::Warning),
126            "warn" => Some(LogLevel::Warn),
127            "info" => Some(LogLevel::Info),
128            "debug" => Some(LogLevel::Debug),
129            "notset" => Some(LogLevel::NotSet),
130            _ => None,
131        }
132    }
133
134    /// Convert to log crate level
135    #[cfg(feature = "local-debug")]
136    pub fn to_tracing_level(self) -> tracing::Level {
137        match self {
138            LogLevel::Critical | LogLevel::Fatal => tracing::Level::ERROR,
139            LogLevel::Error => tracing::Level::ERROR,
140            LogLevel::Warning | LogLevel::Warn => tracing::Level::WARN,
141            LogLevel::Info => tracing::Level::INFO,
142            LogLevel::Debug => tracing::Level::DEBUG,
143            LogLevel::NotSet => tracing::Level::INFO,
144        }
145    }
146}
147
148/// Get current verbosity level
149pub fn get_verbosity() -> Verbosity {
150    let level = *VERBOSITY.get_or_init(|| Verbosity::from_env() as u8);
151    Verbosity::from_u8(level).unwrap_or(Verbosity::Minimal)
152}
153
154/// Set verbosity level
155pub fn set_verbosity(verbosity: Verbosity) {
156    let _ = VERBOSITY.set(verbosity as u8);
157}
158
159/// Truncate message based on current verbosity level
160pub fn truncate_message(msg: &str) -> String {
161    let verbosity = get_verbosity();
162    
163    match verbosity.max_line_size() {
164        None => msg.to_string(),
165        Some(max_size) => {
166            if msg.len() <= max_size {
167                msg.to_string()
168            } else {
169                format!("{}...", &msg[..max_size])
170            }
171        }
172    }
173}
174
175/// Setup logging with specified level
176///
177/// This function initializes the logging system with the given log level.
178/// If the `local-debug` feature is enabled, it uses `tracing_subscriber`.
179///
180/// # Arguments
181///
182/// * `level` - The log level to use
183///
184/// # Examples
185///
186/// ```no_run
187/// use composio_sdk::logging::{setup, LogLevel};
188///
189/// setup(LogLevel::Debug);
190/// ```
191pub fn setup(level: LogLevel) {
192    #[cfg(feature = "local-debug")]
193    {
194        let tracing_level = level.to_tracing_level();
195        
196        let _ = tracing_subscriber::fmt()
197            .with_max_level(tracing_level)
198            .with_target(true)
199            .with_thread_ids(false)
200            .with_line_number(true)
201            .with_file(true)
202            .try_init();
203    }
204
205    #[cfg(not(feature = "local-debug"))]
206    {
207        // When local-debug is not enabled, logging is a no-op
208        let _ = level;
209    }
210}
211
212/// Setup logging from environment variables
213///
214/// Reads `COMPOSIO_LOGGING_LEVEL` and `COMPOSIO_LOG_VERBOSITY` from environment.
215///
216/// # Examples
217///
218/// ```no_run
219/// use composio_sdk::logging::setup_from_env;
220///
221/// // Reads COMPOSIO_LOGGING_LEVEL=debug
222/// setup_from_env();
223/// ```
224pub fn setup_from_env() {
225    let level = LogLevel::from_env().unwrap_or(LogLevel::Info);
226    setup(level);
227}
228
229/// Trait for types that can have logging capabilities
230///
231/// This trait provides a standard way to add logging to any type.
232/// It's the Rust equivalent of Python's `WithLogger` mixin class.
233///
234/// # Examples
235///
236/// ```no_run
237/// use composio_sdk::logging::WithLogger;
238///
239/// struct MyService {
240///     logger_name: String,
241/// }
242///
243/// impl WithLogger for MyService {
244///     fn logger_name(&self) -> &str {
245///         &self.logger_name
246///     }
247/// }
248/// ```
249pub trait WithLogger {
250    /// Get the logger name for this type
251    fn logger_name(&self) -> &str {
252        DEFAULT_LOGGER_NAME
253    }
254
255    /// Log an info message with truncation
256    fn log_info(&self, msg: &str) {
257        #[cfg(feature = "local-debug")]
258        {
259            let truncated = truncate_message(msg);
260            tracing::info!("[{}] {}", self.logger_name(), truncated);
261        }
262        #[cfg(not(feature = "local-debug"))]
263        {
264            let _ = msg;
265        }
266    }
267
268    /// Log a debug message with truncation
269    fn log_debug(&self, msg: &str) {
270        #[cfg(feature = "local-debug")]
271        {
272            let truncated = truncate_message(msg);
273            tracing::debug!("[{}] {}", self.logger_name(), truncated);
274        }
275        #[cfg(not(feature = "local-debug"))]
276        {
277            let _ = msg;
278        }
279    }
280
281    /// Log a warning message (no truncation for warnings)
282    fn log_warning(&self, msg: &str) {
283        #[cfg(feature = "local-debug")]
284        {
285            tracing::warn!("[{}] {}", self.logger_name(), msg);
286        }
287        #[cfg(not(feature = "local-debug"))]
288        {
289            let _ = msg;
290        }
291    }
292
293    /// Log an error message (no truncation for errors)
294    fn log_error(&self, msg: &str) {
295        #[cfg(feature = "local-debug")]
296        {
297            tracing::error!("[{}] {}", self.logger_name(), msg);
298        }
299        #[cfg(not(feature = "local-debug"))]
300        {
301            let _ = msg;
302        }
303    }
304}
305
306/// Log an error with appropriate verbosity level
307///
308/// This function logs errors with context-aware formatting:
309/// - For validation errors (400), uses detailed formatting
310/// - For other errors, uses standard display
311/// - Respects the current verbosity level
312///
313/// # Arguments
314///
315/// * `error` - The error to log
316/// * `context` - Optional context string (e.g., "Session creation", "Tool execution")
317///
318/// # Example
319///
320/// ```rust
321/// use composio_sdk::utils::logging::log_error;
322/// use composio_sdk::error::ComposioError;
323///
324/// let error = ComposioError::ValidationError("Invalid input".to_string());
325/// log_error(&error, Some("Creating session"));
326/// ```
327pub fn log_error(error: &crate::error::ComposioError, context: Option<&str>) {
328    use crate::error::ComposioError;
329    
330    let verbosity = get_verbosity();
331    
332    let prefix = if let Some(ctx) = context {
333        format!("[{}] ", ctx)
334    } else {
335        String::new()
336    };
337    
338    // Use detailed formatting for validation errors
339    let message = match error {
340        ComposioError::ApiError { status: 400, .. } => {
341            format!("{}Validation Error:\n{}", prefix, error.format_validation_error())
342        }
343        ComposioError::ValidationError(_) => {
344            format!("{}{}", prefix, error.format_validation_error())
345        }
346        _ => {
347            format!("{}{}", prefix, error)
348        }
349    };
350    
351    // Log based on verbosity
352    match verbosity {
353        Verbosity::Minimal => {
354            eprintln!("{}", truncate_message(&message));
355        }
356        Verbosity::Normal => {
357            eprintln!("{}", message);
358        }
359        Verbosity::Verbose => {
360            eprintln!("{}", message);
361            if let ComposioError::ApiError { request_id: Some(req_id), .. } = error {
362                eprintln!("Request ID: {}", req_id);
363            }
364        }
365        Verbosity::Full => {
366            eprintln!("{}", message);
367            eprintln!("Error details: {:?}", error);
368        }
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_verbosity_max_line_size() {
378        assert_eq!(Verbosity::Minimal.max_line_size(), Some(256));
379        assert_eq!(Verbosity::Normal.max_line_size(), Some(512));
380        assert_eq!(Verbosity::Verbose.max_line_size(), Some(1024));
381        assert_eq!(Verbosity::Full.max_line_size(), None);
382    }
383
384    #[test]
385    fn test_truncate_message() {
386        // Note: We can't reliably test set_verbosity in unit tests because
387        // OnceLock can only be set once per process. Instead, we test the
388        // truncation logic directly.
389        
390        let short_msg = "Short message";
391        let result = truncate_message(short_msg);
392        // Short messages should never be truncated
393        assert_eq!(result, short_msg);
394        
395        // Test with a very long message that should be truncated
396        // regardless of verbosity level (unless Full)
397        let long_msg = "a".repeat(2000);
398        let result = truncate_message(&long_msg);
399        
400        // The result should either be the full message (if verbosity is Full)
401        // or truncated (if any other verbosity level)
402        if result.len() < long_msg.len() {
403            // Message was truncated
404            assert!(result.ends_with("..."), "Truncated message should end with ...");
405        } else {
406            // Message was not truncated (verbosity is Full)
407            assert_eq!(result, long_msg);
408        }
409    }
410
411    #[test]
412    fn test_log_level_from_str() {
413        assert_eq!(LogLevel::from_str("debug"), Some(LogLevel::Debug));
414        assert_eq!(LogLevel::from_str("DEBUG"), Some(LogLevel::Debug));
415        assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info));
416        assert_eq!(LogLevel::from_str("error"), Some(LogLevel::Error));
417        assert_eq!(LogLevel::from_str("invalid"), None);
418    }
419
420    #[test]
421    fn test_verbosity_from_u8() {
422        assert_eq!(Verbosity::from_u8(0), Some(Verbosity::Minimal));
423        assert_eq!(Verbosity::from_u8(1), Some(Verbosity::Normal));
424        assert_eq!(Verbosity::from_u8(2), Some(Verbosity::Verbose));
425        assert_eq!(Verbosity::from_u8(3), Some(Verbosity::Full));
426        assert_eq!(Verbosity::from_u8(4), None);
427    }
428
429    struct TestLogger {
430        name: String,
431    }
432
433    impl WithLogger for TestLogger {
434        fn logger_name(&self) -> &str {
435            &self.name
436        }
437    }
438
439    #[test]
440    fn test_with_logger_trait() {
441        let logger = TestLogger {
442            name: "test_logger".to_string(),
443        };
444        
445        assert_eq!(logger.logger_name(), "test_logger");
446        
447        // These should not panic
448        logger.log_info("Test info message");
449        logger.log_debug("Test debug message");
450        logger.log_warning("Test warning message");
451        logger.log_error("Test error message");
452    }
453
454    #[test]
455    fn test_log_error_with_validation_error() {
456        use crate::error::{ComposioError, ErrorDetail};
457        
458        let error = ComposioError::ApiError {
459            status: 400,
460            message: "Validation failed".to_string(),
461            code: Some("VALIDATION_ERROR".to_string()),
462            slug: None,
463            request_id: Some("req_test123".to_string()),
464            suggested_fix: Some("Check your input".to_string()),
465            errors: Some(vec![
466                ErrorDetail {
467                    field: Some("user_id".to_string()),
468                    message: "Field required".to_string(),
469                },
470            ]),
471        };
472        
473        // This test just ensures the function doesn't panic
474        // Actual output would go to stderr
475        log_error(&error, Some("Test context"));
476        log_error(&error, None);
477    }
478
479    #[test]
480    fn test_log_error_with_other_errors() {
481        use crate::error::ComposioError;
482        
483        let error1 = ComposioError::ConfigError("Invalid config".to_string());
484        log_error(&error1, Some("Configuration"));
485        
486        let error2 = ComposioError::ValidationError("Invalid input".to_string());
487        log_error(&error2, None);
488    }
489}