Prometheus crate (#883)

* feat: introduce `cdk-prometheus` crate with Prometheus server and CDK-specific metrics support
This commit is contained in:
asmo
2025-09-09 14:26:03 +02:00
committed by GitHub
parent c94979a357
commit 75a3e6d2c7
39 changed files with 4504 additions and 361 deletions

View File

@@ -36,7 +36,7 @@ jobs:
' '
- name: typos - name: typos
run: nix develop -i -L .#nightly --command typos run: nix develop -i -L .#nightly --command typos
examples: examples:
name: "Run examples" name: "Run examples"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -167,7 +167,7 @@ jobs:
[ [
-p cdk-integration-tests, -p cdk-integration-tests,
] ]
database: database:
[ [
SQLITE, SQLITE,
POSTGRES POSTGRES
@@ -195,7 +195,7 @@ jobs:
shared-key: "stable" shared-key: "stable"
- name: Test - name: Test
run: nix develop -i -L .#stable --command just itest ${{ matrix.database }} run: nix develop -i -L .#stable --command just itest ${{ matrix.database }}
fake-mint-itest: fake-mint-itest:
name: "Integration fake mint tests" name: "Integration fake mint tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -207,7 +207,7 @@ jobs:
[ [
-p cdk-integration-tests, -p cdk-integration-tests,
] ]
database: database:
[ [
SQLITE, SQLITE,
] ]
@@ -236,7 +236,7 @@ jobs:
run: nix develop -i -L .#stable --command cargo clippy -- -D warnings run: nix develop -i -L .#stable --command cargo clippy -- -D warnings
- name: Test fake auth mint - name: Test fake auth mint
run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }} run: nix develop -i -L .#stable --command just fake-mint-itest ${{ matrix.database }}
pure-itest: pure-itest:
name: "Integration fake wallet tests" name: "Integration fake wallet tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -244,7 +244,7 @@ jobs:
needs: [pre-commit-checks, clippy] needs: [pre-commit-checks, clippy]
strategy: strategy:
matrix: matrix:
database: database:
[ [
memory, memory,
sqlite, sqlite,
@@ -256,7 +256,7 @@ jobs:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main uses: jlumbroso/free-disk-space@main
with: with:
tool-cache: true tool-cache: true
android: true android: true
dotnet: true dotnet: true
haskell: true haskell: true
@@ -286,7 +286,7 @@ jobs:
needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest, regtest-itest] needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest, regtest-itest]
strategy: strategy:
matrix: matrix:
ln: ln:
[ [
FAKEWALLET, FAKEWALLET,
CLN, CLN,
@@ -298,7 +298,7 @@ jobs:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main uses: jlumbroso/free-disk-space@main
with: with:
tool-cache: true tool-cache: true
android: true android: true
dotnet: true dotnet: true
haskell: true haskell: true
@@ -357,7 +357,7 @@ jobs:
- name: Build - name: Build
run: nix develop -i -L .#msrv --command cargo build ${{ matrix.build-args }} run: nix develop -i -L .#msrv --command cargo build ${{ matrix.build-args }}
check-wasm: check-wasm:
name: Check WASM name: Check WASM
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -389,7 +389,7 @@ jobs:
- name: Build cdk and binding - name: Build cdk and binding
run: nix develop -i -L ".#${{ matrix.rust }}" --command cargo build ${{ matrix.build-args }} --target ${{ matrix.target }} run: nix develop -i -L ".#${{ matrix.rust }}" --command cargo build ${{ matrix.build-args }} --target ${{ matrix.target }}
check-wasm-msrv: check-wasm-msrv:
name: Check WASM name: Check WASM
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -428,7 +428,7 @@ jobs:
needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest] needs: [pre-commit-checks, clippy, pure-itest, fake-mint-itest]
strategy: strategy:
matrix: matrix:
database: database:
[ [
SQLITE, SQLITE,
] ]
@@ -456,7 +456,7 @@ jobs:
- name: Stop and clean up Docker Compose - name: Stop and clean up Docker Compose
run: | run: |
docker compose -f misc/keycloak/docker-compose-recover.yml down docker compose -f misc/keycloak/docker-compose-recover.yml down
doc-tests: doc-tests:
name: "Documentation Tests" name: "Documentation Tests"
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -475,7 +475,7 @@ jobs:
shared-key: "stable" shared-key: "stable"
- name: Run doc tests - name: Run doc tests
run: nix develop -i -L .#stable --command cargo test --doc run: nix develop -i -L .#stable --command cargo test --doc
strict-docs: strict-docs:
name: "Strict Documentation Check" name: "Strict Documentation Check"
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -60,6 +60,7 @@ cdk-sqlite = { path = "./crates/cdk-sqlite", default-features = true, version =
cdk-postgres = { path = "./crates/cdk-postgres", default-features = true, version = "=0.12.0" } cdk-postgres = { path = "./crates/cdk-postgres", default-features = true, version = "=0.12.0" }
cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.12.0", default-features = false } cdk-signatory = { path = "./crates/cdk-signatory", version = "=0.12.0", default-features = false }
cdk-mintd = { path = "./crates/cdk-mintd", version = "=0.12.0", default-features = false } cdk-mintd = { path = "./crates/cdk-mintd", version = "=0.12.0", default-features = false }
cdk-prometheus = { path = "./crates/cdk-prometheus", version = "=0.12.0", default-features = false }
clap = { version = "4.5.31", features = ["derive"] } clap = { version = "4.5.31", features = ["derive"] }
ciborium = { version = "0.2.2", default-features = false, features = ["std"] } ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
cbor-diag = "0.1.12" cbor-diag = "0.1.12"
@@ -108,6 +109,8 @@ tonic-build = "0.13.1"
strum = "0.27.1" strum = "0.27.1"
strum_macros = "0.27.1" strum_macros = "0.27.1"
rustls = { version = "0.23.27", default-features = false, features = ["ring"] } rustls = { version = "0.23.27", default-features = false, features = ["ring"] }
prometheus = { version = "0.13.4", features = ["process"], default-features = false }

View File

@@ -10,7 +10,7 @@ COPY Cargo.toml ./Cargo.toml
COPY crates ./crates COPY crates ./crates
# Start the Nix daemon and develop the environment # Start the Nix daemon and develop the environment
RUN nix develop --extra-experimental-features nix-command --extra-experimental-features flakes --command cargo build --release --bin cdk-mintd --features redis RUN nix develop --extra-experimental-features nix-command --extra-experimental-features flakes --command cargo build --release --bin cdk-mintd --features redis --features prometheus
# Create a runtime stage # Create a runtime stage
FROM debian:trixie-slim FROM debian:trixie-slim

View File

@@ -15,7 +15,7 @@ default = ["auth"]
redis = ["dep:redis"] redis = ["dep:redis"]
swagger = ["cdk/swagger", "dep:utoipa"] swagger = ["cdk/swagger", "dep:utoipa"]
auth = ["cdk/auth"] auth = ["cdk/auth"]
prometheus = ["dep:cdk-prometheus"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-trait.workspace = true async-trait.workspace = true
@@ -27,6 +27,7 @@ tokio.workspace = true
tracing.workspace = true tracing.workspace = true
utoipa = { workspace = true, optional = true } utoipa = { workspace = true, optional = true }
futures.workspace = true futures.workspace = true
cdk-prometheus = { workspace = true , optional = true}
moka = { version = "0.12.10", features = ["future"] } moka = { version = "0.12.10", features = ["future"] }
serde_json.workspace = true serde_json.workspace = true
paste = "1.0.15" paste = "1.0.15"

View File

@@ -17,6 +17,8 @@ use cache::HttpCache;
use cdk::mint::Mint; use cdk::mint::Mint;
use router_handlers::*; use router_handlers::*;
mod metrics;
#[cfg(feature = "auth")] #[cfg(feature = "auth")]
mod auth; mod auth;
mod bolt12_router; mod bolt12_router;
@@ -322,6 +324,11 @@ pub async fn create_mint_router_with_custom_cache(
mint_router mint_router
}; };
#[cfg(feature = "prometheus")]
let mint_router = mint_router.layer(axum::middleware::from_fn_with_state(
state.clone(),
metrics::global_metrics_middleware,
));
let mint_router = mint_router let mint_router = mint_router
.layer(from_fn(cors_middleware)) .layer(from_fn(cors_middleware))
.with_state(state); .with_state(state);

View File

@@ -0,0 +1,41 @@
#[cfg(feature = "prometheus")]
use std::time::Instant;
#[cfg(feature = "prometheus")]
use axum::body::Body;
#[cfg(feature = "prometheus")]
use axum::extract::MatchedPath;
#[cfg(feature = "prometheus")]
use axum::http::Request;
#[cfg(feature = "prometheus")]
use axum::middleware::Next;
#[cfg(feature = "prometheus")]
use axum::response::Response;
#[cfg(feature = "prometheus")]
use cdk_prometheus::global;
/// Global metrics middleware that uses the singleton instance.
/// This version doesn't require access to MintState and can be used in any Axum application.
#[cfg(feature = "prometheus")]
pub async fn global_metrics_middleware(
matched_path: Option<MatchedPath>,
req: Request<Body>,
next: Next,
) -> Response {
let start_time = Instant::now();
let response = next.run(req).await;
let endpoint_path = matched_path
.map(|mp| mp.as_str().to_string())
.unwrap_or_default();
let status_code = response.status().as_u16().to_string();
let request_duration = start_time.elapsed().as_secs_f64();
// Always use global metrics
global::record_http_request(&endpoint_path, &status_code);
global::record_http_request_duration(request_duration, &endpoint_path);
response
}

View File

@@ -18,6 +18,7 @@ bench = []
wallet = ["cashu/wallet"] wallet = ["cashu/wallet"]
mint = ["cashu/mint", "dep:uuid"] mint = ["cashu/mint", "dep:uuid"]
auth = ["cashu/auth"] auth = ["cashu/auth"]
prometheus = ["cdk-prometheus/default"]
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
@@ -30,6 +31,7 @@ lightning-invoice.workspace = true
lightning.workspace = true lightning.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true
cdk-prometheus = { workspace = true, optional = true}
url.workspace = true url.workspace = true
uuid = { workspace = true, optional = true } uuid = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true } utoipa = { workspace = true, optional = true }

View File

@@ -6,6 +6,8 @@ use std::pin::Pin;
use async_trait::async_trait; use async_trait::async_trait;
use cashu::util::hex; use cashu::util::hex;
use cashu::{Bolt11Invoice, MeltOptions}; use cashu::{Bolt11Invoice, MeltOptions};
#[cfg(feature = "prometheus")]
use cdk_prometheus::METRICS;
use futures::Stream; use futures::Stream;
use lightning::offers::offer::Offer; use lightning::offers::offer::Offer;
use lightning_invoice::ParseOrSemanticError; use lightning_invoice::ParseOrSemanticError;
@@ -411,3 +413,189 @@ impl TryFrom<Value> for Bolt11Settings {
serde_json::from_value(value).map_err(|err| err.into()) serde_json::from_value(value).map_err(|err| err.into())
} }
} }
/// Metrics wrapper for MintPayment implementations
///
/// This wrapper implements the Decorator pattern to collect metrics on all
/// MintPayment trait methods. It wraps any existing MintPayment implementation
/// and automatically records timing and operation metrics.
#[derive(Clone)]
#[cfg(feature = "prometheus")]
pub struct MetricsMintPayment<T> {
inner: T,
}
#[cfg(feature = "prometheus")]
impl<T> MetricsMintPayment<T>
where
T: MintPayment,
{
/// Create a new metrics wrapper around a MintPayment implementation
pub fn new(inner: T) -> Self {
Self { inner }
}
/// Get reference to the underlying implementation
pub fn inner(&self) -> &T {
&self.inner
}
/// Consume the wrapper and return the inner implementation
pub fn into_inner(self) -> T {
self.inner
}
}
#[async_trait]
#[cfg(feature = "prometheus")]
impl<T> MintPayment for MetricsMintPayment<T>
where
T: MintPayment + Send + Sync,
{
type Err = T::Err;
async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("get_settings");
let result = self.inner.get_settings().await;
let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram("get_settings", result.is_ok(), duration);
METRICS.dec_in_flight_requests("get_settings");
result
}
async fn create_incoming_payment_request(
&self,
unit: &CurrencyUnit,
options: IncomingPaymentOptions,
) -> Result<CreateIncomingPaymentResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("create_incoming_payment_request");
let result = self
.inner
.create_incoming_payment_request(unit, options)
.await;
let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram(
"create_incoming_payment_request",
result.is_ok(),
duration,
);
METRICS.dec_in_flight_requests("create_incoming_payment_request");
result
}
async fn get_payment_quote(
&self,
unit: &CurrencyUnit,
options: OutgoingPaymentOptions,
) -> Result<PaymentQuoteResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("get_payment_quote");
let result = self.inner.get_payment_quote(unit, options).await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
if let Ok(ref quote) = result {
let amount: f64 = u64::from(quote.amount) as f64;
let fee: f64 = u64::from(quote.fee) as f64;
METRICS.record_lightning_payment(amount, fee);
}
METRICS.record_mint_operation_histogram("get_payment_quote", success, duration);
METRICS.dec_in_flight_requests("get_payment_quote");
result
}
async fn wait_payment_event(
&self,
) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("wait_payment_event");
let result = self.inner.wait_payment_event().await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
METRICS.record_mint_operation_histogram("wait_payment_event", success, duration);
METRICS.dec_in_flight_requests("wait_payment_event");
result
}
async fn make_payment(
&self,
unit: &CurrencyUnit,
options: OutgoingPaymentOptions,
) -> Result<MakePaymentResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("make_payment");
let result = self.inner.make_payment(unit, options).await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
METRICS.record_mint_operation_histogram("make_payment", success, duration);
METRICS.dec_in_flight_requests("make_payment");
result
}
fn is_wait_invoice_active(&self) -> bool {
self.inner.is_wait_invoice_active()
}
fn cancel_wait_invoice(&self) {
self.inner.cancel_wait_invoice()
}
async fn check_incoming_payment_status(
&self,
payment_identifier: &PaymentIdentifier,
) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("check_incoming_payment_status");
let result = self
.inner
.check_incoming_payment_status(payment_identifier)
.await;
let duration = start.elapsed().as_secs_f64();
METRICS.record_mint_operation_histogram(
"check_incoming_payment_status",
result.is_ok(),
duration,
);
METRICS.dec_in_flight_requests("check_incoming_payment_status");
result
}
async fn check_outgoing_payment(
&self,
payment_identifier: &PaymentIdentifier,
) -> Result<MakePaymentResponse, Self::Err> {
let start = std::time::Instant::now();
METRICS.inc_in_flight_requests("check_outgoing_payment");
let result = self.inner.check_outgoing_payment(payment_identifier).await;
let duration = start.elapsed().as_secs_f64();
let success = result.is_ok();
METRICS.record_mint_operation_histogram("check_outgoing_payment", success, duration);
METRICS.dec_in_flight_requests("check_outgoing_payment");
result
}
}

