1pub mod domain {
26 pub const STATE: &str = "degenbot::state";
28 pub const PATH: &str = "degenbot::path";
30 pub const SOLVER: &str = "degenbot::solver";
32 pub const SIM: &str = "degenbot::sim";
34 pub const PUMP: &str = "degenbot::pump";
36 pub const EXEC: &str = "degenbot::exec";
38 pub const VERIFY: &str = "degenbot::verify";
40 pub const INGEST: &str = "degenbot::ingest";
42 pub const RPC: &str = "degenbot::rpc";
44 pub const AAVE: &str = "degenbot::aave";
46
47 pub const ALL: [&str; 10] = [
49 STATE, PATH, SOLVER, SIM, PUMP, EXEC, VERIFY, INGEST, RPC, AAVE,
50 ];
51
52 #[must_use]
54 pub fn is_valid(target: &str) -> bool {
55 ALL.contains(&target)
56 }
57}
58
59pub const ENGINE_SPAN_PREFIX: &str = "degenbot.";
61
62#[must_use]
68pub fn in_engine_span() -> bool {
69 tracing::Span::current()
70 .metadata()
71 .is_some_and(|meta| meta.name().starts_with(ENGINE_SPAN_PREFIX))
72}
73
74static DIAG_GUARD_WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
76
77pub fn guard_diag(context: &str) {
80 if in_engine_span() {
81 return;
82 }
83 #[cfg(feature = "strict-engine-spans")]
87 debug_assert!(
88 false,
89 "diagnostic emitted outside an engine span from {context} (ADR-043 §3)"
90 );
91 if !DIAG_GUARD_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
92 tracing::warn!(
93 target: DIAGNOSTIC_TARGET,
94 context,
95 "diagnostic emitted outside an engine span (this warning fires once; ADR-043 §3)"
96 );
97 }
98}
99
100pub const DIAGNOSTIC_TARGET: &str = "degenbot::diag";
103
104pub fn install_panic_hook() {
113 let previous = std::panic::take_hook();
114 std::panic::set_hook(Box::new(move |info| {
115 let thread = std::thread::current();
116 let thread = thread.name().unwrap_or("<unnamed>").to_owned();
117 let payload = info
118 .payload()
119 .downcast_ref::<&str>()
120 .map(|s| (*s).to_owned())
121 .or_else(|| info.payload().downcast_ref::<String>().cloned())
122 .unwrap_or_else(|| info.to_string());
123 tracing::Span::current().record("otel.status_code", "ERROR");
124 tracing::error!(
125 target: DIAGNOSTIC_TARGET,
126 exception_type = "panic",
127 thread,
128 payload,
129 "panic captured"
130 );
131 previous(info);
132 }));
133}
134
135#[macro_export]
150macro_rules! telemetry_target {
151 (state) => {
152 $crate::telemetry::domain::STATE
153 };
154 (path) => {
155 $crate::telemetry::domain::PATH
156 };
157 (solver) => {
158 $crate::telemetry::domain::SOLVER
159 };
160 (sim) => {
161 $crate::telemetry::domain::SIM
162 };
163 (pump) => {
164 $crate::telemetry::domain::PUMP
165 };
166 (exec) => {
167 $crate::telemetry::domain::EXEC
168 };
169 (verify) => {
170 $crate::telemetry::domain::VERIFY
171 };
172 (ingest) => {
173 $crate::telemetry::domain::INGEST
174 };
175 (rpc) => {
176 $crate::telemetry::domain::RPC
177 };
178 (aave) => {
179 $crate::telemetry::domain::AAVE
180 };
181 ($other:ident) => {
182 ::core::compile_error!(::core::concat!(
183 "unknown telemetry domain `",
184 ::core::stringify!($other),
185 "`; valid domains: state,path,solver,sim,pump,exec,verify,ingest,rpc,aave"
186 ))
187 };
188}
189
190#[macro_export]
192macro_rules! diag {
193 (domain = $domain:ident, $($rest:tt)*) => {{
194 $crate::telemetry::guard_diag(::core::module_path!());
195 ::tracing::debug!(target: $crate::telemetry_target!($domain), $($rest)*)
196 }};
197}
198
199#[macro_export]
203macro_rules! diag_trace {
204 (domain = $domain:ident, $($rest:tt)*) => {{
205 $crate::telemetry::guard_diag(::core::module_path!());
206 ::tracing::trace!(target: $crate::telemetry_target!($domain), $($rest)*)
207 }};
208}
209
210#[macro_export]
215macro_rules! op_debug {
216 (domain = $domain:ident, $($rest:tt)*) => {
217 ::tracing::debug!(target: $crate::telemetry_target!($domain), $($rest)*)
218 };
219}
220
221#[macro_export]
223macro_rules! op_info {
224 (domain = $domain:ident, $($rest:tt)*) => {
225 ::tracing::info!(target: $crate::telemetry_target!($domain), $($rest)*)
226 };
227}
228
229#[macro_export]
231macro_rules! op_warn {
232 (domain = $domain:ident, $($rest:tt)*) => {
233 ::tracing::warn!(target: $crate::telemetry_target!($domain), $($rest)*)
234 };
235}
236
237#[macro_export]
239macro_rules! op_error {
240 (domain = $domain:ident, $($rest:tt)*) => {
241 ::tracing::error!(target: $crate::telemetry_target!($domain), $($rest)*)
242 };
243}
244
245#[macro_export]
247macro_rules! op_span {
248 (domain = $domain:ident, $name:literal $(, $($field:tt)*)?) => {
249 ::tracing::info_span!(
250 target: $crate::telemetry_target!($domain),
251 $name $(, $($field)*)?
252 )
253 };
254}
255
256pub const RETIRED_ENV_NAMES: &[(&str, &str)] = &[
260 ("DEGENBOT_VERIFY_DBG", "degenbot::verify=debug"),
261 ("DEGENBOT_V2_CALC_TRACE", "degenbot::sim=debug"),
262 ("DEGENBOT_SIM_LOG_REVERTED_SWAPS", "degenbot::sim=debug"),
263 ("DEGENBOT_SIM_DIVERGENCE_LOG", "degenbot::sim=debug"),
264 ("DEGENBOT_DUMP_CALL_TRACE", "degenbot::sim=trace"),
265 ("DEGENBOT_DUMP_TICK_MAPS", "degenbot::state=trace"),
266 ("DEGENBOT_WS_TRACE", "degenbot::ingest=debug"),
267 ("DEGENBOT_DRAIN_DBG", "degenbot::pump=debug"),
268 ("DEGENBOT_TRACE_DISPATCH", "degenbot::pump=debug"),
269 ("DEGENBOT_TRACE_REGISTER_SEED", "degenbot::path=debug"),
270 ("DEGENBOT_TRACE_LIQUIDITY", "degenbot::state=debug"),
271 ("DEGENBOT_TRACE_TICK", "degenbot::state=debug"),
272 ("DEGENBOT_GATE_TRACE", "degenbot::solver=debug"),
273 ("DEGENBOT_AAVE_EVTRACE", "degenbot::aave=debug"),
274 ("DEGENBOT_AAVE_TX_TRACE", "degenbot::aave=debug"),
275 (
278 "DEGENBOT_LOG_FMT",
279 "the console writer is derived (one writer per process)",
280 ),
281];
282
283pub fn warn_retired_env_names() {
289 for (name, directive) in RETIRED_ENV_NAMES {
290 if let Ok(value) = std::env::var(name) {
291 crate::op_warn!(
292 domain = pump,
293 retired_env = %name,
294 value = %value,
295 equivalent = %directive,
296 "retired telemetry flag set; use the domain directive instead (ADR-043 section 5)"
297 );
298 }
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::domain;
305 use std::fmt::Write as _;
306 use std::sync::{Arc, Mutex};
307 use tracing_subscriber::layer::SubscriberExt;
308
309 static PANIC_HOOK_LOCK: Mutex<()> = Mutex::new(());
310
311 #[derive(Clone, Default)]
312 struct Capture {
313 events: Arc<Mutex<Vec<String>>>,
314 records: Arc<Mutex<Vec<String>>>,
315 }
316
317 impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Capture {
318 fn on_event(
319 &self,
320 event: &tracing::Event<'_>,
321 _ctx: tracing_subscriber::layer::Context<'_, S>,
322 ) {
323 if *event.metadata().level() != tracing::Level::ERROR {
324 return;
325 }
326 let mut text = String::new();
327 event.record(&mut Fmt(&mut text));
328 self.events
329 .lock()
330 .unwrap_or_else(std::sync::PoisonError::into_inner)
331 .push(text);
332 }
333
334 fn on_record(
335 &self,
336 _span: &tracing::span::Id,
337 values: &tracing::span::Record<'_>,
338 _ctx: tracing_subscriber::layer::Context<'_, S>,
339 ) {
340 let mut text = String::new();
341 values.record(&mut Fmt(&mut text));
342 self.records
343 .lock()
344 .unwrap_or_else(std::sync::PoisonError::into_inner)
345 .push(text);
346 }
347 }
348
349 struct Fmt<'a>(&'a mut String);
350
351 impl tracing::field::Visit for Fmt<'_> {
352 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
353 let _ = write!(self.0, "{}={value:?} ", field.name());
354 }
355
356 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
357 let _ = write!(self.0, "{}={value} ", field.name());
358 }
359 }
360
361 #[test]
362 fn domain_set_is_closed_and_complete() {
363 assert!(domain::is_valid(domain::SIM));
364 assert!(domain::is_valid(domain::AAVE));
365 assert!(!domain::is_valid("degenbot::diag"));
366 assert!(!domain::is_valid("sim"));
367 assert_eq!(domain::ALL.len(), 10);
368 }
369
370 #[test]
371 fn engine_span_detection_uses_the_degenbot_prefix() {
372 let subscriber = tracing_subscriber::registry();
373 tracing::subscriber::with_default(subscriber, || {
374 assert!(!super::in_engine_span());
375 let span = tracing::info_span!("degenbot.test.engine");
376 let _guard = span.enter();
377 assert!(super::in_engine_span());
378 });
379 }
380
381 #[test]
382 #[expect(clippy::panic)] fn panic_hook_emits_one_error_and_marks_the_active_span_error() {
384 let _guard = PANIC_HOOK_LOCK
385 .lock()
386 .unwrap_or_else(std::sync::PoisonError::into_inner);
387
388 let capture = Capture::default();
389 let subscriber = tracing_subscriber::registry().with(capture.clone());
390 tracing::subscriber::with_default(subscriber, || {
391 let span = tracing::info_span!(
394 "degenbot.test.engine",
395 otel.status_code = tracing::field::Empty,
396 );
397 let _entered = span.enter();
398 super::install_panic_hook();
399 let _ = std::panic::catch_unwind(|| panic!("boom-facade-123"));
400 let _ = std::panic::take_hook();
402 });
403
404 let events = capture
405 .events
406 .lock()
407 .unwrap_or_else(std::sync::PoisonError::into_inner);
408 assert_eq!(events.len(), 1, "exactly one ERROR per panic: {events:?}");
409 assert!(
410 events[0].contains("boom-facade-123"),
411 "payload carried: {events:?}"
412 );
413 assert!(
414 events[0].contains("thread="),
415 "thread name carried: {events:?}"
416 );
417 drop(events);
418
419 let records = capture
420 .records
421 .lock()
422 .unwrap_or_else(std::sync::PoisonError::into_inner);
423 assert!(
424 records
425 .iter()
426 .any(|r| r.contains("otel.status_code") && r.contains("ERROR")),
427 "active span marked Status::Error: {records:?}"
428 );
429 }
430
431 #[test]
432 fn facade_macros_expand_on_the_domain_target() {
433 let subscriber = tracing_subscriber::registry();
434 tracing::subscriber::with_default(subscriber, || {
435 crate::op_info!(domain = sim, block = 1u64, "sim probe");
436 let span = crate::op_span!(domain = sim, "degenbot.sim.probe", block = 1u64);
437 let _guard = span.enter();
438 });
439 }
440}