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