1use serde_json::Value;
22
23const ROTATE_AT_BYTES: u64 = 8 * 1024 * 1024;
30
31pub fn is_enabled() -> bool {
36 if std::env::var("WIRE_DIAG")
37 .map(|v| !v.is_empty())
38 .unwrap_or(false)
39 {
40 return true;
41 }
42 if let Ok(state) = crate::config::state_dir()
43 && state.join("diag.enabled").exists()
44 {
45 return true;
46 }
47 false
48}
49
50pub fn emit(event_type: &str, payload: Value) {
55 if !is_enabled() {
56 return;
57 }
58 let state = match crate::config::state_dir() {
59 Ok(s) => s,
60 Err(_) => return,
61 };
62 if std::fs::create_dir_all(&state).is_err() {
63 return;
64 }
65 let path = state.join("diag.jsonl");
66 if let Ok(meta) = std::fs::metadata(&path)
69 && meta.len() >= ROTATE_AT_BYTES
70 {
71 let _ = std::fs::rename(&path, state.join("diag.jsonl.1"));
72 }
73 let line = serde_json::json!({
74 "ts": std::time::SystemTime::now()
75 .duration_since(std::time::UNIX_EPOCH)
76 .map(|d| d.as_secs())
77 .unwrap_or(0),
78 "pid": std::process::id(),
79 "version": env!("CARGO_PKG_VERSION"),
80 "type": event_type,
81 "payload": payload,
82 });
83 let bytes = match serde_json::to_vec(&line) {
84 Ok(mut b) => {
85 b.push(b'\n');
86 b
87 }
88 Err(_) => return,
89 };
90 use std::io::Write;
91 if let Ok(mut f) = std::fs::OpenOptions::new()
92 .create(true)
93 .append(true)
94 .open(&path)
95 {
96 let _ = f.write_all(&bytes);
97 }
98}
99
100pub fn tail(n: usize) -> Vec<Value> {
104 let path = match crate::config::state_dir() {
105 Ok(s) => s.join("diag.jsonl"),
106 Err(_) => return Vec::new(),
107 };
108 let body = match std::fs::read_to_string(&path) {
109 Ok(b) => b,
110 Err(_) => return Vec::new(),
111 };
112 let mut out: Vec<Value> = body
113 .lines()
114 .filter(|l| !l.trim().is_empty())
115 .filter_map(|l| serde_json::from_str(l).ok())
116 .collect();
117 let start = out.len().saturating_sub(n);
118 out.drain(..start);
119 out
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use serde_json::json;
126
127 #[test]
128 fn diag_is_noop_when_disabled() {
129 crate::config::test_support::with_temp_home(|| {
130 assert!(!is_enabled());
132 emit("pull", json!({"events": 3}));
133 let state = crate::config::state_dir().unwrap();
134 let diag = state.join("diag.jsonl");
135 assert!(!diag.exists(), "diag must not write when disabled");
136 });
137 }
138
139 #[test]
140 fn diag_emits_when_env_var_set() {
141 crate::config::test_support::with_temp_home(|| {
142 crate::config::ensure_dirs().unwrap();
143 unsafe { std::env::set_var("WIRE_DIAG", "1") };
145 emit("pull", json!({"events": 2, "rejected": 0}));
146 unsafe { std::env::remove_var("WIRE_DIAG") };
147 let lines = tail(10);
148 assert_eq!(lines.len(), 1);
149 assert_eq!(lines[0]["type"], "pull");
150 assert_eq!(lines[0]["payload"]["events"], 2);
151 assert!(lines[0]["ts"].as_u64().is_some());
152 assert!(lines[0]["pid"].as_u64().is_some());
153 });
154 }
155
156 #[test]
157 fn diag_emits_when_file_knob_present() {
158 crate::config::test_support::with_temp_home(|| {
161 crate::config::ensure_dirs().unwrap();
162 let state = crate::config::state_dir().unwrap();
163 std::fs::write(state.join("diag.enabled"), "1").unwrap();
164 assert!(is_enabled());
165 emit("push", json!({"peer": "willard"}));
166 let lines = tail(10);
167 assert_eq!(lines.len(), 1);
168 assert_eq!(lines[0]["type"], "push");
169 });
170 }
171
172 #[test]
173 fn diag_tail_returns_last_n_entries_in_order() {
174 crate::config::test_support::with_temp_home(|| {
175 crate::config::ensure_dirs().unwrap();
176 unsafe { std::env::set_var("WIRE_DIAG", "1") };
177 for i in 0..5u32 {
178 emit("test", json!({"i": i}));
179 }
180 unsafe { std::env::remove_var("WIRE_DIAG") };
181 let lines = tail(3);
182 assert_eq!(lines.len(), 3);
183 assert_eq!(lines[0]["payload"]["i"], 2);
185 assert_eq!(lines[1]["payload"]["i"], 3);
186 assert_eq!(lines[2]["payload"]["i"], 4);
187 });
188 }
189}