View File

@@ -29,7 +29,7 @@ cdk-sqlite = { workspace = true }
cdk-redb = { workspace = true } cdk-redb = { workspace = true }
cdk-fake-wallet = { workspace = true } cdk-fake-wallet = { workspace = true }
cdk-common = { workspace = true, features = ["mint", "wallet", "auth"] } cdk-common = { workspace = true, features = ["mint", "wallet", "auth"] }
cdk-mintd = { workspace = true, features = ["cln", "lnd", "fakewallet", "grpc-processor", "auth", "lnbits", "management-rpc", "sqlite", "postgres", "ldk-node"] } cdk-mintd = { workspace = true, features = ["cln", "lnd", "fakewallet", "grpc-processor", "auth", "lnbits", "management-rpc", "sqlite", "postgres", "ldk-node", "prometheus"] }
futures = { workspace = true, default-features = false, features = [ futures = { workspace = true, default-features = false, features = [
"executor", "executor",
] } ] }

View File

@@ -286,6 +286,7 @@ fn create_ldk_settings(
grpc_processor: None, grpc_processor: None,
database: cdk_mintd::config::Database::default(), database: cdk_mintd::config::Database::default(),
mint_management_rpc: None, mint_management_rpc: None,
prometheus: None,
auth: None, auth: None,
} }
} }

View File

@@ -219,6 +219,7 @@ pub fn create_fake_wallet_settings(
}, },
mint_management_rpc: None, mint_management_rpc: None,
auth: None, auth: None,
prometheus: Some(Default::default()),
} }
} }
@@ -265,6 +266,7 @@ pub fn create_cln_settings(
database: cdk_mintd::config::Database::default(), database: cdk_mintd::config::Database::default(),
mint_management_rpc: None, mint_management_rpc: None,
auth: None, auth: None,
prometheus: Some(Default::default()),
} }
} }
@@ -310,5 +312,6 @@ pub fn create_lnd_settings(
database: cdk_mintd::config::Database::default(), database: cdk_mintd::config::Database::default(),
mint_management_rpc: None, mint_management_rpc: None,
auth: None, auth: None,
prometheus: Some(Default::default()),
} }
} }

View File

@@ -28,6 +28,7 @@ sqlcipher = ["sqlite", "cdk-sqlite/sqlcipher"]
swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"] swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
redis = ["cdk-axum/redis"] redis = ["cdk-axum/redis"]
auth = ["cdk/auth", "cdk-axum/auth", "cdk-sqlite?/auth", "cdk-postgres?/auth"] auth = ["cdk/auth", "cdk-axum/auth", "cdk-sqlite?/auth", "cdk-postgres?/auth"]
prometheus = ["cdk/prometheus", "dep:cdk-prometheus", "cdk-sqlite?/prometheus", "cdk-axum/prometheus"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
@@ -38,8 +39,9 @@ cdk = { workspace = true, features = [
] } ] }
cdk-sqlite = { workspace = true, features = [ cdk-sqlite = { workspace = true, features = [
"mint" "mint"
], optional = true } ], optional = true }
cdk-postgres = { workspace = true, features = ["mint"], optional = true } cdk-common = {workspace = true, features = ["prometheus"]}
cdk-postgres = { workspace = true, features = ["mint"], optional = true}
cdk-cln = { workspace = true, optional = true } cdk-cln = { workspace = true, optional = true }
cdk-lnbits = { workspace = true, optional = true } cdk-lnbits = { workspace = true, optional = true }
cdk-lnd = { workspace = true, optional = true } cdk-lnd = { workspace = true, optional = true }
@@ -50,6 +52,7 @@ cdk-signatory.workspace = true
cdk-mint-rpc = { workspace = true, optional = true } cdk-mint-rpc = { workspace = true, optional = true }
cdk-payment-processor = { workspace = true, optional = true } cdk-payment-processor = { workspace = true, optional = true }
config.workspace = true config.workspace = true
cdk-prometheus = { workspace = true, optional = true , features = ["system-metrics"]}
clap.workspace = true clap.workspace = true
bitcoin.workspace = true bitcoin.workspace = true
tokio = { workspace = true, default-features = false, features = ["signal"] } tokio = { workspace = true, default-features = false, features = ["signal"] }
@@ -63,11 +66,7 @@ tower-http = { workspace = true, features = ["compression-full", "decompression-
tower.workspace = true tower.workspace = true
lightning-invoice.workspace = true lightning-invoice.workspace = true
home.workspace = true home.workspace = true
url.workspace = true
utoipa = { workspace = true, optional = true } utoipa = { workspace = true, optional = true }
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true } utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }
[build-dependencies] [build-dependencies]
# Dep of utopia 2.5.0 breaks so keeping here for now
zip = "=2.4.2"
time = "=0.3.39"

View File

@@ -1,3 +1,4 @@
[info] [info]
url = "https://mint.thesimplekid.dev/" url = "https://mint.thesimplekid.dev/"
listen_host = "127.0.0.1" listen_host = "127.0.0.1"
@@ -20,7 +21,11 @@ enabled = false
# address = "127.0.0.1" # address = "127.0.0.1"
# port = 8086 # port = 8086
#[prometheus]
#enabled = true
#address = "127.0.0.1"
#port = 9090
#
[info.http_cache] [info.http_cache]
# memory or redis # memory or redis
backend = "memory" backend = "memory"
@@ -130,12 +135,12 @@ reserve_fee_min = 4
# webserver_host = "127.0.0.1" # Default: 127.0.0.1 # webserver_host = "127.0.0.1" # Default: 127.0.0.1
# webserver_port = 0 # 0 = auto-assign available port # webserver_port = 0 # 0 = auto-assign available port
# [fake_wallet] [fake_wallet]
# supported_units = ["sat"] supported_units = ["sat"]
# fee_percent = 0.02 fee_percent = 0.02
# reserve_fee_min = 1 reserve_fee_min = 1
# min_delay_time = 1 min_delay_time = 1
# max_delay_time = 3 max_delay_time = 3
# [grpc_processor] # [grpc_processor]
# gRPC Payment Processor configuration # gRPC Payment Processor configuration

View File

