use std::collections::HashMap;
use std::fmt;
use std::sync::Mutex;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Metadata, Subscriber};
use tracing_subscriber::layer::{Context, Filter, Layer};
const SERVICES_TARGET: &str = "bsv_wallet_toolbox_rs::services::services";
const BACKSTOP_TARGET: &str = "bsv_wallet_toolbox_rs::monitor::tasks::check_for_proofs";
const SERVED: &str = "get_merkle_path: proof served";
const REMEMBERED: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BackstopProof {
pub provider: String,
pub txid: String,
pub height: Option<u32>,
}
pub struct BackstopLayer {
served: Mutex<HashMap<String, String>>,
sink: Box<dyn Fn(BackstopProof) + Send + Sync>,
}
impl BackstopLayer {
pub fn new(sink: impl Fn(BackstopProof) + Send + Sync + 'static) -> Self {
Self {
served: Mutex::new(HashMap::new()),
sink: Box::new(sink),
}
}
pub fn logging() -> Self {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<BackstopProof>();
tokio::spawn(async move {
while let Some(hit) = rx.recv().await {
log(&hit);
}
});
Self::new(move |hit| {
let _ = tx.send(hit);
})
}
}
pub fn log(hit: &BackstopProof) {
tracing::info!(
marker = "proof_backstop_provider",
provider = %hit.provider,
txid = %hit.txid,
height = ?hit.height,
"the monitor's proof backstop stored a proof"
);
}
pub struct BackstopFilter;
impl<S> Filter<S> for BackstopFilter {
fn enabled(&self, meta: &Metadata<'_>, _: &Context<'_, S>) -> bool {
meta.is_event()
&& ((meta.target() == SERVICES_TARGET && *meta.level() == Level::DEBUG)
|| (meta.target() == BACKSTOP_TARGET && *meta.level() == Level::INFO))
}
}
#[derive(Default)]
struct Fields {
message: Option<String>,
marker: Option<String>,
provider: Option<String>,
txid: Option<String>,
block_height: Option<String>,
}
impl Visit for Fields {
fn record_str(&mut self, field: &Field, value: &str) {
self.set(field, value.to_string());
}
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
self.set(field, format!("{value:?}"));
}
}
impl Fields {
fn set(&mut self, field: &Field, value: String) {
match field.name() {
"message" => self.message = Some(value),
"marker" => self.marker = Some(value),
"provider" => self.provider = Some(value),
"txid" => self.txid = Some(value),
"block_height" => self.block_height = Some(value),
_ => {}
}
}
}
fn height_of(text: &str) -> Option<u32> {
let digits = text.trim_start_matches("Some(").trim_end_matches(')');
digits.parse().ok()
}
impl<S: Subscriber> Layer<S> for BackstopLayer {
fn on_event(&self, event: &Event<'_>, _: Context<'_, S>) {
let mut fields = Fields::default();
event.record(&mut fields);
let Some(txid) = fields.txid else {
return;
};
let mut served = self.served.lock().unwrap_or_else(|e| e.into_inner());
if fields.message.as_deref() == Some(SERVED) {
if let Some(provider) = fields.provider {
if served.len() >= REMEMBERED {
served.clear();
}
served.insert(txid, provider);
}
} else if fields.marker.as_deref() == Some("proof_backstop_hit") {
let provider = served
.remove(&txid)
.unwrap_or_else(|| "status-answer".to_string());
drop(served);
(self.sink)(BackstopProof {
provider,
txid,
height: fields.block_height.as_deref().and_then(height_of),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tracing_subscriber::layer::SubscriberExt;
#[test]
fn the_backstop_hit_names_the_provider_that_served_its_path() {
let hits: Arc<Mutex<Vec<BackstopProof>>> = Arc::default();
let sink = hits.clone();
let layer = BackstopLayer::new(move |hit| sink.lock().unwrap().push(hit));
let subscriber = tracing_subscriber::registry().with(layer.with_filter(BackstopFilter));
let txid = "9b".repeat(32);
let inline = "c4".repeat(32);
tracing::subscriber::with_default(subscriber, || {
tracing::debug!(
target: "bsv_wallet_toolbox_rs::services::services",
provider = %"WhatsOnChain",
txid = %txid,
"get_merkle_path: proof served"
);
tracing::info!(
target: "bsv_wallet_toolbox_rs::monitor::tasks::check_for_proofs",
txid = %txid,
status = ?"Completed",
block_height = ?Some(971_555u32),
marker = "proof_backstop_hit",
"Transaction status synchronized by backstop fetch"
);
tracing::info!(
target: "bsv_wallet_toolbox_rs::monitor::tasks::check_for_proofs",
txid = %inline,
block_height = ?Some(971_556u32),
marker = "proof_backstop_hit",
"Transaction status synchronized by backstop fetch"
);
tracing::debug!(
target: "bsv_wallet_cli::elsewhere",
provider = %"Bitails",
txid = %inline,
"get_merkle_path: proof served"
);
});
let hits = hits.lock().unwrap();
println!("{hits:?}");
assert_eq!(
*hits,
vec![
BackstopProof {
provider: "WhatsOnChain".into(),
txid,
height: Some(971_555),
},
BackstopProof {
provider: "status-answer".into(),
txid: inline,
height: Some(971_556),
},
]
);
}
}