1use std::sync::{Mutex, OnceLock};
63
64static ENABLED_TAGS:OnceLock<Vec<String>> = OnceLock::new();
65static SHORT_MODE:OnceLock<bool> = OnceLock::new();
66
67static APP_DATA_PREFIX:OnceLock<Option<String>> = OnceLock::new();
70
71fn DetectAppDataPrefix() -> Option<String> {
72 if let Ok(Home) = std::env::var("HOME") {
74 let Base = format!("{}/Library/Application Support", Home);
75 if let Ok(Entries) = std::fs::read_dir(&Base) {
76 for Entry in Entries.flatten() {
77 let Name = Entry.file_name();
78 let Name = Name.to_string_lossy();
79 if Name.starts_with("land.editor.") && Name.contains("mountain") {
80 return Some(format!("{}/{}", Base, Name));
81 }
82 }
83 }
84 }
85 None
86}
87
88pub fn AppDataPrefix() -> &'static Option<String> { APP_DATA_PREFIX.get_or_init(DetectAppDataPrefix) }
90
91pub fn AliasPath(Input:&str) -> String {
93 if let Some(Prefix) = AppDataPrefix() {
94 Input.replace(Prefix.as_str(), "$APP")
95 } else {
96 Input.to_string()
97 }
98}
99
100pub struct DedupState {
103 pub LastKey:String,
104 pub Count:u64,
105}
106
107pub static DEDUP:Mutex<DedupState> = Mutex::new(DedupState { LastKey:String::new(), Count:0 });
108
109pub fn FlushDedup() {
111 if let Ok(mut State) = DEDUP.lock() {
112 if State.Count > 1 {
113 eprintln!(" (x{})", State.Count);
114 }
115 State.LastKey.clear();
116 State.Count = 0;
117 }
118}
119
120fn EnabledTags() -> &'static Vec<String> {
123 ENABLED_TAGS.get_or_init(|| {
124 match std::env::var("LAND_DEV_LOG") {
125 Ok(Val) => Val.split(',').map(|S| S.trim().to_lowercase()).collect(),
126 Err(_) => vec![],
127 }
128 })
129}
130
131pub fn IsShort() -> bool { *SHORT_MODE.get_or_init(|| EnabledTags().iter().any(|T| T == "short")) }
133
134pub fn IsEnabled(Tag:&str) -> bool {
136 let Tags = EnabledTags();
137 if Tags.is_empty() {
138 return false;
139 }
140 let Lower = Tag.to_lowercase();
141 Tags.iter().any(|T| T == "all" || T == "short" || T == Lower.as_str())
142}
143
144#[macro_export]
150macro_rules! dev_log {
151 ($Tag:expr, $($Arg:tt)*) => {
152 if $crate::IPC::DevLog::IsEnabled($Tag) {
153 let RawMessage = format!($($Arg)*);
154 let TagUpper = $Tag.to_uppercase();
155 if $crate::IPC::DevLog::IsShort() {
156 let Aliased = $crate::IPC::DevLog::AliasPath(&RawMessage);
157 let Key = format!("{}:{}", TagUpper, Aliased);
158 let ShouldPrint = {
159 if let Ok(mut State) = $crate::IPC::DevLog::DEDUP.lock() {
160 if State.LastKey == Key {
161 State.Count += 1;
162 false
163 } else {
164 let PrevCount = State.Count;
165 let HadPrev = !State.LastKey.is_empty();
166 State.LastKey = Key;
167 State.Count = 1;
168 if HadPrev && PrevCount > 1 {
169 eprintln!(" (x{})", PrevCount);
170 }
171 true
172 }
173 } else {
174 true
175 }
176 };
177 if ShouldPrint {
178 eprintln!("[DEV:{}] {}", TagUpper, Aliased);
179 }
180 } else {
181 eprintln!("[DEV:{}] {}", TagUpper, RawMessage);
182 }
183 }
184 };
185}
186
187use std::{
192 sync::atomic::{AtomicBool, Ordering},
193 time::{SystemTime, UNIX_EPOCH},
194};
195
196static OTLP_AVAILABLE:AtomicBool = AtomicBool::new(true);
197static OTLP_TRACE_ID:OnceLock<String> = OnceLock::new();
198
199fn GetTraceId() -> &'static str {
200 OTLP_TRACE_ID.get_or_init(|| {
201 use std::{
202 collections::hash_map::DefaultHasher,
203 hash::{Hash, Hasher},
204 };
205 let mut H = DefaultHasher::new();
206 std::process::id().hash(&mut H);
207 SystemTime::now()
208 .duration_since(UNIX_EPOCH)
209 .unwrap_or_default()
210 .as_nanos()
211 .hash(&mut H);
212 format!("{:032x}", H.finish() as u128)
213 })
214}
215
216pub fn NowNano() -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64 }
217
218pub fn EmitOTLPSpan(Name:&str, StartNano:u64, EndNano:u64, Attributes:&[(&str, &str)]) {
221 if !cfg!(debug_assertions) {
222 return;
223 }
224 if !OTLP_AVAILABLE.load(Ordering::Relaxed) {
225 return;
226 }
227
228 let SpanId = format!("{:016x}", rand_u64());
229 let TraceId = GetTraceId().to_string();
230 let SpanName = Name.to_string();
231
232 let AttributesJson:Vec<String> = Attributes
233 .iter()
234 .map(|(K, V)| {
235 format!(
236 r#"{{"key":"{}","value":{{"stringValue":"{}"}}}}"#,
237 K,
238 V.replace('\\', "\\\\").replace('"', "\\\"")
239 )
240 })
241 .collect();
242
243 let IsError = SpanName.contains("error");
244
245 let StatusCode = if IsError { 2 } else { 1 };
246 let Payload = format!(
247 concat!(
248 r#"{{"resourceSpans":[{{"resource":{{"attributes":["#,
249 r#"{{"key":"service.name","value":{{"stringValue":"land-editor-mountain"}}}},"#,
250 r#"{{"key":"service.version","value":{{"stringValue":"0.0.1"}}}}"#,
251 r#"]}},"scopeSpans":[{{"scope":{{"name":"mountain.ipc","version":"1.0.0"}},"#,
252 r#""spans":[{{"traceId":"{}","spanId":"{}","name":"{}","kind":1,"#,
253 r#""startTimeUnixNano":"{}","endTimeUnixNano":"{}","#,
254 r#""attributes":[{}],"status":{{"code":{}}}}}]}}]}}]}}"#,
255 ),
256 TraceId,
257 SpanId,
258 SpanName,
259 StartNano,
260 EndNano,
261 AttributesJson.join(","),
262 StatusCode,
263 );
264
265 std::thread::spawn(move || {
267 use std::{
268 io::{Read as IoRead, Write as IoWrite},
269 net::TcpStream,
270 time::Duration,
271 };
272
273 let Ok(mut Stream) = TcpStream::connect_timeout(&"127.0.0.1:4318".parse().unwrap(), Duration::from_millis(200))
274 else {
275 OTLP_AVAILABLE.store(false, Ordering::Relaxed);
276 return;
277 };
278 let _ = Stream.set_write_timeout(Some(Duration::from_millis(200)));
279 let _ = Stream.set_read_timeout(Some(Duration::from_millis(200)));
280
281 let HttpReq = format!(
282 "POST /v1/traces HTTP/1.1\r\nHost: 127.0.0.1:4318\r\nContent-Type: application/json\r\nContent-Length: \
283 {}\r\nConnection: close\r\n\r\n",
284 Payload.len()
285 );
286 if Stream.write_all(HttpReq.as_bytes()).is_err() {
287 return;
288 }
289 if Stream.write_all(Payload.as_bytes()).is_err() {
290 return;
291 }
292 let mut Buf = [0u8; 32];
293 let _ = Stream.read(&mut Buf);
294 if !(Buf.starts_with(b"HTTP/1.1 2") || Buf.starts_with(b"HTTP/1.0 2")) {
295 OTLP_AVAILABLE.store(false, Ordering::Relaxed);
296 }
297 });
298}
299
300fn rand_u64() -> u64 {
301 use std::{
302 collections::hash_map::DefaultHasher,
303 hash::{Hash, Hasher},
304 };
305 let mut H = DefaultHasher::new();
306 std::thread::current().id().hash(&mut H);
307 NowNano().hash(&mut H);
308 H.finish()
309}
310
311#[macro_export]
314macro_rules! otel_span {
315 ($Name:expr, $Start:expr, $Attrs:expr) => {
316 $crate::IPC::DevLog::EmitOTLPSpan($Name, $Start, $crate::IPC::DevLog::NowNano(), $Attrs)
317 };
318 ($Name:expr, $Start:expr) => {
319 $crate::IPC::DevLog::EmitOTLPSpan($Name, $Start, $crate::IPC::DevLog::NowNano(), &[])
320 };
321}