use std::fmt;
use std::sync::OnceLock;
use serde::Deserialize;
use serde::Serialize;
pub const RECORD_SEPARATOR: &str = " DETLOG_RECORD=";
pub const RECORD_SCHEMA: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum DetLogEvent {
Other,
Syscall,
SyscallResult {
finished_syscall_number: u64,
},
SchedulerCommit {
scheduler_turn: u64,
virtual_nanoseconds: u64,
internal_io_poll: bool,
runtime_maps_read: bool,
},
SchedulerCommittedTime,
SchedulerEmptyQueueKick,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DetLogRecord {
pub schema: u32,
pub event: DetLogEvent,
}
impl DetLogRecord {
pub fn new(event: DetLogEvent) -> Self {
Self {
schema: RECORD_SCHEMA,
event,
}
}
pub fn split(message: &str) -> Result<(&str, Option<Self>), String> {
let Some((human, encoded)) = message.rsplit_once(RECORD_SEPARATOR) else {
return Ok((message, None));
};
let record: Self = serde_json::from_str(encoded)
.map_err(|error| format!("malformed DETLOG record: {error}"))?;
if record.schema != RECORD_SCHEMA {
return Err(format!(
"unsupported DETLOG record schema {}; expected {}",
record.schema, RECORD_SCHEMA
));
}
Ok((human, Some(record)))
}
}
#[doc(hidden)]
pub fn record_suffix(event: DetLogEvent) -> String {
let encoded = serde_json::to_string(&DetLogRecord::new(event))
.expect("DETLOG record serialization cannot fail");
format!("{RECORD_SEPARATOR}{encoded}")
}
pub type DetlogForwarder = for<'a> fn(&str, fmt::Arguments<'a>);
static FORWARDER: OnceLock<DetlogForwarder> = OnceLock::new();
pub fn set_forwarder(forwarder: DetlogForwarder) -> Result<(), DetlogForwarder> {
FORWARDER.set(forwarder)
}
#[doc(hidden)]
pub fn forwarding_enabled() -> bool {
FORWARDER.get().is_some()
}
#[doc(hidden)]
pub fn emit_forwarded(record_suffix: &str, message: fmt::Arguments<'_>) {
tracing::info!("DETLOG {}{}", message, record_suffix);
FORWARDER.get().expect("forwarder disappeared")(record_suffix, message);
}
#[macro_export]
macro_rules! detlog {
(event = $event:expr; $($arg:tt)+) => {{
if $crate::detlog::forwarding_enabled() || ::tracing::enabled!(::tracing::Level::INFO) {
let record_suffix = $crate::detlog::record_suffix($event);
if $crate::detlog::forwarding_enabled() {
$crate::detlog::emit_forwarded(&record_suffix, format_args!($($arg)+));
} else {
::tracing::info!("DETLOG {}{}", format_args!($($arg)+), record_suffix);
}
}
}};
($($arg:tt)+) => {{
$crate::detlog!(event = $crate::detlog::DetLogEvent::Other; $($arg)+);
}};
}
#[macro_export]
macro_rules! detlog_observed {
() => {
$crate::detlog::forwarding_enabled() || ::tracing::enabled!(::tracing::Level::INFO)
};
}
#[macro_export]
macro_rules! detlog_debug {
(event = $event:expr; $($arg:tt)+) => {{
if ::tracing::enabled!(::tracing::Level::DEBUG) {
let record_suffix = $crate::detlog::record_suffix($event);
::tracing::debug!("DETLOG {}{}", format_args!($($arg)+), record_suffix);
}
}};
($($arg:tt)+) => {{
$crate::detlog_debug!(event = $crate::detlog::DetLogEvent::Other; $($arg)+);
}};
}
#[cfg(test)]
mod tests {
use tracing::Metadata;
use tracing::span;
use tracing::subscriber::Interest;
use super::DetLogEvent;
use super::DetLogRecord;
use super::RECORD_SEPARATOR;
use super::record_suffix;
#[test]
fn test_detlog() {
detlog!("Hello : {}. From {:?}", "World", 31337);
}
#[test]
fn structured_record_round_trips_and_refuses_an_incomplete_current_shape() {
let suffix = record_suffix(DetLogEvent::SyscallResult {
finished_syscall_number: 37,
});
let line = format!("INFO detcore: DETLOG finish syscall #999{suffix}");
let (human, record) = DetLogRecord::split(&line).unwrap();
assert_eq!(human, "INFO detcore: DETLOG finish syscall #999");
assert_eq!(
record.unwrap().event,
DetLogEvent::SyscallResult {
finished_syscall_number: 37
}
);
let missing_number = format!(
"INFO detcore: DETLOG finish syscall #999{RECORD_SEPARATOR}{{\"schema\":1,\"event\":{{\"kind\":\"syscall_result\"}}}}"
);
assert!(
DetLogRecord::split(&missing_number)
.unwrap_err()
.contains("finished_syscall_number"),
"an incomplete current record must fail by field name"
);
}
struct AlwaysEnabled;
impl tracing::Subscriber for AlwaysEnabled {
fn register_callsite(&self, _: &'static Metadata<'static>) -> Interest {
Interest::sometimes()
}
fn enabled(&self, _: &Metadata<'_>) -> bool {
true
}
fn new_span(&self, _: &span::Attributes<'_>) -> span::Id {
span::Id::from_u64(1)
}
fn record(&self, _: &span::Id, _: &span::Record<'_>) {}
fn record_follows_from(&self, _: &span::Id, _: &span::Id) {}
fn event(&self, _: &tracing::Event<'_>) {}
fn enter(&self, _: &span::Id) {}
fn exit(&self, _: &span::Id) {}
}
#[test]
fn detlog_observed_is_true_when_a_subscriber_is_listening() {
tracing::subscriber::with_default(AlwaysEnabled, || {
assert!(detlog_observed!());
});
}
#[test]
fn detlog_observed_is_false_when_nothing_is_listening() {
tracing::subscriber::with_default(tracing::subscriber::NoSubscriber::default(), || {
assert!(!detlog_observed!());
});
}
}