1use std::sync::OnceLock;
26
27pub const ENV_COMPOSIO_LOGGING_LEVEL: &str = "COMPOSIO_LOGGING_LEVEL";
29
30pub const ENV_COMPOSIO_LOG_VERBOSITY: &str = "COMPOSIO_LOG_VERBOSITY";
32
33const DEFAULT_LOGGER_NAME: &str = "composio";
35
36static VERBOSITY: OnceLock<u8> = OnceLock::new();
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Verbosity {
48 Minimal = 0,
50 Normal = 1,
52 Verbose = 2,
54 Full = 3,
56}
57
58impl Verbosity {
59 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum LogLevel {
93 Critical,
95 Fatal,
97 Error,
99 Warning,
101 Warn,
103 Info,
105 Debug,
107 NotSet,
109}
110
111impl LogLevel {
112 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 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 #[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
148pub 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
154pub fn set_verbosity(verbosity: Verbosity) {
156 let _ = VERBOSITY.set(verbosity as u8);
157}
158
159pub 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
175pub 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 let _ = level;
209 }
210}
211
212pub fn setup_from_env() {
225 let level = LogLevel::from_env().unwrap_or(LogLevel::Info);
226 setup(level);
227}
228
229pub trait WithLogger {
250 fn logger_name(&self) -> &str {
252 DEFAULT_LOGGER_NAME
253 }
254
255 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 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 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 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
306pub 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 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 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 let short_msg = "Short message";
391 let result = truncate_message(short_msg);
392 assert_eq!(result, short_msg);
394
395 let long_msg = "a".repeat(2000);
398 let result = truncate_message(&long_msg);
399
400 if result.len() < long_msg.len() {
403 assert!(result.ends_with("..."), "Truncated message should end with ...");
405 } else {
406 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 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 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}