@@ -452,6 +452,16 @@ pub struct Settings {
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
pub mint_management_rpc: Option<MintManagementRpc>, pub mint_management_rpc: Option<MintManagementRpc>,
pub auth: Option<Auth>, pub auth: Option<Auth>,
#[cfg(feature = "prometheus")]
pub prometheus: Option<Prometheus>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[cfg(feature = "prometheus")]
pub struct Prometheus {
pub enabled: bool,
pub address: Option<String>,
pub port: Option<u16>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]

View File

@@ -25,6 +25,8 @@ mod lnbits;
mod lnd; mod lnd;
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
mod management_rpc; mod management_rpc;
#[cfg(feature = "prometheus")]
mod prometheus;
use std::env; use std::env;
use std::str::FromStr; use std::str::FromStr;
@@ -50,6 +52,8 @@ pub use lnd::*;
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]
pub use management_rpc::*; pub use management_rpc::*;
pub use mint_info::*; pub use mint_info::*;
#[cfg(feature = "prometheus")]
pub use prometheus::*;
use crate::config::{DatabaseEngine, LnBackend, Settings}; use crate::config::{DatabaseEngine, LnBackend, Settings};
@@ -98,6 +102,11 @@ impl Settings {
); );
} }
#[cfg(feature = "prometheus")]
{
self.prometheus = Some(self.prometheus.clone().unwrap_or_default().from_env());
}
match self.ln.ln_backend { match self.ln.ln_backend {
#[cfg(feature = "cln")] #[cfg(feature = "cln")]
LnBackend::Cln => { LnBackend::Cln => {

View File

@@ -0,0 +1,31 @@
//! Prometheus environment variables
use std::env;
use crate::config::Prometheus;
pub const ENV_PROMETHEUS_ENABLED: &str = "CDK_MINTD_PROMETHEUS_ENABLED";
pub const ENV_PROMETHEUS_ADDRESS: &str = "CDK_MINTD_PROMETHEUS_ADDRESS";
pub const ENV_PROMETHEUS_PORT: &str = "CDK_MINTD_PROMETHEUS_PORT";
impl Prometheus {
pub fn from_env(mut self) -> Self {
if let Ok(enabled_str) = env::var(ENV_PROMETHEUS_ENABLED) {
if let Ok(enabled) = enabled_str.parse() {
self.enabled = enabled;
}
}
if let Ok(address) = env::var(ENV_PROMETHEUS_ADDRESS) {
self.address = Some(address);
}
if let Ok(port_str) = env::var(ENV_PROMETHEUS_PORT) {
if let Ok(port) = port_str.parse() {
self.port = Some(port);
}
}
self
}
}

View File

@@ -13,10 +13,7 @@ use std::sync::Arc;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use axum::Router; use axum::Router;
use bip39::Mnemonic; use bip39::Mnemonic;
// internal crate modules
use cdk::cdk_database::{self, MintDatabase, MintKVStore, MintKeysDatabase}; use cdk::cdk_database::{self, MintDatabase, MintKVStore, MintKeysDatabase};
use cdk::cdk_payment;
use cdk::cdk_payment::MintPayment;
use cdk::mint::{Mint, MintBuilder, MintMeltLimits}; use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
#[cfg(any( #[cfg(any(
feature = "cln", feature = "cln",
@@ -41,14 +38,22 @@ use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod}; use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
use cdk::types::QuoteTTL; use cdk::types::QuoteTTL;
use cdk_axum::cache::HttpCache; use cdk_axum::cache::HttpCache;
// internal crate modules
#[cfg(feature = "prometheus")]
use cdk_common::payment::MetricsMintPayment;
use cdk_common::payment::MintPayment;
#[cfg(feature = "auth")]
use cdk_postgres::MintPgAuthDatabase;
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase}; use cdk_postgres::MintPgDatabase;
#[cfg(all(feature = "auth", feature = "sqlite"))] #[cfg(all(feature = "auth", feature = "sqlite"))]
use cdk_sqlite::mint::MintSqliteAuthDatabase; use cdk_sqlite::mint::MintSqliteAuthDatabase;
#[cfg(feature = "sqlite")] #[cfg(feature = "sqlite")]
use cdk_sqlite::MintSqliteDatabase; use cdk_sqlite::MintSqliteDatabase;
use cli::CLIArgs; use cli::CLIArgs;
use config::{AuthType, DatabaseEngine, LnBackend}; #[cfg(feature = "auth")]
use config::AuthType;
use config::{DatabaseEngine, LnBackend};
use env_vars::ENV_WORK_DIR; use env_vars::ENV_WORK_DIR;
use setup::LnBackendSetup; use setup::LnBackendSetup;
use tower::ServiceBuilder; use tower::ServiceBuilder;
@@ -440,6 +445,8 @@ async fn configure_lightning_backend(
None, None,
) )
.await?; .await?;
#[cfg(feature = "prometheus")]
let cln = MetricsMintPayment::new(cln);
mint_builder = configure_backend_for_unit( mint_builder = configure_backend_for_unit(
settings, settings,
@@ -463,6 +470,8 @@ async fn configure_lightning_backend(
None, None,
) )
.await?; .await?;
#[cfg(feature = "prometheus")]
let lnbits = MetricsMintPayment::new(lnbits);
mint_builder = configure_backend_for_unit( mint_builder = configure_backend_for_unit(
settings, settings,
@@ -486,6 +495,8 @@ async fn configure_lightning_backend(
None, None,
) )
.await?; .await?;
#[cfg(feature = "prometheus")]
let lnd = MetricsMintPayment::new(lnd);
mint_builder = configure_backend_for_unit( mint_builder = configure_backend_for_unit(
settings, settings,
@@ -512,6 +523,8 @@ async fn configure_lightning_backend(
_kv_store.clone(), _kv_store.clone(),
) )
.await?; .await?;
#[cfg(feature = "prometheus")]
let fake = MetricsMintPayment::new(fake);
mint_builder = configure_backend_for_unit( mint_builder = configure_backend_for_unit(
settings, settings,
@@ -541,6 +554,8 @@ async fn configure_lightning_backend(
let processor = grpc_processor let processor = grpc_processor
.setup(ln_routers, settings, unit.clone(), None, work_dir, None) .setup(ln_routers, settings, unit.clone(), None, work_dir, None)
.await?; .await?;
#[cfg(feature = "prometheus")]
let processor = MetricsMintPayment::new(processor);
mint_builder = configure_backend_for_unit( mint_builder = configure_backend_for_unit(
settings, settings,
@@ -595,7 +610,7 @@ async fn configure_backend_for_unit(
mut mint_builder: MintBuilder, mut mint_builder: MintBuilder,
unit: cdk::nuts::CurrencyUnit, unit: cdk::nuts::CurrencyUnit,
mint_melt_limits: MintMeltLimits, mint_melt_limits: MintMeltLimits,
backend: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>, backend: Arc<dyn MintPayment<Err = cdk_common::payment::Error> + Send + Sync>,
) -> Result<MintBuilder> { ) -> Result<MintBuilder> {
let payment_settings = backend.get_settings().await?; let payment_settings = backend.get_settings().await?;
@@ -974,7 +989,48 @@ async fn start_services_with_shutdown(
); );
} }
} }
// Create a broadcast channel to share shutdown signal between services
let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
// Start Prometheus server if enabled
#[cfg(feature = "prometheus")]
let prometheus_handle = {
if let Some(prometheus_settings) = &settings.prometheus {
if prometheus_settings.enabled {
let addr = prometheus_settings
.address
.clone()
.unwrap_or("127.0.0.1".to_string());
let port = prometheus_settings.port.unwrap_or(9000);
let address = format!("{}:{}", addr, port)
.parse()
.expect("Invalid prometheus address");
let server = cdk_prometheus::PrometheusBuilder::new()
.bind_address(address)
.build_with_cdk_metrics()?;
let mut shutdown_rx = shutdown_tx.subscribe();
let prometheus_shutdown = async move {
let _ = shutdown_rx.recv().await;
};
Some(tokio::spawn(async move {
if let Err(e) = server.start(prometheus_shutdown).await {
tracing::error!("Failed to start prometheus server: {}", e);
}
}))
} else {
None
}
} else {
None
}
};
#[cfg(not(feature = "prometheus"))]
let prometheus_handle: Option<tokio::task::JoinHandle<()>> = None;
for router in ln_routers { for router in ln_routers {
mint_service = mint_service.merge(router); mint_service = mint_service.merge(router);
} }
@@ -987,8 +1043,24 @@ async fn start_services_with_shutdown(
tracing::info!("listening on {}", listener.local_addr().unwrap()); tracing::info!("listening on {}", listener.local_addr().unwrap());
// Create a task to wait for the shutdown signal and broadcast it
let shutdown_broadcast_task = {
let shutdown_tx = shutdown_tx.clone();
tokio::spawn(async move {
shutdown_signal.await;
tracing::info!("Shutdown signal received, broadcasting to all services");
let _ = shutdown_tx.send(());
})
};
// Create shutdown future for axum server
let mut axum_shutdown_rx = shutdown_tx.subscribe();
let axum_shutdown = async move {
let _ = axum_shutdown_rx.recv().await;
};
// Wait for axum server to complete with custom shutdown signal // Wait for axum server to complete with custom shutdown signal
let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(shutdown_signal); let axum_result = axum::serve(listener, mint_service).with_graceful_shutdown(axum_shutdown);
match axum_result.await { match axum_result.await {
Ok(_) => { Ok(_) => {
@@ -1001,6 +1073,17 @@ async fn start_services_with_shutdown(
} }
} }
// Wait for the shutdown broadcast task to complete
let _ = shutdown_broadcast_task.await;
// Wait for prometheus server to shutdown if it was started
#[cfg(feature = "prometheus")]
if let Some(handle) = prometheus_handle {
if let Err(e) = handle.await {
tracing::warn!("Prometheus server task failed: {}", e);
}
}
mint.stop().await?; mint.stop().await?;
#[cfg(feature = "management-rpc")] #[cfg(feature = "management-rpc")]

View File

@@ -0,0 +1,47 @@
[package]
name = "cdk-prometheus"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
readme = "README.md"
description = "Prometheus metrics export server for CDK applications"
[features]
default = ["system-metrics"]
system-metrics = ["sysinfo"]
[dependencies]
# Prometheus
prometheus = "0.13"
# Async runtime
tokio.workspace = true
futures.workspace = true
# Error handling
anyhow.workspace = true
thiserror.workspace = true
# Serialization
serde.workspace = true
serde_json.workspace = true
# System metrics (optional)
sysinfo = { version = "0.32", optional = true }
# Tracing
tracing.workspace = true
# Utility
once_cell.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
reqwest.workspace = true
tracing-subscriber.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,189 @@
# CDK Prometheus
A small, focused crate that provides Prometheus metrics for CDK-based services. It bundles a ready-to-use metrics registry, a background HTTP server to expose metrics, helper functions for common CDK domains (HTTP, auth, Lightning, DB, mint operations), and an ergonomic macro for conditional metrics recording.
- Out-of-the-box metrics for HTTP, auth, Lightning payments, database, and mint operations
- Global, lazily-initialized metrics instance you can use anywhere
- Optional background server to expose metrics on /metrics
- Re-exports the prometheus crate for custom instrumentation
- Optional system metrics (feature-gated)
## Installation
Add the crate to your Cargo.toml (replace the version as needed):
```toml
[dependencies] cdk-prometheus = { version = "0.1", features = ["system-metrics"] }
```
- Feature flags:
- system-metrics: include basic process/system metrics collected periodically.
Note for downstream crates: the provided record_metrics! macro is gated at call-site by a feature named prometheus. If you use that macro, declare a prometheus feature in your application crate and enable it to compile the macro calls into real metrics (otherwise they no-op).
## Quick start
### Docker
Start Prometheus and Grafana with docker-compose:
```
docker compose up -d prometheus grafana
```
Start your mintd
```
./mintd -w ~/.cdk-mintd
```
Check Prometheus and Grafana
* `curl localhost:9000/metrics` for checking CDK metrics
* `http://localhost:9090/targets?search=` checking the prometheus collector (you should see http://host.docker.internal:9000/metrics)
* `http://localhost:3011/d/cdk-mint-dashboard/cdk-mint-dashboard` Grafana dashboard (default login: admin/admin)
### Rust
Expose a Prometheus endpoint with a default registry and CDK metrics:
```rust
use cdk_prometheus::start_default_server_with_metrics;
#[tokio::main] async fn main() -> anyhow::Result<()> { // Starts an HTTP server (default bind and path) and registers CDK metrics into its registry start_default_server_with_metrics().await?; Ok(()) }
```
Or start it in the background (e.g., from your application bootstrap):
```rust
use cdk_prometheus::start_background_server_with_metrics;
fn main() -> anyhow::Result<()> { let _handle = start_background_server_with_metrics()?; // Continue bootstrapping your application... Ok(()) }
```
## Recording metrics
You can record metrics using:
- The global helpers (simple functions)
- The global singleton METRICS (direct methods)
- The record_metrics! macro (conditional recording with an optional instance)
### Global helpers
```rust
use cdk_prometheus::global;
fn handle_request() {
global::record_http_request("/health", "200"); global::record_http_request_duration(0.003, "/health");
global::record_auth_attempt();
global::record_auth_success();
// Lightning and DB
global::record_lightning_payment(1500.0, 2.0); // amount, fee (both in base units you track)
global::record_db_operation(0.015, "select_user");
global::set_db_connections_active(8);
// Mint operations
global::inc_in_flight_requests("get_payment_quote");
// ... do work ...
global::record_mint_operation("get_payment_quote", true);
global::record_mint_operation_histogram("get_payment_quote", true, 0.021);
global::dec_in_flight_requests("get_payment_quote");
// Errors
global::record_error();
}
```
### Using the global METRICS instance directly
```rust
use cdk_prometheus::METRICS;
fn do_db_work() { METRICS.record_db_operation(0.005, "update_user"); }
```
### Using the record_metrics! macro
The macro lets you write grouped calls concisely and optionally pass an instance to use; if no instance is present, it automatically falls back to the global helpers. At call-site, wrap your invocations with a prometheus feature so they can be disabled in minimal builds.
```rust
use cdk_prometheus::record_metrics;
fn run_operation(metrics_opt: Option<cdk_prometheus::CdkMetrics>) { // Use instance if present, otherwise fallback to global record_metrics!(metrics_opt => { inc_in_flight_requests("make_payment"); record_mint_operation("make_payment", true); record_mint_operation_histogram("make_payment", true, 0.123); dec_in_flight_requests("make_payment"); });
// Or call directly on the global helpers
record_metrics!({
record_error();
});
}
```
## Exposing the /metrics endpoint
If you just need sane defaults, use the convenience starters shown above. If you want finer control (bind address, path, system metrics), build the server explicitly:
```rust
use cdk_prometheus::{PrometheusBuilder, PrometheusServer, CdkMetrics, prometheus::Registry};
fn build_and_run() -> anyhow::Result<tokio::task::JoinHandle<anyhow::Result<()>>> { // Build a server wired up with the default CDK metrics let server = PrometheusBuilder::new().build_with_cdk_metrics()?; let handle = server.start_background(); Ok(handle) }
```
Notes:
- Default bind address and metrics path are set by the server configuration (commonly 127.0.0.1:9090 and /metrics).
- With system-metrics enabled, the server periodically updates process/system gauges.
## Whats included
The default CDK metrics instance (CdkMetrics) registers and maintains counters, histograms, and gauges for common areas:
- HTTP: request totals, durations
- Auth: attempts and successes
- Lightning: payment totals, amounts, fees
- Database: operation totals, latencies, active connections
- Mint: operation totals, in-flight gauges, per-operation latencies
- Errors: a general counter
You can use these immediately through the global helpers or the METRICS instance.
## Adding custom metrics
This crate re-exports the prometheus crate and exposes the underlying Registry so you can define and register your own metrics:
```rust
use cdk_prometheus::{prometheus, global};
fn register_custom_metric() -> Result<(), prometheus::Error> { let my_counter = prometheus::IntCounter::new("my_counter", "A custom counter")?; let registry = global::registry(); // Arcregistry.register(Box::new(my_counter.clone()))?;
my_counter.inc();
Ok(())
}
```
If you prefer instance-level control:
```rust
use std::sync::Arc; use cdk_prometheus::{create_cdk_metrics, prometheus};
fn with_instance() -> anyhow::Result<()> { let metrics = create_cdk_metrics()?; let registry: Arc[prometheus::Registry]() = metrics.registry();
let hist = prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new("my_latency_seconds", "My op latency")
)?;
registry.register(Box::new(hist))?;
Ok(())
}
```
## Scraping with Prometheus
Example scrape_config:
```yaml
scrape_configs:
- job_name: 'cdk'
scrape_interval: 15s
static_configs:
- targets: ['127.0.0.1:9090']
```
If you changed the bind address or path, make sure to update targets or the metrics_path in your Prometheus configuration accordingly.
## System metrics (optional)
Enable the system-metrics feature to export basic process/system metrics. The server updates these at a configurable interval.
```toml
cdk-prometheus = { version = "0.1", features = ["system-metrics"] }
```
## Error handling
Common error types surfaced by this crate include:
- Server bind failures
- Metrics collection/registry errors
- System metrics collection errors (when enabled)
Handle these at startup and monitor logs during runtime.
## Best practices
- Run the metrics server on localhost or a private interface and use a Prometheus agent/sidecar if needed.
- Register application-specific metrics early in your bootstrap so they are visible from the first scrape.
- Use histograms for latencies and size distributions; use counters for event totals; use gauges for in-flight or current-state values.
- Keep label cardinality bounded.
## License
MIT
```

View File

@@ -0,0 +1,32 @@
use thiserror::Error;
/// Errors that can occur in the Prometheus crate
#[derive(Error, Debug)]
pub enum PrometheusError {
/// Server binding error
#[error("Failed to bind to address {address}: {source}")]
ServerBind {
address: String,
#[source]
source: std::io::Error,
},
/// Metrics collection error
#[error("Failed to collect metrics: {0}")]
MetricsCollection(String),
/// Registry error
#[error("Registry error: {source}")]
Registry {
#[from]
source: prometheus::Error,
},
/// System metrics error
#[cfg(feature = "system-metrics")]
#[error("System metrics error: {0}")]
SystemMetrics(String),
}
/// Result type for Prometheus operations
pub type Result<T> = std::result::Result<T, PrometheusError>;

View File

@@ -0,0 +1,84 @@
//! # CDK Prometheus
pub mod error;
pub mod metrics;
pub mod server;
#[cfg(feature = "system-metrics")]
pub mod process;
// Re-exports for convenience
pub use error::{PrometheusError, Result};
pub use metrics::{global, CdkMetrics, METRICS};
#[cfg(feature = "system-metrics")]
pub use process::SystemMetrics;
// Re-export prometheus crate for custom metrics
pub use prometheus;
pub use server::{PrometheusBuilder, PrometheusConfig, PrometheusServer};
/// Macro for recording metrics with optional fallback to global instance
///
/// Usage:
/// ```rust
/// use cdk_prometheus::record_metrics;
///
/// // With optional metrics instance
/// record_metrics!(metrics_option => {
/// dec_in_flight_requests("operation");
/// record_mint_operation("operation", true);
/// });
///
/// // Direct global calls
/// record_metrics!({
/// dec_in_flight_requests("operation");
/// record_mint_operation("operation", true);
/// });
/// ```
#[macro_export]
macro_rules! record_metrics {
// Pattern for using optional metrics with fallback to global
($metrics_opt:expr => { $($method:ident($($arg:expr),*));* $(;)? }) => {
#[cfg(feature = "prometheus")]
{
if let Some(metrics) = $metrics_opt.as_ref() {
$(
metrics.$method($($arg),*);
)*
} else {
$(
$crate::global::$method($($arg),*);
)*
}
}
};
// Pattern for using global metrics directly
({ $($method:ident($($arg:expr),*));* $(;)? }) => {
#[cfg(feature = "prometheus")]
{
$(
$crate::global::$method($($arg),*);
)*
}
};
}
/// Convenience function to create a new CDK metrics instance
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
pub fn create_cdk_metrics() -> Result<CdkMetrics> {
CdkMetrics::new()
}
/// Convenience function to start a Prometheus server with specific metrics
///
/// # Errors
/// Returns an error if the server cannot be created or started
pub async fn start_default_server_with_metrics(
shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
) -> Result<()> {
let server = PrometheusBuilder::new().build_with_cdk_metrics()?;
server.start(shutdown_signal).await
}

View File

@@ -0,0 +1,427 @@
use std::sync::Arc;
use prometheus::{
Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry,
};
/// Global metrics instance
pub static METRICS: std::sync::LazyLock<CdkMetrics> = std::sync::LazyLock::new(CdkMetrics::default);
/// Custom metrics for CDK applications
#[derive(Clone, Debug)]
pub struct CdkMetrics {
registry: Arc<Registry>,
// HTTP metrics
http_requests_total: IntCounterVec,
http_request_duration: HistogramVec,
// Authentication metrics
auth_attempts_total: IntCounter,
auth_successes_total: IntCounter,
// Lightning metrics
lightning_payments_total: IntCounter,
lightning_payment_amount: Histogram,
lightning_payment_fees: Histogram,
// Database metrics
db_operations_total: IntCounter,
db_operation_duration: HistogramVec,
db_connections_active: IntGauge,
// Error metrics
errors_total: IntCounter,
// Mint metrics
mint_operations_total: IntCounterVec,
mint_in_flight_requests: IntGaugeVec,
mint_operation_duration: HistogramVec,
}
impl CdkMetrics {
/// Create a new instance with default metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
pub fn new() -> crate::Result<Self> {
let registry = Arc::new(Registry::new());
// Create and register HTTP metrics
let (http_requests_total, http_request_duration) = Self::create_http_metrics(&registry)?;
// Create and register authentication metrics
let (auth_attempts_total, auth_successes_total) = Self::create_auth_metrics(&registry)?;
// Create and register Lightning metrics
let (lightning_payments_total, lightning_payment_amount, lightning_payment_fees) =
Self::create_lightning_metrics(&registry)?;
// Create and register database metrics
let (db_operations_total, db_operation_duration, db_connections_active) =
Self::create_db_metrics(&registry)?;
// Create and register error metrics
let errors_total = Self::create_error_metrics(&registry)?;
// Create and register mint metrics
let (mint_operations_total, mint_operation_duration, mint_in_flight_requests) =
Self::create_mint_metrics(&registry)?;
Ok(Self {
registry,
http_requests_total,
http_request_duration,
auth_attempts_total,
auth_successes_total,
lightning_payments_total,
lightning_payment_amount,
lightning_payment_fees,
db_operations_total,
db_operation_duration,
db_connections_active,
errors_total,
mint_operations_total,
mint_in_flight_requests,
mint_operation_duration,
})
}
/// Create and register HTTP metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
fn create_http_metrics(registry: &Registry) -> crate::Result<(IntCounterVec, HistogramVec)> {
let http_requests_total = IntCounterVec::new(
prometheus::Opts::new("cdk_http_requests_total", "Total number of HTTP requests"),
&["endpoint", "status"],
)?;
registry.register(Box::new(http_requests_total.clone()))?;
let http_request_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_http_request_duration_seconds",
"HTTP request duration in seconds",
)
.buckets(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
]),
&["endpoint"],
)?;
registry.register(Box::new(http_request_duration.clone()))?;
Ok((http_requests_total, http_request_duration))
}
/// Create and register authentication metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
fn create_auth_metrics(registry: &Registry) -> crate::Result<(IntCounter, IntCounter)> {
let auth_attempts_total =
IntCounter::new("cdk_auth_attempts_total", "Total authentication attempts")?;
registry.register(Box::new(auth_attempts_total.clone()))?;
let auth_successes_total = IntCounter::new(
"cdk_auth_successes_total",
"Total successful authentications",
)?;
registry.register(Box::new(auth_successes_total.clone()))?;
Ok((auth_attempts_total, auth_successes_total))
}
/// Create and register Lightning metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
fn create_lightning_metrics(
registry: &Registry,
) -> crate::Result<(IntCounter, Histogram, Histogram)> {
let wallet_operations_total =
IntCounter::new("cdk_wallet_operations_total", "Total wallet operations")?;
registry.register(Box::new(wallet_operations_total))?;
let lightning_payments_total =
IntCounter::new("cdk_lightning_payments_total", "Total Lightning payments")?;
registry.register(Box::new(lightning_payments_total.clone()))?;
let lightning_payment_amount = Histogram::with_opts(
prometheus::HistogramOpts::new(
"cdk_lightning_payment_amount_sats",
"Lightning payment amounts in satoshis",
)
.buckets(vec![
1.0,
10.0,
100.0,
1000.0,
10_000.0,
100_000.0,
1_000_000.0,
]),
)?;
registry.register(Box::new(lightning_payment_amount.clone()))?;
let lightning_payment_fees = Histogram::with_opts(
prometheus::HistogramOpts::new(
"cdk_lightning_payment_fees_sats",
"Lightning payment fees in satoshis",
)
.buckets(vec![0.0, 1.0, 5.0, 10.0, 50.0, 100.0, 500.0, 1000.0]),
)?;
registry.register(Box::new(lightning_payment_fees.clone()))?;
Ok((
lightning_payments_total,
lightning_payment_amount,
lightning_payment_fees,
))
}
/// Create and register database metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
fn create_db_metrics(
registry: &Registry,
) -> crate::Result<(IntCounter, HistogramVec, IntGauge)> {
let db_operations_total =
IntCounter::new("cdk_db_operations_total", "Total database operations")?;
registry.register(Box::new(db_operations_total.clone()))?;
let db_operation_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_db_operation_duration_seconds",
"Database operation duration in seconds",
)
.buckets(vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]),
&["operation"],
)?;
registry.register(Box::new(db_operation_duration.clone()))?;
let db_connections_active = IntGauge::new(
"cdk_db_connections_active",
"Number of active database connections",
)?;
registry.register(Box::new(db_connections_active.clone()))?;
Ok((
db_operations_total,
db_operation_duration,
db_connections_active,
))
}
/// Create and register error metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
fn create_error_metrics(registry: &Registry) -> crate::Result<IntCounter> {
let errors_total = IntCounter::new("cdk_errors_total", "Total errors")?;
registry.register(Box::new(errors_total.clone()))?;
Ok(errors_total)
}
/// Create and register mint metrics
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
fn create_mint_metrics(
registry: &Registry,
) -> crate::Result<(IntCounterVec, HistogramVec, IntGaugeVec)> {
let mint_operations_total = IntCounterVec::new(
prometheus::Opts::new(
"cdk_mint_operations_total",
"Total number of mint operations",
),
&["operation", "status"],
)?;
registry.register(Box::new(mint_operations_total.clone()))?;
let mint_operation_duration = HistogramVec::new(
prometheus::HistogramOpts::new(
"cdk_mint_operation_duration_seconds",
"Duration of mint operations in seconds",
)
.buckets(vec![
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
]),
&["operation", "status"],
)?;
registry.register(Box::new(mint_operation_duration.clone()))?;
let mint_in_flight_requests = IntGaugeVec::new(
prometheus::Opts::new(
"cdk_mint_in_flight_requests",
"Number of in-flight mint requests",
),
&["operation"],
)?;
registry.register(Box::new(mint_in_flight_requests.clone()))?;
Ok((
mint_operations_total,
mint_operation_duration,
mint_in_flight_requests,
))
}
/// Get the metrics registry
#[must_use]
pub fn registry(&self) -> Arc<Registry> {
Arc::<Registry>::clone(&self.registry)
}
// HTTP metrics methods
pub fn record_http_request(&self, endpoint: &str, status: &str) {
self.http_requests_total
.with_label_values(&[endpoint, status])
.inc();
}
pub fn record_http_request_duration(&self, duration_seconds: f64, endpoint: &str) {
self.http_request_duration
.with_label_values(&[endpoint])
.observe(duration_seconds);
}
// Authentication metrics methods
pub fn record_auth_attempt(&self) {
self.auth_attempts_total.inc();
}
pub fn record_auth_success(&self) {
self.auth_successes_total.inc();
}
// Lightning metrics methods
pub fn record_lightning_payment(&self, amount: f64, fee: f64) {
self.lightning_payments_total.inc();
self.lightning_payment_amount.observe(amount);
self.lightning_payment_fees.observe(fee);
}
// Database metrics methods
pub fn record_db_operation(&self, duration_seconds: f64, op: &str) {
self.db_operations_total.inc();
self.db_operation_duration
.with_label_values(&[op])
.observe(duration_seconds);
}
pub fn set_db_connections_active(&self, count: i64) {
self.db_connections_active.set(count);
}
// Error metrics methods
pub fn record_error(&self) {
self.errors_total.inc();
}
// Mint metrics methods
pub fn record_mint_operation(&self, operation: &str, success: bool) {
let status = if success { "success" } else { "error" };
self.mint_operations_total
.with_label_values(&[operation, status])
.inc();
}
pub fn record_mint_operation_histogram(
&self,
operation: &str,
success: bool,
duration_seconds: f64,
) {
let status = if success { "success" } else { "error" };
self.mint_operation_duration
.with_label_values(&[operation, status])
.observe(duration_seconds);
}
pub fn inc_in_flight_requests(&self, operation: &str) {
self.mint_in_flight_requests
.with_label_values(&[operation])
.inc();
}
pub fn dec_in_flight_requests(&self, operation: &str) {
self.mint_in_flight_requests
.with_label_values(&[operation])
.dec();
}
}
impl Default for CdkMetrics {
fn default() -> Self {
Self::new().expect("Failed to create default CdkMetrics")
}
}
/// Helper functions for recording metrics using the global instance
pub mod global {
use super::METRICS;
/// Record an HTTP request using the global metrics instance
pub fn record_http_request(endpoint: &str, status: &str) {
METRICS.record_http_request(endpoint, status);
}
/// Record HTTP request duration using the global metrics instance
pub fn record_http_request_duration(duration_seconds: f64, endpoint: &str) {
METRICS.record_http_request_duration(duration_seconds, endpoint);
}
/// Record authentication attempt using the global metrics instance
pub fn record_auth_attempt() {
METRICS.record_auth_attempt();
}
/// Record authentication success using the global metrics instance
pub fn record_auth_success() {
METRICS.record_auth_success();
}
/// Record Lightning payment using the global metrics instance
pub fn record_lightning_payment(amount: f64, fee: f64) {
METRICS.record_lightning_payment(amount, fee);
}
/// Record database operation using the global metrics instance
pub fn record_db_operation(duration_seconds: f64, op: &str) {
METRICS.record_db_operation(duration_seconds, op);
}
/// Set database connections active using the global metrics instance
pub fn set_db_connections_active(count: i64) {
METRICS.set_db_connections_active(count);
}
/// Record error using the global metrics instance
pub fn record_error() {
METRICS.record_error();
}
/// Record mint operation using the global metrics instance
pub fn record_mint_operation(operation: &str, success: bool) {
METRICS.record_mint_operation(operation, success);
}
/// Record mint operation with histogram using the global metrics instance
pub fn record_mint_operation_histogram(operation: &str, success: bool, duration_seconds: f64) {
METRICS.record_mint_operation_histogram(operation, success, duration_seconds);
}
/// Increment in-flight requests using the global metrics instance
pub fn inc_in_flight_requests(operation: &str) {
METRICS.inc_in_flight_requests(operation);
}
/// Decrement in-flight requests using the global metrics instance
pub fn dec_in_flight_requests(operation: &str) {
METRICS.dec_in_flight_requests(operation);
}
/// Get the metrics registry from the global instance
pub fn registry() -> std::sync::Arc<prometheus::Registry> {
METRICS.registry()
}
}

View File

@@ -0,0 +1,107 @@
use std::sync::Arc;
#[cfg(feature = "system-metrics")]
use prometheus::{Gauge, IntGauge, Registry};
#[cfg(feature = "system-metrics")]
use sysinfo::{Pid, System};
/// System metrics collector that provides CPU, memory, disk, network, and process metrics
#[cfg(feature = "system-metrics")]
#[derive(Clone, Debug)]
pub struct SystemMetrics {
registry: Arc<Registry>,
system: Arc<std::sync::Mutex<System>>,
// Process metrics (for the CDK process)
process_cpu_usage_percent: Gauge,
process_memory_bytes: IntGauge,
process_memory_percent: Gauge,
}
#[cfg(feature = "system-metrics")]
impl SystemMetrics {
/// Create a new `SystemMetrics` instance
///
/// # Errors
/// Returns an error if any of the metrics cannot be created or registered
pub fn new() -> crate::Result<Self> {
let registry = Arc::new(Registry::new());
// Process metrics
let process_cpu_usage_percent = Gauge::new(
"process_cpu_usage_percent",
"CPU usage percentage of the CDK process (0-100)",
)?;
registry.register(Box::new(process_cpu_usage_percent.clone()))?;
let process_memory_bytes = IntGauge::new(
"process_memory_bytes",
"Memory usage of the CDK process in bytes",
)?;
registry.register(Box::new(process_memory_bytes.clone()))?;
let process_memory_percent = Gauge::new(
"process_memory_percent",
"Memory usage percentage of the CDK process (0-100)",
)?;
registry.register(Box::new(process_memory_percent.clone()))?;
// Initialize system with all needed refresh kinds
let system = Arc::new(std::sync::Mutex::new(System::new()));
let result = Self {
registry,
system,
process_cpu_usage_percent,
process_memory_bytes,
process_memory_percent,
};
Ok(result)
}
/// Get the metrics registry
#[must_use]
pub fn registry(&self) -> Arc<Registry> {
Arc::<Registry>::clone(&self.registry)
}
/// Update all system metrics
///
/// # Errors
/// Returns an error if the system mutex cannot be locked
pub fn update_metrics(&self) -> crate::Result<()> {
let mut system = self.system.lock().map_err(|e| {
crate::error::PrometheusError::SystemMetrics(format!("Failed to lock system: {e}"))
})?;
// Refresh system information
system.refresh_all();
// Update memory metrics
let total_memory = i64::try_from(system.total_memory()).unwrap_or(i64::MAX);
// Update process metrics for the current process
// This is a simplified approach that may not work perfectly in all cases
if let Some(process) = system.process(Pid::from(std::process::id() as usize)) {
// Get CPU usage if available
let process_cpu = process.cpu_usage();
self.process_cpu_usage_percent.set(f64::from(process_cpu));
// Get memory usage if available
let process_memory = i64::try_from(process.memory()).unwrap_or(i64::MAX);
self.process_memory_bytes.set(process_memory);
// Calculate memory percentage
if total_memory > 0 {
// Precision loss is acceptable for percentage calculation
#[allow(clippy::cast_precision_loss)]
let process_memory_percent = (process_memory as f64 / total_memory as f64) * 100.0;
self.process_memory_percent.set(process_memory_percent);
}
}
// Drop the system lock early to avoid resource contention
drop(system);
Ok(())
}
}

View File

@@ -0,0 +1,317 @@
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use prometheus::{Registry, TextEncoder};
use crate::metrics::METRICS;
#[cfg(feature = "system-metrics")]
use crate::process::SystemMetrics;
/// Configuration for the Prometheus server
#[derive(Debug, Clone)]
pub struct PrometheusConfig {
/// Address to bind the server to (default: "127.0.0.1:9090")
pub bind_address: SocketAddr,
/// Path to serve metrics on (default: "/metrics")
pub metrics_path: String,
/// Whether to include system metrics (default: true if feature enabled)
#[cfg(feature = "system-metrics")]
pub include_system_metrics: bool,
/// How often to update system metrics in seconds (default: 15)
#[cfg(feature = "system-metrics")]
pub system_metrics_interval: u64,
}
impl Default for PrometheusConfig {
fn default() -> Self {
Self {
bind_address: "127.0.0.1:9090".parse().expect("Invalid default address"),
metrics_path: "/metrics".to_string(),
#[cfg(feature = "system-metrics")]
include_system_metrics: true,
#[cfg(feature = "system-metrics")]
system_metrics_interval: 15,
}
}
}
/// Prometheus metrics server
#[derive(Debug)]
pub struct PrometheusServer {
config: PrometheusConfig,
registry: Arc<Registry>,
#[cfg(feature = "system-metrics")]
system_metrics: Option<SystemMetrics>,
}
impl PrometheusServer {
/// Create a new Prometheus server with CDK metrics
///
/// # Errors
/// Returns an error if system metrics cannot be created (when enabled)
pub fn new(config: PrometheusConfig) -> crate::Result<Self> {
let registry = METRICS.registry();
#[cfg(feature = "system-metrics")]
let system_metrics = if config.include_system_metrics {
let sys_metrics = SystemMetrics::new()?;
Some(sys_metrics)
} else {
None
};
Ok(Self {
config,
registry,
#[cfg(feature = "system-metrics")]
system_metrics,
})
}
/// Create a new Prometheus server with custom registry
#[must_use]
pub const fn with_registry(config: PrometheusConfig, registry: Arc<Registry>) -> Self {
Self {
config,
registry,
#[cfg(feature = "system-metrics")]
system_metrics: None,
}
}
/// Create a metrics handler function that gathers and encodes metrics
fn create_metrics_handler(
registry: Arc<Registry>,
#[cfg(feature = "system-metrics")] system_metrics: Option<SystemMetrics>,
) -> impl Fn() -> String {
move || {
let encoder = TextEncoder::new();
// Collect metrics from our registry
#[cfg(feature = "system-metrics")]
let mut metric_families = registry.gather();
#[cfg(not(feature = "system-metrics"))]
let metric_families = registry.gather();
// Add system metrics if available
#[cfg(feature = "system-metrics")]
if let Some(ref sys_metrics) = system_metrics {
// Update system metrics before collection
if let Err(e) = sys_metrics.update_metrics() {
tracing::warn!("Failed to update system metrics: {e}");
}
let sys_registry = sys_metrics.registry();
let mut sys_families = sys_registry.gather();
metric_families.append(&mut sys_families);
}
// Encode metrics to string
encoder
.encode_to_string(&metric_families)
.unwrap_or_else(|e| {
tracing::error!("Failed to encode metrics: {e}");
format!("Failed to encode metrics: {e}")
})
}
}
/// Start the Prometheus HTTP server
///
/// # Errors
/// This function always returns Ok as errors are handled internally
pub async fn start(
self,
shutdown_signal: impl std::future::Future<Output = ()> + Send + 'static,
) -> crate::Result<()> {
// Create and start the exporter
let binding = self.config.bind_address;
let registry_clone = Arc::<Registry>::clone(&self.registry);
// Create a handler that exposes our registry
#[cfg(feature = "system-metrics")]
let metrics_handler =
Self::create_metrics_handler(registry_clone, self.system_metrics.clone());
#[cfg(not(feature = "system-metrics"))]
let metrics_handler = Self::create_metrics_handler(registry_clone);
// Start the exporter in a background task
let path = self.config.metrics_path.clone();
// Create a channel for signaling the server task to shutdown
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
// Spawn the server task
let server_handle = tokio::spawn(async move {
// We're using a simple HTTP server to expose our metrics
use std::io::{Read, Write};
use std::net::TcpListener;
// Create a TCP listener
let listener = match TcpListener::bind(binding) {
Ok(listener) => {
// Set non-blocking mode to allow for shutdown checking
if let Err(e) = listener.set_nonblocking(true) {
tracing::error!("Failed to set non-blocking mode: {e}");
return;
}
listener
}
Err(e) => {
tracing::error!("Failed to bind TCP listener: {e}");
return;
}
};
tracing::info!("Started Prometheus server on {} at path {}", binding, path);
// Accept connections with shutdown signal handling
loop {
// Check for shutdown signal
if shutdown_rx.try_recv().is_ok() {
tracing::info!("Shutdown signal received, stopping Prometheus server");
break;
}
// Try to accept a connection (non-blocking)
match listener.accept() {
Ok((mut stream, _)) => {
// Handle the connection
let mut buffer = [0; 1024];
match stream.read(&mut buffer) {
Ok(0) => {}
Ok(bytes_read) => {
// Convert the buffer to a string
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
// Check if the request is for our metrics path
if request.contains(&format!("GET {path} HTTP")) {
// Get the metrics
let metrics = metrics_handler();
// Write the response
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{}",
metrics.len(),
metrics
);
if let Err(e) = stream.write_all(response.as_bytes()) {
tracing::error!("Failed to write response: {e}");
}
} else {
// Write a 404 response
let response = "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\n\r\nNot Found";
if let Err(e) = stream.write_all(response.as_bytes()) {
tracing::error!("Failed to write response: {e}");
}
}
}
Err(e) => {
tracing::error!("Failed to read from stream: {e}");
}
}
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
// No connection available, continue the loop
tokio::time::sleep(Duration::from_millis(10)).await;
}
Err(e) => {
tracing::error!("Failed to accept connection: {e}");
// Add a small delay to prevent busy looping on persistent errors
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
tracing::info!("Prometheus server stopped");
});
// Wait for the shutdown signal
shutdown_signal.await;
// Signal the server to shutdown
let _ = shutdown_tx.send(());
// Wait for the server task to complete (with a timeout)
match tokio::time::timeout(Duration::from_secs(5), server_handle).await {
Ok(result) => {
if let Err(e) = result {
tracing::error!("Server task failed: {e}");
}
}
Err(_) => {
tracing::warn!("Server shutdown timed out after 5 seconds");
}
}
Ok(())
}
}
/// Builder for easy Prometheus server setup
#[derive(Debug)]
pub struct PrometheusBuilder {
config: PrometheusConfig,
}
impl PrometheusBuilder {
/// Create a new builder with default configuration
#[must_use]
pub fn new() -> Self {
Self {
config: PrometheusConfig::default(),
}
}
/// Set the bind address
#[must_use]
pub const fn bind_address(mut self, addr: SocketAddr) -> Self {
self.config.bind_address = addr;
self
}
/// Set the metrics path
#[must_use]
pub fn metrics_path<S: Into<String>>(mut self, path: S) -> Self {
self.config.metrics_path = path.into();
self
}
/// Enable or disable system metrics
#[cfg(feature = "system-metrics")]
#[must_use]
pub const fn system_metrics(mut self, enabled: bool) -> Self {
self.config.include_system_metrics = enabled;
self
}
/// Set system metrics update interval
#[cfg(feature = "system-metrics")]
#[must_use]
pub const fn system_metrics_interval(mut self, seconds: u64) -> Self {
self.config.system_metrics_interval = seconds;
self
}
/// Build the server with specific CDK metrics instance
///
/// # Errors
/// Returns an error if system metrics cannot be created (when enabled)
pub fn build_with_cdk_metrics(self) -> crate::Result<PrometheusServer> {
PrometheusServer::new(self.config)
}
/// Build the server with custom registry
#[must_use]
pub fn build_with_registry(self, registry: Arc<Registry>) -> PrometheusServer {
PrometheusServer::with_registry(self.config, registry)
}
}
impl Default for PrometheusBuilder {
fn default() -> Self {
Self::new()
}
}

View File

@@ -16,10 +16,11 @@ default = ["mint", "wallet", "auth"]
mint = ["cdk-common/mint"] mint = ["cdk-common/mint"]
wallet = ["cdk-common/wallet"] wallet = ["cdk-common/wallet"]
auth = ["cdk-common/auth"] auth = ["cdk-common/auth"]
prometheus = ["cdk-prometheus"]
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
cdk-common = { workspace = true, features = ["test"] } cdk-common = { workspace = true, features = ["test"] }
cdk-prometheus = { workspace = true, optional = true }
bitcoin.workspace = true bitcoin.workspace = true
thiserror.workspace = true thiserror.workspace = true
tracing.workspace = true tracing.workspace = true

View File

@@ -57,6 +57,8 @@ mod migrations;
#[cfg(feature = "auth")] #[cfg(feature = "auth")]
pub use auth::SQLMintAuthDatabase; pub use auth::SQLMintAuthDatabase;
#[cfg(feature = "prometheus")]
use cdk_prometheus::METRICS;
/// Mint SQL Database /// Mint SQL Database
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -299,11 +301,27 @@ where
type Err = Error; type Err = Error;
async fn commit(self: Box<Self>) -> Result<(), Error> { async fn commit(self: Box<Self>) -> Result<(), Error> {
self.inner.commit().await let result = self.inner.commit().await;
#[cfg(feature = "prometheus")]
{
let success = result.is_ok();
METRICS.record_mint_operation("transaction_commit", success);
METRICS.record_mint_operation_histogram("transaction_commit", success, 1.0);
}
Ok(result?)
} }
async fn rollback(self: Box<Self>) -> Result<(), Error> { async fn rollback(self: Box<Self>) -> Result<(), Error> {
self.inner.rollback().await let result = self.inner.rollback().await;
#[cfg(feature = "prometheus")]
{
let success = result.is_ok();
METRICS.record_mint_operation("transaction_rollback", success);
METRICS.record_mint_operation_histogram("transaction_rollback", success, 1.0);
}
Ok(result?)
} }
} }
@@ -443,12 +461,14 @@ where
async fn begin_transaction<'a>( async fn begin_transaction<'a>(
&'a self, &'a self,
) -> Result<Box<dyn MintKeyDatabaseTransaction<'a, Error> + Send + Sync + 'a>, Error> { ) -> Result<Box<dyn MintKeyDatabaseTransaction<'a, Error> + Send + Sync + 'a>, Error> {
Ok(Box::new(SQLTransaction { let tx = SQLTransaction {
inner: ConnectionWithTransaction::new( inner: ConnectionWithTransaction::new(
self.pool.get().map_err(|e| Error::Database(Box::new(e)))?, self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
) )
.await?, .await?,
})) };
Ok(Box::new(tx))
} }
async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err> { async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err> {
@@ -1072,35 +1092,58 @@ where
type Err = Error; type Err = Error;
async fn get_mint_quote(&self, quote_id: &QuoteId) -> Result<Option<MintQuote>, Self::Err> { async fn get_mint_quote(&self, quote_id: &QuoteId) -> Result<Option<MintQuote>, Self::Err> {
#[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("get_mint_quote");
#[cfg(feature = "prometheus")]
let start_time = std::time::Instant::now();
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
let payments = get_mint_quote_payments(&*conn, quote_id).await?; let result = async {
let issuance = get_mint_quote_issuance(&*conn, quote_id).await?; let payments = get_mint_quote_payments(&*conn, quote_id).await?;
let issuance = get_mint_quote_issuance(&*conn, quote_id).await?;
Ok(query( query(
r#" r#"
SELECT SELECT
id, id,
amount, amount,
unit, unit,
request, request,
expiry, expiry,
request_lookup_id, request_lookup_id,
pubkey, pubkey,
created_time, created_time,
amount_paid, amount_paid,
amount_issued, amount_issued,
payment_method, payment_method,
request_lookup_id_kind request_lookup_id_kind
FROM FROM
mint_quote mint_quote
WHERE id = :id"#, WHERE id = :id"#,
)? )?
.bind("id", quote_id.to_string()) .bind("id", quote_id.to_string())
.fetch_one(&*conn) .fetch_one(&*conn)
.await? .await?
.map(|row| sql_row_to_mint_quote(row, payments, issuance)) .map(|row| sql_row_to_mint_quote(row, payments, issuance))
.transpose()?) .transpose()
}
.await;
#[cfg(feature = "prometheus")]
{
let success = result.is_ok();
METRICS.record_mint_operation("get_mint_quote", success);
METRICS.record_mint_operation_histogram(
"get_mint_quote",
success,
start_time.elapsed().as_secs_f64(),
);
METRICS.dec_in_flight_requests("get_mint_quote");
}
result
} }
async fn get_mint_quote_by_request( async fn get_mint_quote_by_request(
@@ -1228,35 +1271,59 @@ where
&self, &self,
quote_id: &QuoteId, quote_id: &QuoteId,
) -> Result<Option<mint::MeltQuote>, Self::Err> { ) -> Result<Option<mint::MeltQuote>, Self::Err> {
#[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("get_melt_quote");
#[cfg(feature = "prometheus")]
let start_time = std::time::Instant::now();
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
Ok(query(
r#" let result = async {
SELECT query(
id, r#"
unit, SELECT
amount, id,
request, unit,
fee_reserve, amount,
expiry, request,
state, fee_reserve,
payment_preimage, expiry,
request_lookup_id, state,
created_time, payment_preimage,
paid_time, request_lookup_id,
payment_method, created_time,
options, paid_time,
request_lookup_id_kind payment_method,
FROM options,
melt_quote request_lookup_id_kind
WHERE FROM
id=:id melt_quote
"#, WHERE
)? id=:id
.bind("id", quote_id.to_string()) "#,
.fetch_one(&*conn) )?
.await? .bind("id", quote_id.to_string())
.map(sql_row_to_melt_quote) .fetch_one(&*conn)
.transpose()?) .await?
.map(sql_row_to_melt_quote)
.transpose()
}
.await;
#[cfg(feature = "prometheus")]
{
let success = result.is_ok();
METRICS.record_mint_operation("get_melt_quote", success);
METRICS.record_mint_operation_histogram(
"get_melt_quote",
success,
start_time.elapsed().as_secs_f64(),
);
METRICS.dec_in_flight_requests("get_melt_quote");
}
result
} }
async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> { async fn get_melt_quotes(&self) -> Result<Vec<mint::MeltQuote>, Self::Err> {
@@ -1826,20 +1893,64 @@ where
async fn begin_transaction<'a>( async fn begin_transaction<'a>(
&'a self, &'a self,
) -> Result<Box<dyn database::MintTransaction<'a, Error> + Send + Sync + 'a>, Error> { ) -> Result<Box<dyn database::MintTransaction<'a, Error> + Send + Sync + 'a>, Error> {
Ok(Box::new(SQLTransaction { let tx = SQLTransaction {
inner: ConnectionWithTransaction::new( inner: ConnectionWithTransaction::new(
self.pool.get().map_err(|e| Error::Database(Box::new(e)))?, self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
) )
.await?, .await?,
})) };
Ok(Box::new(tx))
} }
async fn get_mint_info(&self) -> Result<MintInfo, Error> { async fn get_mint_info(&self) -> Result<MintInfo, Error> {
Ok(self.fetch_from_config("mint_info").await?) #[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("get_mint_info");
#[cfg(feature = "prometheus")]
let start_time = std::time::Instant::now();
let result = self.fetch_from_config("mint_info").await;
#[cfg(feature = "prometheus")]
{
let success = result.is_ok();
METRICS.record_mint_operation("get_mint_info", success);
METRICS.record_mint_operation_histogram(
"get_mint_info",
success,
start_time.elapsed().as_secs_f64(),
);
METRICS.dec_in_flight_requests("get_mint_info");
}
Ok(result?)
} }
async fn get_quote_ttl(&self) -> Result<QuoteTTL, Error> { async fn get_quote_ttl(&self) -> Result<QuoteTTL, Error> {
Ok(self.fetch_from_config("quote_ttl").await?) #[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("get_quote_ttl");
#[cfg(feature = "prometheus")]
let start_time = std::time::Instant::now();
let result = self.fetch_from_config("quote_ttl").await;
#[cfg(feature = "prometheus")]
{
let success = result.is_ok();
METRICS.record_mint_operation("get_quote_ttl", success);
METRICS.record_mint_operation_histogram(
"get_quote_ttl",
success,
start_time.elapsed().as_secs_f64(),
);
METRICS.dec_in_flight_requests("get_quote_ttl");
}
Ok(result?)
} }
} }

View File

@@ -6,9 +6,10 @@ use std::fmt::Debug;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration; use std::time::{Duration, Instant};
use tokio::time::Instant; #[cfg(feature = "prometheus")]
use cdk_prometheus::metrics::METRICS;
use crate::database::DatabaseConnector; use crate::database::DatabaseConnector;
@@ -86,6 +87,8 @@ where
{ {
resource: Option<(Arc<AtomicBool>, RM::Connection)>, resource: Option<(Arc<AtomicBool>, RM::Connection)>,
pool: Arc<Pool<RM>>, pool: Arc<Pool<RM>>,
#[cfg(feature = "prometheus")]
start_time: Instant,
} }
impl<RM> Debug for PooledResource<RM> impl<RM> Debug for PooledResource<RM>
@@ -105,7 +108,16 @@ where
if let Some(resource) = self.resource.take() { if let Some(resource) = self.resource.take() {
let mut active_resource = self.pool.queue.lock().expect("active_resource"); let mut active_resource = self.pool.queue.lock().expect("active_resource");
active_resource.push(resource); active_resource.push(resource);
self.pool.in_use.fetch_sub(1, Ordering::AcqRel); let _in_use = self.pool.in_use.fetch_sub(1, Ordering::AcqRel);
#[cfg(feature = "prometheus")]
{
METRICS.set_db_connections_active(_in_use as i64);
let duration = self.start_time.elapsed().as_secs_f64();
METRICS.record_db_operation(duration, "drop");
}
// Notify a waiting thread // Notify a waiting thread
self.pool.waiter.notify_one(); self.pool.waiter.notify_one();
@@ -155,6 +167,18 @@ where
self.get_timeout(self.default_timeout) self.get_timeout(self.default_timeout)
} }
/// Increments the in_use connection counter and updates the metric
fn increment_connection_counter(&self) -> usize {
let in_use = self.in_use.fetch_add(1, Ordering::AcqRel);
#[cfg(feature = "prometheus")]
{
METRICS.set_db_connections_active(in_use as i64);
}
in_use
}
/// Get a new resource or fail after timeout is reached. /// Get a new resource or fail after timeout is reached.
/// ///
/// This function will return a free resource or create a new one if there is still room for it; /// This function will return a free resource or create a new one if there is still room for it;
@@ -171,18 +195,20 @@ where
if let Some((stale, resource)) = resources.pop() { if let Some((stale, resource)) = resources.pop() {
if !stale.load(Ordering::SeqCst) { if !stale.load(Ordering::SeqCst) {
drop(resources); drop(resources);
self.in_use.fetch_add(1, Ordering::AcqRel); self.increment_connection_counter();
return Ok(PooledResource { return Ok(PooledResource {
resource: Some((stale, resource)), resource: Some((stale, resource)),
pool: self.clone(), pool: self.clone(),
#[cfg(feature = "prometheus")]
start_time: Instant::now(),
}); });
} }
} }
if self.in_use.load(Ordering::Relaxed) < self.max_size { if self.in_use.load(Ordering::Relaxed) < self.max_size {
drop(resources); drop(resources);
self.in_use.fetch_add(1, Ordering::AcqRel); self.increment_connection_counter();
let stale: Arc<AtomicBool> = Arc::new(false.into()); let stale: Arc<AtomicBool> = Arc::new(false.into());
return Ok(PooledResource { return Ok(PooledResource {
@@ -191,6 +217,8 @@ where
RM::new_resource(&self.config, stale, timeout)?, RM::new_resource(&self.config, stale, timeout)?,
)), )),
pool: self.clone(), pool: self.clone(),
#[cfg(feature = "prometheus")]
start_time: Instant::now(),
}); });
} }

