feat(cli): add 'deepseek metrics' command (closes #70)

Implement `deepseek metrics` as a dispatcher-handled subcommand (no TUI
binary roundtrip) that reads ~/.deepseek/audit.log, session JSON files,
and tasks runtime JSONL event streams, then prints a human-readable
usage rollup aggregated by tool name, compaction events, sub-agent
spawns, and capacity-controller interventions.

Flags: --json (machine-readable) and --since DURATION (e.g. 7d, 24h,
30m, now-2h, 2h30m). Empty/missing audit log exits 0 with an empty
rollup; malformed lines are skipped silently via tracing::trace!.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hunter Bown
2026-04-26 14:17:58 -05:00
parent e9970fcad3
commit 9804c92c21
4 changed files with 1065 additions and 0 deletions
Generated
+4
View File
@@ -999,8 +999,12 @@ dependencies = [
"deepseek-execpolicy", "deepseek-execpolicy",
"deepseek-mcp", "deepseek-mcp",
"deepseek-state", "deepseek-state",
"dirs",
"serde",
"serde_json", "serde_json",
"tempfile",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
+6
View File
@@ -21,5 +21,11 @@ deepseek-execpolicy = { path = "../execpolicy", version = "0.6.0" }
deepseek-mcp = { path = "../mcp", version = "0.6.0" } deepseek-mcp = { path = "../mcp", version = "0.6.0" }
deepseek-state = { path = "../state", version = "0.6.0" } deepseek-state = { path = "../state", version = "0.6.0" }
chrono.workspace = true chrono.workspace = true
dirs.workspace = true
serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
tokio.workspace = true tokio.workspace = true
tracing.workspace = true
[dev-dependencies]
tempfile = "3.16"
+30
View File
@@ -1,3 +1,5 @@
mod metrics;
use std::io::{self, Read}; use std::io::{self, Read};
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
@@ -135,6 +137,18 @@ enum Commands {
#[arg(value_enum)] #[arg(value_enum)]
shell: Shell, shell: Shell,
}, },
/// Print a usage rollup from the audit log and session store.
Metrics(MetricsArgs),
}
#[derive(Debug, Args)]
struct MetricsArgs {
/// Emit machine-readable JSON.
#[arg(long)]
json: bool,
/// Restrict to events newer than this duration (e.g. 7d, 24h, 30m, now-2h).
#[arg(long, value_name = "DURATION")]
since: Option<String>,
} }
#[derive(Debug, Args)] #[derive(Debug, Args)]
@@ -392,6 +406,7 @@ fn run() -> Result<()> {
generate(shell, &mut cmd, "deepseek", &mut io::stdout()); generate(shell, &mut cmd, "deepseek", &mut io::stdout());
Ok(()) Ok(())
} }
Some(Commands::Metrics(args)) => run_metrics_command(args),
None => { None => {
let mut forwarded = Vec::new(); let mut forwarded = Vec::new();
if let Some(prompt) = cli.prompt.clone() { if let Some(prompt) = cli.prompt.clone() {
@@ -865,6 +880,19 @@ fn delegate_simple_tui(args: Vec<String>) -> Result<()> {
} }
} }
fn run_metrics_command(args: MetricsArgs) -> Result<()> {
let since = match args.since.as_deref() {
Some(s) => {
Some(metrics::parse_since(s).with_context(|| format!("invalid --since value: {s:?}"))?)
}
None => None,
};
metrics::run(metrics::MetricsArgs {
json: args.json,
since,
})
}
fn read_api_key_from_stdin() -> Result<String> { fn read_api_key_from_stdin() -> Result<String> {
let mut input = String::new(); let mut input = String::new();
io::stdin() io::stdin()
@@ -1234,6 +1262,7 @@ mod tests {
"sandbox", "sandbox",
"app-server", "app-server",
"completion", "completion",
"metrics",
"--provider", "--provider",
"--model", "--model",
"--config", "--config",
@@ -1279,6 +1308,7 @@ mod tests {
vec!["--host", "--port", "--config", "--stdio"], vec!["--host", "--port", "--config", "--stdio"],
), ),
("completion", vec!["<SHELL>", "bash"]), ("completion", vec!["<SHELL>", "bash"]),
("metrics", vec!["--json", "--since"]),
]; ];
for (subcommand, expected_tokens) in cases { for (subcommand, expected_tokens) in cases {
File diff suppressed because it is too large Load Diff