feat(cli): Add verbosity controls, JSON log format, and triple-layer subscriber
Overhaul the CLI logging infrastructure for production observability: CLI flags: - Add -v/-vv/-vvv (--verbose) for progressive stderr verbosity control: 0=INFO, 1=DEBUG app, 2=DEBUG all, 3+=TRACE - Add --log-format text|json for structured stderr output in automation - Existing -q/--quiet overrides verbosity for silent operation Subscriber architecture (main.rs): - Replace single-layer subscriber with triple-layer setup: 1. stderr layer: human-readable or JSON, filtered by -v flags 2. file layer: always-on JSON to daily-rotated logs (lore.YYYY-MM-DD.log) 3. MetricsLayer: captures span timing for robot-mode performance payloads - Parse CLI before subscriber init so verbosity is known at setup time - Load LoggingConfig early (with graceful fallback for pre-init commands) - Clean up old log files before subscriber init to avoid holding deleted handles - Hold WorkerGuard at function scope to ensure flush on exit Doctor command: - Add logging health check: validates log directory exists, reports file count and total size, warns on missing or inaccessible log directory Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ use serde::Serialize;
|
||||
use crate::core::config::Config;
|
||||
use crate::core::db::{create_connection, get_schema_version, verify_pragmas};
|
||||
use crate::core::error::LoreError;
|
||||
use crate::core::paths::{get_config_path, get_db_path};
|
||||
use crate::core::paths::{get_config_path, get_db_path, get_log_dir};
|
||||
use crate::gitlab::GitLabClient;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -37,6 +37,7 @@ pub struct DoctorChecks {
|
||||
pub gitlab: GitLabCheck,
|
||||
pub projects: ProjectsCheck,
|
||||
pub ollama: OllamaCheck,
|
||||
pub logging: LoggingCheck,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -87,6 +88,18 @@ pub struct OllamaCheck {
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LoggingCheck {
|
||||
#[serde(flatten)]
|
||||
pub result: CheckResult,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub log_dir: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_count: Option<usize>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub total_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
/// Run the doctor command.
|
||||
pub async fn run_doctor(config_path: Option<&str>) -> DoctorResult {
|
||||
let config_path_buf = get_config_path(config_path);
|
||||
@@ -107,7 +120,10 @@ pub async fn run_doctor(config_path: Option<&str>) -> DoctorResult {
|
||||
// Check Ollama
|
||||
let ollama_check = check_ollama(config.as_ref()).await;
|
||||
|
||||
// Success if all required checks pass (ollama is optional)
|
||||
// Check logging
|
||||
let logging_check = check_logging(config.as_ref());
|
||||
|
||||
// Success if all required checks pass (ollama and logging are optional)
|
||||
let success = config_check.result.status == CheckStatus::Ok
|
||||
&& database_check.result.status == CheckStatus::Ok
|
||||
&& gitlab_check.result.status == CheckStatus::Ok
|
||||
@@ -121,6 +137,7 @@ pub async fn run_doctor(config_path: Option<&str>) -> DoctorResult {
|
||||
gitlab: gitlab_check,
|
||||
projects: projects_check,
|
||||
ollama: ollama_check,
|
||||
logging: logging_check,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -457,6 +474,59 @@ async fn check_ollama(config: Option<&Config>) -> OllamaCheck {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_logging(config: Option<&Config>) -> LoggingCheck {
|
||||
let log_dir = get_log_dir(config.and_then(|c| c.logging.log_dir.as_deref()));
|
||||
let log_dir_str = log_dir.display().to_string();
|
||||
|
||||
if !log_dir.exists() {
|
||||
return LoggingCheck {
|
||||
result: CheckResult {
|
||||
status: CheckStatus::Ok,
|
||||
message: Some(format!("{log_dir_str} (no log files yet)")),
|
||||
},
|
||||
log_dir: Some(log_dir_str),
|
||||
file_count: Some(0),
|
||||
total_bytes: Some(0),
|
||||
};
|
||||
}
|
||||
|
||||
let mut file_count = 0usize;
|
||||
let mut total_bytes = 0u64;
|
||||
|
||||
if let Ok(entries) = std::fs::read_dir(&log_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = name.to_string_lossy();
|
||||
if name_str.starts_with("lore.") {
|
||||
file_count += 1;
|
||||
if let Ok(meta) = entry.metadata() {
|
||||
total_bytes += meta.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let size_display = if total_bytes < 1024 {
|
||||
format!("{total_bytes} B")
|
||||
} else if total_bytes < 1024 * 1024 {
|
||||
format!("{:.1} KB", total_bytes as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{:.1} MB", total_bytes as f64 / (1024.0 * 1024.0))
|
||||
};
|
||||
|
||||
LoggingCheck {
|
||||
result: CheckResult {
|
||||
status: CheckStatus::Ok,
|
||||
message: Some(format!(
|
||||
"{log_dir_str} ({file_count} files, {size_display})"
|
||||
)),
|
||||
},
|
||||
log_dir: Some(log_dir_str),
|
||||
file_count: Some(file_count),
|
||||
total_bytes: Some(total_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format and print doctor results to console.
|
||||
pub fn print_doctor_results(result: &DoctorResult) {
|
||||
println!("\nlore doctor\n");
|
||||
@@ -466,6 +536,7 @@ pub fn print_doctor_results(result: &DoctorResult) {
|
||||
print_check("GitLab", &result.checks.gitlab.result);
|
||||
print_check("Projects", &result.checks.projects.result);
|
||||
print_check("Ollama", &result.checks.ollama.result);
|
||||
print_check("Logging", &result.checks.logging.result);
|
||||
|
||||
println!();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user