Skip to main content

opencode_orchestrator_mcp/
logging.rs

1use agentic_logging::LogWriter;
2use agentic_logging::ToolCallRecord;
3use chrono::Utc;
4use std::path::PathBuf;
5
6pub const OPENCODE_ORCHESTRATOR_LOG_DIR: &str = "OPENCODE_ORCHESTRATOR_LOG_DIR";
7
8pub fn resolve_logs_dir() -> Option<PathBuf> {
9    if let Ok(value) = std::env::var(OPENCODE_ORCHESTRATOR_LOG_DIR) {
10        let value = value.trim();
11        if !value.is_empty() {
12            return Some(PathBuf::from(value));
13        }
14    }
15
16    thoughts_tool::documents::active_logs_dir().ok()
17}
18
19pub fn append_record_best_effort(record: &ToolCallRecord) {
20    let Some(dir) = resolve_logs_dir() else {
21        return;
22    };
23
24    let writer = LogWriter::new(dir);
25    if let Err(error) = writer.append_jsonl(record) {
26        tracing::warn!(error = %error, "Failed to append orchestrator JSONL log");
27    }
28}
29
30pub fn write_markdown_best_effort(
31    completed_at: agentic_logging::chrono::DateTime<Utc>,
32    call_id: &str,
33    content: &str,
34) -> Option<String> {
35    let dir = resolve_logs_dir()?;
36
37    let writer = LogWriter::new(dir);
38    match writer.write_markdown_response(completed_at, call_id, content) {
39        Ok(filename) if !filename.is_empty() => Some(filename),
40        Ok(_) => None,
41        Err(error) => {
42            tracing::warn!(error = %error, "Failed to write orchestrator markdown log");
43            None
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use agentic_logging::CallTimer;
52    use agentic_logging::ToolCallRecord;
53    use serial_test::serial;
54    use std::io::Read;
55
56    struct EnvVarGuard {
57        key: &'static str,
58        previous: Option<std::ffi::OsString>,
59    }
60
61    impl Drop for EnvVarGuard {
62        fn drop(&mut self) {
63            match &self.previous {
64                // SAFETY: Test serialized by #[serial(env)], preventing concurrent env access.
65                Some(value) => unsafe { std::env::set_var(self.key, value) },
66                // SAFETY: Test serialized by #[serial(env)], preventing concurrent env access.
67                None => unsafe { std::env::remove_var(self.key) },
68            }
69        }
70    }
71
72    fn sample_record() -> ToolCallRecord {
73        let timer = CallTimer::start();
74        let (completed_at, duration_ms) = timer.finish();
75        ToolCallRecord {
76            call_id: timer.call_id,
77            server: "opencode-orchestrator-mcp".into(),
78            tool: "run".into(),
79            started_at: timer.started_at,
80            completed_at,
81            duration_ms,
82            request: serde_json::json!({"message": "hello"}),
83            response_file: None,
84            success: true,
85            error: None,
86            model: None,
87            token_usage: None,
88            summary: None,
89        }
90    }
91
92    #[test]
93    #[serial(env)]
94    fn env_log_dir_writes_jsonl() {
95        let _env = EnvVarGuard {
96            key: OPENCODE_ORCHESTRATOR_LOG_DIR,
97            previous: std::env::var_os(OPENCODE_ORCHESTRATOR_LOG_DIR),
98        };
99        let tmp = tempfile::tempdir().unwrap();
100
101        // SAFETY: Test serialized by #[serial(env)], preventing concurrent env access.
102        unsafe { std::env::set_var(OPENCODE_ORCHESTRATOR_LOG_DIR, tmp.path()) };
103
104        let record = sample_record();
105        append_record_best_effort(&record);
106
107        let bucket = format!("tool_logs_{}", record.completed_at.format("%Y-%m-%d"));
108        let path = tmp.path().join(format!("{bucket}.jsonl"));
109        assert!(path.exists());
110
111        let mut content = String::new();
112        std::fs::File::open(path)
113            .unwrap()
114            .read_to_string(&mut content)
115            .unwrap();
116        assert!(content.contains("opencode-orchestrator-mcp"));
117    }
118
119    #[test]
120    #[serial(env)]
121    fn invalid_log_dir_is_swallowed() {
122        let _env = EnvVarGuard {
123            key: OPENCODE_ORCHESTRATOR_LOG_DIR,
124            previous: std::env::var_os(OPENCODE_ORCHESTRATOR_LOG_DIR),
125        };
126        let tmp = tempfile::tempdir().unwrap();
127        let invalid_path = tmp.path().join("not-a-directory");
128        std::fs::write(&invalid_path, "file").unwrap();
129
130        // SAFETY: Test serialized by #[serial(env)], preventing concurrent env access.
131        unsafe { std::env::set_var(OPENCODE_ORCHESTRATOR_LOG_DIR, &invalid_path) };
132
133        append_record_best_effort(&sample_record());
134        assert!(invalid_path.is_file());
135    }
136}