1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use vtcode_commons::VtCodePaths;
7
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "lowercase")]
12#[derive(Default)]
13pub enum TraceLevel {
14 Error,
15 Warn,
16 #[default]
17 Info,
18 Debug,
19 Trace,
20}
21
22impl TraceLevel {
23 pub fn as_str(self) -> &'static str {
24 match self {
25 Self::Error => "error",
26 Self::Warn => "warn",
27 Self::Info => "info",
28 Self::Debug => "debug",
29 Self::Trace => "trace",
30 }
31 }
32
33 fn parse(value: &str) -> Option<Self> {
34 match value.trim().to_lowercase().as_str() {
35 "error" => Some(Self::Error),
36 "warn" => Some(Self::Warn),
37 "info" => Some(Self::Info),
38 "debug" => Some(Self::Debug),
39 "trace" => Some(Self::Trace),
40 _ => None,
41 }
42 }
43}
44
45impl std::fmt::Display for TraceLevel {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.write_str(self.as_str())
48 }
49}
50
51impl<'de> Deserialize<'de> for TraceLevel {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: serde::Deserializer<'de>,
55 {
56 let raw = String::deserialize(deserializer)?;
57 if let Some(parsed) = Self::parse(&raw) {
58 Ok(parsed)
59 } else {
60 Ok(Self::default())
61 }
62 }
63}
64
65#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct DebugConfig {
69 #[serde(default)]
71 pub enable_tracing: bool,
72
73 #[serde(default)]
75 pub trace_level: TraceLevel,
76
77 #[serde(default)]
80 pub trace_targets: Vec<String>,
81
82 #[serde(default)]
84 pub debug_log_dir: Option<String>,
85
86 #[serde(default = "default_max_debug_log_size_mb")]
88 pub max_debug_log_size_mb: u64,
89
90 #[serde(default = "default_max_debug_log_age_days")]
92 pub max_debug_log_age_days: u32,
93}
94
95fn default_max_debug_log_size_mb() -> u64 {
96 50
97}
98
99fn default_max_debug_log_age_days() -> u32 {
100 7
101}
102
103impl Default for DebugConfig {
104 fn default() -> Self {
105 Self {
106 enable_tracing: false,
107 trace_level: TraceLevel::Info,
108 trace_targets: Vec::new(),
109 debug_log_dir: None,
110 max_debug_log_size_mb: 50,
111 max_debug_log_age_days: 7,
112 }
113 }
114}
115
116impl DebugConfig {
117 pub fn debug_log_path(&self) -> Result<PathBuf> {
119 if let Some(dir) = self.debug_log_dir.as_ref() {
120 if dir.starts_with("~") {
121 let home = dirs::home_dir().context("could not resolve home directory for debug logs")?;
122 return Ok(home.join(dir.trim_start_matches('~').trim_start_matches('/')));
123 }
124 return Ok(PathBuf::from(dir));
125 }
126
127 VtCodePaths::resolve()
128 .map(|paths| paths.logs_dir())
129 .context("could not resolve VT Code state directory for debug logs")
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_trace_level_parsing() {
139 assert_eq!(TraceLevel::parse("error"), Some(TraceLevel::Error));
140 assert_eq!(TraceLevel::parse("WARN"), Some(TraceLevel::Warn));
141 assert_eq!(TraceLevel::parse("info"), Some(TraceLevel::Info));
142 assert_eq!(TraceLevel::parse("DEBUG"), Some(TraceLevel::Debug));
143 assert_eq!(TraceLevel::parse("trace"), Some(TraceLevel::Trace));
144 assert_eq!(TraceLevel::parse("invalid"), None);
145 }
146
147 #[test]
148 fn test_debug_config_default() {
149 let cfg = DebugConfig::default();
150 assert!(!cfg.enable_tracing);
151 assert_eq!(cfg.trace_level, TraceLevel::Info);
152 assert!(cfg.trace_targets.is_empty());
153 assert_eq!(cfg.max_debug_log_size_mb, 50);
154 assert_eq!(cfg.max_debug_log_age_days, 7);
155 }
156
157 #[test]
158 fn test_debug_log_path_defaults_to_state_logs_dir() {
159 let cfg = DebugConfig::default();
160 let expected = VtCodePaths::resolve().expect("path resolver").logs_dir();
161 assert_eq!(cfg.debug_log_path().expect("debug log path"), expected);
162 }
163}