View File

@@ -17,10 +17,11 @@ mint = ["cdk-common/mint", "cdk-sql-common/mint"]
wallet = ["cdk-common/wallet", "cdk-sql-common/wallet"] wallet = ["cdk-common/wallet", "cdk-sql-common/wallet"]
auth = ["cdk-common/auth", "cdk-sql-common/auth"] auth = ["cdk-common/auth", "cdk-sql-common/auth"]
sqlcipher = ["rusqlite/bundled-sqlcipher"] sqlcipher = ["rusqlite/bundled-sqlcipher"]
prometheus = ["cdk-sql-common/prometheus", "cdk-prometheus"]
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
cdk-common = { workspace = true, features = ["test"] } cdk-common = { workspace = true, features = ["test"] }
cdk-prometheus = { workspace = true, optional = true }
bitcoin.workspace = true bitcoin.workspace = true
cdk-sql-common = { workspace = true } cdk-sql-common = { workspace = true }
rusqlite = { version = "0.31", features = ["bundled"]} rusqlite = { version = "0.31", features = ["bundled"]}

View File

@@ -20,7 +20,7 @@ bip353 = ["dep:trust-dns-resolver"]
swagger = ["mint", "dep:utoipa", "cdk-common/swagger"] swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
bench = [] bench = []
http_subscription = [] http_subscription = []
prometheus = ["dep:cdk-prometheus"]
[dependencies] [dependencies]
cdk-common.workspace = true cdk-common.workspace = true
@@ -44,7 +44,7 @@ utoipa = { workspace = true, optional = true }
uuid.workspace = true uuid.workspace = true
jsonwebtoken = { workspace = true, optional = true } jsonwebtoken = { workspace = true, optional = true }
trust-dns-resolver = { version = "0.23.2", optional = true } trust-dns-resolver = { version = "0.23.2", optional = true }
cdk-prometheus = {workspace = true, optional = true}
# -Z minimal-versions # -Z minimal-versions
sync_wrapper = "0.1.2" sync_wrapper = "0.1.2"
bech32 = "0.9.1" bech32 = "0.9.1"

