Skip to main content

sage_runtime/
tracing.rs

1//! Tracing support for Sage programs.
2//!
3//! Emits newline-delimited JSON (NDJSON) trace events to stderr or a file.
4
5use 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
12/// Global flag indicating whether tracing is enabled.
13static TRACING_ENABLED: AtomicBool = AtomicBool::new(false);
14
15/// Global trace output (file or stderr).
16static TRACE_OUTPUT: Mutex<Option<TraceOutput>> = Mutex::new(None);
17
18enum TraceOutput {
19    Stderr,
20    File(std::fs::File),
21}
22
23/// Initialize tracing from environment variables.
24///
25/// Checks SAGE_TRACE and SAGE_TRACE_FILE environment variables.
26pub 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/// Check if tracing is enabled.
47#[inline]
48pub fn is_enabled() -> bool {
49    TRACING_ENABLED.load(Ordering::Relaxed)
50}
51
52/// Get current timestamp in milliseconds.
53fn 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/// A trace event.
61#[derive(Serialize)]
62struct TraceEvent<'a> {
63    /// Timestamp in milliseconds since Unix epoch.
64    t: u64,
65    /// Event kind.
66    kind: &'a str,
67    /// Additional fields (flattened).
68    #[serde(flatten)]
69    data: serde_json::Value,
70}
71
72/// Emit a trace event.
73fn 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
100/// Trace an agent spawn event.
101pub 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
111/// Trace an agent emit event.
112pub 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
123/// Trace an agent stop event.
124pub 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
135/// Trace an agent error event.
136pub 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
150/// Trace an infer start event.
151pub 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
163/// Trace an infer complete event.
164pub 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
177/// Trace an infer error event.
178pub 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
192/// Trace a user-defined event (via the trace() keyword).
193pub 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        // Should be a reasonable timestamp (after year 2020)
210        assert!(ts > 1_577_836_800_000);
211    }
212
213    #[test]
214    fn test_is_enabled_default_false() {
215        // In tests, tracing should be disabled by default
216        // (unless SAGE_TRACE is set in the environment)
217        // We can't reliably test this without modifying global state
218    }
219}