1use crate::FrenError;
4use serde::{Deserialize, Serialize};
5use std::fs::{File, OpenOptions};
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8use uuid::Uuid;
9
10pub trait LogSink {
13 fn append(&mut self, record: &LogRecord) -> Result<(), FrenError>;
16 fn path(&self) -> Option<&Path>;
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(tag = "type", rename_all = "snake_case")]
24pub enum LogRecord {
25 Batch {
27 #[serde(rename = "v")]
29 v: u32,
30 id: Uuid,
32 ts: String,
34 cmd: String,
36 args: Vec<String>,
38 cwd: PathBuf,
40 fren_version: String,
42 },
43 Rename {
45 #[serde(rename = "v")]
47 v: u32,
48 ts: String,
50 from: PathBuf,
52 to: PathBuf,
54 kind: String,
56 },
57 End {
59 #[serde(rename = "v")]
61 v: u32,
62 ts: String,
64 status: String,
66 applied: usize,
68 skipped: usize,
70 errors: usize,
72 },
73}
74
75pub struct JsonlLogSink {
78 writer: BufWriter<File>,
79 path: PathBuf,
80}
81
82impl JsonlLogSink {
83 pub fn open(log_dir: Option<&Path>, batch_id: Uuid, ts: &str) -> Result<Self, FrenError> {
87 let dir = match log_dir {
88 Some(p) => p.to_path_buf(),
89 None => default_log_dir(),
90 };
91 std::fs::create_dir_all(&dir).map_err(|source| FrenError::Io {
92 path: dir.clone(),
93 source,
94 })?;
95 let filename = format!("{ts}-{batch_id}.jsonl");
96 let path = dir.join(filename);
97 let file = OpenOptions::new()
98 .create(true)
99 .append(true)
100 .open(&path)
101 .map_err(|source| FrenError::Io {
102 path: path.clone(),
103 source,
104 })?;
105 Ok(Self {
106 writer: BufWriter::new(file),
107 path,
108 })
109 }
110}
111
112impl LogSink for JsonlLogSink {
113 fn append(&mut self, record: &LogRecord) -> Result<(), FrenError> {
114 let line =
115 serde_json::to_string(record).map_err(|e| FrenError::InvalidInput(e.to_string()))?;
116 writeln!(self.writer, "{line}").map_err(|source| FrenError::Io {
117 path: self.path.clone(),
118 source,
119 })?;
120 self.writer.flush().map_err(|source| FrenError::Io {
121 path: self.path.clone(),
122 source,
123 })?;
124 Ok(())
125 }
126
127 fn path(&self) -> Option<&Path> {
128 Some(&self.path)
129 }
130}
131
132pub struct NullLogSink;
134
135impl LogSink for NullLogSink {
136 fn append(&mut self, _record: &LogRecord) -> Result<(), FrenError> {
137 Ok(())
138 }
139 fn path(&self) -> Option<&Path> {
140 None
141 }
142}
143
144fn default_log_dir() -> PathBuf {
145 if let Some(state) = std::env::var_os("XDG_STATE_HOME") {
146 return PathBuf::from(state).join("fren").join("log");
147 }
148 if let Some(home) = std::env::var_os("HOME") {
149 return PathBuf::from(home)
150 .join(".local")
151 .join("state")
152 .join("fren")
153 .join("log");
154 }
155 PathBuf::from(".fren-log")
157}