1use async_trait::async_trait;
2use std::path::PathBuf;
3use tokio::io::AsyncWriteExt;
4use tokio::sync::Mutex;
5
6use crate::{CallbackHandler, RunTree};
7use lc_schema::Message;
8
9pub enum LogFormat {
10 Plain,
11 Json,
12 JsonLines,
13}
14
15pub struct FileCallbackHandler {
16 file: Mutex<tokio::fs::File>,
17 path: PathBuf,
18 format: LogFormat,
19}
20
21impl FileCallbackHandler {
22 pub fn new(path: impl Into<PathBuf>) -> Result<Self, std::io::Error> {
23 let path = path.into();
24 let std_file = std::fs::OpenOptions::new()
26 .create(true)
27 .append(true)
28 .open(&path)?;
29 let file = tokio::fs::File::from_std(std_file);
30 Ok(Self {
31 file: Mutex::new(file),
32 path,
33 format: LogFormat::JsonLines,
34 })
35 }
36
37 pub fn with_format(mut self, format: LogFormat) -> Self {
38 self.format = format;
39 self
40 }
41
42 async fn write_log(&self, event: &str, run: &RunTree, data: serde_json::Value) {
43 let mut file = self.file.lock().await;
44 match self.format {
45 LogFormat::JsonLines => {
46 let entry = serde_json::json!({
47 "timestamp": chrono::Utc::now().to_rfc3339(),
48 "event": event,
49 "run_id": run.id,
50 "run_name": run.name,
51 "run_type": run.run_type.as_str(),
52 "data": data
53 });
54 if let Err(e) = file.write_all(format!("{}\n", entry).as_bytes()).await {
56 log::warn!("FileCallbackHandler write failed: {}", e);
57 }
58 if let Err(e) = file.flush().await {
59 log::warn!("FileCallbackHandler flush failed: {}", e);
60 }
61 }
62 LogFormat::Json => {
63 let entry = serde_json::json!({
64 "timestamp": chrono::Utc::now().to_rfc3339(),
65 "event": event,
66 "run_id": run.id,
67 "run_name": run.name,
68 "run_type": run.run_type.as_str(),
69 "data": data
70 });
71 if let Err(e) = file
72 .write_all(
73 format!(
74 "{}\n",
75 serde_json::to_string_pretty(&entry).unwrap_or_default()
76 )
77 .as_bytes(),
78 )
79 .await
80 {
81 log::warn!("FileCallbackHandler write failed: {}", e);
82 }
83 if let Err(e) = file.flush().await {
84 log::warn!("FileCallbackHandler flush failed: {}", e);
85 }
86 }
87 LogFormat::Plain => {
88 let line = format!(
89 "[{}] {} - {} ({})\n",
90 chrono::Utc::now().to_rfc3339(),
91 event,
92 run.name,
93 run.run_type.as_str()
94 );
95 if let Err(e) = file.write_all(line.as_bytes()).await {
96 log::warn!("FileCallbackHandler write failed: {}", e);
97 }
98 if let Err(e) = file.flush().await {
99 log::warn!("FileCallbackHandler flush failed: {}", e);
100 }
101 }
102 }
103 }
104}
105
106#[async_trait]
107impl CallbackHandler for FileCallbackHandler {
108 async fn on_run_start(&self, run: &RunTree) {
109 self.write_log("run_start", run, serde_json::json!({"inputs": run.inputs}))
110 .await;
111 }
112
113 async fn on_run_end(&self, run: &RunTree) {
114 self.write_log(
115 "run_end",
116 run,
117 serde_json::json!({"outputs": run.outputs, "duration_ms": run.duration_ms()}),
118 )
119 .await;
120 }
121
122 async fn on_run_error(&self, run: &RunTree, error: &str) {
123 self.write_log("run_error", run, serde_json::json!({"error": error}))
124 .await;
125 }
126
127 async fn on_llm_start(&self, run: &RunTree, messages: &[Message]) {
128 self.write_log(
129 "llm_start",
130 run,
131 serde_json::json!({
132 "messages_count": messages.len(),
133 "messages": messages.iter().map(|m| m.content.clone()).collect::<Vec<_>>()
134 }),
135 )
136 .await;
137 }
138
139 async fn on_llm_end(&self, run: &RunTree, response: &str) {
140 self.write_log(
141 "llm_end",
142 run,
143 serde_json::json!({"response_length": response.len()}),
144 )
145 .await;
146 }
147
148 async fn on_llm_new_token(&self, run: &RunTree, token: &str) {
149 self.write_log("llm_new_token", run, serde_json::json!({"token": token}))
150 .await;
151 }
152
153 async fn on_llm_error(&self, run: &RunTree, error: &str) {
154 self.write_log("llm_error", run, serde_json::json!({"error": error}))
155 .await;
156 }
157
158 async fn on_chain_start(&self, run: &RunTree, inputs: &serde_json::Value) {
159 self.write_log("chain_start", run, serde_json::json!({"inputs": inputs}))
160 .await;
161 }
162
163 async fn on_chain_end(&self, run: &RunTree, outputs: &serde_json::Value) {
164 self.write_log("chain_end", run, serde_json::json!({"outputs": outputs}))
165 .await;
166 }
167
168 async fn on_chain_error(&self, run: &RunTree, error: &str) {
169 self.write_log("chain_error", run, serde_json::json!({"error": error}))
170 .await;
171 }
172
173 async fn on_tool_start(&self, run: &RunTree, tool_name: &str, input: &str) {
174 self.write_log(
175 "tool_start",
176 run,
177 serde_json::json!({"tool_name": tool_name, "input": input}),
178 )
179 .await;
180 }
181
182 async fn on_tool_end(&self, run: &RunTree, output: &str) {
183 self.write_log("tool_end", run, serde_json::json!({"output": output}))
184 .await;
185 }
186
187 async fn on_tool_error(&self, run: &RunTree, error: &str) {
188 self.write_log("tool_error", run, serde_json::json!({"error": error}))
189 .await;
190 }
191
192 async fn on_retriever_start(&self, run: &RunTree, query: &str) {
193 self.write_log("retriever_start", run, serde_json::json!({"query": query}))
194 .await;
195 }
196
197 async fn on_retriever_end(&self, run: &RunTree, documents: &[serde_json::Value]) {
198 self.write_log(
199 "retriever_end",
200 run,
201 serde_json::json!({"documents_count": documents.len()}),
202 )
203 .await;
204 }
205
206 async fn on_retriever_error(&self, run: &RunTree, error: &str) {
207 self.write_log("retriever_error", run, serde_json::json!({"error": error}))
208 .await;
209 }
210}
211
212impl std::fmt::Debug for FileCallbackHandler {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 f.debug_struct("FileCallbackHandler")
215 .field("path", &self.path)
216 .field(
217 "format",
218 &match self.format {
219 LogFormat::Plain => "Plain",
220 LogFormat::Json => "Json",
221 LogFormat::JsonLines => "JsonLines",
222 },
223 )
224 .finish()
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use tempfile::NamedTempFile;
232
233 #[tokio::test]
234 async fn test_file_handler_creation() {
235 let temp_file = NamedTempFile::new().unwrap();
236 let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
237 assert!(handler.path.exists());
238 }
239
240 #[tokio::test]
241 async fn test_write_log_json_lines() {
242 let temp_file = NamedTempFile::new().unwrap();
243 let handler = FileCallbackHandler::new(temp_file.path()).unwrap();
244
245 let run = RunTree::new(
246 "test_run",
247 crate::RunType::Llm,
248 serde_json::json!({"input": "test"}),
249 );
250 handler
251 .write_log("test_event", &run, serde_json::json!({"data": "value"}))
252 .await;
253
254 let content = std::fs::read_to_string(temp_file.path()).unwrap();
255 assert!(content.contains("test_event"));
256 assert!(content.contains("\"run_name\":\"test_run\""));
257 }
258
259 #[tokio::test]
260 async fn test_write_log_plain_format() {
261 let temp_file = NamedTempFile::new().unwrap();
262 let handler = FileCallbackHandler::new(temp_file.path())
263 .unwrap()
264 .with_format(LogFormat::Plain);
265
266 let run = RunTree::new(
267 "test_run",
268 crate::RunType::Llm,
269 serde_json::json!({"input": "test"}),
270 );
271 handler
272 .write_log("test_event", &run, serde_json::json!({}))
273 .await;
274
275 let content = std::fs::read_to_string(temp_file.path()).unwrap();
276 assert!(content.contains("test_event"));
277 assert!(content.contains("test_run"));
278 }
279}