View File

@@ -268,7 +268,6 @@ impl MintBuilder {
self.payment_processors.insert(key, payment_processor); self.payment_processors.insert(key, payment_processor);
Ok(()) Ok(())
} }
/// Sets the input fee ppk for a given unit /// Sets the input fee ppk for a given unit
/// ///
/// The unit **MUST** already have been added with a ln backend /// The unit **MUST** already have been added with a ln backend

View File

@@ -10,6 +10,8 @@ use cdk_common::{
MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState, MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState,
MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey, MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey,
}; };
#[cfg(feature = "prometheus")]
use cdk_prometheus::METRICS;
use tracing::instrument; use tracing::instrument;
use crate::mint::Verification; use crate::mint::Verification;
@@ -187,130 +189,147 @@ impl Mint {
&self, &self,
mint_quote_request: MintQuoteRequest, mint_quote_request: MintQuoteRequest,
) -> Result<MintQuoteResponse, Error> { ) -> Result<MintQuoteResponse, Error> {
let unit: CurrencyUnit; #[cfg(feature = "prometheus")]
let amount; METRICS.inc_in_flight_requests("get_mint_quote");
let pubkey;
let payment_method;
let create_invoice_response = match mint_quote_request { let result = async {
MintQuoteRequest::Bolt11(bolt11_request) => { let unit: CurrencyUnit;
unit = bolt11_request.unit; let amount;
amount = Some(bolt11_request.amount); let pubkey;
pubkey = bolt11_request.pubkey; let payment_method;
payment_method = PaymentMethod::Bolt11;
self.check_mint_request_acceptable( let create_invoice_response = match mint_quote_request {
Some(bolt11_request.amount), MintQuoteRequest::Bolt11(bolt11_request) => {
&unit, unit = bolt11_request.unit;
&payment_method, amount = Some(bolt11_request.amount);
) pubkey = bolt11_request.pubkey;
.await?; payment_method = PaymentMethod::Bolt11;
let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?; self.check_mint_request_acceptable(
Some(bolt11_request.amount),
let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl; &unit,
&payment_method,
let quote_expiry = unix_time() + mint_ttl; )
let settings = ln.get_settings().await?;
let settings: Bolt11Settings = serde_json::from_value(settings)?;
let description = bolt11_request.description;
if description.is_some() && !settings.invoice_description {
tracing::error!("Backend does not support invoice description");
return Err(Error::InvoiceDescriptionUnsupported);
}
let bolt11_options = Bolt11IncomingPaymentOptions {
description,
amount: bolt11_request.amount,
unix_expiry: Some(quote_expiry),
};
let incoming_options = IncomingPaymentOptions::Bolt11(bolt11_options);
ln.create_incoming_payment_request(&unit, incoming_options)
.await
.map_err(|err| {
tracing::error!("Could not create invoice: {}", err);
Error::InvalidPaymentRequest
})?
}
MintQuoteRequest::Bolt12(bolt12_request) => {
unit = bolt12_request.unit;
amount = bolt12_request.amount;
pubkey = Some(bolt12_request.pubkey);
payment_method = PaymentMethod::Bolt12;
self.check_mint_request_acceptable(amount, &unit, &payment_method)
.await?; .await?;
let ln = self.get_payment_processor(unit.clone(), payment_method.clone())?; let ln = self.get_payment_processor(unit.clone(), PaymentMethod::Bolt11)?;
let description = bolt12_request.description; let mint_ttl = self.localstore.get_quote_ttl().await?.mint_ttl;
let bolt12_options = Bolt12IncomingPaymentOptions { let quote_expiry = unix_time() + mint_ttl;
description,
amount,
unix_expiry: None,
};
let incoming_options = IncomingPaymentOptions::Bolt12(Box::new(bolt12_options)); let settings = ln.get_settings().await?;
let settings: Bolt11Settings = serde_json::from_value(settings)?;
ln.create_incoming_payment_request(&unit, incoming_options) let description = bolt11_request.description;
.await
.map_err(|err| { if description.is_some() && !settings.invoice_description {
tracing::error!("Could not create invoice: {}", err); tracing::error!("Backend does not support invoice description");
Error::InvalidPaymentRequest return Err(Error::InvoiceDescriptionUnsupported);
})? }
let bolt11_options = Bolt11IncomingPaymentOptions {
description,
amount: bolt11_request.amount,
unix_expiry: Some(quote_expiry),
};
let incoming_options = IncomingPaymentOptions::Bolt11(bolt11_options);
ln.create_incoming_payment_request(&unit, incoming_options)
.await
.map_err(|err| {
tracing::error!("Could not create invoice: {}", err);
Error::InvalidPaymentRequest
})?
}
MintQuoteRequest::Bolt12(bolt12_request) => {
unit = bolt12_request.unit;
amount = bolt12_request.amount;
pubkey = Some(bolt12_request.pubkey);
payment_method = PaymentMethod::Bolt12;
self.check_mint_request_acceptable(amount, &unit, &payment_method)
.await?;
let ln = self.get_payment_processor(unit.clone(), payment_method.clone())?;
let description = bolt12_request.description;
let bolt12_options = Bolt12IncomingPaymentOptions {
description,
amount,
unix_expiry: None,
};
let incoming_options = IncomingPaymentOptions::Bolt12(Box::new(bolt12_options));
ln.create_incoming_payment_request(&unit, incoming_options)
.await
.map_err(|err| {
tracing::error!("Could not create invoice: {}", err);
Error::InvalidPaymentRequest
})?
}
};
let quote = MintQuote::new(
None,
create_invoice_response.request.to_string(),
unit.clone(),
amount,
create_invoice_response.expiry.unwrap_or(0),
create_invoice_response.request_lookup_id.clone(),
pubkey,
Amount::ZERO,
Amount::ZERO,
payment_method.clone(),
unix_time(),
vec![],
vec![],
);
tracing::debug!(
"New {} mint quote {} for {:?} {} with request id {:?}",
payment_method,
quote.id,
amount,
unit,
create_invoice_response.request_lookup_id.to_string(),
);
let mut tx = self.localstore.begin_transaction().await?;
tx.add_mint_quote(quote.clone()).await?;
tx.commit().await?;
match payment_method {
PaymentMethod::Bolt11 => {
let res: MintQuoteBolt11Response<QuoteId> = quote.clone().into();
self.pubsub_manager
.broadcast(NotificationPayload::MintQuoteBolt11Response(res));
}
PaymentMethod::Bolt12 => {
let res: MintQuoteBolt12Response<QuoteId> = quote.clone().try_into()?;
self.pubsub_manager
.broadcast(NotificationPayload::MintQuoteBolt12Response(res));
}
PaymentMethod::Custom(_) => {}
} }
};
let quote = MintQuote::new( quote.try_into()
None, }
create_invoice_response.request.to_string(), .await;
unit.clone(),
amount,
create_invoice_response.expiry.unwrap_or(0),
create_invoice_response.request_lookup_id.clone(),
pubkey,
Amount::ZERO,
Amount::ZERO,
payment_method.clone(),
unix_time(),
vec![],
vec![],
);
tracing::debug!( #[cfg(feature = "prometheus")]
"New {} mint quote {} for {:?} {} with request id {:?}", {
payment_method, METRICS.dec_in_flight_requests("get_mint_quote");
quote.id, METRICS.record_mint_operation("get_mint_quote", result.is_ok());
amount, if result.is_err() {
unit, METRICS.record_error();
create_invoice_response.request_lookup_id.to_string(),
);
let mut tx = self.localstore.begin_transaction().await?;
tx.add_mint_quote(quote.clone()).await?;
tx.commit().await?;
match payment_method {
PaymentMethod::Bolt11 => {
let res: MintQuoteBolt11Response<QuoteId> = quote.clone().into();
self.pubsub_manager
.broadcast(NotificationPayload::MintQuoteBolt11Response(res));
} }
PaymentMethod::Bolt12 => {
let res: MintQuoteBolt12Response<QuoteId> = quote.clone().try_into()?;
self.pubsub_manager
.broadcast(NotificationPayload::MintQuoteBolt12Response(res));
}
PaymentMethod::Custom(_) => {}
} }
quote.try_into() result
} }
/// Retrieves all mint quotes from the database /// Retrieves all mint quotes from the database
@@ -320,8 +339,25 @@ impl Mint {
/// * `Error` if database access fails /// * `Error` if database access fails
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> { pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
let quotes = self.localstore.get_mint_quotes().await?; #[cfg(feature = "prometheus")]
Ok(quotes) METRICS.inc_in_flight_requests("mint_quotes");
let result = async {
let quotes = self.localstore.get_mint_quotes().await?;
Ok(quotes)
}
.await;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("mint_quotes");
METRICS.record_mint_operation("mint_quotes", result.is_ok());
if result.is_err() {
METRICS.record_error();
}
}
result
} }
/// Removes a mint quote from the database /// Removes a mint quote from the database
@@ -334,11 +370,27 @@ impl Mint {
/// * `Error` if the quote doesn't exist or removal fails /// * `Error` if the quote doesn't exist or removal fails
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn remove_mint_quote(&self, quote_id: &QuoteId) -> Result<(), Error> { pub async fn remove_mint_quote(&self, quote_id: &QuoteId) -> Result<(), Error> {
let mut tx = self.localstore.begin_transaction().await?; #[cfg(feature = "prometheus")]
tx.remove_mint_quote(quote_id).await?; METRICS.inc_in_flight_requests("remove_mint_quote");
tx.commit().await?;
Ok(()) let result = async {
let mut tx = self.localstore.begin_transaction().await?;
tx.remove_mint_quote(quote_id).await?;
tx.commit().await?;
Ok(())
}
.await;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("remove_mint_quote");
METRICS.record_mint_operation("remove_mint_quote", result.is_ok());
if result.is_err() {
METRICS.record_error();
}
}
result
} }
/// Marks a mint quote as paid based on the payment request ID /// Marks a mint quote as paid based on the payment request ID
@@ -357,33 +409,48 @@ impl Mint {
&self, &self,
wait_payment_response: WaitPaymentResponse, wait_payment_response: WaitPaymentResponse,
) -> Result<(), Error> { ) -> Result<(), Error> {
if wait_payment_response.payment_amount == Amount::ZERO { #[cfg(feature = "prometheus")]
tracing::warn!( METRICS.inc_in_flight_requests("pay_mint_quote_for_request_id");
"Received payment response with 0 amount with payment id {}.", let result = async {
wait_payment_response.payment_id.to_string() if wait_payment_response.payment_amount == Amount::ZERO {
); tracing::warn!(
"Received payment response with 0 amount with payment id {}.",
wait_payment_response.payment_id.to_string()
);
return Err(Error::AmountUndefined);
}
return Err(Error::AmountUndefined); let mut tx = self.localstore.begin_transaction().await?;
if let Ok(Some(mint_quote)) = tx
.get_mint_quote_by_request_lookup_id(&wait_payment_response.payment_identifier)
.await
{
self.pay_mint_quote(&mut tx, &mint_quote, wait_payment_response)
.await?;
} else {
tracing::warn!(
"Could not get request for request lookup id {:?}.",
wait_payment_response.payment_identifier
);
}
tx.commit().await?;
Ok(())
} }
.await;
let mut tx = self.localstore.begin_transaction().await?; #[cfg(feature = "prometheus")]
if let Ok(Some(mint_quote)) = tx
.get_mint_quote_by_request_lookup_id(&wait_payment_response.payment_identifier)
.await
{ {
self.pay_mint_quote(&mut tx, &mint_quote, wait_payment_response) METRICS.dec_in_flight_requests("pay_mint_quote_for_request_id");
.await?; METRICS.record_mint_operation("pay_mint_quote_for_request_id", result.is_ok());
} else { if result.is_err() {
tracing::warn!( METRICS.record_error();
"Could not get request for request lookup id {:?}.", }
wait_payment_response.payment_identifier
);
} }
tx.commit().await?; result
Ok(())
} }
/// Marks a specific mint quote as paid /// Marks a specific mint quote as paid
@@ -405,8 +472,30 @@ impl Mint {
mint_quote: &MintQuote, mint_quote: &MintQuote,
wait_payment_response: WaitPaymentResponse, wait_payment_response: WaitPaymentResponse,
) -> Result<(), Error> { ) -> Result<(), Error> {
Self::handle_mint_quote_payment(tx, mint_quote, wait_payment_response, &self.pubsub_manager) #[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("pay_mint_quote");
let result = async {
Self::handle_mint_quote_payment(
tx,
mint_quote,
wait_payment_response,
&self.pubsub_manager,
)
.await .await
}
.await;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("pay_mint_quote");
METRICS.record_mint_operation("pay_mint_quote", result.is_ok());
if result.is_err() {
METRICS.record_error();
}
}
result
} }
/// Checks the status of a mint quote and updates it if necessary /// Checks the status of a mint quote and updates it if necessary
@@ -422,17 +511,33 @@ impl Mint {
/// * `Error` if the quote doesn't exist or checking fails /// * `Error` if the quote doesn't exist or checking fails
#[instrument(skip(self))] #[instrument(skip(self))]
pub async fn check_mint_quote(&self, quote_id: &QuoteId) -> Result<MintQuoteResponse, Error> { pub async fn check_mint_quote(&self, quote_id: &QuoteId) -> Result<MintQuoteResponse, Error> {
let mut quote = self #[cfg(feature = "prometheus")]
.localstore METRICS.inc_in_flight_requests("check_mint_quote");
.get_mint_quote(quote_id) let result = async {
.await? let mut quote = self
.ok_or(Error::UnknownQuote)?; .localstore
.get_mint_quote(quote_id)
.await?
.ok_or(Error::UnknownQuote)?;
if quote.payment_method == PaymentMethod::Bolt11 { if quote.payment_method == PaymentMethod::Bolt11 {
self.check_mint_quote_paid(&mut quote).await?; self.check_mint_quote_paid(&mut quote).await?;
}
quote.try_into()
}
.await;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("check_mint_quote");
METRICS.record_mint_operation("check_mint_quote", result.is_ok());
if result.is_err() {
METRICS.record_error();
}
} }
quote.try_into() result
} }
/// Processes a mint request to issue new tokens /// Processes a mint request to issue new tokens
@@ -456,15 +561,18 @@ impl Mint {
&self, &self,
mint_request: MintRequest<QuoteId>, mint_request: MintRequest<QuoteId>,
) -> Result<MintResponse, Error> { ) -> Result<MintResponse, Error> {
let mut mint_quote = self #[cfg(feature = "prometheus")]
.localstore METRICS.inc_in_flight_requests("process_mint_request");
.get_mint_quote(&mint_request.quote) let result = async {
.await? let mut mint_quote = self
.ok_or(Error::UnknownQuote)?; .localstore
if mint_quote.payment_method == PaymentMethod::Bolt11 { .get_mint_quote(&mint_request.quote)
self.check_mint_quote_paid(&mut mint_quote).await?; .await?
} .ok_or(Error::UnknownQuote)?;
if mint_quote.payment_method == PaymentMethod::Bolt11 {
self.check_mint_quote_paid(&mut mint_quote).await?;
}
// get the blind signatures before having starting the db transaction, if there are any // get the blind signatures before having starting the db transaction, if there are any
// rollbacks this blind_signatures will be lost, and the signature is stateless. It is not a // rollbacks this blind_signatures will be lost, and the signature is stateless. It is not a
// good idea to call an external service (which is really a trait, it could be anything // good idea to call an external service (which is really a trait, it could be anything
@@ -504,10 +612,10 @@ impl Mint {
PaymentMethod::Bolt12 => { PaymentMethod::Bolt12 => {
if mint_quote.amount_issued() > mint_quote.amount_paid() { if mint_quote.amount_issued() > mint_quote.amount_paid() {
tracing::error!( tracing::error!(
"Quote state should not be issued if issued {} is > paid {}.", "Quote state should not be issued if issued {} is > paid {}.",
mint_quote.amount_issued(), mint_quote.amount_issued(),
mint_quote.amount_paid() mint_quote.amount_paid()
); );
return Err(Error::UnpaidQuote); return Err(Error::UnpaidQuote);
} }
mint_quote.amount_paid() - mint_quote.amount_issued() mint_quote.amount_paid() - mint_quote.amount_issued()
@@ -565,7 +673,7 @@ impl Mint {
&blind_signatures, &blind_signatures,
Some(mint_request.quote.clone()), Some(mint_request.quote.clone()),
) )
.await?; .await?;
let amount_issued = mint_request.total_amount()?; let amount_issued = mint_request.total_amount()?;
@@ -582,4 +690,16 @@ impl Mint {
signatures: blind_signatures, signatures: blind_signatures,
}) })
} }
.await;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("process_mint_request");
METRICS.record_mint_operation("process_mint_request", result.is_ok());
if result.is_err() {
METRICS.record_error();
}
}
result
}
} }

