use serde::Serialize;
use crate::daemon::clock_parameters::ClockParameters;
use crate::daemon::clock_sync_algorithm::{SourceInfo, SyncParameters};
use crate::shm::ClockStatus;
pub mod ffevents;
pub mod layer;
pub mod subscriber;
pub mod synchronization;
use layer::LogHandle;
pub struct LogHandles {
handles: Vec<LogHandle>,
}
impl LogHandles {
pub fn reopen_all(&self) {
for handle in &self.handles {
handle.reopen();
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum StructuredLog {
FFEvents,
Synchronization,
AlgoAnalysis,
}
impl StructuredLog {
const ALL: &'static [StructuredLog] = &[
StructuredLog::FFEvents,
StructuredLog::Synchronization,
StructuredLog::AlgoAnalysis,
];
pub const fn target(self) -> &'static str {
match self {
Self::FFEvents => "clock_bound::ffevents",
Self::Synchronization => "clock_bound::synchronization",
Self::AlgoAnalysis => "clock_bound::algo_analysis",
}
}
pub const fn file_name(self) -> &'static str {
match self {
Self::FFEvents => "ffevents.log",
Self::Synchronization => "synchronization.log",
Self::AlgoAnalysis => "algo_analysis.log",
}
}
}
pub fn is_structured_target(target: &str) -> bool {
StructuredLog::ALL
.iter()
.map(|stream| stream.target())
.any(|t| target.starts_with(t))
}
pub(crate) trait LogEvent: Serialize {
const STREAM: StructuredLog;
const EVENT: &'static str;
}
pub(crate) fn emit_event<E: LogEvent>(event: &E) {
emit_event_inner(event, None);
}
pub(crate) fn emit_event_with_seq<E: LogEvent>(event: &E, seq: u64) {
emit_event_inner(event, Some(seq));
}
fn emit_event_inner<E: LogEvent>(event: &E, seq: Option<u64>) {
let body = serde_json::to_value(event).unwrap();
let mut envelope = serde_json::Map::new();
envelope.insert("event".to_string(), serde_json::Value::from(E::EVENT));
if let Some(seq) = seq {
envelope.insert("seq".to_string(), serde_json::Value::from(seq));
}
envelope.insert(E::EVENT.to_string(), body);
let Ok(json) = serde_json::to_string(&envelope) else {
return;
};
emit_entry(E::STREAM, &json);
}
fn emit_entry(stream: StructuredLog, entry: &str) {
match stream {
StructuredLog::FFEvents => {
tracing::info!(target: StructuredLog::FFEvents.target(), entry = entry);
}
StructuredLog::Synchronization => {
tracing::info!(target: StructuredLog::Synchronization.target(), entry = entry);
}
StructuredLog::AlgoAnalysis => {
tracing::info!(target: StructuredLog::AlgoAnalysis.target(), entry = entry);
}
}
}
const LOG_TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
pub(crate) fn format_log_timestamp(ts: chrono::DateTime<chrono::Utc>) -> String {
ts.format(LOG_TIMESTAMP_FORMAT).to_string()
}
#[derive(Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
enum ClockStatusName {
Unknown,
Synchronized,
FreeRunning,
Disrupted,
}
impl From<ClockStatus> for ClockStatusName {
fn from(status: ClockStatus) -> Self {
match status {
ClockStatus::Unknown => Self::Unknown,
ClockStatus::Synchronized => Self::Synchronized,
ClockStatus::FreeRunning => Self::FreeRunning,
ClockStatus::Disrupted => Self::Disrupted,
}
}
}
#[derive(Serialize)]
struct Source {
#[serde(rename = "type")]
#[expect(clippy::struct_field_names, reason = "intentional")]
source_type: &'static str,
identifier: String,
selected_at: String,
clock_error_bound_ns: i64,
}
impl From<&SyncParameters> for Source {
fn from(params: &SyncParameters) -> Self {
let selected_at = format_log_timestamp(chrono::DateTime::from_timestamp_nanos(
params.selected_at.as_nanos(),
));
let clock_error_bound_ns = params.selected_at_clock_error_bound.as_nanos();
match ¶ms.source_info {
SourceInfo::AmazonTimeSync(addr, _) | SourceInfo::NtpSource(addr, _) => Self {
source_type: "ntp",
identifier: addr.to_string(),
selected_at,
clock_error_bound_ns,
},
SourceInfo::Phc(device_path) => Self {
source_type: "phc",
identifier: device_path.to_string(),
selected_at,
clock_error_bound_ns,
},
}
}
}
#[derive(Serialize)]
struct FFClock {
time_sync: String,
clock_error_bound_ns: i64,
status: ClockStatusName,
}
impl FFClock {
fn from_params(params: &ClockParameters, clock_status: ClockStatus) -> Self {
let time_sync = format_log_timestamp(chrono::DateTime::from_timestamp_nanos(
params.time.as_nanos(),
));
Self {
time_sync,
clock_error_bound_ns: params.clock_error_bound.as_nanos(),
status: clock_status.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::StructuredLog;
#[test]
fn all_lists_every_variant() {
for variant in [
StructuredLog::FFEvents,
StructuredLog::Synchronization,
StructuredLog::AlgoAnalysis,
] {
match variant {
StructuredLog::FFEvents
| StructuredLog::Synchronization
| StructuredLog::AlgoAnalysis => {}
}
assert!(
StructuredLog::ALL.contains(&variant),
"{variant:?} missing from StructuredLog::ALL"
);
}
assert_eq!(StructuredLog::ALL.len(), 3);
}
}
#[cfg(test)]
pub(crate) mod test_support {
use std::fs::File;
use std::io::Read;
use serde_json::Value;
use tempfile::TempDir;
use tracing_subscriber::layer::SubscriberExt;
use super::StructuredLog;
use super::layer::StructuredLogLayer;
pub(crate) fn with_layer(
stream: StructuredLog,
version: &'static str,
f: impl FnOnce(),
) -> (String, TempDir) {
let tmp_dir = TempDir::new().unwrap();
let (layer, _handle) = StructuredLogLayer::for_stream(tmp_dir.path(), stream, version);
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, f);
let mut contents = String::new();
File::open(tmp_dir.path().join(stream.file_name()))
.unwrap()
.read_to_string(&mut contents)
.unwrap();
(contents, tmp_dir)
}
pub(crate) fn assert_matches_expected(actual_line: &str, expected_json: &str) {
let actual: Value =
serde_json::from_str(actual_line).expect("actual output is not valid JSON");
let mut expected: Value =
serde_json::from_str(expected_json).expect("expected JSON is not valid JSON");
expected["timestamp"] = actual["timestamp"].clone();
assert_eq!(actual, expected);
}
}