commit 855b99f64e42347e35d5d24e77cd121fd1f8486c
parent 4d474e165afb91752337b996fb60cc9d4188c9e3
Author: Antoine A <>
Date: Thu, 10 Sep 2026 11:41:05 +0200
common: add Observability API
Diffstat:
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"),
+ )?,
})
}
}