1use serde::Serialize;
6use std::fs::OpenOptions;
7use std::io::Write;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Mutex;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12static TRACING_ENABLED: AtomicBool = AtomicBool::new(false);
14
15static TRACE_OUTPUT: Mutex<Option<TraceOutput>> = Mutex::new(None);
17
18enum TraceOutput {
19 Stderr,
20 File(std::fs::File),
21}
22
23pub fn init() {
27 if std::env::var("SAGE_TRACE").is_ok() || std::env::var("SAGE_TRACE_FILE").is_ok() {
28 TRACING_ENABLED.store(true, Ordering::SeqCst);
29
30 let output = if let Ok(path) = std::env::var("SAGE_TRACE_FILE") {
31 match OpenOptions::new().create(true).append(true).open(&path) {
32 Ok(file) => TraceOutput::File(file),
33 Err(e) => {
34 eprintln!("Warning: Could not open trace file {}: {}", path, e);
35 TraceOutput::Stderr
36 }
37 }
38 } else {
39 TraceOutput::Stderr
40 };
41
42 *TRACE_OUTPUT.lock().unwrap() = Some(output);
43 }
44}
45
46#[inline]
48pub fn is_enabled() -> bool {
49 TRACING_ENABLED.load(Ordering::Relaxed)
50}
51
52fn timestamp_ms() -> u64 {
54 SystemTime::now()
55 .duration_since(UNIX_EPOCH)
56 .map(|d| d.as_millis() as u64)
57 .unwrap_or(0)
58}
59
60#[derive(Serialize)]
62struct TraceEvent<'a> {
63 t: u64,
65 kind: &'a str,
67 #[serde(flatten)]
69 data: serde_json::Value,
70}
71
72fn emit_event(kind: &str, data: serde_json::Value) {
74 if !is_enabled() {
75 return;
76 }
77
78 let event = TraceEvent {
79 t: timestamp_ms(),
80 kind,
81 data,
82 };
83
84 if let Ok(json) = serde_json::to_string(&event) {
85 let mut guard = TRACE_OUTPUT.lock().unwrap();
86 if let Some(ref mut output) = *guard {
87 let line = format!("{}\n", json);
88 match output {
89 TraceOutput::Stderr => {
90 let _ = std::io::stderr().write_all(line.as_bytes());
91 }
92 TraceOutput::File(f) => {
93 let _ = f.write_all(line.as_bytes());
94 }
95 }
96 }
97 }
98}
99
100pub fn agent_spawn(agent: &str, id: &str) {
102 emit_event(
103 "agent.spawn",
104 serde_json::json!({
105 "agent": agent,
106 "id": id,
107 }),
108 );
109}
110
111pub fn agent_emit(agent: &str, id: &str, value_type: &str) {
113 emit_event(
114 "agent.emit",
115 serde_json::json!({
116 "agent": agent,
117 "id": id,
118 "value_type": value_type,
119 }),
120 );
121}
122
123pub fn agent_stop(agent: &str, id: &str, duration_ms: u64) {
125 emit_event(
126 "agent.stop",
127 serde_json::json!({
128 "agent": agent,
129 "id": id,
130 "duration_ms": duration_ms,
131 }),
132 );
133}
134
135pub fn agent_error(agent: &str, id: &str, error_kind: &str, message: &str) {
137 emit_event(
138 "agent.error",
139 serde_json::json!({
140 "agent": agent,
141 "id": id,
142 "error": {
143 "kind": error_kind,
144 "message": message,
145 },
146 }),
147 );
148}
149
150pub fn infer_start(agent: &str, id: &str, model: &str, prompt_len: usize) {
152 emit_event(
153 "infer.start",
154 serde_json::json!({
155 "agent": agent,
156 "id": id,
157 "model": model,
158 "prompt_len": prompt_len,
159 }),
160 );
161}
162
163pub fn infer_complete(agent: &str, id: &str, model: &str, response_len: usize, duration_ms: u64) {
165 emit_event(
166 "infer.complete",
167 serde_json::json!({
168 "agent": agent,
169 "id": id,
170 "model": model,
171 "response_len": response_len,
172 "duration_ms": duration_ms,
173 }),
174 );
175}
176
177pub fn infer_error(agent: &str, id: &str, error_kind: &str, message: &str) {
179 emit_event(
180 "infer.error",
181 serde_json::json!({
182 "agent": agent,
183 "id": id,
184 "error": {
185 "kind": error_kind,
186 "message": message,
187 },
188 }),
189 );
190}
191
192pub fn user(message: &str) {
194 emit_event(
195 "user",
196 serde_json::json!({
197 "message": message,
198 }),
199 );
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_timestamp_ms() {
208 let ts = timestamp_ms();
209 assert!(ts > 1_577_836_800_000);
211 }
212
213 #[test]
214 fn test_is_enabled_default_false() {
215 }
219}