use crate::FrenError;
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub trait LogSink {
fn append(&mut self, record: &LogRecord) -> Result<(), FrenError>;
fn path(&self) -> Option<&Path>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LogRecord {
Batch {
#[serde(rename = "v")]
v: u32,
id: Uuid,
ts: String,
cmd: String,
args: Vec<String>,
cwd: PathBuf,
fren_version: String,
},
Rename {
#[serde(rename = "v")]
v: u32,
ts: String,
from: PathBuf,
to: PathBuf,
kind: String,
},
End {
#[serde(rename = "v")]
v: u32,
ts: String,
status: String,
applied: usize,
skipped: usize,
errors: usize,
},
}
pub struct JsonlLogSink {
writer: BufWriter<File>,
path: PathBuf,
}
impl JsonlLogSink {
pub fn open(log_dir: Option<&Path>, batch_id: Uuid, ts: &str) -> Result<Self, FrenError> {
let dir = match log_dir {
Some(p) => p.to_path_buf(),
None => default_log_dir(),
};
std::fs::create_dir_all(&dir).map_err(|source| FrenError::Io {
path: dir.clone(),
source,
})?;
let filename = format!("{ts}-{batch_id}.jsonl");
let path = dir.join(filename);
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|source| FrenError::Io {
path: path.clone(),
source,
})?;
Ok(Self {
writer: BufWriter::new(file),
path,
})
}
}
impl LogSink for JsonlLogSink {
fn append(&mut self, record: &LogRecord) -> Result<(), FrenError> {
let line =
serde_json::to_string(record).map_err(|e| FrenError::InvalidInput(e.to_string()))?;
writeln!(self.writer, "{line}").map_err(|source| FrenError::Io {
path: self.path.clone(),
source,
})?;
self.writer.flush().map_err(|source| FrenError::Io {
path: self.path.clone(),
source,
})?;
Ok(())
}
fn path(&self) -> Option<&Path> {
Some(&self.path)
}
}
pub struct NullLogSink;
impl LogSink for NullLogSink {
fn append(&mut self, _record: &LogRecord) -> Result<(), FrenError> {
Ok(())
}
fn path(&self) -> Option<&Path> {
None
}
}
fn default_log_dir() -> PathBuf {
if let Some(state) = std::env::var_os("XDG_STATE_HOME") {
return PathBuf::from(state).join("fren").join("log");
}
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home)
.join(".local")
.join("state")
.join("fren")
.join("log");
}
PathBuf::from(".fren-log")
}