libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 3510a910ae9816f3a7ede0fc94f1f21367b0f25f
parent 248a7e70dade362cff8b12140696baf91acdbeca
Author: Antoine A <>
Date:   Sat, 12 Sep 2026 12:15:36 +0200

common: add observability API back

Diffstat:
MCargo.lock | 131+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
MCargo.toml | 1+
Mcontrib/nexus.conf | 4++--
Mlibeufin-bank/Cargo.toml | 1+
Mlibeufin-bank/src/api.rs | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alibeufin-bank/src/api/observability.rs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlibeufin-bank/src/api/tan.rs | 1+
Mlibeufin-bank/src/auth.rs | 7+++++--
Mlibeufin-nexus/Cargo.toml | 1+
Mlibeufin-nexus/src/api.rs | 105++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mlibeufin-nexus/src/config.rs | 2++
Mlibeufin-nexus/src/lib.rs | 4++++
12 files changed, 334 insertions(+), 61 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -308,9 +308,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "serde_core", ] @@ -537,9 +537,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.4" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" dependencies = [ "encode_unicode", "libc", @@ -678,7 +678,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm_winapi", "derive_more", "document-features", @@ -920,6 +920,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -942,9 +948,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.40" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a7a45518d2863d18aa47f4a0cf9faec2aa4304cc09df5e41299f276b3ad135e" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", "core_detect", @@ -1361,7 +1367,7 @@ dependencies = [ [[package]] name = "http-client" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#344a0399178b0efe0bd5f47aa7e2dd5095d455fc" +source = "git+git://git.taler.net/taler-rust.git/#b2cc6de2b7551860d1c864df316c408df3708a14" dependencies = [ "compact_str", "futures-util", @@ -1403,9 +1409,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -1803,6 +1809,7 @@ dependencies = [ "futures", "jiff", "pretty_assertions", + "prometheus-client", "rand 0.10.2", "regex", "serde", @@ -1898,6 +1905,7 @@ dependencies = [ "jiff", "libeufin-ebics", "pretty_assertions", + "prometheus-client", "regex", "serde_json", "shlex", @@ -1931,11 +1939,11 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "plain", "redox_syscall 0.9.4", @@ -2085,24 +2093,23 @@ dependencies = [ [[package]] name = "multiversion" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edb7f0ff51249dfda9ab96b5823695e15a052dc15074c9dbf3d118afaf2c201" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" dependencies = [ "multiversion-macros", - "target-features", ] [[package]] name = "multiversion-macros" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093064383341eb3271f42e381cb8f10a01459478446953953c75d24bd339fc0" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "target-features", + "rustversion", + "syn 3.0.5", ] [[package]] @@ -2117,7 +2124,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -2271,7 +2278,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5e456864a7a304047bff84977dc6fb162bd956475d40ba50b2dcecaada7f753" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "itoa", "memchr", "ryu", @@ -2400,6 +2407,29 @@ dependencies = [ ] [[package]] +name = "prometheus-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1784dc11a05f5a8f57c4a62915686be140f24f70301290b997e317241fa9ffc2" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e34894696ff94f64a20c2c373a6440903e9c2789a303d68ec6e6f953f890e4" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] name = "quick-xml" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2592,7 +2622,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -2601,7 +2631,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -2655,11 +2685,11 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", "encoding_rs", "futures-core", @@ -2782,7 +2812,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -2907,7 +2937,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3132,9 +3162,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" dependencies = [ "serde", ] @@ -3264,7 +3294,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "bytes", "crc", @@ -3307,7 +3337,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "crc", "dotenvy", @@ -3475,7 +3505,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3493,7 +3523,7 @@ dependencies = [ [[package]] name = "taler-api" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#344a0399178b0efe0bd5f47aa7e2dd5095d455fc" +source = "git+git://git.taler.net/taler-rust.git/#b2cc6de2b7551860d1c864df316c408df3708a14" dependencies = [ "aws-lc-rs", "axum", @@ -3502,6 +3532,7 @@ dependencies = [ "http-body-util", "jiff", "listenfd", + "prometheus-client", "rand 0.10.2", "regex", "serde", @@ -3521,12 +3552,12 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#344a0399178b0efe0bd5f47aa7e2dd5095d455fc" +source = "git+git://git.taler.net/taler-rust.git/#b2cc6de2b7551860d1c864df316c408df3708a14" [[package]] name = "taler-common" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#344a0399178b0efe0bd5f47aa7e2dd5095d455fc" +source = "git+git://git.taler.net/taler-rust.git/#b2cc6de2b7551860d1c864df316c408df3708a14" dependencies = [ "anyhow", "aws-lc-rs", @@ -3556,7 +3587,7 @@ dependencies = [ [[package]] name = "taler-macros" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#344a0399178b0efe0bd5f47aa7e2dd5095d455fc" +source = "git+git://git.taler.net/taler-rust.git/#b2cc6de2b7551860d1c864df316c408df3708a14" dependencies = [ "proc-macro2", "quote", @@ -3566,7 +3597,7 @@ dependencies = [ [[package]] name = "taler-test-utils" version = "1.5.0" -source = "git+git://git.taler.net/taler-rust.git/#344a0399178b0efe0bd5f47aa7e2dd5095d455fc" +source = "git+git://git.taler.net/taler-rust.git/#b2cc6de2b7551860d1c864df316c408df3708a14" dependencies = [ "aws-lc-rs", "axum", @@ -3592,12 +3623,6 @@ dependencies = [ ] [[package]] -name = "target-features" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1bbb9f3c5c463a01705937a24fdabc5047929ac764b2d5b9cf681c1f5041ed5" - -[[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3819,7 +3844,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -3837,7 +3862,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-core", "futures-util", @@ -4073,9 +4098,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.26.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -4592,18 +4617,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml @@ -47,6 +47,7 @@ tower-http = { version = "0.7", features = ["fs"] } shlex = "2.0" tempfile = "3" url = "2.5" +prometheus-client = { version = "0.25" } 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/" } diff --git a/contrib/nexus.conf b/contrib/nexus.conf @@ -135,7 +135,7 @@ AUTH_METHOD = bearer # USERNAME = # Password for basic authentication scheme -# PASSWORD = +# PASSWORD = # Token for bearer authentication scheme TOKEN = @@ -151,7 +151,7 @@ AUTH_METHOD = bearer # USERNAME = # Password for basic authentication scheme -# PASSWORD = +# PASSWORD = # Token for bearer authentication scheme TOKEN = diff --git a/libeufin-bank/Cargo.toml b/libeufin-bank/Cargo.toml @@ -33,6 +33,7 @@ dialoguer.workspace = true tower-http.workspace = true url.workspace = true pretty_assertions.workspace = true +prometheus-client.workspace = true futures = "0.3" regex = "1.12" bcrypt = "0.19.0" diff --git a/libeufin-bank/src/api.rs b/libeufin-bank/src/api.rs @@ -25,6 +25,11 @@ use axum::{ response::{IntoResponse, Redirect, Response}, routing::get, }; +use prometheus_client::{ + encoding::EncodeLabelSet, + metrics::{counter::Counter, family::Family, gauge::Gauge}, + registry::Registry, +}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use taler_api::{error::ApiResult, extract::Query, notification::NotificationChannel}; @@ -43,6 +48,7 @@ use crate::{ account::account_api, cashout::cashout_api, conversion::conversion_api, + observability::observability_api, prepared::prepared_api, revenue::revenue_api, tan::tan_api, @@ -59,6 +65,7 @@ use crate::{ pub mod account; pub mod cashout; pub mod conversion; +pub mod observability; pub mod prepared; pub mod revenue; pub mod tan; @@ -71,6 +78,7 @@ const IMPLEMENTATION: &str = "urn:net:taler:specs:libeufin-bank:taler-rust"; const COREBANK_API_VERSION: LibtoolVersion = LibtoolVersion::new(12, 1, 0); const CONVERSION_API_VERSION: LibtoolVersion = LibtoolVersion::new(2, 1, 1); const INTEGRATION_API_VERSION: LibtoolVersion = LibtoolVersion::new(5, 1, 5); +const OBSERVABILITY_API_VERSION: LibtoolVersion = LibtoolVersion::new(0, 0, 0); pub struct BankState { pub db: PgPool, @@ -80,6 +88,51 @@ pub struct BankState { pub taler_in_channel: NotificationChannel<u64, i64>, pub revenue_channel: NotificationChannel<u64, i64>, pub withdrawal_channel: NotificationChannel<Uuid, Option<WithdrawalStatus>>, + pub metrics: Metrics, + registry: Registry, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +pub struct TanChannelLabels { + channel: &'static str, + exit: i32, +} + +#[derive(Default)] +pub struct Metrics { + db_access: Gauge, + tan_channel: Family<TanChannelLabels, Counter>, +} + +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 fn register_tan_result(&self, channel: TanChannel, exit: i32) { + self.tan_channel + .get_or_create(&TanChannelLabels { + channel: match channel { + TanChannel::sms => "sms", + TanChannel::email => "email", + }, + exit, + }) + .inc(); + } } impl BankState { @@ -89,6 +142,7 @@ impl BankState { let taler_out_channel = NotificationChannel::new(); let revenue_channel = NotificationChannel::new(); let withdrawal_channel = NotificationChannel::new(); + tokio::spawn(notification_listener( pool.clone(), tx_channel.clone(), @@ -97,6 +151,9 @@ impl BankState { revenue_channel.clone(), withdrawal_channel.clone(), )); + + let metrics = Metrics::default(); + Self { cfg, db: pool, @@ -105,6 +162,8 @@ impl BankState { taler_out_channel, revenue_channel, withdrawal_channel, + registry: metrics.registry(), + metrics, } } } @@ -246,6 +305,7 @@ pub fn bank_api(state: Arc<BankState>) -> Router { .merge(wire_api()) .merge(prepared_api()) .merge(revenue_api()) + .merge(observability_api()) .with_state(state) } diff --git a/libeufin-bank/src/api/observability.rs b/libeufin-bank/src/api/observability.rs @@ -0,0 +1,78 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 Taler Systems S.A. + +* LibEuFin is free software; you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation; either version 3, or +* (at your option) any later version. + +* LibEuFin is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +* Public License for more details. + +* You should have received a copy of the GNU Affero General Public +* License along with LibEuFin; see the file COPYING. If not, see +* <http://www.gnu.org/licenses/> +*/ + +use std::sync::Arc; + +use axum::{Json, Router, extract::State, http::header::CONTENT_TYPE, routing::get}; +use prometheus_client::encoding::text::encode; +use taler_api::error::ApiResult; +use taler_common::api::observability::Config; + +use crate::{ + api::{BankState, IMPLEMENTATION, OBSERVABILITY_API_VERSION}, + auth::{AdminAuth, ObservabilityScope}, +}; + +pub fn observability_api() -> Router<Arc<BankState>> { + Router::new() + .route( + "/taler-observability/config", + get(async || { + Json(Config { + name: (), + version: OBSERVABILITY_API_VERSION, + implementation: Some(IMPLEMENTATION), + }) + }), + ) + .route( + "/taler-observability/metrics", + get( + async |_: AdminAuth<ObservabilityScope>, State(state): State<Arc<BankState>>| { + state.metrics.sync(&state.db).await; + let mut buffer = String::new(); + encode(&mut buffer, &state.registry).unwrap(); + ApiResult::Ok(( + [( + CONTENT_TYPE, + "application/openmetrics-text; version=1.0.0; charset=utf-8", + )], + buffer, + )) + }, + ), + ) +} + +#[cfg(test)] +mod test { + use taler_test_utils::server::TestServer as _; + + use crate::api::test::bank_setup; + + #[tokio::test] + async fn observability() { + let ctx = bank_setup().await; + + ctx.get("/taler-observability/config").await.assert_ok(); + ctx.get_admin("/taler-observability/metrics") + .await + .assert_ok(); + } +} diff --git a/libeufin-bank/src/api/tan.rs b/libeufin-bank/src/api/tan.rs @@ -120,6 +120,7 @@ pub fn tan_api() -> Router<Arc<BankState>> { }; let code = output.status.code().unwrap_or(-1); + state.metrics.register_tan_result(channel, code); if code != 0 { let out = String::from_utf8_lossy(&output.stdout); tracing::error!(target: "tan", "{channel} {script}: {code} {out}"); diff --git a/libeufin-bank/src/auth.rs b/libeufin-bank/src/auth.rs @@ -403,17 +403,20 @@ pub type AdminRWAuth = AdminAuth<RootRWScope>; pub type AdminRAuth = AdminAuth<RootRScope>; pub struct RootRWScope; - impl RootAuthScope for RootRWScope { const SCOPE: TokenLogicalScope = TokenLogicalScope::readwrite; } pub struct RootRScope; - impl RootAuthScope for RootRScope { const SCOPE: TokenLogicalScope = TokenLogicalScope::readonly; } +pub struct ObservabilityScope; +impl RootAuthScope for ObservabilityScope { + const SCOPE: TokenLogicalScope = TokenLogicalScope::observability; +} + pub struct AdminAuth<S> { scope: PhantomData<S>, } diff --git a/libeufin-nexus/Cargo.toml b/libeufin-nexus/Cargo.toml @@ -26,6 +26,7 @@ compact_str.workspace = true uuid.workspace = true shlex.workspace = true pretty_assertions.workspace = true +prometheus-client.workspace = true indexmap = "2" url = "2.5" regex = "1.12" diff --git a/libeufin-nexus/src/api.rs b/libeufin-nexus/src/api.rs @@ -17,15 +17,22 @@ * <http://www.gnu.org/licenses/> */ +use const_format::formatcp; use jiff::Timestamp; use libeufin_ebics::{ - ebics::rand_ebics_id, + ebics::{TaskStatus, rand_ebics_id}, iso20022::model::{InId, InTx}, }; -use sqlx::PgPool; +use prometheus_client::{ + encoding::EncodeLabelSet, + metrics::{family::Family, gauge::Gauge}, + registry::{Registry, Unit}, +}; +use sqlx::{PgPool, types::Json}; use taler_api::{ api::{ TalerApi, + observability::Observability, prepared::{PreparedTransfer, simple_subject}, revenue::Revenue, wire::WireGateway, @@ -57,7 +64,7 @@ use taler_common::{ }; use tokio::sync::watch::Sender; -use crate::db::{ +use crate::{FETCH_TASK_KEY, SUBMIT_TASK_KEY, db::{ self, exchange::{ TransferResult, incoming_history, outgoing_history, revenue_history, transfer, @@ -65,7 +72,7 @@ use crate::db::{ }, payment::{IncomingRegistrationResult, register_in_talerable}, transfer::{RegistrationResult, transfer_register, transfer_unregister}, -}; +}}; pub struct NexusApi { pub pool: sqlx::PgPool, @@ -75,6 +82,84 @@ pub struct NexusApi { pub in_channel: Sender<i64>, pub taler_in_channel: Sender<i64>, pub taler_out_channel: Sender<i64>, + metrics: Metrics, + registry: Registry, +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)] +struct TaskLabel { + name: &'static str, +} + +#[derive(Default)] +pub struct Metrics { + db_access: Gauge, + task_execution: Family<TaskLabel, Gauge>, + task_success: Family<TaskLabel, 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.register_with_unit( + "task_execution_timestamp_seconds", + "Unix timestamp of the last task execution", + Unit::Seconds, + self.task_execution.clone(), + ); + + registry.register_with_unit( + "task_success_timestamp_seconds", + "Unix timestamp of the last successful task execution", + Unit::Seconds, + self.task_success.clone(), + ); + + registry + } + + pub async fn sync(&self, db: &PgPool) { + let success = + match sqlx::query_as::<_, (Option<Json<TaskStatus>>, Option<Json<TaskStatus>>)>( + formatcp!( + " + SELECT + (SELECT value FROM kv WHERE key='{SUBMIT_TASK_KEY}'), + (SELECT value FROM kv WHERE key='{FETCH_TASK_KEY}') + " + ), + ) + .fetch_one(db) + .await + { + Ok((submit, fetch)) => { + for (name, status) in [("submit", submit), ("fetch", fetch)] { + if let Some(status) = status { + if let Some(time) = status.last_trial { + self.task_execution + .get_or_create(&TaskLabel { name }) + .set(time.as_second()); + } + if let Some(time) = status.last_successfull { + self.task_success + .get_or_create(&TaskLabel { name }) + .set(time.as_second()); + } + } + } + true + } + Err(_) => false, + }; + self.db_access.set(if success { 1 } else { 0 }); + } } impl NexusApi { @@ -87,6 +172,9 @@ impl NexusApi { 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, @@ -95,6 +183,8 @@ impl NexusApi { in_channel: in_channel.clone(), taler_in_channel: taler_in_channel.clone(), taler_out_channel: taler_out_channel.clone(), + registry: metrics.registry(), + metrics, }; tokio::spawn(db::notification_listener( pool, @@ -313,6 +403,13 @@ impl PreparedTransfer for NexusApi { } } +impl Observability for NexusApi { + async fn metrics(&self) -> ApiResult<&Registry> { + self.metrics.sync(&self.pool).await; + Ok(&self.registry) + } +} + #[cfg(test)] pub mod test { use std::sync::Arc; diff --git a/libeufin-nexus/src/config.rs b/libeufin-nexus/src/config.rs @@ -238,6 +238,7 @@ pub struct NexusCfg { pub setup: OnceCell<NexusSetupConfig>, pub wire_cfg: Option<ApiCfg>, pub revenue_cfg: Option<ApiCfg>, + pub observability_cfg: Option<ApiCfg>, pub db_cfg: DbCfg, pub serve_cfg: Serve, } @@ -250,6 +251,7 @@ impl<'a> NexusCfg { account_type: s.parse("account type", "ACCOUNT_TYPE").require()?, wire_cfg: ApiCfg::parse(cfg.section("nexus-httpd-wire-gateway-api"))?, revenue_cfg: ApiCfg::parse(cfg.section("nexus-httpd-revenue-api"))?, + observability_cfg: ApiCfg::parse(cfg.section("nexus-httpd-observability-api"))?, serve_cfg: Serve::parse(&cfg.section("nexus-httpd"))?, keys: OnceCell::new(), host: OnceCell::new(), diff --git a/libeufin-nexus/src/lib.rs b/libeufin-nexus/src/lib.rs @@ -553,6 +553,7 @@ pub async fn run(cfg: &Config, cmd: Cmd) -> anyhow::Result<()> { for (name, api) in [ ("Wire Gateway API", cfg.wire_cfg), ("Revenue API", cfg.revenue_cfg), + ("Observability API", cfg.observability_cfg), ] { if api.is_some() { start_server = true; @@ -585,6 +586,9 @@ pub async fn run(cfg: &Config, cmd: Cmd) -> anyhow::Result<()> { if let Some(it) = cfg.revenue_cfg { server = server.revenue(api.clone(), it.auth.method()) } + if let Some(it) = cfg.observability_cfg { + server = server.observability(api.clone(), it.auth.method()) + } server.serve(&cfg.serve_cfg, None).await?; } }