statsig-rust 0.19.1-beta.2604130314

Statsig Rust SDK for usage in multi-user server environments.
Documentation
use log::{debug, error, info, warn, Level};
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

use crate::logging_utils::sanitize_secret_key;
const MAX_CHARS: usize = 400;
const TRUNCATED_SUFFIX: &str = "...[TRUNCATED]";

const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Warn;

lazy_static::lazy_static! {
    static ref LOGGER_STATE: RwLock<LoggerState> = RwLock::new(LoggerState {
        level: DEFAULT_LOG_LEVEL,
        provider: None,
    });
}

struct LoggerState {
    level: LogLevel,
    provider: Option<Arc<dyn OutputLogProvider>>,
}

static INITIALIZED: AtomicBool = AtomicBool::new(false);

#[derive(Clone, Debug)]
pub enum LogLevel {
    None,
    Debug,
    Info,
    Warn,
    Error,
}

impl From<&str> for LogLevel {
    fn from(level: &str) -> Self {
        match level.to_lowercase().as_str() {
            "debug" => LogLevel::Debug,
            "info" => LogLevel::Info,
            "warn" => LogLevel::Warn,
            "error" => LogLevel::Error,
            "none" => LogLevel::None,
            _ => DEFAULT_LOG_LEVEL,
        }
    }
}

impl From<u32> for LogLevel {
    fn from(level: u32) -> Self {
        match level {
            0 => LogLevel::None,
            1 => LogLevel::Error,
            2 => LogLevel::Warn,
            3 => LogLevel::Info,
            4 => LogLevel::Debug,
            _ => DEFAULT_LOG_LEVEL,
        }
    }
}

impl LogLevel {
    fn to_third_party_level(&self) -> Option<Level> {
        match self {
            LogLevel::Debug => Some(Level::Debug),
            LogLevel::Info => Some(Level::Info),
            LogLevel::Warn => Some(Level::Warn),
            LogLevel::Error => Some(Level::Error),
            LogLevel::None => None,
        }
    }

    fn to_number(&self) -> u32 {
        match self {
            LogLevel::Debug => 4,
            LogLevel::Info => 3,
            LogLevel::Warn => 2,
            LogLevel::Error => 1,
            LogLevel::None => 0,
        }
    }
}

pub trait OutputLogProvider: Send + Sync {
    fn initialize(&self);
    fn debug(&self, tag: &str, msg: String);
    fn info(&self, tag: &str, msg: String);
    fn warn(&self, tag: &str, msg: String);
    fn error(&self, tag: &str, msg: String);
    fn shutdown(&self);
}

pub fn initialize_output_logger(
    level: &Option<LogLevel>,
    provider: Option<Arc<dyn OutputLogProvider>>,
) {
    let was_initialized = INITIALIZED.swap(true, Ordering::SeqCst);
    if was_initialized {
        return;
    }

    let mut state = match LOGGER_STATE.try_write_for(Duration::from_secs(5)) {
        Some(state) => state,
        None => {
            eprintln!(
                "[Statsig] Failed to acquire write lock for initialize_output_logger: Failed to lock LOGGER_STATE"
            );
            return;
        }
    };
    let level = level.as_ref().unwrap_or(&DEFAULT_LOG_LEVEL).clone();
    state.level = level.clone();

    if let Some(provider_impl) = provider {
        provider_impl.initialize();
        state.provider = Some(provider_impl);
    } else {
        let final_level = match level {
            LogLevel::None => {
                return;
            }
            _ => match level.to_third_party_level() {
                Some(level) => level,
                None => return,
            },
        };

        match simple_logger::init_with_level(final_level) {
            Ok(()) => {}
            Err(_) => {
                log::set_max_level(final_level.to_level_filter());
            }
        }
    }
}

pub fn shutdown_output_logger() {
    let mut state = match LOGGER_STATE.try_write_for(Duration::from_secs(5)) {
        Some(state) => state,
        None => {
            eprintln!(
                "[Statsig] Failed to acquire write lock for shutdown_output_logger: Failed to lock LOGGER_STATE"
            );
            return;
        }
    };

    if let Some(provider) = &mut state.provider {
        provider.shutdown();
    }

    INITIALIZED.store(false, Ordering::SeqCst);
}

