feat(log): survive a bare deployment and name what each line is about

P0 (foundation) + P1 (diagnostic depth) of the logging plan:

- main.rs initializes the timed builder with a default filter of
  `info,hyper_util=warn,reqwest=warn`. Without RUST_LOG nothing was logged at
  all (env_logger falls back to `error`), so `docker run --env-file .env` was
  silent, and the plain `init` had no timestamps.
- info-and-above lines stop printing user URLs (fetch/send failures, inline
  fetch, bsky's remux warnings). The full URL, the message text and the inline
  query move to `trace`, so a `debug` log can be handed to someone else.
- Lifecycle lines name the chat and the post: sent/failed/queued plus the
  total `ms`, the edit prompt, the channel forward, and every queue line
  (`chat=` + `[key=…]` + per-attempt `ms`, dead-letters included).
- Queue work is visible: `x-media`'s fetch line carries its duration (ugoira
  encode and HLS remux included), and the 300s sweep reports the pending count
  and how overdue the oldest task is — only when the queue is non-empty.
- URL workers are supervised like the queue workers: a panicking worker used
  to die silently and shrink the pool for the rest of the process.
- Degradations that still serve the user (cache/state write or read failures,
  a failed chat action) are `warn`, not `error`.

Verified against the scripted fake-API harness: unset RUST_LOG logs info with
timestamps, `debug` carries no user URL, `trace` does, a cache-hit send logs
`chat=111 in 5ms`, a failing send queues and dead-letters with chat+key, and
the sweep reports the pending retry.
This commit is contained in:
2026-09-20 19:07:23 +08:00
parent 9e873131d4
commit 3f9821d475
13 changed files with 265 additions and 56 deletions
+28 -1
View File
@@ -43,7 +43,18 @@ fn spawn_sigterm_handler(_stop_token: StopToken) {}
#[tokio::main]
async fn main() {
dotenv().ok();
pretty_env_logger::init();
// Without RUST_LOG nothing at all was logged (env_logger falls back to
// `error`), so a deployment that forgot the variable looked like a bot
// with no logs; and at `debug` the HTTP client's own lines (hyper_util,
// reqwest) outnumbered the bot's by two to one. The timed builder adds
// the timestamp the plain `init` omitted, so a line can be compared with
// a user's report. An explicit RUST_LOG still wins outright.
pretty_env_logger::formatted_timed_builder()
.parse_filters(
&std::env::var("RUST_LOG")
.unwrap_or_else(|_| "info,hyper_util=warn,reqwest=warn".to_string()),
)
.init();
log::info!("Starting bot");
let bot = Bot::from_env();
@@ -118,6 +129,22 @@ async fn main() {
if idle_limiters > 0 {
log::debug!("rate limiter: dropped {idle_limiters} idle bucket(s)");
}
// Only speaks up when the queue is not empty: a healthy bot
// has nothing to report, and a periodic "0 pending" line is
// noise that hides the lines that matter.
if let Some((pending, oldest_run_after)) = TASK_QUEUE.pending_backlog().await {
let overdue = crate::db::now_f64() - oldest_run_after;
if overdue >= 0.0 {
log::info!(
"queue: {pending} pending task(s), oldest {overdue:.0}s overdue"
);
} else {
log::info!(
"queue: {pending} pending task(s), oldest retry in {:.0}s",
-overdue
);
}
}
for (chat_id, prompt_message_id) in removed {
// Rewritten in place, not announced: the sweep is a
// background timer, and a fresh message would wake the chat