View File

@@ -13,6 +13,8 @@ use cdk_common::payment::{
}; };
use cdk_common::quote_id::QuoteId; use cdk_common::quote_id::QuoteId;
use cdk_common::{MeltOptions, MeltQuoteBolt12Request}; use cdk_common::{MeltOptions, MeltQuoteBolt12Request};
#[cfg(feature = "prometheus")]
use cdk_prometheus::METRICS;
use lightning::offers::offer::Offer; use lightning::offers::offer::Offer;
use tracing::instrument; use tracing::instrument;
@@ -131,6 +133,8 @@ impl Mint {
&self, &self,
melt_request: &MeltQuoteBolt11Request, melt_request: &MeltQuoteBolt11Request,
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> { ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
#[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("get_melt_bolt11_quote");
let MeltQuoteBolt11Request { let MeltQuoteBolt11Request {
request, request,
unit, unit,
@@ -183,6 +187,12 @@ impl Mint {
err err
); );
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("get_melt_bolt11_quote");
METRICS.record_mint_operation("get_melt_bolt11_quote", false);
METRICS.record_error();
}
Error::UnsupportedUnit Error::UnsupportedUnit
})?; })?;
@@ -315,6 +325,12 @@ impl Mint {
tx.add_melt_quote(quote.clone()).await?; tx.add_melt_quote(quote.clone()).await?;
tx.commit().await?; tx.commit().await?;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("get_melt_bolt11_quote");
METRICS.record_mint_operation("get_melt_bolt11_quote", true);
}
Ok(quote.into()) Ok(quote.into())
} }
@@ -324,20 +340,50 @@ impl Mint {
&self, &self,
quote_id: &QuoteId, quote_id: &QuoteId,
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> { ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
let quote = self #[cfg(feature = "prometheus")]
.localstore METRICS.inc_in_flight_requests("check_melt_quote");
.get_melt_quote(quote_id) let quote = match self.localstore.get_melt_quote(quote_id).await {
.await? Ok(Some(quote)) => quote,
.ok_or(Error::UnknownQuote)?; Ok(None) => {
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("check_melt_quote");
METRICS.record_mint_operation("check_melt_quote", false);
METRICS.record_error();
}
return Err(Error::UnknownQuote);
}
Err(err) => {
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("check_melt_quote");
METRICS.record_mint_operation("check_melt_quote", false);
METRICS.record_error();
}
return Err(err.into());
}
};
let blind_signatures = self let blind_signatures = match self
.localstore .localstore
.get_blind_signatures_for_quote(quote_id) .get_blind_signatures_for_quote(quote_id)
.await?; .await
{
Ok(signatures) => signatures,
Err(err) => {
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("check_melt_quote");
METRICS.record_mint_operation("check_melt_quote", false);
METRICS.record_error();
}
return Err(err.into());
}
};
let change = (!blind_signatures.is_empty()).then_some(blind_signatures); let change = (!blind_signatures.is_empty()).then_some(blind_signatures);
Ok(MeltQuoteBolt11Response { let response = MeltQuoteBolt11Response {
quote: quote.id, quote: quote.id,
paid: Some(quote.state == MeltQuoteState::Paid), paid: Some(quote.state == MeltQuoteState::Paid),
state: quote.state, state: quote.state,
@@ -348,7 +394,15 @@ impl Mint {
change, change,
request: Some(quote.request.to_string()), request: Some(quote.request.to_string()),
unit: Some(quote.unit.clone()), unit: Some(quote.unit.clone()),
}) };
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("check_melt_quote");
METRICS.record_mint_operation("check_melt_quote", true);
}
Ok(response)
} }
/// Get melt quotes /// Get melt quotes
@@ -520,6 +574,9 @@ impl Mint {
&self, &self,
melt_request: &MeltRequest<QuoteId>, melt_request: &MeltRequest<QuoteId>,
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> { ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
#[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("melt_bolt11");
use std::sync::Arc; use std::sync::Arc;
async fn check_payment_state( async fn check_payment_state(
ln: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>, ln: Arc<dyn MintPayment<Err = cdk_payment::Error> + Send + Sync>,
@@ -543,21 +600,43 @@ impl Mint {
let mut tx = self.localstore.begin_transaction().await?; let mut tx = self.localstore.begin_transaction().await?;
let (proof_writer, quote) = self let (proof_writer, quote) = match self
.verify_melt_request(&mut tx, verification, melt_request) .verify_melt_request(&mut tx, verification, melt_request)
.await .await
.map_err(|err| { {
Ok(result) => result,
Err(err) => {
tracing::debug!("Error attempting to verify melt quote: {}", err); tracing::debug!("Error attempting to verify melt quote: {}", err);
err
})?;
let settled_internally_amount = self #[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("melt_bolt11");
METRICS.record_mint_operation("melt_bolt11", false);
METRICS.record_error();
}
return Err(err);
}
};
let settled_internally_amount = match self
.handle_internal_melt_mint(&mut tx, &quote, melt_request) .handle_internal_melt_mint(&mut tx, &quote, melt_request)
.await .await
.map_err(|err| { {
Ok(amount) => amount,
Err(err) => {
tracing::error!("Attempting to settle internally failed: {}", err); tracing::error!("Attempting to settle internally failed: {}", err);
err
})?; #[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("melt_bolt11");
METRICS.record_mint_operation("melt_bolt11", false);
METRICS.record_error();
}
return Err(err);
}
};
let (tx, preimage, amount_spent_quote_unit, quote) = match settled_internally_amount { let (tx, preimage, amount_spent_quote_unit, quote) = match settled_internally_amount {
Some(amount_spent) => (tx, None, amount_spent, quote), Some(amount_spent) => (tx, None, amount_spent, quote),
@@ -669,6 +748,14 @@ impl Mint {
melt_request.quote() melt_request.quote()
); );
proof_writer.rollback().await?; proof_writer.rollback().await?;
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("melt_bolt11");
METRICS.record_mint_operation("melt_bolt11", false);
METRICS.record_error();
}
return Err(Error::PaymentFailed); return Err(Error::PaymentFailed);
} }
MeltQuoteState::Pending => { MeltQuoteState::Pending => {
@@ -677,6 +764,13 @@ impl Mint {
melt_request.quote() melt_request.quote()
); );
proof_writer.commit(); proof_writer.commit();
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("melt_bolt11");
METRICS.record_mint_operation("melt_bolt11", false);
METRICS.record_error();
}
return Err(Error::PendingQuote); return Err(Error::PendingQuote);
} }
} }
@@ -716,7 +810,7 @@ impl Mint {
// If we made it here the payment has been made. // If we made it here the payment has been made.
// We process the melt burning the inputs and returning change // We process the melt burning the inputs and returning change
let res = self let res = match self
.process_melt_request( .process_melt_request(
tx, tx,
proof_writer, proof_writer,
@@ -726,10 +820,27 @@ impl Mint {
amount_spent_quote_unit, amount_spent_quote_unit,
) )
.await .await
.map_err(|err| { {
Ok(response) => response,
Err(err) => {
tracing::error!("Could not process melt request: {}", err); tracing::error!("Could not process melt request: {}", err);
err
})?; #[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("melt_bolt11");
METRICS.record_mint_operation("melt_bolt11", false);
METRICS.record_error();
}
return Err(err);
}
};
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("melt_bolt11");
METRICS.record_mint_operation("melt_bolt11", true);
}
Ok(res) Ok(res)
} }
@@ -747,11 +858,20 @@ impl Mint {
payment_preimage: Option<String>, payment_preimage: Option<String>,
total_spent: Amount, total_spent: Amount,
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> { ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
#[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("process_melt_request");
let input_ys = melt_request.inputs().ys()?; let input_ys = melt_request.inputs().ys()?;
proof_writer let update_proof_states_result = proof_writer
.update_proofs_states(&mut tx, &input_ys, State::Spent) .update_proofs_states(&mut tx, &input_ys, State::Spent)
.await?; .await;
if update_proof_states_result.is_err() {
#[cfg(feature = "prometheus")]
self.record_melt_quote_failure("process_melt_request");
return Err(update_proof_states_result.err().unwrap());
}
tx.update_melt_quote_state( tx.update_melt_quote_state(
melt_request.quote(), melt_request.quote(),
@@ -853,7 +973,6 @@ impl Mint {
change.clone(), change.clone(),
MeltQuoteState::Paid, MeltQuoteState::Paid,
); );
tracing::debug!( tracing::debug!(
"Melt for quote {} completed total spent {}, total inputs: {}, change given: {}", "Melt for quote {} completed total spent {}, total inputs: {}, change given: {}",
quote.id, quote.id,
@@ -865,8 +984,7 @@ impl Mint {
.expect("Change cannot overflow")) .expect("Change cannot overflow"))
.unwrap_or_default() .unwrap_or_default()
); );
let response = MeltQuoteBolt11Response {
Ok(MeltQuoteBolt11Response {
amount: quote.amount, amount: quote.amount,
paid: Some(true), paid: Some(true),
payment_preimage, payment_preimage,
@@ -877,6 +995,21 @@ impl Mint {
expiry: quote.expiry, expiry: quote.expiry,
request: Some(quote.request.to_string()), request: Some(quote.request.to_string()),
unit: Some(quote.unit.clone()), unit: Some(quote.unit.clone()),
}) };
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("process_melt_request");
METRICS.record_mint_operation("process_melt_request", true);
}
Ok(response)
}
#[cfg(feature = "prometheus")]
fn record_melt_quote_failure(&self, operation: &str) {
METRICS.dec_in_flight_requests(operation);
METRICS.record_mint_operation(operation, false);
METRICS.record_error();
} }
} }

