Skip to main content

lc_callbacks/handlers/
file_handler.rs

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        // Open synchronously for construction, then convert to tokio::fs::File
25        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                // M10 fix: log write errors instead of silently discarding
55                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}