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