msgpack-tracing 0.1.2

Compact storage for tracing using msgpack
Documentation
use crate::{
    storage::Store,
    string_cache::{CacheInstruction, CacheInstructionSet},
    tape::{Instruction, InstructionSet, TapeMachine},
};
use std::{
    fs::File,
    io::{self, Read, Seek},
    path::{Path, PathBuf},
};

pub struct Rotate {
    file: Option<File>,
    path: PathBuf,
    path1: Option<PathBuf>,
    max_len: u64,
}
impl Rotate {
    pub fn new<P: AsRef<Path>>(path: P, max_len: u64) -> io::Result<Self> {
        let file = File::options().append(true).create(true).open(&path)?;
        let path1 = Self::path1(path.as_ref());

        Ok(Self {
            file: Some(file),
            path: path.as_ref().to_owned(),
            path1,
            max_len,
        })
    }

    pub fn file_mut(&mut self) -> io::Result<&mut File> {
        self.file
            .as_mut()
            .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "file closed"))
    }

    pub fn do_needs_restart(&mut self) -> io::Result<bool> {
        let max_len = self.max_len;
        let file = self.file_mut()?;

        if file.stream_position()? <= max_len {
            return Ok(false);
        }

        self.file = None;

        if let Some(path1) = self.path1.as_ref() {
            std::fs::rename(&self.path, path1)?;
        }
        self.file = Some(File::create(&self.path)?);

        Ok(true)
    }

    pub fn extract_logs<P: AsRef<Path>, W>(path: P, mut to: W) -> io::Result<()>
    where
        W: io::Write,
    {
        let path1 = Self::path1(path.as_ref());

        let mut buf = vec![0; 4096];

        let files = [
            path1.and_then(|path| File::open(path).ok()),
            File::open(path).ok(),
        ];
        let files = files.into_iter().flatten();

        for mut file in files {
            loop {
                let n = file.read(&mut buf)?;
                let buf = &buf[..n];
                if buf.is_empty() {
                    break;
                }

                to.write_all(buf)?;
                to.flush()?;
            }
        }

        Ok(())
    }

    fn path1(path: &Path) -> Option<PathBuf> {
        path.to_str().map(|str| PathBuf::from(format!("{str}.1")))
    }
}
impl TapeMachine<CacheInstructionSet> for Rotate {
    fn needs_restart(&mut self) -> bool {
        self.do_needs_restart().unwrap_or_default()
    }

    fn handle(&mut self, instruction: CacheInstruction) {
        let Ok(file) = self.file_mut() else {
            return;
        };

        let _ = Store::do_handle_cached(file, instruction);
    }
}
impl TapeMachine<InstructionSet> for Rotate {
    fn needs_restart(&mut self) -> bool {
        self.do_needs_restart().unwrap_or_default()
    }

    fn handle(&mut self, instruction: Instruction) {
        let Ok(file) = self.file_mut() else {
            return;
        };

        let _ = Store::do_handle(file, instruction);
    }
}

#[cfg(test)]
pub mod tests {
    use super::*;
    use crate::{
        storage::Load,
        tape::{FieldValue, Value},
    };

    #[test]
    fn extract_logs() {
        let dir = tempfile::TempDir::new().unwrap();

        let path = dir.path().join("log");
        let mut rotate = Rotate::new(&path, 8).unwrap();
        TapeMachine::<InstructionSet>::handle(&mut rotate, Instruction::Restart);

        assert!(!TapeMachine::<InstructionSet>::needs_restart(&mut rotate));
        for i in 0..8 {
            TapeMachine::<InstructionSet>::handle(
                &mut rotate,
                Instruction::AddValue(FieldValue {
                    name: "name",
                    value: Value::Integer(i),
                }),
            );
        }
        assert!(TapeMachine::<InstructionSet>::needs_restart(&mut rotate));
        for i in 8..16 {
            TapeMachine::<InstructionSet>::handle(
                &mut rotate,
                Instruction::AddValue(FieldValue {
                    name: "name",
                    value: Value::Integer(i),
                }),
            );
        }

        let load = dir.path().join("extracted");
        Rotate::extract_logs(&path, File::create(&load).unwrap()).unwrap();
        let mut load = Load::new(File::open(load).unwrap());

        let next = load.fetch_one().unwrap();
        match next {
            Some(Instruction::Restart) => {}
            unexpected => panic!("restart: unexpected instruction {unexpected:?}"),
        }

        for i in 0..16 {
            let next = load.fetch_one().unwrap();

            match next {
                Some(Instruction::AddValue(FieldValue {
                    name,
                    value: Value::Integer(value),
                })) => {
                    assert_eq!(name, "name");
                    assert_eq!(value, i);
                }
                unexpected => panic!("{i}: unexpected instruction {unexpected:?}"),
            }
        }
    }
}