Trace ids, failure kinds and one line per event in the JSON log (#91)
From Dash0's structured logging guide, what applies here: - Each JSON line inside a traced span ends with its trace_id and span_id, so a line in Loki leads to its trace in Tempo; the access log is written inside its request's span so it has one too. The JSON formatter takes no extra fields, so WithTrace appends them to the object it writes. - A feed or download failure carries error.type (the HTTP status, or dns, redirect_loop, timeout, ...) and http.response.status_code, from failure_kind beside explain_failure, so failures group by kind without a regex over msg. - Each event was logged twice: words under ipx::scan and fields under ipx::io. It is now one line under ipx::scan with both; the wire copy is at debug, for the admin page's Daemon I/O tab, and out of production's log. The healthcheck's status reply stays under ipx::io. - The access log's ms is duration_ms. The dashboard and the prod-check skill follow. - error fields are Display with the anyhow chain everywhere, not a mix of Debug and Display. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
87
src/ipc.rs
87
src/ipc.rs
@@ -131,34 +131,8 @@ impl Emitter {
|
||||
// Level by how much it matters. With 80-odd feeds in an OPML subscription, one
|
||||
// line per feed per tick for "not due yet" would push everything worth reading
|
||||
// out of the buffer within a few minutes.
|
||||
let routine = match &e {
|
||||
Event::Progress { .. } | Event::FeedSkip { .. } | Event::FeedStart { .. } => true,
|
||||
Event::FeedDone { new, downloaded, failed, torrents, .. } => {
|
||||
*new == 0 && *downloaded == 0 && *failed == 0 && *torrents == 0
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let bad = matches!(
|
||||
&e,
|
||||
Event::FeedError { .. } | Event::DownloadError { .. } | Event::Error { .. }
|
||||
);
|
||||
if let Some(line) = e.human() {
|
||||
let line = line.trim();
|
||||
if bad {
|
||||
tracing::warn!(target: "ipx::scan", "{line}");
|
||||
} else if routine {
|
||||
tracing::debug!(target: "ipx::scan", "{line}");
|
||||
} else {
|
||||
tracing::info!(target: "ipx::scan", "{line}");
|
||||
}
|
||||
}
|
||||
|
||||
log_event(&e, self.tx.is_some());
|
||||
if let Some(tx) = &self.tx {
|
||||
// The outbound half of the protocol, as it goes on the wire. Progress is the
|
||||
// high-volume one, so it sits at debug.
|
||||
if let Ok(json) = serde_json::to_string(&e) {
|
||||
log_wire(&json, matches!(e, Event::Progress { .. }));
|
||||
}
|
||||
// An error here only means nobody is listening yet.
|
||||
let _ = tx.send(e.clone());
|
||||
}
|
||||
@@ -168,26 +142,61 @@ impl Emitter {
|
||||
}
|
||||
}
|
||||
|
||||
/// An event as it goes on the wire, `<- {json}`, with its fields as the log line's own as well,
|
||||
/// so Loki reads `ev`, `feed`, `new` and the rest from the JSON log without parsing the message
|
||||
/// (#91). A field an event lacks is left out.
|
||||
fn log_wire(json: &str, debug: bool) {
|
||||
let v: serde_json::Value = serde_json::from_str(json).unwrap_or_default();
|
||||
/// An event, logged once (#91): its words as the message, and `ev`, `feed`, `new` and the rest as
|
||||
/// fields, so Loki reads them without parsing the message. A failure also gets `error.type` and,
|
||||
/// from an HTTP error, `http.response.status_code`, so failures group by kind without a regex.
|
||||
/// Level by how much it matters: with 80-odd feeds in an OPML subscription, a line per feed per
|
||||
/// tick for "not due yet" would push everything worth reading out of the log view in minutes,
|
||||
/// and Progress fires on every whole percent. `wire` also logs the event as it goes on the
|
||||
/// socket, at debug: the admin page's Daemon I/O tab shows it, production's log leaves it out.
|
||||
fn log_event(e: &Event, wire: bool) {
|
||||
let json = serde_json::to_string(e).unwrap_or_default();
|
||||
let v: serde_json::Value = serde_json::from_str(&json).unwrap_or_default();
|
||||
let s = |k: &str| v.get(k).and_then(|x| x.as_str());
|
||||
let n = |k: &str| v.get(k).and_then(|x| x.as_u64());
|
||||
macro_rules! wire {
|
||||
($level:ident) => {
|
||||
let (kind, code) = match e {
|
||||
Event::FeedError { msg, .. } | Event::DownloadError { msg, .. } | Event::Error { msg } => {
|
||||
let (k, c) = crate::feed::failure_kind(msg);
|
||||
(Some(k), c)
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
let routine = match e {
|
||||
Event::Progress { .. } | Event::FeedSkip { .. } | Event::FeedStart { .. } => true,
|
||||
Event::FeedDone { new, downloaded, failed, torrents, .. } => {
|
||||
*new == 0 && *downloaded == 0 && *failed == 0 && *torrents == 0
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
macro_rules! line {
|
||||
($level:ident, $target:literal, $text:expr) => {
|
||||
tracing::$level!(
|
||||
target: "ipx::io",
|
||||
target: $target,
|
||||
ev = s("ev"), feed = s("feed"), msg = s("msg"), url = s("url"), reason = s("reason"),
|
||||
new = n("new"), downloaded = n("downloaded"), failed = n("failed"),
|
||||
torrents = n("torrents"), bytes = n("bytes"), feeds = n("feeds"),
|
||||
pending = n("pending"), enclosure = n("enclosure"), files = n("files"),
|
||||
"<- {json}"
|
||||
"error.type" = kind, "http.response.status_code" = code,
|
||||
"{}", $text
|
||||
)
|
||||
};
|
||||
}
|
||||
if debug { wire!(debug) } else { wire!(info) }
|
||||
// The healthcheck's answer, every 30s: a reply on the socket rather than work done, so it
|
||||
// stays with the rest of the conversation, and the Scans tab stays about scans.
|
||||
if matches!(e, Event::Status { .. }) {
|
||||
return line!(info, "ipx::io", format!("<- {json}"));
|
||||
}
|
||||
if wire {
|
||||
tracing::debug!(target: "ipx::io", "<- {json}");
|
||||
}
|
||||
let text = e.human().map(|l| l.trim().to_owned()).unwrap_or_else(|| json.clone());
|
||||
if kind.is_some() {
|
||||
line!(warn, "ipx::scan", text)
|
||||
} else if routine {
|
||||
line!(debug, "ipx::scan", text)
|
||||
} else {
|
||||
line!(info, "ipx::scan", text)
|
||||
}
|
||||
}
|
||||
|
||||
/// True when something is already listening -- i.e. a daemon owns this socket.
|
||||
@@ -275,9 +284,7 @@ async fn handle(
|
||||
Ok(Command::Status) => {
|
||||
tracing::info!(target: "ipx::io", "-> {line}");
|
||||
let ev = status().await;
|
||||
if let Ok(json) = serde_json::to_string(&ev) {
|
||||
log_wire(&json, false);
|
||||
}
|
||||
log_event(&ev, true);
|
||||
let _ = reply.send(ev).await;
|
||||
}
|
||||
Ok(cmd) => {
|
||||
|
||||
Reference in New Issue
Block a user