View File

@@ -14,6 +14,8 @@ use cdk_common::nuts::{self, BlindSignature, BlindedMessage, CurrencyUnit, Id, K
use cdk_common::payment::WaitPaymentResponse; use cdk_common::payment::WaitPaymentResponse;
pub use cdk_common::quote_id::QuoteId; pub use cdk_common::quote_id::QuoteId;
use cdk_common::secret; use cdk_common::secret;
#[cfg(feature = "prometheus")]
use cdk_prometheus::global;
use cdk_signatory::signatory::{Signatory, SignatoryKeySet}; use cdk_signatory::signatory::{Signatory, SignatoryKeySet};
use futures::StreamExt; use futures::StreamExt;
#[cfg(feature = "auth")] #[cfg(feature = "auth")]
@@ -724,41 +726,66 @@ impl Mint {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn blind_sign( pub async fn blind_sign(
&self, &self,
blinded_messages: Vec<BlindedMessage>, blinded_message: Vec<BlindedMessage>,
) -> Result<Vec<BlindSignature>, Error> { ) -> Result<Vec<BlindSignature>, Error> {
self.signatory.blind_sign(blinded_messages).await #[cfg(feature = "prometheus")]
global::inc_in_flight_requests("blind_sign");
let result = self.signatory.blind_sign(blinded_message).await;
#[cfg(feature = "prometheus")]
{
global::dec_in_flight_requests("blind_sign");
global::record_mint_operation("blind_sign", result.is_ok());
}
result
} }
/// Verify [`Proof`] meets conditions and is signed /// Verify [`Proof`] meets conditions and is signed
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn verify_proofs(&self, proofs: Proofs) -> Result<(), Error> { pub async fn verify_proofs(&self, proofs: Proofs) -> Result<(), Error> {
proofs #[cfg(feature = "prometheus")]
.iter() global::inc_in_flight_requests("verify_proofs");
.map(|proof| {
// Check if secret is a nut10 secret with conditions let result = async {
if let Ok(secret) = proofs
<&secret::Secret as TryInto<nuts::nut10::Secret>>::try_into(&proof.secret) .iter()
{ .map(|proof| {
// Checks and verifies known secret kinds. // Check if secret is a nut10 secret with conditions
// If it is an unknown secret kind it will be treated as a normal secret. if let Ok(secret) =
// Spending conditions will **not** be check. It is up to the wallet to ensure <&secret::Secret as TryInto<nuts::nut10::Secret>>::try_into(&proof.secret)
// only supported secret kinds are used as there is no way for the mint to {
// enforce only signing supported secrets as they are blinded at // Checks and verifies known secret kinds.
// that point. // If it is an unknown secret kind it will be treated as a normal secret.
match secret.kind() { // Spending conditions will **not** be check. It is up to the wallet to ensure
Kind::P2PK => { // only supported secret kinds are used as there is no way for the mint to
proof.verify_p2pk()?; // enforce only signing supported secrets as they are blinded at
} // that point.
Kind::HTLC => { match secret.kind() {
proof.verify_htlc()?; Kind::P2PK => {
proof.verify_p2pk()?;
}
Kind::HTLC => {
proof.verify_htlc()?;
}
} }
} }
} Ok(())
Ok(()) })
}) .collect::<Result<Vec<()>, Error>>()?;
.collect::<Result<Vec<()>, Error>>()?;
self.signatory.verify_proofs(proofs).await self.signatory.verify_proofs(proofs).await
}
.await;
#[cfg(feature = "prometheus")]
{
global::dec_in_flight_requests("verify_proofs");
global::record_mint_operation("verify_proofs", result.is_ok());
}
result
} }
/// Verify melt request is valid /// Verify melt request is valid
@@ -836,61 +863,92 @@ impl Mint {
/// Restore /// Restore
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> { pub async fn restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
let output_len = request.outputs.len(); #[cfg(feature = "prometheus")]
global::inc_in_flight_requests("restore");
let mut outputs = Vec::with_capacity(output_len); let result = async {
let mut signatures = Vec::with_capacity(output_len); let output_len = request.outputs.len();
let blinded_message: Vec<PublicKey> = let mut outputs = Vec::with_capacity(output_len);
request.outputs.iter().map(|b| b.blinded_secret).collect(); let mut signatures = Vec::with_capacity(output_len);
let blinded_signatures = self let blinded_message: Vec<PublicKey> =
.localstore request.outputs.iter().map(|b| b.blinded_secret).collect();
.get_blind_signatures(&blinded_message)
.await?;
assert_eq!(blinded_signatures.len(), output_len); let blinded_signatures = self
.localstore
.get_blind_signatures(&blinded_message)
.await?;
for (blinded_message, blinded_signature) in assert_eq!(blinded_signatures.len(), output_len);
request.outputs.into_iter().zip(blinded_signatures)
{ for (blinded_message, blinded_signature) in
if let Some(blinded_signature) = blinded_signature { request.outputs.into_iter().zip(blinded_signatures)
outputs.push(blinded_message); {
signatures.push(blinded_signature); if let Some(blinded_signature) = blinded_signature {
outputs.push(blinded_message);
signatures.push(blinded_signature);
}
} }
Ok(RestoreResponse {
outputs,
signatures: signatures.clone(),
promises: Some(signatures),
})
}
.await;
#[cfg(feature = "prometheus")]
{
global::dec_in_flight_requests("restore");
global::record_mint_operation("restore", result.is_ok());
} }
Ok(RestoreResponse { result
outputs,
signatures: signatures.clone(),
promises: Some(signatures),
})
} }
/// Get the total amount issed by keyset /// Get the total amount issed by keyset
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn total_issued(&self) -> Result<HashMap<Id, Amount>, Error> { pub async fn total_issued(&self) -> Result<HashMap<Id, Amount>, Error> {
let keysets = self.keysets().keysets; #[cfg(feature = "prometheus")]
global::inc_in_flight_requests("total_issued");
let mut total_issued = HashMap::new(); let result = async {
let keysets = self.keysets().keysets;
for keyset in keysets { let mut total_issued = HashMap::new();
let blinded = self
.localstore
.get_blind_signatures_for_keyset(&keyset.id)
.await?;
let total = Amount::try_sum(blinded.iter().map(|b| b.amount))?; for keyset in keysets {
let blinded = self
.localstore
.get_blind_signatures_for_keyset(&keyset.id)
.await?;
total_issued.insert(keyset.id, total); let total = Amount::try_sum(blinded.iter().map(|b| b.amount))?;
total_issued.insert(keyset.id, total);
}
Ok(total_issued)
}
.await;
#[cfg(feature = "prometheus")]
{
global::dec_in_flight_requests("total_issued");
global::record_mint_operation("total_issued", result.is_ok());
} }
Ok(total_issued) result
} }
/// Total redeemed for keyset /// Total redeemed for keyset
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn total_redeemed(&self) -> Result<HashMap<Id, Amount>, Error> { pub async fn total_redeemed(&self) -> Result<HashMap<Id, Amount>, Error> {
#[cfg(feature = "prometheus")]
global::inc_in_flight_requests("total_redeemed");
let keysets = self.signatory.keysets().await?; let keysets = self.signatory.keysets().await?;
let mut total_redeemed = HashMap::new(); let mut total_redeemed = HashMap::new();
@@ -909,6 +967,9 @@ impl Mint {
total_redeemed.insert(keyset.id, total_spent); total_redeemed.insert(keyset.id, total_spent);
} }
#[cfg(feature = "prometheus")]
global::dec_in_flight_requests("total_redeemed");
Ok(total_redeemed) Ok(total_redeemed)
} }
} }

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "prometheus")]
use cdk_prometheus::METRICS;
use tracing::instrument; use tracing::instrument;
use super::nut11::{enforce_sig_flag, EnforceSigFlag}; use super::nut11::{enforce_sig_flag, EnforceSigFlag};
@@ -12,6 +14,8 @@ impl Mint {
&self, &self,
swap_request: SwapRequest, swap_request: SwapRequest,
) -> Result<SwapResponse, Error> { ) -> Result<SwapResponse, Error> {
#[cfg(feature = "prometheus")]
METRICS.inc_in_flight_requests("process_swap_request");
// Do the external call before beginning the db transaction // Do the external call before beginning the db transaction
// Check any overflow before talking to the signatory // Check any overflow before talking to the signatory
swap_request.input_amount()?; swap_request.input_amount()?;
@@ -25,7 +29,6 @@ impl Mint {
tracing::debug!("Input verification failed: {:?}", err); tracing::debug!("Input verification failed: {:?}", err);
err err
})?; })?;
let mut tx = self.localstore.begin_transaction().await?; let mut tx = self.localstore.begin_transaction().await?;
if let Err(err) = self if let Err(err) = self
@@ -38,20 +41,50 @@ impl Mint {
.await .await
{ {
tracing::debug!("Attempt to swap unbalanced transaction, aborting: {err}"); tracing::debug!("Attempt to swap unbalanced transaction, aborting: {err}");
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("process_swap_request");
METRICS.record_mint_operation("process_swap_request", false);
METRICS.record_error();
}
return Err(err); return Err(err);
}; };
self.validate_sig_flag(&swap_request).await?; let validate_sig_result = self.validate_sig_flag(&swap_request).await;
if validate_sig_result.is_err() {
#[cfg(feature = "prometheus")]
self.record_swap_failure("process_swap_request");
return Err(validate_sig_result.err().unwrap());
}
let mut proof_writer = let mut proof_writer =
ProofWriter::new(self.localstore.clone(), self.pubsub_manager.clone()); ProofWriter::new(self.localstore.clone(), self.pubsub_manager.clone());
let input_ys = proof_writer let input_ys = match proof_writer
.add_proofs(&mut tx, swap_request.inputs()) .add_proofs(&mut tx, swap_request.inputs())
.await?; .await
{
Ok(ys) => ys,
Err(err) => {
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("process_swap_request");
METRICS.record_mint_operation("process_swap_request", false);
METRICS.record_error();
}
return Err(err);
}
};
proof_writer let update_proof_states_result = proof_writer
.update_proofs_states(&mut tx, &input_ys, State::Spent) .update_proofs_states(&mut tx, &input_ys, State::Spent)
.await?; .await;
if update_proof_states_result.is_err() {
#[cfg(feature = "prometheus")]
self.record_swap_failure("process_swap_request");
return Err(update_proof_states_result.err().unwrap());
}
tx.add_blind_signatures( tx.add_blind_signatures(
&swap_request &swap_request
@@ -67,7 +100,15 @@ impl Mint {
proof_writer.commit(); proof_writer.commit();
tx.commit().await?; tx.commit().await?;
Ok(SwapResponse::new(promises)) let response = SwapResponse::new(promises);
#[cfg(feature = "prometheus")]
{
METRICS.dec_in_flight_requests("process_swap_request");
METRICS.record_mint_operation("process_swap_request", true);
}
Ok(response)
} }
async fn validate_sig_flag(&self, swap_request: &SwapRequest) -> Result<(), Error> { async fn validate_sig_flag(&self, swap_request: &SwapRequest) -> Result<(), Error> {
@@ -79,4 +120,10 @@ impl Mint {
Ok(()) Ok(())
} }
#[cfg(feature = "prometheus")]
fn record_swap_failure(&self, operation: &str) {
METRICS.dec_in_flight_requests(operation);
METRICS.record_mint_operation(operation, false);
METRICS.record_error();
}
} }

