use std::sync::OnceLock;
pub const ENV_COMPOSIO_LOGGING_LEVEL: &str = "COMPOSIO_LOGGING_LEVEL";
pub const ENV_COMPOSIO_LOG_VERBOSITY: &str = "COMPOSIO_LOG_VERBOSITY";
const DEFAULT_LOGGER_NAME: &str = "composio";
static VERBOSITY: OnceLock<u8> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verbosity {
Minimal = 0,
Normal = 1,
Verbose = 2,
Full = 3,
}
impl Verbosity {
pub fn max_line_size(self) -> Option<usize> {
match self {
Verbosity::Minimal => Some(256),
Verbosity::Normal => Some(512),
Verbosity::Verbose => Some(1024),
Verbosity::Full => None,
}
}
fn from_env() -> Self {
std::env::var(ENV_COMPOSIO_LOG_VERBOSITY)
.ok()
.and_then(|v| v.parse::<u8>().ok())
.and_then(Self::from_u8)
.unwrap_or(Verbosity::Minimal)
}
fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Verbosity::Minimal),
1 => Some(Verbosity::Normal),
2 => Some(Verbosity::Verbose),
3 => Some(Verbosity::Full),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Critical,
Fatal,
Error,
Warning,
Warn,
Info,
Debug,
NotSet,
}
impl LogLevel {
pub fn from_env() -> Option<Self> {
std::env::var(ENV_COMPOSIO_LOGGING_LEVEL)
.ok()
.and_then(|s| Self::from_str(&s))
}
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"critical" => Some(LogLevel::Critical),
"fatal" => Some(LogLevel::Fatal),
"error" => Some(LogLevel::Error),
"warning" => Some(LogLevel::Warning),
"warn" => Some(LogLevel::Warn),
"info" => Some(LogLevel::Info),
"debug" => Some(LogLevel::Debug),
"notset" => Some(LogLevel::NotSet),
_ => None,
}
}
#[cfg(feature = "local-debug")]
pub fn to_tracing_level(self) -> tracing::Level {
match self {
LogLevel::Critical | LogLevel::Fatal => tracing::Level::ERROR,
LogLevel::Error => tracing::Level::ERROR,
LogLevel::Warning | LogLevel::Warn => tracing::Level::WARN,
LogLevel::Info => tracing::Level::INFO,
LogLevel::Debug => tracing::Level::DEBUG,
LogLevel::NotSet => tracing::Level::INFO,
}
}
}
pub fn get_verbosity() -> Verbosity {
let level = *VERBOSITY.get_or_init(|| Verbosity::from_env() as u8);
Verbosity::from_u8(level).unwrap_or(Verbosity::Minimal)
}
pub fn set_verbosity(verbosity: Verbosity) {
let _ = VERBOSITY.set(verbosity as u8);
}
pub fn truncate_message(msg: &str) -> String {
let verbosity = get_verbosity();
match verbosity.max_line_size() {
None => msg.to_string(),
Some(max_size) => {
if msg.len() <= max_size {
msg.to_string()
} else {
format!("{}...", &msg[..max_size])
}
}
}
}
pub fn setup(level: LogLevel) {
#[cfg(feature = "local-debug")]
{
let tracing_level = level.to_tracing_level();
let _ = tracing_subscriber::fmt()
.with_max_level(tracing_level)
.with_target(true)
.with_thread_ids(false)
.with_line_number(true)
.with_file(true)
.try_init();
}
#[cfg(not(feature = "local-debug"))]
{
let _ = level;
}
}
pub fn setup_from_env() {
let level = LogLevel::from_env().unwrap_or(LogLevel::Info);
setup(level);
}
pub trait WithLogger {
fn logger_name(&self) -> &str {
DEFAULT_LOGGER_NAME
}
fn log_info(&self, msg: &str) {
#[cfg(feature = "local-debug")]
{
let truncated = truncate_message(msg);
tracing::info!("[{}] {}", self.logger_name(), truncated);
}
#[cfg(not(feature = "local-debug"))]
{
let _ = msg;
}
}
fn log_debug(&self, msg: &str) {
#[cfg(feature = "local-debug")]
{
let truncated = truncate_message(msg);
tracing::debug!("[{}] {}", self.logger_name(), truncated);
}
#[cfg(not(feature = "local-debug"))]
{
let _ = msg;
}
}
fn log_warning(&self, msg: &str) {
#[cfg(feature = "local-debug")]
{
tracing::warn!("[{}] {}", self.logger_name(), msg);
}
#[cfg(not(feature = "local-debug"))]
{
let _ = msg;
}
}
fn log_error(&self, msg: &str) {
#[cfg(feature = "local-debug")]
{
tracing::error!("[{}] {}", self.logger_name(), msg);
}
#[cfg(not(feature = "local-debug"))]
{
let _ = msg;
}
}
}
pub fn log_error(error: &crate::error::ComposioError, context: Option<&str>) {
use crate::error::ComposioError;
let verbosity = get_verbosity();
let prefix = if let Some(ctx) = context {
format!("[{}] ", ctx)
} else {
String::new()
};
let message = match error {
ComposioError::ApiError { status: 400, .. } => {
format!("{}Validation Error:\n{}", prefix, error.format_validation_error())
}
ComposioError::ValidationError(_) => {
format!("{}{}", prefix, error.format_validation_error())
}
_ => {
format!("{}{}", prefix, error)
}
};
match verbosity {
Verbosity::Minimal => {
eprintln!("{}", truncate_message(&message));
}
Verbosity::Normal => {
eprintln!("{}", message);
}
Verbosity::Verbose => {
eprintln!("{}", message);
if let ComposioError::ApiError { request_id: Some(req_id), .. } = error {
eprintln!("Request ID: {}", req_id);
}
}
Verbosity::Full => {
eprintln!("{}", message);
eprintln!("Error details: {:?}", error);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verbosity_max_line_size() {
assert_eq!(Verbosity::Minimal.max_line_size(), Some(256));
assert_eq!(Verbosity::Normal.max_line_size(), Some(512));
assert_eq!(Verbosity::Verbose.max_line_size(), Some(1024));
assert_eq!(Verbosity::Full.max_line_size(), None);
}
#[test]
fn test_truncate_message() {
let short_msg = "Short message";
let result = truncate_message(short_msg);
assert_eq!(result, short_msg);
let long_msg = "a".repeat(2000);
let result = truncate_message(&long_msg);
if result.len() < long_msg.len() {
assert!(result.ends_with("..."), "Truncated message should end with ...");
} else {
assert_eq!(result, long_msg);
}
}
#[test]
fn test_log_level_from_str() {
assert_eq!(LogLevel::from_str("debug"), Some(LogLevel::Debug));
assert_eq!(LogLevel::from_str("DEBUG"), Some(LogLevel::Debug));
assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info));
assert_eq!(LogLevel::from_str("error"), Some(LogLevel::Error));
assert_eq!(LogLevel::from_str("invalid"), None);
}
#[test]
fn test_verbosity_from_u8() {
assert_eq!(Verbosity::from_u8(0), Some(Verbosity::Minimal));
assert_eq!(Verbosity::from_u8(1), Some(Verbosity::Normal));
assert_eq!(Verbosity::from_u8(2), Some(Verbosity::Verbose));
assert_eq!(Verbosity::from_u8(3), Some(Verbosity::Full));
assert_eq!(Verbosity::from_u8(4), None);
}
struct TestLogger {
name: String,
}
impl WithLogger for TestLogger {
fn logger_name(&self) -> &str {
&self.name
}
}
#[test]
fn test_with_logger_trait() {
let logger = TestLogger {
name: "test_logger".to_string(),
};
assert_eq!(logger.logger_name(), "test_logger");
logger.log_info("Test info message");
logger.log_debug("Test debug message");
logger.log_warning("Test warning message");
logger.log_error("Test error message");
}
#[test]
fn test_log_error_with_validation_error() {
use crate::error::{ComposioError, ErrorDetail};
let error = ComposioError::ApiError {
status: 400,
message: "Validation failed".to_string(),
code: Some("VALIDATION_ERROR".to_string()),
slug: None,
request_id: Some("req_test123".to_string()),
suggested_fix: Some("Check your input".to_string()),
errors: Some(vec![
ErrorDetail {
field: Some("user_id".to_string()),
message: "Field required".to_string(),
},
]),
};
log_error(&error, Some("Test context"));
log_error(&error, None);
}
#[test]
fn test_log_error_with_other_errors() {
use crate::error::ComposioError;
let error1 = ComposioError::ConfigError("Invalid config".to_string());
log_error(&error1, Some("Configuration"));
let error2 = ComposioError::ValidationError("Invalid input".to_string());
log_error(&error2, None);
}
}