use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::Mutex;
use rustev_contract::trace::{TraceLine, TraceLineError};
pub const DEFAULT_MAX_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq)]
pub enum JsonlError {
FileFull,
Io(String),
Line(TraceLineError),
}
impl fmt::Display for JsonlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JsonlError::FileFull => f.write_str("trace file reached its size bound"),
JsonlError::Io(e) => write!(f, "trace file: {e}"),
JsonlError::Line(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for JsonlError {}
pub fn encode(lines: &[TraceLine]) -> Result<Vec<u8>, TraceLineError> {
let mut out = Vec::new();
for l in lines {
out.extend_from_slice(&l.to_line()?);
out.push(b'\n');
}
Ok(out)
}
struct State {
file: File,
written: u64,
}
pub struct JsonlExporter {
state: Mutex<State>,
max_bytes: u64,
}
impl fmt::Debug for JsonlExporter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JsonlExporter")
.field("max_bytes", &self.max_bytes)
.finish_non_exhaustive()
}
}
impl JsonlExporter {
pub fn open(path: &Path, max_bytes: u64) -> Result<Self, JsonlError> {
let mut o = OpenOptions::new();
o.append(true).create(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
o.mode(0o600);
}
let file = o.open(path).map_err(|e| JsonlError::Io(e.to_string()))?;
let written = file
.metadata()
.map_err(|e| JsonlError::Io(e.to_string()))?
.len();
Ok(JsonlExporter {
state: Mutex::new(State { file, written }),
max_bytes,
})
}
pub fn export(&self, lines: &[TraceLine]) -> Result<u64, JsonlError> {
let bytes = encode(lines).map_err(JsonlError::Line)?;
let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
let len = bytes.len() as u64;
if s.written.saturating_add(len) > self.max_bytes {
return Err(JsonlError::FileFull);
}
s.file
.write_all(&bytes)
.and_then(|()| s.file.flush())
.map_err(|e| JsonlError::Io(e.to_string()))?;
s.written += len;
Ok(len)
}
}