commit 3510a910ae9816f3a7ede0fc94f1f21367b0f25f
parent 248a7e70dade362cff8b12140696baf91acdbeca
Author: Antoine A <>
Date: Sat, 12 Sep 2026 12:15:36 +0200
common: add observability API back
Diffstat:
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?;
}
}