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