depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

commit 855b99f64e42347e35d5d24e77cd121fd1f8486c
parent 4d474e165afb91752337b996fb60cc9d4188c9e3
Author: Antoine A <>
Date:   Thu, 10 Sep 2026 11:41:05 +0200

common: add Observability API

Diffstat:
MCargo.toml | 23+++++++++--------------
Mdebian/etc/depolymerizer-bitcoin/conf.d/depolymerizer-bitcoin-httpd.conf | 3+++
Mdebian/etc/depolymerizer-bitcoin/secrets/depolymerizer-bitcoin-httpd.secret.conf | 4++++
Mdepolymerizer-bitcoin/Cargo.toml | 6+++---
Mdepolymerizer-bitcoin/depolymerizer-bitcoin.conf | 16++++++++++++++++
Mdepolymerizer-bitcoin/src/api.rs | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mdepolymerizer-bitcoin/src/cli.rs | 10++++++++--
Mdepolymerizer-bitcoin/src/config.rs | 14++++++++------
8 files changed, 110 insertions(+), 26 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -1,10 +1,6 @@ [workspace] resolver = "3" -members = [ - "depolymerizer-bitcoin", - "depolymerizer-common", - "uri-pack" -] +members = ["depolymerizer-bitcoin", "depolymerizer-common", "uri-pack"] [workspace.package] version = "0.0.1" @@ -24,22 +20,19 @@ serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.42", features = ["macros"] } axum = "0.8.1" sqlx = { version = "0.8", features = [ - "postgres", - "runtime-tokio-rustls", - "tls-rustls-ring", - "time", + "postgres", + "runtime-tokio-rustls", + "tls-rustls-ring", + "time", ], default-features = false } url = { version = "2.2", features = ["serde"] } taler-common = { git = "git://git.taler.net/taler-rust.git/" } taler-api = { git = "git://git.taler.net/taler-rust.git/" } taler-build = { git = "git://git.taler.net/taler-rust.git/" } taler-test-utils = { git = "git://git.taler.net/taler-rust.git/" } -failure-injection = { git = "git://git.taler.net/taler-rust.git/" } +failure-injection = { git = "git://git.taler.net/taler-rust.git/" } depolymerizer-common = { path = "depolymerizer-common" } -bitcoin = { version = "0.32.5", features = [ - "std", - "serde", -], default-features = false } +bitcoin = { version = "0.32.5", features = ["std", "serde"], default-features = false } hex = { package = "const-hex", version = "1.9.1" } clap = { version = "4.5", features = ["derive"] } anyhow = "1" @@ -51,3 +44,4 @@ tracing-subscriber = "0.3" jiff = { version = "0.2", default-features = false, features = ["perf-inline", "std"] } compact_str = { version = "0.9.0", features = ["serde", "sqlx-postgres"] } owo-colors = "4.2.3" +prometheus-client = { version = "0.25" } +\ No newline at end of file diff --git a/debian/etc/depolymerizer-bitcoin/conf.d/depolymerizer-bitcoin-httpd.conf b/debian/etc/depolymerizer-bitcoin/conf.d/depolymerizer-bitcoin-httpd.conf @@ -11,3 +11,6 @@ SERVE = systemd # ENABLED = YES @inline-secret@ depolymerizer-bitcoin-httpd-revenue-api ../secrets/depolymerizer-bitcoin-httpd.secret.conf +[depolymerizer-bitcoin-httpd-observability-api] +# ENABLED = YES +@inline-secret@ depolymerizer-bitcoin-httpd-observability-api ../secrets/depolymerizer-bitcoin-httpd.secret.conf diff --git a/debian/etc/depolymerizer-bitcoin/secrets/depolymerizer-bitcoin-httpd.secret.conf b/debian/etc/depolymerizer-bitcoin/secrets/depolymerizer-bitcoin-httpd.secret.conf @@ -4,4 +4,8 @@ [depolymerizer-bitcoin-httpd-revenue-api] # AUTH_METHOD = bearer +# TOKEN = + +[depolymerizer-bitcoin-httpd-observability-api] +# AUTH_METHOD = bearer # TOKEN = \ No newline at end of file diff --git a/depolymerizer-bitcoin/Cargo.toml b/depolymerizer-bitcoin/Cargo.toml @@ -15,7 +15,7 @@ depolymerizer-common.workspace = true bitcoin.workspace = true clap.workspace = true serde.workspace = true -serde_json = {workspace = true, features = ["raw_value"]} +serde_json = { workspace = true, features = ["raw_value"] } thiserror.workspace = true hex.workspace = true anyhow.workspace = true @@ -34,10 +34,11 @@ jiff.workspace = true compact_str.workspace = true owo-colors.workspace = true taler-test-utils.workspace = true +prometheus-client.workspace = true [dev-dependencies] criterion.workspace = true [[bench]] name = "metadata" -harness = false -\ No newline at end of file +harness = false diff --git a/depolymerizer-bitcoin/depolymerizer-bitcoin.conf b/depolymerizer-bitcoin/depolymerizer-bitcoin.conf @@ -98,6 +98,22 @@ AUTH_METHOD = bearer # Token for bearer authentication scheme TOKEN = +[depolymerizer-bitcoin-httpd-observability-api] +# Whether to serve the Revenue API +ENABLED = NO + +# Authentication scheme, this can either can be basic, bearer or none. +AUTH_METHOD = bearer + +# User name for basic authentication scheme +# USERNAME = + +# Password for basic authentication scheme +# PASSWORD = + +# Token for bearer authentication scheme +TOKEN = + [depolymerizer-bitcoindb-postgres] # DB connection string CONFIG = postgres:///depolymerizer-bitcoin diff --git a/depolymerizer-bitcoin/src/api.rs b/depolymerizer-bitcoin/src/api.rs @@ -26,10 +26,12 @@ use axum::{ response::{IntoResponse as _, Response}, }; use jiff::Timestamp; +use prometheus_client::{metrics::gauge::Gauge, registry::Registry}; use sqlx::{PgPool, postgres::PgListener}; use taler_api::{ api::{ TalerApi, + observability::Observability, prepared::{PreparedTransfer, simple_subject}, revenue::Revenue, wire::WireGateway, @@ -75,6 +77,31 @@ pub struct ServerState { in_channel: Sender<i64>, taler_in_channel: Sender<i64>, taler_out_channel: Sender<i64>, + metrics: Metrics, + registry: Registry, +} + +#[derive(Default)] +struct Metrics { + db_access: Gauge, +} + +impl Metrics { + pub fn registry(&self) -> Registry { + let mut registry = Registry::default(); + + registry.register( + "db_access", + "Whether the last database metrics refresh succeeded", + self.db_access.clone(), + ); + registry + } + + pub async fn sync(&self, db: &PgPool) { + let test = sqlx::query("SELECT 1").fetch_one(db).await.is_ok(); + self.db_access.set(if test { 1 } else { 0 }); + } } pub async fn notification_listener( @@ -101,6 +128,9 @@ impl ServerState { let in_channel = Sender::new(0); let taler_in_channel = Sender::new(0); let taler_out_channel = Sender::new(0); + + let metrics = Metrics::default(); + let tmp = Self { pool: pool.clone(), payto, @@ -109,6 +139,8 @@ impl ServerState { in_channel: in_channel.clone(), taler_in_channel: taler_in_channel.clone(), taler_out_channel: taler_out_channel.clone(), + registry: metrics.registry(), + metrics, }; let state = Arc::new(tmp); tokio::spawn(status_watcher(state.clone())); @@ -302,6 +334,13 @@ impl PreparedTransfer for ServerState { } } +impl Observability for ServerState { + async fn metrics(&self) -> ApiResult<&Registry> { + self.metrics.sync(&self.pool).await; + Ok(&self.registry) + } +} + pub async fn status_middleware( State(state): State<Arc<ServerState>>, request: Request, @@ -367,7 +406,12 @@ pub mod test { api::TalerRouter as _, auth::AuthMethod, db::TypeHelper, subject::OutgoingSubject, }; use taler_common::{ - api::wire::{TransferState, WireConfig}, + api::{ + observability::Config, + prepared::PreparedTransferConfig, + revenue::RevenueConfig, + wire::{TransferState, WireConfig}, + }, db::IncomingType, types::amount::{Currency, amount}, }; @@ -421,6 +465,7 @@ pub mod test { .wire_gateway(api.clone(), AuthMethod::None) .prepared_transfer(api.clone()) .revenue(api.clone(), AuthMethod::None) + .observability(api, AuthMethod::None) .finalize(); (server, pool) @@ -433,6 +478,19 @@ pub mod test { .get("/taler-wire-gateway/config") .await .assert_ok_json::<WireConfig>(); + server + .get("/taler-revenue/config") + .await + .assert_ok_json::<RevenueConfig>(); + server + .get("/taler-prepared-transfer/config") + .await + .assert_ok_json::<PreparedTransferConfig>(); + server + .get("/taler-observability/config") + .await + .assert_ok_json::<Config>(); + server.get("/taler-observability/metrics").await.assert_ok(); } #[tokio::test] diff --git a/depolymerizer-bitcoin/src/cli.rs b/depolymerizer-bitcoin/src/cli.rs @@ -97,7 +97,10 @@ pub async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { Command::Serve { check } => { if check { let cfg = ServeCfg::parse(cfg)?; - if cfg.wire_gateway.is_none() { + if cfg.wire_gateway.is_none() + && cfg.revenue.is_none() + && cfg.observability.is_none() + { std::process::exit(1); } } else { @@ -110,9 +113,12 @@ pub async fn run(cmd: Command, cfg: &Config) -> anyhow::Result<()> { .wire_gateway(api.clone(), cfg.auth.method()) .prepared_transfer(api.clone()) } - if let Some(cfg) = cfg.revenue { + if let Some(cfg) = cfg.observability { router = router.revenue(api.clone(), cfg.auth.method()); } + if let Some(cfg) = cfg.revenue { + router = router.observability(api.clone(), cfg.auth.method()); + } router .layer(middleware::from_fn_with_state(api, status_middleware)) diff --git a/depolymerizer-bitcoin/src/config.rs b/depolymerizer-bitcoin/src/config.rs @@ -56,6 +56,7 @@ pub struct ServeCfg { pub serve: Serve, pub wire_gateway: Option<ApiCfg>, pub revenue: Option<ApiCfg>, + pub observability: Option<ApiCfg>, pub currency: Currency, pub lifetime: Option<u32>, } @@ -70,18 +71,19 @@ impl ServeCfg { let serve = Serve::parse(&s)?; - let wire_gateway = - ApiCfg::parse(cfg.section("depolymerizer-bitcoin-httpd-wire-gateway-api"))?; - let revenue = ApiCfg::parse(cfg.section("depolymerizer-bitcoin-httpd-revenue-api"))?; - let sect = cfg.section("depolymerizer-bitcoin"); Ok(Self { currency: sect.parse("currency", "CURRENCY").require()?, lifetime, payto, serve, - wire_gateway, - revenue, + wire_gateway: ApiCfg::parse( + cfg.section("depolymerizer-bitcoin-httpd-wire-gateway-api"), + )?, + revenue: ApiCfg::parse(cfg.section("depolymerizer-bitcoin-httpd-revenue-api"))?, + observability: ApiCfg::parse( + cfg.section("depolymerizer-bitcoin-httpd-observability-api"), + )?, }) } }