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