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 {
111 enable_tracing: false,
112 trace_level: TraceLevel::Info,
113 trace_targets: Vec::new(),
114 debug_log_dir: None,
115 max_debug_log_size_mb: default_max_debug_log_size_mb(),
116 max_debug_log_age_days: default_max_debug_log_age_days(),
117 }
118 }
119}
120
121impl DebugConfig {
122 pub fn debug_log_path(&self) -> Result<PathBuf> {
124 if let Some(dir) = self.debug_log_dir.as_ref() {
125 if dir.starts_with("~") {
126 let home = dirs::home_dir().context("could not resolve home directory for debug logs")?;
127 return Ok(home.join(dir.trim_start_matches('~').trim_start_matches('/')));
128 }
129 return Ok(PathBuf::from(dir));
130 }
131
132 VtCodePaths::resolve()
133 .map(|paths| paths.logs_dir())
134 .context("could not resolve VT Code state directory for debug logs")
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_trace_level_parsing() {
144 assert_eq!(TraceLevel::parse("error"), Some(TraceLevel::Error));
145 assert_eq!(TraceLevel::parse("WARN"), Some(TraceLevel::Warn));
146 assert_eq!(TraceLevel::parse("info"), Some(TraceLevel::Info));
147 assert_eq!(TraceLevel::parse("DEBUG"), Some(TraceLevel::Debug));
148 assert_eq!(TraceLevel::parse("trace"), Some(TraceLevel::Trace));
149 assert_eq!(TraceLevel::parse("invalid"), None);
150 }
151
152 #[test]
155 fn trace_level_string_paths_stay_in_lockstep() {
156 use crate::test_support::assert_string_enum_lockstep;
157
158 assert_string_enum_lockstep!(
159 TraceLevel,
160 [
161 TraceLevel::Error,
162 TraceLevel::Warn,
163 TraceLevel::Info,
164 TraceLevel::Debug,
165 TraceLevel::Trace,
166 ]
167 );
168 }
169
170 #[test]
171 fn test_debug_config_default() {
172 let cfg = DebugConfig::default();
173 assert!(!cfg.enable_tracing);
174 assert_eq!(cfg.trace_level, TraceLevel::Info);
175 assert!(cfg.trace_targets.is_empty());
176 assert_eq!(cfg.max_debug_log_size_mb, 50);
177 assert_eq!(cfg.max_debug_log_age_days, 7);
178 }
179
180 #[test]
181 fn test_debug_log_path_defaults_to_state_logs_dir() {
182 let cfg = DebugConfig::default();
183 let expected = VtCodePaths::resolve().expect("path resolver").logs_dir();
184 assert_eq!(cfg.debug_log_path().expect("debug log path"), expected);
185 }
186}