use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use vtcode_commons::VtCodePaths;
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum TraceLevel {
Error,
Warn,
#[default]
Info,
Debug,
Trace,
}
impl TraceLevel {
pub fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warn => "warn",
Self::Info => "info",
Self::Debug => "debug",
Self::Trace => "trace",
}
}
fn parse(value: &str) -> Option<Self> {
match value.trim().to_lowercase().as_str() {
"error" => Some(Self::Error),
"warn" => Some(Self::Warn),
"info" => Some(Self::Info),
"debug" => Some(Self::Debug),
"trace" => Some(Self::Trace),
_ => None,
}
}
}
impl std::fmt::Display for TraceLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for TraceLevel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
if let Some(parsed) = Self::parse(&raw) {
Ok(parsed)
} else {
Ok(Self::default())
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DebugConfig {
#[serde(default)]
pub enable_tracing: bool,
#[serde(default)]
pub trace_level: TraceLevel,
#[serde(default)]
pub trace_targets: Vec<String>,
#[serde(default)]
pub debug_log_dir: Option<String>,
#[serde(default = "default_max_debug_log_size_mb")]
pub max_debug_log_size_mb: u64,
#[serde(default = "default_max_debug_log_age_days")]
pub max_debug_log_age_days: u32,
}
fn default_max_debug_log_size_mb() -> u64 {
50
}
fn default_max_debug_log_age_days() -> u32 {
7
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
enable_tracing: false,
trace_level: TraceLevel::Info,
trace_targets: Vec::new(),
debug_log_dir: None,
max_debug_log_size_mb: 50,
max_debug_log_age_days: 7,
}
}
}
impl DebugConfig {
pub fn debug_log_path(&self) -> Result<PathBuf> {
if let Some(dir) = self.debug_log_dir.as_ref() {
if dir.starts_with("~") {
let home = dirs::home_dir().context("could not resolve home directory for debug logs")?;
return Ok(home.join(dir.trim_start_matches('~').trim_start_matches('/')));
}
return Ok(PathBuf::from(dir));
}
VtCodePaths::resolve()
.map(|paths| paths.logs_dir())
.context("could not resolve VT Code state directory for debug logs")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trace_level_parsing() {
assert_eq!(TraceLevel::parse("error"), Some(TraceLevel::Error));
assert_eq!(TraceLevel::parse("WARN"), Some(TraceLevel::Warn));
assert_eq!(TraceLevel::parse("info"), Some(TraceLevel::Info));
assert_eq!(TraceLevel::parse("DEBUG"), Some(TraceLevel::Debug));
assert_eq!(TraceLevel::parse("trace"), Some(TraceLevel::Trace));
assert_eq!(TraceLevel::parse("invalid"), None);
}
#[test]
fn test_debug_config_default() {
let cfg = DebugConfig::default();
assert!(!cfg.enable_tracing);
assert_eq!(cfg.trace_level, TraceLevel::Info);
assert!(cfg.trace_targets.is_empty());
assert_eq!(cfg.max_debug_log_size_mb, 50);
assert_eq!(cfg.max_debug_log_age_days, 7);
}
#[test]
fn test_debug_log_path_defaults_to_state_logs_dir() {
let cfg = DebugConfig::default();
let expected = VtCodePaths::resolve().expect("path resolver").logs_dir();
assert_eq!(cfg.debug_log_path().expect("debug log path"), expected);
}
}