chore(hooks): tracing::warn on hook failures (#455 follow-up)

Hook failures were silent — the executor returned a `HookResult`
with `success=false`, but every call site discards it with
`let _ = ...`. Operators tailing `deepseek` had no visibility
into hook errors short of running each hook command by hand.

Centralizes the logging inside `HookExecutor::execute` so every
fire site benefits without sprinkling instrumentation. Logs
through `tracing::warn!` with structured fields (`hook`,
`event`, `exit_code`, `duration_ms`, `error`, `stderr_head`)
so operators can `RUST_LOG=warn deepseek` and immediately see
which hooks are misbehaving.

Successful runs log nothing — `tool_call_before` /
`tool_call_after` fire on every tool dispatch, so per-call
success logging would be unreadably noisy.

No behavioral change for users with no hooks (the function
fast-paths out before reaching this branch). No behavioral
change for users with passing hooks. Failed hooks still
respect `continue_on_error` and the surrounding loop is
unchanged.
This commit is contained in:
Hunter Bown
2026-05-03 07:10:19 -05:00
parent c0b6c2a1e5
commit 8a679bf662
+18
View File
@@ -521,6 +521,24 @@ impl HookExecutor {
self.execute_sync(hook, &env_vars)
};
// Log failures via tracing so operators tailing
// `deepseek` with `RUST_LOG=warn` can see hook errors
// without instrumenting each call site. Successful runs
// log nothing (would be too noisy on per-tool events).
if !result.success {
let label = result.name.as_deref().unwrap_or("(unnamed)");
tracing::warn!(
target: "hooks",
hook = label,
event = event.as_str(),
exit_code = ?result.exit_code,
duration_ms = result.duration.as_millis() as u64,
error = result.error.as_deref().unwrap_or(""),
stderr_head = %result.stderr.lines().next().unwrap_or(""),
"hook failed"
);
}
let should_continue = result.success || hook.continue_on_error;
results.push(result);