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