refactor(core): compact human log format, quieter lock lifecycle, nonzero_summary helper

Three quality-of-life improvements to reduce log noise and improve readability:

1. logging.rs: Add CompactHumanFormat for stderr tracing output. Replaces the
   default format with a minimal 'HH:MM:SS LEVEL  message key=value' layout —
   no span context, no full timestamps, no target module. The JSON file log
   layer is unaffected. This makes watching 'lore sync' output much cleaner.

2. lock.rs: Downgrade AppLock acquire/release messages from info! to debug!.
   Lock lifecycle events (acquired new, acquired existing, released) are
   operational bookkeeping that clutters normal output. They remain visible
   at -vv verbosity for troubleshooting.

3. ingestion/mod.rs: Add nonzero_summary() utility that formats named counters
   as a compact middle-dot-separated string, suppressing zero values. Produces
   output like '42 fetched · 3 labels · 12 notes' instead of verbose key=value
   structured fields. Returns 'nothing to update' when all values are zero.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-13 22:31:30 -05:00
parent 5ee8b0841c
commit a7f86b26e4
3 changed files with 58 additions and 4 deletions

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, warn};
use uuid::Uuid; use uuid::Uuid;
use super::db::create_connection; use super::db::create_connection;
@@ -75,7 +75,7 @@ impl AppLock {
"INSERT INTO app_locks (name, owner, acquired_at, heartbeat_at) VALUES (?, ?, ?, ?)", "INSERT INTO app_locks (name, owner, acquired_at, heartbeat_at) VALUES (?, ?, ?, ?)",
(&self.name, &self.owner, now, now), (&self.name, &self.owner, now, now),
)?; )?;
info!(owner = %self.owner, "Lock acquired (new)"); debug!(owner = %self.owner, "Lock acquired (new)");
} }
Some((existing_owner, acquired_at, heartbeat_at)) => { Some((existing_owner, acquired_at, heartbeat_at)) => {
let is_stale = now - heartbeat_at > self.stale_lock_ms; let is_stale = now - heartbeat_at > self.stale_lock_ms;
@@ -85,7 +85,7 @@ impl AppLock {
"UPDATE app_locks SET owner = ?, acquired_at = ?, heartbeat_at = ? WHERE name = ?", "UPDATE app_locks SET owner = ?, acquired_at = ?, heartbeat_at = ? WHERE name = ?",
(&self.owner, now, now, &self.name), (&self.owner, now, now, &self.name),
)?; )?;
info!( debug!(
owner = %self.owner, owner = %self.owner,
previous_owner = %existing_owner, previous_owner = %existing_owner,
was_stale = is_stale, was_stale = is_stale,
@@ -125,7 +125,7 @@ impl AppLock {
"DELETE FROM app_locks WHERE name = ? AND owner = ?", "DELETE FROM app_locks WHERE name = ? AND owner = ?",
(&self.name, &self.owner), (&self.name, &self.owner),
) { ) {
Ok(_) => info!(owner = %self.owner, "Lock released"), Ok(_) => debug!(owner = %self.owner, "Lock released"),
Err(e) => error!( Err(e) => error!(
owner = %self.owner, owner = %self.owner,
error = %e, error = %e,

View File

@@ -1,7 +1,45 @@
use std::fmt;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt::format::{FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;
/// Compact stderr formatter: `HH:MM:SS LEVEL message key=value`
///
/// No span context, no full timestamps, no target — just the essentials.
/// The JSON file log is unaffected (it uses its own layer).
pub struct CompactHumanFormat;
impl<S, N> FormatEvent<S, N> for CompactHumanFormat
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
fn format_event(
&self,
ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>,
mut writer: tracing_subscriber::fmt::format::Writer<'_>,
event: &tracing::Event<'_>,
) -> fmt::Result {
let now = chrono::Local::now();
let time = now.format("%H:%M:%S");
let level = *event.metadata().level();
let styled = match level {
tracing::Level::ERROR => console::style("ERROR").red().bold(),
tracing::Level::WARN => console::style(" WARN").yellow(),
tracing::Level::INFO => console::style(" INFO").green(),
tracing::Level::DEBUG => console::style("DEBUG").dim(),
tracing::Level::TRACE => console::style("TRACE").dim(),
};
write!(writer, "{time} {styled} ")?;
ctx.format_fields(writer.by_ref(), event)?;
writeln!(writer)
}
}
pub fn build_stderr_filter(verbose: u8, quiet: bool) -> EnvFilter { pub fn build_stderr_filter(verbose: u8, quiet: bool) -> EnvFilter {
if std::env::var("RUST_LOG").is_ok() { if std::env::var("RUST_LOG").is_ok() {

View File

@@ -14,6 +14,22 @@ pub use merge_requests::{
ingest_merge_requests, ingest_merge_requests,
}; };
pub use mr_discussions::{IngestMrDiscussionsResult, ingest_mr_discussions}; pub use mr_discussions::{IngestMrDiscussionsResult, ingest_mr_discussions};
/// Format a set of named counters as a compact human-readable summary,
/// filtering out zero values and joining with middle-dot separators.
/// Returns `"nothing to update"` when all values are zero.
pub(crate) fn nonzero_summary(pairs: &[(&str, usize)]) -> String {
let parts: Vec<String> = pairs
.iter()
.filter(|(_, v)| *v > 0)
.map(|(k, v)| format!("{v} {k}"))
.collect();
if parts.is_empty() {
"nothing to update".to_string()
} else {
parts.join(" \u{b7} ")
}
}
pub use orchestrator::{ pub use orchestrator::{
DrainResult, IngestMrProjectResult, IngestProjectResult, ProgressCallback, ProgressEvent, DrainResult, IngestMrProjectResult, IngestProjectResult, ProgressCallback, ProgressEvent,
ingest_project_issues, ingest_project_issues_with_progress, ingest_project_merge_requests, ingest_project_issues, ingest_project_issues_with_progress, ingest_project_merge_requests,