use crate::events::{Event, EventKind};
use crate::subscribers::Subscribe;
const MAX_VALUE_CHARS: usize = 4096;
fn format_value(value: &str) -> String {
let mut chars = value.chars();
let mut value = chars.by_ref().take(MAX_VALUE_CHARS).collect::<String>();
if chars.next().is_some() {
value.push_str("…[truncated]");
}
format!("{value:?}")
}
#[cfg_attr(docsrs, doc(cfg(feature = "logging")))]
#[derive(Default)]
pub struct LogWriter;
impl Subscribe for LogWriter {
fn on_event(&self, e: &Event) {
self.print_event(e);
}
fn name(&self) -> &str {
"LogWriter"
}
}
impl LogWriter {
fn print_event(&self, e: &Event) {
let head = event_head(e);
fn fmt_ms(ms: Option<u32>) -> String {
match ms {
Some(v) if v >= 1000 && v % 1000 == 0 => format!("{}s", v / 1000),
Some(v) if v >= 1000 => format!("{:.1}s", v as f64 / 1000.0),
Some(v) => format!("{}ms", v),
None => "0ms".to_string(),
}
}
fn or<'a>(s: Option<&'a str>, def: &'a str) -> &'a str {
s.unwrap_or(def)
}
match e.kind {
EventKind::ShutdownRequested
| EventKind::AllStoppedWithinGrace
| EventKind::GraceExceeded => {
println!("{head}");
}
EventKind::AttemptSucceeded
| EventKind::AttemptCanceled
| EventKind::TaskAddRequested
| EventKind::TaskAdded
| EventKind::TaskRemoveRequested
| EventKind::TaskRemoved => {
println!(
"{head} task={}",
format_value(or(e.task.as_deref(), "none"))
);
}
EventKind::AttemptStarting => {
println!(
"{head} task={} attempt={}",
format_value(or(e.task.as_deref(), "none")),
e.attempt.unwrap_or(0)
);
}
EventKind::AttemptFailed => {
println!(
"{head} task={} reason={} attempt={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), "unknown")),
e.attempt.unwrap_or(0)
);
}
EventKind::TaskAddFailed => {
println!(
"{head} task={} reason={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), "unknown"))
);
}
EventKind::AttemptTimedOut => {
println!(
"{head} task={} timeout={}",
format_value(or(e.task.as_deref(), "none")),
fmt_ms(e.timeout_ms)
);
}
EventKind::BackoffScheduled => {
let src = e.backoff_source.map_or("unknown", |s| s.as_label());
println!(
"{head} task={} source={} delay={} after_attempt={} reason={}",
format_value(or(e.task.as_deref(), "none")),
src,
fmt_ms(e.delay_ms),
e.attempt.unwrap_or(0),
format_value(or(e.reason.as_deref(), "none"))
);
}
EventKind::SubscriberOverflow => {
println!(
"{head} subscriber={} reason={} dropped={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), "unknown")),
e.dropped.unwrap_or(0)
);
}
EventKind::SubscriberPanicked => {
println!(
"{head} subscriber={} reason={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), "unknown"))
);
}
EventKind::RuntimeFailure => {
println!(
"{head} component={} reason={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), "unknown"))
);
}
EventKind::TaskFinished => {
let task = or(e.task.as_deref(), "none");
let outcome = e
.outcome_kind
.map(|kind| kind.as_label())
.unwrap_or("unknown");
if let Some(reason) = e.reason.as_deref() {
println!(
"{head} task={} outcome={outcome} reason={}",
format_value(task),
format_value(reason)
);
} else {
println!("{head} task={} outcome={outcome}", format_value(task));
}
}
#[cfg(feature = "controller")]
EventKind::ControllerRejected => {
println!(
"{head} slot={} rejection={} reason={}",
format_value(or(e.task.as_deref(), "none")),
e.rejection_kind
.map(|kind| kind.as_label())
.unwrap_or("unknown"),
format_value(or(e.reason.as_deref(), "unknown"))
);
}
#[cfg(feature = "controller")]
EventKind::ControllerSlotTransition => {
println!(
"{head} slot={} reason={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), "unknown"))
);
}
#[cfg(feature = "controller")]
EventKind::ControllerSubmitted => {
println!(
"{head} slot={} reason={}",
format_value(or(e.task.as_deref(), "none")),
format_value(or(e.reason.as_deref(), ""))
);
}
}
}
}
fn event_head(e: &Event) -> String {
format!("[{:03}] [{}]", e.seq, e.kind.as_label())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_head_keeps_the_full_sequence_number() {
let mut event = Event::new(EventKind::AttemptStarting);
event.seq = 12_345;
assert_eq!(event_head(&event), "[12345] [attempt_starting]");
}
#[test]
fn values_are_escaped_and_bounded() {
assert_eq!(
format_value("first\nsecond\t\"quoted\""),
"\"first\\nsecond\\t\\\"quoted\\\"\""
);
let long = "x".repeat(MAX_VALUE_CHARS + 1);
let rendered = format_value(&long);
assert!(rendered.ends_with("…[truncated]\""));
assert!(!rendered.contains('\n'));
}
}