mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 05:35:18 +01:00
Prometheus crate (#883)
* feat: introduce `cdk-prometheus` crate with Prometheus server and CDK-specific metrics support
This commit is contained in:
@@ -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-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-prometheus = { path = "./crates/cdk-prometheus", version = "=0.12.0", default-features = false }
|
||||
clap = { version = "4.5.31", features = ["derive"] }
|
||||
ciborium = { version = "0.2.2", default-features = false, features = ["std"] }
|
||||
cbor-diag = "0.1.12"
|
||||
@@ -108,6 +109,8 @@ tonic-build = "0.13.1"
|
||||
strum = "0.27.1"
|
||||
strum_macros = "0.27.1"
|
||||
rustls = { version = "0.23.27", default-features = false, features = ["ring"] }
|
||||
prometheus = { version = "0.13.4", features = ["process"], default-features = false }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ COPY Cargo.toml ./Cargo.toml
|
||||
COPY crates ./crates
|
||||
|
||||
# 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
|
||||
FROM debian:trixie-slim
|
||||
|
||||
@@ -15,7 +15,7 @@ default = ["auth"]
|
||||
redis = ["dep:redis"]
|
||||
swagger = ["cdk/swagger", "dep:utoipa"]
|
||||
auth = ["cdk/auth"]
|
||||
|
||||
prometheus = ["dep:cdk-prometheus"]
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
@@ -27,6 +27,7 @@ tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
utoipa = { workspace = true, optional = true }
|
||||
futures.workspace = true
|
||||
cdk-prometheus = { workspace = true , optional = true}
|
||||
moka = { version = "0.12.10", features = ["future"] }
|
||||
serde_json.workspace = true
|
||||
paste = "1.0.15"
|
||||
|
||||
@@ -17,6 +17,8 @@ use cache::HttpCache;
|
||||
use cdk::mint::Mint;
|
||||
use router_handlers::*;
|
||||
|
||||
mod metrics;
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
mod auth;
|
||||
mod bolt12_router;
|
||||
@@ -322,6 +324,11 @@ pub async fn create_mint_router_with_custom_cache(
|
||||
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
|
||||
.layer(from_fn(cors_middleware))
|
||||
.with_state(state);
|
||||
|
||||
41
crates/cdk-axum/src/metrics.rs
Normal file
41
crates/cdk-axum/src/metrics.rs
Normal 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
|
||||
}
|
||||
@@ -18,6 +18,7 @@ bench = []
|
||||
wallet = ["cashu/wallet"]
|
||||
mint = ["cashu/mint", "dep:uuid"]
|
||||
auth = ["cashu/auth"]
|
||||
prometheus = ["cdk-prometheus/default"]
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
@@ -30,6 +31,7 @@ lightning-invoice.workspace = true
|
||||
lightning.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
cdk-prometheus = { workspace = true, optional = true}
|
||||
url.workspace = true
|
||||
uuid = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::pin::Pin;
|
||||
use async_trait::async_trait;
|
||||
use cashu::util::hex;
|
||||
use cashu::{Bolt11Invoice, MeltOptions};
|
||||
#[cfg(feature = "prometheus")]
|
||||
use cdk_prometheus::METRICS;
|
||||
use futures::Stream;
|
||||
use lightning::offers::offer::Offer;
|
||||
use lightning_invoice::ParseOrSemanticError;
|
||||
@@ -411,3 +413,189 @@ impl TryFrom<Value> for Bolt11Settings {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ cdk-sqlite = { workspace = true }
|
||||
cdk-redb = { workspace = true }
|
||||
cdk-fake-wallet = { workspace = true }
|
||||
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 = [
|
||||
"executor",
|
||||
] }
|
||||
|
||||
@@ -286,6 +286,7 @@ fn create_ldk_settings(
|
||||
grpc_processor: None,
|
||||
database: cdk_mintd::config::Database::default(),
|
||||
mint_management_rpc: None,
|
||||
prometheus: None,
|
||||
auth: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ pub fn create_fake_wallet_settings(
|
||||
},
|
||||
mint_management_rpc: None,
|
||||
auth: None,
|
||||
prometheus: Some(Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +266,7 @@ pub fn create_cln_settings(
|
||||
database: cdk_mintd::config::Database::default(),
|
||||
mint_management_rpc: None,
|
||||
auth: None,
|
||||
prometheus: Some(Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,5 +312,6 @@ pub fn create_lnd_settings(
|
||||
database: cdk_mintd::config::Database::default(),
|
||||
mint_management_rpc: None,
|
||||
auth: None,
|
||||
prometheus: Some(Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ sqlcipher = ["sqlite", "cdk-sqlite/sqlcipher"]
|
||||
swagger = ["cdk-axum/swagger", "dep:utoipa", "dep:utoipa-swagger-ui"]
|
||||
redis = ["cdk-axum/redis"]
|
||||
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]
|
||||
anyhow.workspace = true
|
||||
@@ -39,7 +40,8 @@ cdk = { workspace = true, features = [
|
||||
cdk-sqlite = { workspace = true, features = [
|
||||
"mint"
|
||||
], 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-lnbits = { 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-payment-processor = { workspace = true, optional = true }
|
||||
config.workspace = true
|
||||
cdk-prometheus = { workspace = true, optional = true , features = ["system-metrics"]}
|
||||
clap.workspace = true
|
||||
bitcoin.workspace = true
|
||||
tokio = { workspace = true, default-features = false, features = ["signal"] }
|
||||
@@ -63,11 +66,7 @@ tower-http = { workspace = true, features = ["compression-full", "decompression-
|
||||
tower.workspace = true
|
||||
lightning-invoice.workspace = true
|
||||
home.workspace = true
|
||||
url.workspace = true
|
||||
utoipa = { workspace = true, optional = true }
|
||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
# Dep of utopia 2.5.0 breaks so keeping here for now
|
||||
zip = "=2.4.2"
|
||||
time = "=0.3.39"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
[info]
|
||||
url = "https://mint.thesimplekid.dev/"
|
||||
listen_host = "127.0.0.1"
|
||||
@@ -20,7 +21,11 @@ enabled = false
|
||||
# address = "127.0.0.1"
|
||||
# port = 8086
|
||||
|
||||
|
||||
#[prometheus]
|
||||
#enabled = true
|
||||
#address = "127.0.0.1"
|
||||
#port = 9090
|
||||
#
|
||||
[info.http_cache]
|
||||
# memory or redis
|
||||
backend = "memory"
|
||||
@@ -130,12 +135,12 @@ reserve_fee_min = 4
|
||||
# webserver_host = "127.0.0.1" # Default: 127.0.0.1
|
||||
# webserver_port = 0 # 0 = auto-assign available port
|
||||
|
||||
# [fake_wallet]
|
||||
# supported_units = ["sat"]
|
||||
# fee_percent = 0.02
|
||||
# reserve_fee_min = 1
|
||||
# min_delay_time = 1
|
||||
# max_delay_time = 3
|
||||
[fake_wallet]
|
||||
supported_units = ["sat"]
|
||||
fee_percent = 0.02
|
||||
reserve_fee_min = 1
|
||||
min_delay_time = 1
|
||||
max_delay_time = 3
|
||||
|
||||
# [grpc_processor]
|
||||
# gRPC Payment Processor configuration
|
||||
|
||||
@@ -452,6 +452,16 @@ pub struct Settings {
|
||||
#[cfg(feature = "management-rpc")]
|
||||
pub mint_management_rpc: Option<MintManagementRpc>,
|
||||
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)]
|
||||
|
||||
@@ -25,6 +25,8 @@ mod lnbits;
|
||||
mod lnd;
|
||||
#[cfg(feature = "management-rpc")]
|
||||
mod management_rpc;
|
||||
#[cfg(feature = "prometheus")]
|
||||
mod prometheus;
|
||||
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
@@ -50,6 +52,8 @@ pub use lnd::*;
|
||||
#[cfg(feature = "management-rpc")]
|
||||
pub use management_rpc::*;
|
||||
pub use mint_info::*;
|
||||
#[cfg(feature = "prometheus")]
|
||||
pub use prometheus::*;
|
||||
|
||||
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 {
|
||||
#[cfg(feature = "cln")]
|
||||
LnBackend::Cln => {
|
||||
|
||||
31
crates/cdk-mintd/src/env_vars/prometheus.rs
Normal file
31
crates/cdk-mintd/src/env_vars/prometheus.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,7 @@ use std::sync::Arc;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use axum::Router;
|
||||
use bip39::Mnemonic;
|
||||
// internal crate modules
|
||||
use cdk::cdk_database::{self, MintDatabase, MintKVStore, MintKeysDatabase};
|
||||
use cdk::cdk_payment;
|
||||
use cdk::cdk_payment::MintPayment;
|
||||
use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
|
||||
#[cfg(any(
|
||||
feature = "cln",
|
||||
@@ -41,14 +38,22 @@ use cdk::nuts::{AuthRequired, Method, ProtectedEndpoint, RoutePath};
|
||||
use cdk::nuts::{ContactInfo, MintVersion, PaymentMethod};
|
||||
use cdk::types::QuoteTTL;
|
||||
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")]
|
||||
use cdk_postgres::{MintPgAuthDatabase, MintPgDatabase};
|
||||
use cdk_postgres::MintPgDatabase;
|
||||
#[cfg(all(feature = "auth", feature = "sqlite"))]
|
||||
use cdk_sqlite::mint::MintSqliteAuthDatabase;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use cdk_sqlite::MintSqliteDatabase;
|
||||
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 setup::LnBackendSetup;
|
||||
use tower::ServiceBuilder;
|
||||
@@ -440,6 +445,8 @@ async fn configure_lightning_backend(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(feature = "prometheus")]
|
||||
let cln = MetricsMintPayment::new(cln);
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
settings,
|
||||
@@ -463,6 +470,8 @@ async fn configure_lightning_backend(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(feature = "prometheus")]
|
||||
let lnbits = MetricsMintPayment::new(lnbits);
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
settings,
|
||||
@@ -486,6 +495,8 @@ async fn configure_lightning_backend(
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
#[cfg(feature = "prometheus")]
|
||||
let lnd = MetricsMintPayment::new(lnd);
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
settings,
|
||||
@@ -512,6 +523,8 @@ async fn configure_lightning_backend(
|
||||
_kv_store.clone(),
|
||||
)
|
||||
.await?;
|
||||
#[cfg(feature = "prometheus")]
|
||||
let fake = MetricsMintPayment::new(fake);
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
settings,
|
||||
@@ -541,6 +554,8 @@ async fn configure_lightning_backend(
|
||||
let processor = grpc_processor
|
||||
.setup(ln_routers, settings, unit.clone(), None, work_dir, None)
|
||||
.await?;
|
||||
#[cfg(feature = "prometheus")]
|
||||
let processor = MetricsMintPayment::new(processor);
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
settings,
|
||||
@@ -595,7 +610,7 @@ async fn configure_backend_for_unit(
|
||||
mut mint_builder: MintBuilder,
|
||||
unit: cdk::nuts::CurrencyUnit,
|
||||
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> {
|
||||
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 {
|
||||
mint_service = mint_service.merge(router);
|
||||
}
|
||||
@@ -987,8 +1043,24 @@ async fn start_services_with_shutdown(
|
||||
|
||||
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
|
||||
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 {
|
||||
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?;
|
||||
|
||||
#[cfg(feature = "management-rpc")]
|
||||
|
||||
47
crates/cdk-prometheus/Cargo.toml
Normal file
47
crates/cdk-prometheus/Cargo.toml
Normal 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
|
||||
189
crates/cdk-prometheus/README.md
Normal file
189
crates/cdk-prometheus/README.md
Normal 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.
|
||||
|
||||
## What’s 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
|
||||
```
|
||||
32
crates/cdk-prometheus/src/error.rs
Normal file
32
crates/cdk-prometheus/src/error.rs
Normal 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>;
|
||||
84
crates/cdk-prometheus/src/lib.rs
Normal file
84
crates/cdk-prometheus/src/lib.rs
Normal 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
|
||||
}
|
||||
427
crates/cdk-prometheus/src/metrics.rs
Normal file
427
crates/cdk-prometheus/src/metrics.rs
Normal 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(®istry)?;
|
||||
|
||||
// Create and register authentication metrics
|
||||
let (auth_attempts_total, auth_successes_total) = Self::create_auth_metrics(®istry)?;
|
||||
|
||||
// Create and register Lightning metrics
|
||||
let (lightning_payments_total, lightning_payment_amount, lightning_payment_fees) =
|
||||
Self::create_lightning_metrics(®istry)?;
|
||||
|
||||
// Create and register database metrics
|
||||
let (db_operations_total, db_operation_duration, db_connections_active) =
|
||||
Self::create_db_metrics(®istry)?;
|
||||
|
||||
// Create and register error metrics
|
||||
let errors_total = Self::create_error_metrics(®istry)?;
|
||||
|
||||
// Create and register mint metrics
|
||||
let (mint_operations_total, mint_operation_duration, mint_in_flight_requests) =
|
||||
Self::create_mint_metrics(®istry)?;
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
107
crates/cdk-prometheus/src/process.rs
Normal file
107
crates/cdk-prometheus/src/process.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
317
crates/cdk-prometheus/src/server.rs
Normal file
317
crates/cdk-prometheus/src/server.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,11 @@ default = ["mint", "wallet", "auth"]
|
||||
mint = ["cdk-common/mint"]
|
||||
wallet = ["cdk-common/wallet"]
|
||||
auth = ["cdk-common/auth"]
|
||||
|
||||
prometheus = ["cdk-prometheus"]
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
cdk-common = { workspace = true, features = ["test"] }
|
||||
cdk-prometheus = { workspace = true, optional = true }
|
||||
bitcoin.workspace = true
|
||||
thiserror.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -57,6 +57,8 @@ mod migrations;
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
pub use auth::SQLMintAuthDatabase;
|
||||
#[cfg(feature = "prometheus")]
|
||||
use cdk_prometheus::METRICS;
|
||||
|
||||
/// Mint SQL Database
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -299,11 +301,27 @@ where
|
||||
type Err = 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> {
|
||||
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>(
|
||||
&'a self,
|
||||
) -> Result<Box<dyn MintKeyDatabaseTransaction<'a, Error> + Send + Sync + 'a>, Error> {
|
||||
Ok(Box::new(SQLTransaction {
|
||||
let tx = SQLTransaction {
|
||||
inner: ConnectionWithTransaction::new(
|
||||
self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
|
||||
)
|
||||
.await?,
|
||||
}))
|
||||
};
|
||||
|
||||
Ok(Box::new(tx))
|
||||
}
|
||||
|
||||
async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result<Option<Id>, Self::Err> {
|
||||
@@ -1072,12 +1092,18 @@ where
|
||||
type Err = Error;
|
||||
|
||||
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 result = async {
|
||||
let payments = get_mint_quote_payments(&*conn, quote_id).await?;
|
||||
let issuance = get_mint_quote_issuance(&*conn, quote_id).await?;
|
||||
|
||||
Ok(query(
|
||||
query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
@@ -1100,7 +1126,24 @@ where
|
||||
.fetch_one(&*conn)
|
||||
.await?
|
||||
.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(
|
||||
@@ -1228,8 +1271,15 @@ where
|
||||
&self,
|
||||
quote_id: &QuoteId,
|
||||
) -> 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)))?;
|
||||
Ok(query(
|
||||
|
||||
let result = async {
|
||||
query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
@@ -1256,7 +1306,24 @@ where
|
||||
.fetch_one(&*conn)
|
||||
.await?
|
||||
.map(sql_row_to_melt_quote)
|
||||
.transpose()?)
|
||||
.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> {
|
||||
@@ -1826,20 +1893,64 @@ where
|
||||
async fn begin_transaction<'a>(
|
||||
&'a self,
|
||||
) -> Result<Box<dyn database::MintTransaction<'a, Error> + Send + Sync + 'a>, Error> {
|
||||
Ok(Box::new(SQLTransaction {
|
||||
let tx = SQLTransaction {
|
||||
inner: ConnectionWithTransaction::new(
|
||||
self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
|
||||
)
|
||||
.await?,
|
||||
}))
|
||||
};
|
||||
|
||||
Ok(Box::new(tx))
|
||||
}
|
||||
|
||||
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> {
|
||||
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?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ use std::fmt::Debug;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
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;
|
||||
|
||||
@@ -86,6 +87,8 @@ where
|
||||
{
|
||||
resource: Option<(Arc<AtomicBool>, RM::Connection)>,
|
||||
pool: Arc<Pool<RM>>,
|
||||
#[cfg(feature = "prometheus")]
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl<RM> Debug for PooledResource<RM>
|
||||
@@ -105,7 +108,16 @@ where
|
||||
if let Some(resource) = self.resource.take() {
|
||||
let mut active_resource = self.pool.queue.lock().expect("active_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
|
||||
self.pool.waiter.notify_one();
|
||||
@@ -155,6 +167,18 @@ where
|
||||
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.
|
||||
///
|
||||
/// 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 !stale.load(Ordering::SeqCst) {
|
||||
drop(resources);
|
||||
self.in_use.fetch_add(1, Ordering::AcqRel);
|
||||
self.increment_connection_counter();
|
||||
|
||||
return Ok(PooledResource {
|
||||
resource: Some((stale, resource)),
|
||||
pool: self.clone(),
|
||||
#[cfg(feature = "prometheus")]
|
||||
start_time: Instant::now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.in_use.load(Ordering::Relaxed) < self.max_size {
|
||||
drop(resources);
|
||||
self.in_use.fetch_add(1, Ordering::AcqRel);
|
||||
self.increment_connection_counter();
|
||||
let stale: Arc<AtomicBool> = Arc::new(false.into());
|
||||
|
||||
return Ok(PooledResource {
|
||||
@@ -191,6 +217,8 @@ where
|
||||
RM::new_resource(&self.config, stale, timeout)?,
|
||||
)),
|
||||
pool: self.clone(),
|
||||
#[cfg(feature = "prometheus")]
|
||||
start_time: Instant::now(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@ mint = ["cdk-common/mint", "cdk-sql-common/mint"]
|
||||
wallet = ["cdk-common/wallet", "cdk-sql-common/wallet"]
|
||||
auth = ["cdk-common/auth", "cdk-sql-common/auth"]
|
||||
sqlcipher = ["rusqlite/bundled-sqlcipher"]
|
||||
|
||||
prometheus = ["cdk-sql-common/prometheus", "cdk-prometheus"]
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
cdk-common = { workspace = true, features = ["test"] }
|
||||
cdk-prometheus = { workspace = true, optional = true }
|
||||
bitcoin.workspace = true
|
||||
cdk-sql-common = { workspace = true }
|
||||
rusqlite = { version = "0.31", features = ["bundled"]}
|
||||
|
||||
@@ -20,7 +20,7 @@ bip353 = ["dep:trust-dns-resolver"]
|
||||
swagger = ["mint", "dep:utoipa", "cdk-common/swagger"]
|
||||
bench = []
|
||||
http_subscription = []
|
||||
|
||||
prometheus = ["dep:cdk-prometheus"]
|
||||
|
||||
[dependencies]
|
||||
cdk-common.workspace = true
|
||||
@@ -44,7 +44,7 @@ utoipa = { workspace = true, optional = true }
|
||||
uuid.workspace = true
|
||||
jsonwebtoken = { workspace = true, optional = true }
|
||||
trust-dns-resolver = { version = "0.23.2", optional = true }
|
||||
|
||||
cdk-prometheus = {workspace = true, optional = true}
|
||||
# -Z minimal-versions
|
||||
sync_wrapper = "0.1.2"
|
||||
bech32 = "0.9.1"
|
||||
|
||||
@@ -268,7 +268,6 @@ impl MintBuilder {
|
||||
self.payment_processors.insert(key, payment_processor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the input fee ppk for a given unit
|
||||
///
|
||||
/// The unit **MUST** already have been added with a ln backend
|
||||
|
||||
@@ -10,6 +10,8 @@ use cdk_common::{
|
||||
MintQuoteBolt11Response, MintQuoteBolt12Request, MintQuoteBolt12Response, MintQuoteState,
|
||||
MintRequest, MintResponse, NotificationPayload, PaymentMethod, PublicKey,
|
||||
};
|
||||
#[cfg(feature = "prometheus")]
|
||||
use cdk_prometheus::METRICS;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::mint::Verification;
|
||||
@@ -187,6 +189,10 @@ impl Mint {
|
||||
&self,
|
||||
mint_quote_request: MintQuoteRequest,
|
||||
) -> Result<MintQuoteResponse, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("get_mint_quote");
|
||||
|
||||
let result = async {
|
||||
let unit: CurrencyUnit;
|
||||
let amount;
|
||||
let pubkey;
|
||||
@@ -312,6 +318,19 @@ impl Mint {
|
||||
|
||||
quote.try_into()
|
||||
}
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
{
|
||||
METRICS.dec_in_flight_requests("get_mint_quote");
|
||||
METRICS.record_mint_operation("get_mint_quote", result.is_ok());
|
||||
if result.is_err() {
|
||||
METRICS.record_error();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Retrieves all mint quotes from the database
|
||||
///
|
||||
@@ -320,9 +339,26 @@ impl Mint {
|
||||
/// * `Error` if database access fails
|
||||
#[instrument(skip_all)]
|
||||
pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
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
|
||||
///
|
||||
@@ -334,12 +370,28 @@ impl Mint {
|
||||
/// * `Error` if the quote doesn't exist or removal fails
|
||||
#[instrument(skip_all)]
|
||||
pub async fn remove_mint_quote(&self, quote_id: &QuoteId) -> Result<(), Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("remove_mint_quote");
|
||||
|
||||
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
|
||||
///
|
||||
@@ -357,12 +409,14 @@ impl Mint {
|
||||
&self,
|
||||
wait_payment_response: WaitPaymentResponse,
|
||||
) -> Result<(), Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("pay_mint_quote_for_request_id");
|
||||
let result = async {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -385,6 +439,19 @@ impl Mint {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
{
|
||||
METRICS.dec_in_flight_requests("pay_mint_quote_for_request_id");
|
||||
METRICS.record_mint_operation("pay_mint_quote_for_request_id", result.is_ok());
|
||||
if result.is_err() {
|
||||
METRICS.record_error();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Marks a specific mint quote as paid
|
||||
///
|
||||
@@ -405,9 +472,31 @@ impl Mint {
|
||||
mint_quote: &MintQuote,
|
||||
wait_payment_response: WaitPaymentResponse,
|
||||
) -> 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;
|
||||
|
||||
#[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
|
||||
///
|
||||
@@ -422,6 +511,9 @@ impl Mint {
|
||||
/// * `Error` if the quote doesn't exist or checking fails
|
||||
#[instrument(skip(self))]
|
||||
pub async fn check_mint_quote(&self, quote_id: &QuoteId) -> Result<MintQuoteResponse, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("check_mint_quote");
|
||||
let result = async {
|
||||
let mut quote = self
|
||||
.localstore
|
||||
.get_mint_quote(quote_id)
|
||||
@@ -434,6 +526,19 @@ impl Mint {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Processes a mint request to issue new tokens
|
||||
///
|
||||
@@ -456,15 +561,18 @@ impl Mint {
|
||||
&self,
|
||||
mint_request: MintRequest<QuoteId>,
|
||||
) -> Result<MintResponse, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("process_mint_request");
|
||||
let result = async {
|
||||
let mut mint_quote = self
|
||||
.localstore
|
||||
.get_mint_quote(&mint_request.quote)
|
||||
.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
|
||||
// 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
|
||||
@@ -582,4 +690,16 @@ impl Mint {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ use cdk_common::payment::{
|
||||
};
|
||||
use cdk_common::quote_id::QuoteId;
|
||||
use cdk_common::{MeltOptions, MeltQuoteBolt12Request};
|
||||
#[cfg(feature = "prometheus")]
|
||||
use cdk_prometheus::METRICS;
|
||||
use lightning::offers::offer::Offer;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -131,6 +133,8 @@ impl Mint {
|
||||
&self,
|
||||
melt_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("get_melt_bolt11_quote");
|
||||
let MeltQuoteBolt11Request {
|
||||
request,
|
||||
unit,
|
||||
@@ -183,6 +187,12 @@ impl Mint {
|
||||
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
|
||||
})?;
|
||||
|
||||
@@ -315,6 +325,12 @@ impl Mint {
|
||||
tx.add_melt_quote(quote.clone()).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())
|
||||
}
|
||||
|
||||
@@ -324,20 +340,50 @@ impl Mint {
|
||||
&self,
|
||||
quote_id: &QuoteId,
|
||||
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
|
||||
let quote = self
|
||||
.localstore
|
||||
.get_melt_quote(quote_id)
|
||||
.await?
|
||||
.ok_or(Error::UnknownQuote)?;
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("check_melt_quote");
|
||||
let quote = match self.localstore.get_melt_quote(quote_id).await {
|
||||
Ok(Some(quote)) => quote,
|
||||
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
|
||||
.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);
|
||||
|
||||
Ok(MeltQuoteBolt11Response {
|
||||
let response = MeltQuoteBolt11Response {
|
||||
quote: quote.id,
|
||||
paid: Some(quote.state == MeltQuoteState::Paid),
|
||||
state: quote.state,
|
||||
@@ -348,7 +394,15 @@ impl Mint {
|
||||
change,
|
||||
request: Some(quote.request.to_string()),
|
||||
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
|
||||
@@ -520,6 +574,9 @@ impl Mint {
|
||||
&self,
|
||||
melt_request: &MeltRequest<QuoteId>,
|
||||
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("melt_bolt11");
|
||||
|
||||
use std::sync::Arc;
|
||||
async fn check_payment_state(
|
||||
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 (proof_writer, quote) = self
|
||||
let (proof_writer, quote) = match self
|
||||
.verify_melt_request(&mut tx, verification, melt_request)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(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, "e, melt_request)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
{
|
||||
Ok(amount) => amount,
|
||||
Err(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 {
|
||||
Some(amount_spent) => (tx, None, amount_spent, quote),
|
||||
@@ -669,6 +748,14 @@ impl Mint {
|
||||
melt_request.quote()
|
||||
);
|
||||
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);
|
||||
}
|
||||
MeltQuoteState::Pending => {
|
||||
@@ -677,6 +764,13 @@ impl Mint {
|
||||
melt_request.quote()
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -716,7 +810,7 @@ impl Mint {
|
||||
|
||||
// If we made it here the payment has been made.
|
||||
// We process the melt burning the inputs and returning change
|
||||
let res = self
|
||||
let res = match self
|
||||
.process_melt_request(
|
||||
tx,
|
||||
proof_writer,
|
||||
@@ -726,10 +820,27 @@ impl Mint {
|
||||
amount_spent_quote_unit,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(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)
|
||||
}
|
||||
@@ -747,11 +858,20 @@ impl Mint {
|
||||
payment_preimage: Option<String>,
|
||||
total_spent: Amount,
|
||||
) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("process_melt_request");
|
||||
|
||||
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)
|
||||
.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(
|
||||
melt_request.quote(),
|
||||
@@ -853,7 +973,6 @@ impl Mint {
|
||||
change.clone(),
|
||||
MeltQuoteState::Paid,
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
"Melt for quote {} completed total spent {}, total inputs: {}, change given: {}",
|
||||
quote.id,
|
||||
@@ -865,8 +984,7 @@ impl Mint {
|
||||
.expect("Change cannot overflow"))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
Ok(MeltQuoteBolt11Response {
|
||||
let response = MeltQuoteBolt11Response {
|
||||
amount: quote.amount,
|
||||
paid: Some(true),
|
||||
payment_preimage,
|
||||
@@ -877,6 +995,21 @@ impl Mint {
|
||||
expiry: quote.expiry,
|
||||
request: Some(quote.request.to_string()),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ use cdk_common::nuts::{self, BlindSignature, BlindedMessage, CurrencyUnit, Id, K
|
||||
use cdk_common::payment::WaitPaymentResponse;
|
||||
pub use cdk_common::quote_id::QuoteId;
|
||||
use cdk_common::secret;
|
||||
#[cfg(feature = "prometheus")]
|
||||
use cdk_prometheus::global;
|
||||
use cdk_signatory::signatory::{Signatory, SignatoryKeySet};
|
||||
use futures::StreamExt;
|
||||
#[cfg(feature = "auth")]
|
||||
@@ -724,14 +726,29 @@ impl Mint {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn blind_sign(
|
||||
&self,
|
||||
blinded_messages: Vec<BlindedMessage>,
|
||||
blinded_message: Vec<BlindedMessage>,
|
||||
) -> 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
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn verify_proofs(&self, proofs: Proofs) -> Result<(), Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
global::inc_in_flight_requests("verify_proofs");
|
||||
|
||||
let result = async {
|
||||
proofs
|
||||
.iter()
|
||||
.map(|proof| {
|
||||
@@ -760,6 +777,16 @@ impl Mint {
|
||||
|
||||
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
|
||||
/// Check to see if there is a corresponding mint quote for a melt.
|
||||
@@ -836,6 +863,10 @@ impl Mint {
|
||||
/// Restore
|
||||
#[instrument(skip_all)]
|
||||
pub async fn restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
global::inc_in_flight_requests("restore");
|
||||
|
||||
let result = async {
|
||||
let output_len = request.outputs.len();
|
||||
|
||||
let mut outputs = Vec::with_capacity(output_len);
|
||||
@@ -866,10 +897,24 @@ impl Mint {
|
||||
promises: Some(signatures),
|
||||
})
|
||||
}
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
{
|
||||
global::dec_in_flight_requests("restore");
|
||||
global::record_mint_operation("restore", result.is_ok());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Get the total amount issed by keyset
|
||||
#[instrument(skip_all)]
|
||||
pub async fn total_issued(&self) -> Result<HashMap<Id, Amount>, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
global::inc_in_flight_requests("total_issued");
|
||||
|
||||
let result = async {
|
||||
let keysets = self.keysets().keysets;
|
||||
|
||||
let mut total_issued = HashMap::new();
|
||||
@@ -887,10 +932,23 @@ impl Mint {
|
||||
|
||||
Ok(total_issued)
|
||||
}
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
{
|
||||
global::dec_in_flight_requests("total_issued");
|
||||
global::record_mint_operation("total_issued", result.is_ok());
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Total redeemed for keyset
|
||||
#[instrument(skip_all)]
|
||||
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 mut total_redeemed = HashMap::new();
|
||||
@@ -909,6 +967,9 @@ impl Mint {
|
||||
total_redeemed.insert(keyset.id, total_spent);
|
||||
}
|
||||
|
||||
#[cfg(feature = "prometheus")]
|
||||
global::dec_in_flight_requests("total_redeemed");
|
||||
|
||||
Ok(total_redeemed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(feature = "prometheus")]
|
||||
use cdk_prometheus::METRICS;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::nut11::{enforce_sig_flag, EnforceSigFlag};
|
||||
@@ -12,6 +14,8 @@ impl Mint {
|
||||
&self,
|
||||
swap_request: SwapRequest,
|
||||
) -> Result<SwapResponse, Error> {
|
||||
#[cfg(feature = "prometheus")]
|
||||
METRICS.inc_in_flight_requests("process_swap_request");
|
||||
// Do the external call before beginning the db transaction
|
||||
// Check any overflow before talking to the signatory
|
||||
swap_request.input_amount()?;
|
||||
@@ -25,7 +29,6 @@ impl Mint {
|
||||
tracing::debug!("Input verification failed: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
let mut tx = self.localstore.begin_transaction().await?;
|
||||
|
||||
if let Err(err) = self
|
||||
@@ -38,20 +41,50 @@ impl Mint {
|
||||
.await
|
||||
{
|
||||
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);
|
||||
};
|
||||
|
||||
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 =
|
||||
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())
|
||||
.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)
|
||||
.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(
|
||||
&swap_request
|
||||
@@ -67,7 +100,15 @@ impl Mint {
|
||||
proof_writer.commit();
|
||||
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> {
|
||||
@@ -79,4 +120,10 @@ impl Mint {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# 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:
|
||||
build:
|
||||
context: .
|
||||
@@ -26,7 +61,15 @@ services:
|
||||
# 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_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"]
|
||||
depends_on:
|
||||
- prometheus
|
||||
- grafana
|
||||
networks:
|
||||
- cdk
|
||||
# Uncomment when using PostgreSQL:
|
||||
# depends_on:
|
||||
# - postgres
|
||||
@@ -78,3 +121,9 @@ volumes:
|
||||
driver: local
|
||||
# redis_data:
|
||||
# driver: local
|
||||
|
||||
|
||||
|
||||
networks:
|
||||
cdk:
|
||||
driver: bridge
|
||||
|
||||
1983
misc/provisioning/dashboards/dashboard.json
Normal file
1983
misc/provisioning/dashboards/dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
8
misc/provisioning/dashboards/dashboard.yaml
Normal file
8
misc/provisioning/dashboards/dashboard.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: 1
|
||||
providers:
|
||||
- name: 'default'
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
8
misc/provisioning/datasources/datasource.yml
Normal file
8
misc/provisioning/datasources/datasource.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
8
misc/provisioning/prometheus.yml
Normal file
8
misc/provisioning/prometheus.yml
Normal 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']
|
||||
Reference in New Issue
Block a user