commit 9d2bcbf4d1fe2e4efb2a024966077efeaddec0ae
parent d71d14b7cf5edad9ebcc0bd7171774701258596c
Author: Antoine A <>
Date: Tue, 8 Apr 2025 11:14:10 +0200
common: improve routing, logging and errors
Diffstat:
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)