Skip to main content

vtcode_config/
debug.rs

1//! Debug and tracing configuration
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use vtcode_commons::VtCodePaths;
7
8/// Trace level for structured logging
9#[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/// Debug and tracing configuration
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67#[derive(Debug, Clone, Deserialize, Serialize)]
68pub struct DebugConfig {
69    /// Enable structured logging for development and troubleshooting
70    #[serde(default)]
71    pub enable_tracing: bool,
72
73    /// Trace level (error, warn, info, debug, trace)
74    #[serde(default)]
75    pub trace_level: TraceLevel,
76
77    /// List of tracing targets to enable
78    /// Examples: "vtcode_core::agent", "vtcode_core::tools", "vtcode::*"
79    #[serde(default)]
80    pub trace_targets: Vec<String>,
81
82    /// Directory for debug logs
83    #[serde(default)]
84    pub debug_log_dir: Option<String>,
85
86    /// Maximum size of debug logs before rotating (in MB)
87    #[serde(default = "default_max_debug_log_size_mb")]
88    pub max_debug_log_size_mb: u64,
89
90    /// Maximum age of debug logs to keep (in days)
91    #[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        // Every per-field serde default (`max_debug_log_size_mb`,
106        // `max_debug_log_age_days`, `enabled`, …) must also apply to a config
107        // built with `DebugConfig::default()`. Calling the `default_*()` fns
108        // keeps that in sync; restating their values here would silently keep
109        // the old number when one of them changes.
110        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    /// Get the debug log directory, expanding ~ to home directory.
123    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    /// `TraceLevel` keeps its `as_str()`/`parse()`/serde paths in lockstep; see
153    /// [`crate::test_support::assert_string_enum_lockstep`].
154    #[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}