bsv_wallet_cli/
backstop_log.rs1use std::collections::HashMap;
28use std::fmt;
29use std::sync::Mutex;
30
31use tracing::field::{Field, Visit};
32use tracing::{Event, Level, Metadata, Subscriber};
33use tracing_subscriber::layer::{Context, Filter, Layer};
34
35const SERVICES_TARGET: &str = "bsv_wallet_toolbox_rs::services::services";
37const BACKSTOP_TARGET: &str = "bsv_wallet_toolbox_rs::monitor::tasks::check_for_proofs";
39const SERVED: &str = "get_merkle_path: proof served";
40const REMEMBERED: usize = 1024;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct BackstopProof {
47 pub provider: String,
50 pub txid: String,
51 pub height: Option<u32>,
53}
54
55pub struct BackstopLayer {
58 served: Mutex<HashMap<String, String>>,
59 sink: Box<dyn Fn(BackstopProof) + Send + Sync>,
60}
61
62impl BackstopLayer {
63 pub fn new(sink: impl Fn(BackstopProof) + Send + Sync + 'static) -> Self {
64 Self {
65 served: Mutex::new(HashMap::new()),
66 sink: Box::new(sink),
67 }
68 }
69
70 pub fn logging() -> Self {
73 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<BackstopProof>();
74 tokio::spawn(async move {
75 while let Some(hit) = rx.recv().await {
76 log(&hit);
77 }
78 });
79 Self::new(move |hit| {
80 let _ = tx.send(hit);
81 })
82 }
83}
84
85pub fn log(hit: &BackstopProof) {
87 tracing::info!(
88 marker = "proof_backstop_provider",
89 provider = %hit.provider,
90 txid = %hit.txid,
91 height = ?hit.height,
92 "the monitor's proof backstop stored a proof"
93 );
94}
95
96pub struct BackstopFilter;
98
99impl<S> Filter<S> for BackstopFilter {
100 fn enabled(&self, meta: &Metadata<'_>, _: &Context<'_, S>) -> bool {
101 meta.is_event()
102 && ((meta.target() == SERVICES_TARGET && *meta.level() == Level::DEBUG)
103 || (meta.target() == BACKSTOP_TARGET && *meta.level() == Level::INFO))
104 }
105}
106
107#[derive(Default)]
108struct Fields {
109 message: Option<String>,
110 marker: Option<String>,
111 provider: Option<String>,
112 txid: Option<String>,
113 block_height: Option<String>,
114}
115
116impl Visit for Fields {
117 fn record_str(&mut self, field: &Field, value: &str) {
118 self.set(field, value.to_string());
119 }
120
121 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
122 self.set(field, format!("{value:?}"));
123 }
124}
125
126impl Fields {
127 fn set(&mut self, field: &Field, value: String) {
128 match field.name() {
129 "message" => self.message = Some(value),
130 "marker" => self.marker = Some(value),
131 "provider" => self.provider = Some(value),
132 "txid" => self.txid = Some(value),
133 "block_height" => self.block_height = Some(value),
134 _ => {}
135 }
136 }
137}
138
139fn height_of(text: &str) -> Option<u32> {
142 let digits = text.trim_start_matches("Some(").trim_end_matches(')');
143 digits.parse().ok()
144}
145
146impl<S: Subscriber> Layer<S> for BackstopLayer {
147 fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) {
148 let mut fields = Fields::default();
149 event.record(&mut fields);
150 let Some(txid) = fields.txid else {
151 return;
152 };
153 let mut served = self.served.lock().unwrap_or_else(|e| e.into_inner());
154 if fields.message.as_deref() == Some(SERVED) {
155 if let Some(provider) = fields.provider {
156 if served.len() >= REMEMBERED {
157 served.clear();
158 }
159 served.insert(txid, provider);
160 }
161 } else if fields.marker.as_deref() == Some("proof_backstop_hit") {
162 let provider = served
163 .remove(&txid)
164 .unwrap_or_else(|| "status-answer".to_string());
165 drop(served);
166 (self.sink)(BackstopProof {
167 provider,
168 txid,
169 height: fields.block_height.as_deref().and_then(height_of),
170 });
171 }
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use std::sync::Arc;
179 use tracing_subscriber::layer::SubscriberExt;
180
181 #[test]
185 fn the_backstop_hit_names_the_provider_that_served_its_path() {
186 let hits: Arc<Mutex<Vec<BackstopProof>>> = Arc::default();
187 let sink = hits.clone();
188 let layer = BackstopLayer::new(move |hit| sink.lock().unwrap().push(hit));
189 let subscriber = tracing_subscriber::registry().with(layer.with_filter(BackstopFilter));
190 let txid = "9b".repeat(32);
191 let inline = "c4".repeat(32);
192 tracing::subscriber::with_default(subscriber, || {
193 tracing::debug!(
194 target: "bsv_wallet_toolbox_rs::services::services",
195 provider = %"WhatsOnChain",
196 txid = %txid,
197 "get_merkle_path: proof served"
198 );
199 tracing::info!(
200 target: "bsv_wallet_toolbox_rs::monitor::tasks::check_for_proofs",
201 txid = %txid,
202 status = ?"Completed",
203 block_height = ?Some(971_555u32),
204 marker = "proof_backstop_hit",
205 "Transaction status synchronized by backstop fetch"
206 );
207 tracing::info!(
208 target: "bsv_wallet_toolbox_rs::monitor::tasks::check_for_proofs",
209 txid = %inline,
210 block_height = ?Some(971_556u32),
211 marker = "proof_backstop_hit",
212 "Transaction status synchronized by backstop fetch"
213 );
214 tracing::debug!(
216 target: "bsv_wallet_cli::elsewhere",
217 provider = %"Bitails",
218 txid = %inline,
219 "get_merkle_path: proof served"
220 );
221 });
222 let hits = hits.lock().unwrap();
223 println!("{hits:?}");
224 assert_eq!(
225 *hits,
226 vec![
227 BackstopProof {
228 provider: "WhatsOnChain".into(),
229 txid,
230 height: Some(971_555),
231 },
232 BackstopProof {
233 provider: "status-answer".into(),
234 txid: inline,
235 height: Some(971_556),
236 },
237 ]
238 );
239 }
240}