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