use std::{error::Error as StdError, fmt, io, io::Write};
use super::{RuntimeObservationRecord, RuntimeTopologyRecord, SealedRuntimeObservationSnapshot};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RuntimeObservationExportErrorKind {
InvalidRecord,
DestinationUnavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeObservationExportErrorStatus {
Permanent,
Temporary,
}
#[derive(Debug)]
enum RuntimeObservationExportErrorSource {
Encode(serde_json::Error),
Write(io::Error),
}
impl fmt::Display for RuntimeObservationExportErrorSource {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Encode(error) => error.fmt(formatter),
Self::Write(error) => error.fmt(formatter),
}
}
}
impl StdError for RuntimeObservationExportErrorSource {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Encode(error) => Some(error),
Self::Write(error) => Some(error),
}
}
}
#[derive(Debug)]
pub struct RuntimeObservationExportError {
kind: RuntimeObservationExportErrorKind,
status: RuntimeObservationExportErrorStatus,
record_index: usize,
source: RuntimeObservationExportErrorSource,
}
impl RuntimeObservationExportError {
pub const fn kind(&self) -> RuntimeObservationExportErrorKind {
self.kind
}
pub const fn status(&self) -> RuntimeObservationExportErrorStatus {
self.status
}
pub const fn record_index(&self) -> usize {
self.record_index
}
pub const fn is_retryable(&self) -> bool {
matches!(self.status, RuntimeObservationExportErrorStatus::Temporary)
}
fn encode(record_index: usize, source: serde_json::Error) -> Self {
if let Some(kind) = source.io_error_kind() {
let status = match kind {
io::ErrorKind::Interrupted | io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => {
RuntimeObservationExportErrorStatus::Temporary
},
_ => RuntimeObservationExportErrorStatus::Permanent,
};
return Self {
kind: RuntimeObservationExportErrorKind::DestinationUnavailable,
status,
record_index,
source: RuntimeObservationExportErrorSource::Encode(source),
};
}
Self {
kind: RuntimeObservationExportErrorKind::InvalidRecord,
status: RuntimeObservationExportErrorStatus::Permanent,
record_index,
source: RuntimeObservationExportErrorSource::Encode(source),
}
}
fn write(record_index: usize, source: io::Error) -> Self {
let status = match source.kind() {
io::ErrorKind::Interrupted | io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => {
RuntimeObservationExportErrorStatus::Temporary
},
_ => RuntimeObservationExportErrorStatus::Permanent,
};
Self {
kind: RuntimeObservationExportErrorKind::DestinationUnavailable,
status,
record_index,
source: RuntimeObservationExportErrorSource::Write(source),
}
}
}
impl fmt::Display for RuntimeObservationExportError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"runtime observation JSONL export failed: kind={:?}, status={:?}, record_index={}: {}",
self.kind, self.status, self.record_index, self.source
)
}
}
impl StdError for RuntimeObservationExportError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.source)
}
}
pub fn write_runtime_observation_jsonl<W: Write>(
snapshot: &SealedRuntimeObservationSnapshot,
writer: &mut W,
) -> Result<(), RuntimeObservationExportError> {
let data = snapshot.data();
let mut record_index = 0;
write_record(writer, record_index, &RuntimeObservationRecord::Manifest(data.manifest.clone()))?;
record_index += 1;
for retained in &data.topology {
let record = match &retained.record {
RuntimeTopologyRecord::PlanNode(row) => {
RuntimeObservationRecord::PlanNode { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::PlanEdge(row) => {
RuntimeObservationRecord::PlanEdge { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::TaskDefinition(row) => {
RuntimeObservationRecord::TaskDefinition { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::CompiledEdge(row) => {
RuntimeObservationRecord::CompiledEdge { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::BarrierDefinition(row) => {
RuntimeObservationRecord::BarrierDefinition { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::SinkDefinition(row) => {
RuntimeObservationRecord::SinkDefinition { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::JobDefinition(row) => {
RuntimeObservationRecord::JobDefinition { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::WorkerDefinition(row) => {
RuntimeObservationRecord::WorkerDefinition { sequence: retained.sequence, row: row.clone() }
},
RuntimeTopologyRecord::Resource(row) => {
RuntimeObservationRecord::Resource { sequence: retained.sequence, row: row.clone() }
},
};
write_record(writer, record_index, &record)?;
record_index += 1;
}
for event in &data.events {
write_record(writer, record_index, &RuntimeObservationRecord::TimelineEvent(event.clone()))?;
record_index += 1;
}
write_record(writer, record_index, &RuntimeObservationRecord::Footer(data.footer.clone()))
}
fn write_record<W: Write>(
writer: &mut W,
record_index: usize,
record: &RuntimeObservationRecord,
) -> Result<(), RuntimeObservationExportError> {
serde_json::to_writer(&mut *writer, record)
.map_err(|error| RuntimeObservationExportError::encode(record_index, error))?;
writer.write_all(b"\n").map_err(|error| RuntimeObservationExportError::write(record_index, error))
}