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