taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 9d2bcbf4d1fe2e4efb2a024966077efeaddec0ae
parent d71d14b7cf5edad9ebcc0bd7171774701258596c
Author: Antoine A <>
Date:   Tue,  8 Apr 2025 11:14:10 +0200

common: improve routing, logging and errors

Diffstat:
MCargo.lock | 20++++++++++----------
Mcommon/taler-api/src/api.rs | 104+++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mcommon/taler-api/src/error.rs | 38++++++++++++++++++++------------------
Mcommon/taler-api/tests/common/mod.rs | 9++++-----
Mcommon/taler-common/src/api_common.rs | 20++++++++++----------
Mcommon/taler-common/src/types/base32.rs | 4++--
Mtaler-magnet-bank/src/main.rs | 10+++++-----
Mtaler-magnet-bank/tests/api.rs | 9++-------
8 files changed, 109 insertions(+), 105 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -829,9 +829,9 @@ dependencies = [ [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -1207,9 +1207,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" +checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488" dependencies = [ "jiff-static", "log", @@ -1221,9 +1221,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19" dependencies = [ "proc-macro2", "quote", @@ -1663,9 +1663,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" dependencies = [ "bitflags", ] @@ -2421,9 +2421,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", diff --git a/common/taler-api/src/api.rs b/common/taler-api/src/api.rs @@ -23,7 +23,6 @@ use std::{ }; use axum::{ - Router, extract::{Request, State}, middleware::{self, Next}, response::Response, @@ -31,7 +30,7 @@ use axum::{ use revenue::Revenue; use taler_common::{error_code::ErrorCode, types::amount::Amount}; use tokio::signal; -use tracing::info; +use tracing::{Level, info}; use wire::WireGateway; use crate::{ @@ -43,6 +42,8 @@ use crate::{ pub mod revenue; pub mod wire; +pub use axum::Router; + pub trait TalerApi: Send + Sync + 'static { fn currency(&self) -> &str; fn implementation(&self) -> Option<&str>; @@ -52,7 +53,7 @@ pub trait TalerApi: Send + Sync + 'static { Err(failure( ErrorCode::GENERIC_CURRENCY_MISMATCH, format!( - "Wrong currency: expected {} got {}", + "wrong currency expected {} got {}", currency, amount.currency ), )) @@ -64,6 +65,8 @@ pub trait TalerApi: Send + Sync + 'static { pub trait TalerRouter { fn auth(self, auth: AuthMethod) -> Self; + fn wire_gateway<T: WireGateway>(self, api: Arc<T>, auth: AuthMethod) -> Self; + fn revenue<T: Revenue>(self, api: Arc<T>, auth: AuthMethod) -> Self; fn finalize(self) -> Self; fn serve( self, @@ -80,6 +83,14 @@ impl TalerRouter for Router { )) } + fn wire_gateway<T: WireGateway>(self, api: Arc<T>, auth: AuthMethod) -> Self { + self.nest("/taler-wire-gateway", wire::router(api).auth(auth)) + } + + fn revenue<T: Revenue>(self, api: Arc<T>, auth: AuthMethod) -> Self { + self.nest("/taler-revenue", revenue::router(api).auth(auth)) + } + fn finalize(self) -> Router { self.method_not_allowed_fallback(|| async { failure_code(ErrorCode::GENERIC_METHOD_INVALID) @@ -121,44 +132,6 @@ impl TalerRouter for Router { } } -pub struct TalerApiBuilder { - router: Router, -} - -impl TalerApiBuilder { - pub fn new() -> Self { - Self { - router: Router::new(), - } - } - - pub fn wire_gateway<T: WireGateway>(mut self, api: Arc<T>, auth: AuthMethod) -> Self { - self.router = self.router.nest( - "/taler-wire-gateway", - wire::router(api).layer(middleware::from_fn_with_state( - Arc::new(auth), - crate::auth::auth_middleware, - )), - ); - self - } - - pub fn revenue<T: Revenue>(mut self, api: Arc<T>, auth: AuthMethod) -> Self { - self.router = self.router.nest( - "/taler-revenue", - revenue::router(api).layer(middleware::from_fn_with_state( - Arc::new(auth), - crate::auth::auth_middleware, - )), - ); - self - } - - pub fn build(self) -> Router { - self.router - } -} - struct LifetimeMiddlewareState { lifetime: AtomicU32, notify: Arc<tokio::sync::Notify>, @@ -215,17 +188,52 @@ async fn shutdown_signal(manual_shutdown: Arc<tokio::sync::Notify>) { } } +#[macro_export] +macro_rules! dyn_event { + ($lvl:ident, $($arg:tt)+) => { + match $lvl { + ::tracing::Level::TRACE => ::tracing::trace!($($arg)+), + ::tracing::Level::DEBUG => ::tracing::debug!($($arg)+), + ::tracing::Level::INFO => ::tracing::info!($($arg)+), + ::tracing::Level::WARN => ::tracing::warn!($($arg)+), + ::tracing::Level::ERROR => ::tracing::error!($($arg)+), + } + }; +} + /** Taler API logger */ async fn logger_middleware(request: Request, next: Next) -> Response { - let request_info = format!("{} {}", request.method(), request.uri().path()); + let request_info = format!( + "{} {}", + request.method(), + request + .uri() + .path_and_query() + .map(|it| it.as_str()) + .unwrap_or_default() + ); let now = Instant::now(); let response = next.run(request).await; let elapsed = now.elapsed(); - // TODO log error message - info!(target: "api", - "{} {request_info} {}ms", - response.status(), - elapsed.as_millis() - ); + let status = response.status(); + let level = match status.as_u16() { + 400..500 => Level::WARN, + 500..600 => Level::ERROR, + _ => Level::INFO, + }; + + if let Some(log) = response.extensions().get::<Box<str>>() { + dyn_event!(level, target: "api", + "{} {request_info} {}ms: {log}", + response.status(), + elapsed.as_millis() + ); + } else { + dyn_event!(level, target: "api", + "{} {request_info} {}ms", + response.status(), + elapsed.as_millis() + ); + } response } diff --git a/common/taler-api/src/error.rs b/common/taler-api/src/error.rs @@ -27,10 +27,10 @@ pub type ApiResult<T> = Result<T, ApiError>; pub struct ApiError { code: ErrorCode, - hint: Option<String>, - log: Option<String>, + hint: Option<Box<str>>, + log: Option<Box<str>>, status: Option<StatusCode>, - path: Option<String>, + path: Option<Box<str>>, } impl From<sqlx::Error> for ApiError { @@ -52,7 +52,7 @@ impl From<sqlx::Error> for ApiError { code, hint: None, status: Some(status), - log: Some(format!("db: {value}")), + log: Some(format!("db: {value}").into_boxed_str()), path: None, } } @@ -62,7 +62,7 @@ impl From<PaytoErr> for ApiError { fn from(value: PaytoErr) -> Self { Self { code: ErrorCode::GENERIC_PAYTO_URI_MALFORMED, - hint: Some(value.to_string()), + hint: Some(value.to_string().into_boxed_str()), log: None, status: None, path: None, @@ -74,38 +74,35 @@ impl From<ParamsErr> for ApiError { fn from(value: ParamsErr) -> Self { Self { code: ErrorCode::GENERIC_PARAMETER_MALFORMED, - hint: Some(value.to_string()), + hint: Some(value.to_string().into_boxed_str()), log: None, status: None, - path: Some(value.param.to_owned()), + path: Some(value.param.to_owned().into_boxed_str()), } } } impl From<serde_path_to_error::Error<serde_json::Error>> for ApiError { fn from(value: serde_path_to_error::Error<serde_json::Error>) -> Self { - let e = value.inner(); Self { code: ErrorCode::GENERIC_JSON_INVALID, - hint: Some(e.to_string()), - log: None, + hint: Some(value.inner().to_string().into_boxed_str()), + log: Some(value.to_string().into_boxed_str()), status: None, - path: Some(value.path().to_string()), + path: Some(value.path().to_string().into_boxed_str()), } } } impl IntoResponse for ApiError { fn into_response(self) -> Response { - if let Some(log) = self.log.as_deref().or(self.hint.as_deref()) { - tracing::error!(target: "api", "{log}"); - } let status_code = self.status.unwrap_or_else(|| { let (status_code, _) = self.code.metadata(); StatusCode::from_u16(status_code).expect("Invalid status code") }); + let log = self.log.or(self.hint.clone()); - ( + let mut resp = ( status_code, Json(ErrorDetail { code: self.code as u32, @@ -122,7 +119,12 @@ impl IntoResponse for ApiError { extra: None, }), ) - .into_response() + .into_response(); + if let Some(log) = log { + resp.extensions_mut().insert(log); + }; + + resp } } @@ -136,7 +138,7 @@ pub fn failure_code(code: ErrorCode) -> ApiError { } } -pub fn failure(code: ErrorCode, hint: impl Into<String>) -> ApiError { +pub fn failure(code: ErrorCode, hint: impl Into<Box<str>>) -> ApiError { ApiError { code, hint: Some(hint.into()), @@ -146,7 +148,7 @@ pub fn failure(code: ErrorCode, hint: impl Into<String>) -> ApiError { } } -pub fn failure_status(code: ErrorCode, hint: impl Into<String>, status: StatusCode) -> ApiError { +pub fn failure_status(code: ErrorCode, hint: impl Into<Box<str>>, status: StatusCode) -> ApiError { ApiError { code, hint: Some(hint.into()), diff --git a/common/taler-api/tests/common/mod.rs b/common/taler-api/tests/common/mod.rs @@ -16,11 +16,10 @@ use std::sync::Arc; -use axum::Router; use db::notification_listener; use sqlx::PgPool; use taler_api::{ - api::{TalerApi, TalerApiBuilder, TalerRouter as _, revenue::Revenue, wire::WireGateway}, + api::{Router, TalerApi, TalerRouter as _, revenue::Revenue, wire::WireGateway}, auth::AuthMethod, db::IncomingType, error::{ApiResult, failure}, @@ -179,7 +178,7 @@ impl Revenue for TestApi { } } -pub async fn test_api(pool: PgPool, currency: String) -> TalerApiBuilder { +pub async fn test_api(pool: PgPool, currency: String) -> Router { let outgoing_channel = Sender::new(0); let incoming_channel = Sender::new(0); let wg = TestApi { @@ -194,7 +193,7 @@ pub async fn test_api(pool: PgPool, currency: String) -> TalerApiBuilder { incoming_channel, )); let state = Arc::new(wg); - TalerApiBuilder::new() + Router::new() .wire_gateway(state.clone(), AuthMethod::None) .revenue(state, AuthMethod::None) } @@ -203,5 +202,5 @@ pub async fn setup() -> (Router, PgPool) { let pool = db_test_setup("taler-api").await; let api = test_api(pool.clone(), "EUR".to_string()).await; - (api.build().finalize(), pool) + (api.finalize(), pool) } diff --git a/common/taler-common/src/api_common.rs b/common/taler-common/src/api_common.rs @@ -25,16 +25,16 @@ use crate::types::base32::Base32; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ErrorDetail { pub code: u32, - pub hint: Option<String>, - pub detail: Option<String>, - pub parameter: Option<String>, - pub path: Option<String>, - pub offset: Option<String>, - pub index: Option<String>, - pub object: Option<String>, - pub currency: Option<String>, - pub type_expected: Option<String>, - pub type_actual: Option<String>, + pub hint: Option<Box<str>>, + pub detail: Option<Box<str>>, + pub parameter: Option<Box<str>>, + pub path: Option<Box<str>>, + pub offset: Option<Box<str>>, + pub index: Option<Box<str>>, + pub object: Option<Box<str>>, + pub currency: Option<Box<str>>, + pub type_expected: Option<Box<str>>, + pub type_actual: Option<Box<str>>, pub extra: Option<Box<RawValue>>, } diff --git a/common/taler-common/src/types/base32.rs b/common/taler-common/src/types/base32.rs @@ -69,9 +69,9 @@ fn encode_batch(bytes: &[u8], encoded: &mut [u8]) { #[derive(Debug, thiserror::Error)] pub enum Base32Error<const N: usize> { - #[error("Invalid Crockford's base32 format")] + #[error("invalid Crockford's base32 format")] Format, - #[error("Invalid length: expected {N} bytess got {0}")] + #[error("invalid length expected {N} bytess got {0}")] Length(usize), } diff --git a/taler-magnet-bank/src/main.rs b/taler-magnet-bank/src/main.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use clap::Parser; -use taler_api::api::{TalerApiBuilder, TalerRouter as _}; +use taler_api::api::{Router, TalerRouter as _}; use taler_common::{ CommonArgs, cli::ConfigCmd, @@ -112,14 +112,14 @@ async fn app(args: Args, cfg: Config) -> anyhow::Result<()> { let pool = pool(db.cfg, "magnet_bank").await?; let cfg = ServeCfg::parse(&cfg)?; let api = Arc::new(MagnetApi::start(pool, cfg.payto).await); - let mut builder = TalerApiBuilder::new(); + let mut router = Router::new(); if let Some(cfg) = cfg.wire_gateway { - builder = builder.wire_gateway(api.clone(), cfg.auth); + router = router.wire_gateway(api.clone(), cfg.auth); } if let Some(cfg) = cfg.revenue { - builder = builder.revenue(api, cfg.auth); + router = router.revenue(api, cfg.auth); } - builder.build().serve(cfg.serve, None).await?; + router.serve(cfg.serve, None).await?; } } Command::Worker { transient: _ } => { diff --git a/taler-magnet-bank/tests/api.rs b/taler-magnet-bank/tests/api.rs @@ -17,11 +17,7 @@ use std::sync::Arc; use sqlx::PgPool; -use taler_api::{ - api::{TalerApiBuilder, TalerRouter as _}, - auth::AuthMethod, - subject::OutgoingSubject, -}; +use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; use taler_common::{ api_common::ShortHashCode, api_wire::{OutgoingHistory, TransferState}, @@ -42,10 +38,9 @@ async fn setup() -> (Router, PgPool) { ) .await, ); - let server = TalerApiBuilder::new() + let server = Router::new() .wire_gateway(api.clone(), AuthMethod::None) .revenue(api, AuthMethod::None) - .build() .finalize(); (server, pool)