Skip to main content

rustev_otel/
jsonl.rs

1//! The JSONL development exporter (spec 028, 3.10.2): `rustev.trace/1`
2//! lines appended to a host-given file, one decision per write, stopping at
3//! `max_bytes`. It writes what the projection produced, so the same
4//! redaction policy and value bound apply as for OTLP; there is no
5//! unredacted mode.
6
7use std::fmt;
8use std::fs::{File, OpenOptions};
9use std::io::Write;
10use std::path::Path;
11use std::sync::Mutex;
12
13use rustev_contract::trace::{TraceLine, TraceLineError};
14
15/// The default size bound, 256 MiB (3.10.2).
16pub const DEFAULT_MAX_BYTES: u64 = 256 * 1024 * 1024;
17
18/// Why a decision's lines were not written.
19#[derive(Debug, Clone, PartialEq)]
20pub enum JsonlError {
21    /// The file reached `max_bytes`; counted as `file_full`.
22    FileFull,
23    /// The file could not be opened or written.
24    Io(String),
25    /// A line could not be written (never expected from a projection).
26    Line(TraceLineError),
27}
28
29impl fmt::Display for JsonlError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            JsonlError::FileFull => f.write_str("trace file reached its size bound"),
33            JsonlError::Io(e) => write!(f, "trace file: {e}"),
34            JsonlError::Line(e) => write!(f, "{e}"),
35        }
36    }
37}
38
39impl std::error::Error for JsonlError {}
40
41/// The lines of one decision, each followed by a newline.
42pub fn encode(lines: &[TraceLine]) -> Result<Vec<u8>, TraceLineError> {
43    let mut out = Vec::new();
44    for l in lines {
45        out.extend_from_slice(&l.to_line()?);
46        out.push(b'\n');
47    }
48    Ok(out)
49}
50
51struct State {
52    file: File,
53    written: u64,
54}
55
56/// Appends each decision's lines in one write.
57pub struct JsonlExporter {
58    state: Mutex<State>,
59    max_bytes: u64,
60}
61
62impl fmt::Debug for JsonlExporter {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        f.debug_struct("JsonlExporter")
65            .field("max_bytes", &self.max_bytes)
66            .finish_non_exhaustive()
67    }
68}
69
70impl JsonlExporter {
71    /// Open `path` append-only, created with mode `0600` on Unix if absent.
72    /// Bytes already in the file count toward `max_bytes`.
73    pub fn open(path: &Path, max_bytes: u64) -> Result<Self, JsonlError> {
74        let mut o = OpenOptions::new();
75        o.append(true).create(true);
76        #[cfg(unix)]
77        {
78            use std::os::unix::fs::OpenOptionsExt;
79            o.mode(0o600);
80        }
81        let file = o.open(path).map_err(|e| JsonlError::Io(e.to_string()))?;
82        let written = file
83            .metadata()
84            .map_err(|e| JsonlError::Io(e.to_string()))?
85            .len();
86        Ok(JsonlExporter {
87            state: Mutex::new(State { file, written }),
88            max_bytes,
89        })
90    }
91
92    /// Append one decision's lines in one write, or none of them.
93    pub fn export(&self, lines: &[TraceLine]) -> Result<u64, JsonlError> {
94        let bytes = encode(lines).map_err(JsonlError::Line)?;
95        let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
96        let len = bytes.len() as u64;
97        if s.written.saturating_add(len) > self.max_bytes {
98            return Err(JsonlError::FileFull);
99        }
100        s.file
101            .write_all(&bytes)
102            .and_then(|()| s.file.flush())
103            .map_err(|e| JsonlError::Io(e.to_string()))?;
104        s.written += len;
105        Ok(len)
106    }
107}