pub fn log_message(tag: &str, level: LogLevel, msg: String) {
    let truncated_msg = if msg.chars().count() > MAX_CHARS {
        let visible_chars = MAX_CHARS.saturating_sub(TRUNCATED_SUFFIX.len());
        format!(
            "{}{}",
            msg.chars().take(visible_chars).collect::<String>(),
            TRUNCATED_SUFFIX
        )
    } else {
        msg
    };

    let sanitized_msg = sanitize_secret_key(&truncated_msg);

    if let Some(state) = LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
        if let Some(provider) = &state.provider {
            match level {
                LogLevel::Debug => provider.debug(tag, sanitized_msg),
                LogLevel::Info => provider.info(tag, sanitized_msg),
                LogLevel::Warn => provider.warn(tag, sanitized_msg),
                LogLevel::Error => provider.error(tag, sanitized_msg),
                _ => {}
            }
            return;
        }
    } else {
        eprintln!("[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE");
    }

    if let Some(level) = level.to_third_party_level() {
        let mut target = String::from("Statsig::");
        target += tag;

        match level {
            Level::Debug => debug!(target: target.as_str(), "{}", sanitized_msg),
            Level::Info => info!(target: target.as_str(), "{}", sanitized_msg),
            Level::Warn => warn!(target: target.as_str(), "{}", sanitized_msg),
            Level::Error => error!(target: target.as_str(), "{}", sanitized_msg),
            _ => {}
        };
    }
}

pub fn has_valid_log_level(level: &LogLevel) -> bool {
    let state = match LOGGER_STATE.try_read_for(Duration::from_secs(5)) {
        Some(state) => state,
        None => {
            eprintln!(
                "[Statsig] Failed to acquire read lock for logger: Failed to lock LOGGER_STATE"
            );
            return false;
        }
    };
    let current_level = &state.level;
    level.to_number() <= current_level.to_number()
}

#[macro_export]
macro_rules! log_d {
  ($tag:expr, $($arg:tt)*) => {
        {
            let level = $crate::output_logger::LogLevel::Debug;
            if $crate::output_logger::has_valid_log_level(&level) {
                $crate::output_logger::log_message($tag, level, format!($($arg)*));
            }
        }
    }
}

#[macro_export]
macro_rules! log_i {
  ($tag:expr, $($arg:tt)*) => {
        {
            let level = $crate::output_logger::LogLevel::Info;
            if $crate::output_logger::has_valid_log_level(&level) {
                $crate::output_logger::log_message($tag, level, format!($($arg)*));
            }
        }
    }
}

#[macro_export]
macro_rules! log_w {
  ($tag:expr, $($arg:tt)*) => {
        {
            let level = $crate::output_logger::LogLevel::Warn;
            if $crate::output_logger::has_valid_log_level(&level) {
                $crate::output_logger::log_message($tag, level, format!($($arg)*));
            }
        }
    }
}

#[macro_export]
macro_rules! log_e {
  ($tag:expr, $($arg:tt)*) => {
        {
            let level = $crate::output_logger::LogLevel::Error;
            if $crate::output_logger::has_valid_log_level(&level) {
                $crate::output_logger::log_message($tag, level, format!($($arg)*));
            }
        }
    }
}

#[macro_export]
macro_rules! log_error_to_statsig_and_console {
    ($ops_stats:expr, $tag:expr, $err:expr) => {
        let event = ErrorBoundaryEvent {
            bypass_dedupe: false,
            exception: $err.name().to_string(),
            info: serde_json::to_string(&$err).unwrap_or_default(),
            tag: $tag.to_string(),
            extra: None,
            dedupe_key: None,
        };
        $ops_stats.log_error(event);

        $crate::output_logger::log_message(
            &$tag,
            $crate::output_logger::LogLevel::Error,
            $err.to_string(),
        );
    };
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    #[test]
    fn test_sanitize_url_for_logging() {
        let test_cases = HashMap::from(
            [
                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json"),
                ("https://api.statsigcdn.com/v1/log_event/","https://api.statsigcdn.com/v1/log_event/"),
                ("https://api.statsigcdn.com/v2/download_config_specs/secret-jadkfjalkjnsdlvcnjsdfaf.json?sinceTime=1", "https://api.statsigcdn.com/v2/download_config_specs/secret-jadkf*****.json?sinceTime=1"),
            ]
        );
        for (before, expected) in test_cases {
            let sanitized = sanitize_secret_key(before);
            assert!(sanitized == expected);
        }
    }

    #[test]
    fn test_multiple_secrets() {
        let input = "Multiple secrets: secret-key1 and secret-key2";
        let sanitized = sanitize_secret_key(input);
        assert_eq!(
            sanitized,
            "Multiple secrets: secret-key1***** and secret-key2*****"
        );
    }

    #[test]
    fn test_short_secret() {
        let input = "Short secret: secret-a";
        let sanitized = sanitize_secret_key(input);
        assert_eq!(sanitized, "Short secret: secret-a*****");
    }
}