use bincode::config::Configuration;
use bincode::enc::write::Writer;
use bincode::enc::Encode;
use bincode::enc::{Encoder, EncoderImpl};
use bincode::error::EncodeError;
use cu29_clock::RobotClock;
#[cfg(debug_assertions)]
use cu29_intern_strs::read_interned_strings;
use cu29_log::CuLogEntry;
use cu29_traits::{CuResult, WriteStream};
#[cfg(debug_assertions)]
use log::{Log, Record};
use std::fmt::{Debug, Formatter};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
#[cfg(debug_assertions)]
use std::sync::Arc;
use std::sync::{Mutex, OnceLock};
static WRITER: OnceLock<(Mutex<Box<dyn WriteStream<CuLogEntry>>>, RobotClock)> = OnceLock::new();
#[cfg(debug_assertions)]
static EXTRA_TEXT_LOGGER: OnceLock<Option<ExtraTextLogger>> = OnceLock::new();
pub struct LoggerRuntime {}
impl LoggerRuntime {
pub fn init(
clock: RobotClock,
destination: impl WriteStream<CuLogEntry> + 'static,
#[allow(unused_variables)] extra_text_logger: Option<ExtraTextLogger>,
) -> Self {
let runtime = LoggerRuntime {};
if let Some((writer, _)) = WRITER.get() {
let mut writer_guard = writer.lock().unwrap();
*writer_guard = Box::new(destination);
} else {
WRITER
.set((Mutex::new(Box::new(destination)), clock))
.unwrap();
}
#[cfg(debug_assertions)]
if let Some(logger) = extra_text_logger {
let _ = EXTRA_TEXT_LOGGER.set(Some(logger));
}
runtime
}
pub fn flush(&self) {
if let Some((writer, _clock)) = WRITER.get() {
if let Ok(mut writer) = writer.lock() {
if let Err(err) = writer.flush() {
eprintln!("cu29_log: Failed to flush writer: {}", err);
}
} else {
eprintln!("cu29_log: Failed to lock writer.");
}
} else {
eprintln!("cu29_log: Logger not initialized.");
}
}
}
impl Drop for LoggerRuntime {
fn drop(&mut self) {
self.flush();
}
}
pub struct ExtraTextLogger {
#[cfg(debug_assertions)]
all_strings: Vec<String>,
#[cfg(debug_assertions)]
inner: Arc<dyn Log>,
}
#[cfg(debug_assertions)]
impl ExtraTextLogger {
pub fn new(path_to_index: PathBuf, logger: Box<dyn Log>) -> Self {
let all_strings = read_interned_strings(&path_to_index).unwrap();
ExtraTextLogger {
all_strings,
inner: Arc::new(logger),
}
}
}
#[inline(always)]
pub fn log(mut entry: CuLogEntry) -> CuResult<()> {
let d = WRITER.get().map(|(writer, clock)| (writer, clock));
if d.is_none() {
return Err("Logger not initialized.".into());
}
let (writer, clock) = d.unwrap();
entry.time = clock.now();
if let Err(err) = writer.lock().unwrap().log(&entry) {
eprintln!("Failed to log data: {}", err);
}
#[cfg(debug_assertions)]
{
let guarded_logger = EXTRA_TEXT_LOGGER.get();
if guarded_logger.is_none() {
return Ok(());
}
if let Some(logger) = guarded_logger.unwrap() {
let stringified = cu29_log::rebuild_logline(&logger.all_strings, &entry);
match stringified {
Ok(s) => {
let s = format!("[{}] {}", entry.time, s);
logger.inner.log(
&Record::builder()
.level(log::Level::Debug)
.args(format_args!("{}", s))
.build(),
); }
Err(e) => {
eprintln!("Failed to rebuild log line: {}", e);
}
}
}
}
Ok(())
}
pub struct OwningIoWriter<W: Write> {
writer: BufWriter<W>,
bytes_written: usize,
}
impl<'a, W: Write> OwningIoWriter<W> {
pub fn new(writer: W) -> Self {
Self {
writer: BufWriter::new(writer),
bytes_written: 0,
}
}
pub fn bytes_written(&self) -> usize {
self.bytes_written
}
pub fn flush(&mut self) -> Result<(), EncodeError> {
self.writer.flush().map_err(|inner| EncodeError::Io {
inner,
index: self.bytes_written,
})
}
}
impl<W: Write> Writer for OwningIoWriter<W> {
#[inline(always)]
fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
self.writer
.write_all(bytes)
.map_err(|inner| EncodeError::Io {
inner,
index: self.bytes_written,
})?;
self.bytes_written += bytes.len();
Ok(())
}
}
pub struct SimpleFileWriter {
path: PathBuf,
encoder: EncoderImpl<OwningIoWriter<File>, Configuration>,
}
impl SimpleFileWriter {
pub fn new(path: &PathBuf) -> CuResult<Self> {
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(path)
.map_err(|e| format!("Failed to open file: {:?}", e))?;
let writer = OwningIoWriter::new(file);
let encoder = EncoderImpl::new(writer, bincode::config::standard());
Ok(SimpleFileWriter {
path: path.clone(),
encoder,
})
}
}
impl Debug for SimpleFileWriter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "SimpleFileWriter for path {:?}", self.path)
}
}
impl WriteStream<CuLogEntry> for SimpleFileWriter {
#[inline(always)]
fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
obj.encode(&mut self.encoder)
.map_err(|e| format!("Failed to write to file: {:?}", e))?;
Ok(())
}
fn flush(&mut self) -> CuResult<()> {
self.encoder
.writer()
.flush()
.map_err(|e| format!("Failed to flush file: {:?}", e))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::CuLogEntry;
use bincode::config::standard;
use cu29_log::value::Value;
use smallvec::smallvec;
#[test]
fn test_encode_decode_structured_log() {
let log_entry = CuLogEntry {
time: 0.into(),
msg_index: 1,
paramname_indexes: smallvec![2, 3],
params: smallvec![Value::String("test".to_string())],
};
let encoded = bincode::encode_to_vec(&log_entry, standard()).unwrap();
println!("{:?}", encoded);
let decoded_tuple: (CuLogEntry, usize) =
bincode::decode_from_slice(&encoded, standard()).unwrap();
assert_eq!(log_entry, decoded_tuple.0);
}
}