View File

@@ -1,5 +1,40 @@
version: '3.8'
services: services:
# CDK Mint service # CDK Mint service
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./misc/provisioning/prometheus.yml:/etc/prometheus/prometheus.yml:ro
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--web.enable-lifecycle'
- '--enable-feature=otlp-write-receiver'
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- cdk
# Grafana for visualization
grafana:
image: grafana/grafana:latest
ports:
- "3011:3000"
volumes:
- ./misc/provisioning/datasources:/etc/grafana/provisioning/datasources
- ./misc/provisioning/dashboards:/etc/grafana/provisioning/dashboards
environment:
- GF_DASHBOARDS_JSON_ENABLED=true
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_PROVISIONING_PATHS=/etc/grafana/provisioning
networks:
- cdk
mintd: mintd:
build: build:
context: . context: .
@@ -16,7 +51,7 @@ services:
# Database configuration - choose one: # Database configuration - choose one:
# Option 1: SQLite (embedded, no additional setup needed) # Option 1: SQLite (embedded, no additional setup needed)
- CDK_MINTD_DATABASE=sqlite - CDK_MINTD_DATABASE=sqlite
# Option 2: ReDB (embedded, no additional setup needed) # Option 2: ReDB (embedded, no additional setup needed)
# - CDK_MINTD_DATABASE=redb # - CDK_MINTD_DATABASE=redb
# Option 3: PostgreSQL (requires postgres service, enable with: docker-compose --profile postgres up) # Option 3: PostgreSQL (requires postgres service, enable with: docker-compose --profile postgres up)
# - CDK_MINTD_DATABASE=postgres # - CDK_MINTD_DATABASE=postgres
@@ -24,9 +59,17 @@ services:
# Cache configuration # Cache configuration
- CDK_MINTD_CACHE_BACKEND=memory - CDK_MINTD_CACHE_BACKEND=memory
# For Redis cache (requires redis service, enable with: docker-compose --profile redis up): # For Redis cache (requires redis service, enable with: docker-compose --profile redis up):
# - CDK_MINTD_CACHE_REDIS_URL=redis://redis:6379 # - CDK_MINTD_CACHE_REDIS_URL=redis://redis:6379
# - CDK_MINTD_CACHE_REDIS_KEY_PREFIX=cdk-mintd # - CDK_MINTD_CACHE_REDIS_KEY_PREFIX=cdk-mintd
- CDK_MINTD_PROMETHEUS_ENABLED=true
- CDK_MINTD_PROMETHEUS_ADDRESS=0.0.0.0
- CDK_MINTD_PROMETHEUS_PORT=9000
command: ["cdk-mintd"] command: ["cdk-mintd"]
depends_on:
- prometheus
- grafana
networks:
- cdk
# Uncomment when using PostgreSQL: # Uncomment when using PostgreSQL:
# depends_on: # depends_on:
# - postgres # - postgres
@@ -78,3 +121,9 @@ volumes:
driver: local driver: local
# redis_data: # redis_data:
# driver: local # driver: local
networks:
cdk:
driver: bridge

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
apiVersion: 1
providers:
- name: 'default'
type: file
disableDeletion: false
updateIntervalSeconds: 10
options:
path: /etc/grafana/provisioning/dashboards

View File

@@ -0,0 +1,8 @@
apiVersion: 1
datasources:
- name: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true

View File

@@ -0,0 +1,8 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['host.docker.internal:9000','mintd:9000']