Skip to content

Commit 7431487

Browse files
authored
Merge pull request #117 from Anyitechs/introduce-telemetry
2 parents 12ce2d1 + cc3a55a commit 7431487

10 files changed

Lines changed: 921 additions & 32 deletions

File tree

e2e-tests/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,24 @@ pub struct LdkServerHandle {
9797
client: LdkServerClient,
9898
}
9999

100+
pub struct LdkServerConfig {
101+
pub metrics_auth: Option<(String, String)>,
102+
}
103+
104+
impl Default for LdkServerConfig {
105+
fn default() -> Self {
106+
Self { metrics_auth: None }
107+
}
108+
}
109+
100110
impl LdkServerHandle {
101111
/// Starts a new ldk-server instance against the given bitcoind.
102112
/// Waits until the server is ready to accept requests.
103113
pub async fn start(bitcoind: &TestBitcoind) -> Self {
114+
Self::start_with_config(bitcoind, LdkServerConfig::default()).await
115+
}
116+
117+
pub async fn start_with_config(bitcoind: &TestBitcoind, config: LdkServerConfig) -> Self {
104118
#[allow(deprecated)]
105119
let storage_dir = tempfile::tempdir().unwrap().into_path();
106120
let rest_port = find_available_port();
@@ -111,6 +125,12 @@ impl LdkServerHandle {
111125

112126
let exchange_name = format!("e2e_test_exchange_{rest_port}");
113127

128+
let metrics_auth_config = if let Some((user, pass)) = config.metrics_auth {
129+
format!("username = \"{}\"\npassword = \"{}\"", user, pass)
130+
} else {
131+
String::new()
132+
};
133+
114134
let config_content = format!(
115135
r#"[node]
116136
network = "regtest"
@@ -140,6 +160,11 @@ max_client_to_self_delay = 1024
140160
min_payment_size_msat = 0
141161
max_payment_size_msat = 1000000000
142162
client_trusts_lsp = true
163+
164+
[metrics]
165+
enabled = true
166+
poll_metrics_interval = 1
167+
{metrics_auth_config}
143168
"#,
144169
storage_dir = storage_dir.display(),
145170
);

e2e-tests/tests/e2e.rs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use std::time::Duration;
1212

1313
use e2e_tests::{
1414
find_available_port, mine_and_sync, run_cli, run_cli_raw, setup_funded_channel,
15-
wait_for_onchain_balance, LdkServerHandle, RabbitMqEventConsumer, TestBitcoind,
15+
wait_for_onchain_balance, LdkServerConfig, LdkServerHandle, RabbitMqEventConsumer,
16+
TestBitcoind,
1617
};
1718
use hex_conservative::{DisplayHex, FromHex};
1819
use ldk_node::bitcoin::hashes::{sha256, Hash};
@@ -995,3 +996,132 @@ async fn test_hodl_invoice_fail() {
995996
events_a.iter().map(|e| &e.event).collect::<Vec<_>>()
996997
);
997998
}
999+
1000+
#[tokio::test]
1001+
async fn test_metrics_endpoint() {
1002+
let bitcoind = TestBitcoind::new();
1003+
1004+
// Test with metrics enabled
1005+
let server_a = LdkServerHandle::start(&bitcoind).await;
1006+
let server_b = LdkServerHandle::start(&bitcoind).await;
1007+
1008+
let client = server_a.client();
1009+
let metrics_result = client.get_metrics().await;
1010+
1011+
assert!(metrics_result.is_ok(), "Expected metrics to succeed when enabled");
1012+
let metrics = metrics_result.unwrap();
1013+
1014+
// Verify initial state
1015+
assert!(metrics.contains("ldk_server_total_peers_count 0"));
1016+
assert!(metrics.contains("ldk_server_total_payments_count 0"));
1017+
assert!(metrics.contains("ldk_server_total_successful_payments_count 0"));
1018+
assert!(metrics.contains("ldk_server_total_pending_payments_count 0"));
1019+
assert!(metrics.contains("ldk_server_total_failed_payments_count 0"));
1020+
assert!(metrics.contains("ldk_server_total_channels_count 0"));
1021+
assert!(metrics.contains("ldk_server_total_public_channels_count 0"));
1022+
assert!(metrics.contains("ldk_server_total_private_channels_count 0"));
1023+
assert!(metrics.contains("ldk_server_total_onchain_balance_sats 0"));
1024+
assert!(metrics.contains("ldk_server_spendable_onchain_balance_sats 0"));
1025+
assert!(metrics.contains("ldk_server_total_anchor_channels_reserve_sats 0"));
1026+
assert!(metrics.contains("ldk_server_total_lightning_balance_sats 0"));
1027+
1028+
// Set up channel and make a payment to trigger metrics update
1029+
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;
1030+
1031+
// Poll for channel, peer and balance metrics.
1032+
let timeout = Duration::from_secs(10);
1033+
let start = std::time::Instant::now();
1034+
loop {
1035+
let metrics = client.get_metrics().await.unwrap();
1036+
if metrics.contains("ldk_server_total_peers_count 1")
1037+
&& metrics.contains("ldk_server_total_channels_count 1")
1038+
&& metrics.contains("ldk_server_total_public_channels_count 1")
1039+
&& metrics.contains("ldk_server_total_payments_count 2")
1040+
&& !metrics.contains("ldk_server_total_lightning_balance_sats 0")
1041+
&& !metrics.contains("ldk_server_total_onchain_balance_sats 0")
1042+
&& !metrics.contains("ldk_server_spendable_onchain_balance_sats 0")
1043+
&& !metrics.contains("ldk_server_total_anchor_channels_reserve_sats 0")
1044+
{
1045+
break;
1046+
}
1047+
1048+
if start.elapsed() > timeout {
1049+
let current_metrics = client.get_metrics().await.unwrap();
1050+
panic!(
1051+
"Timed out waiting for channel, peer and balance metrics to update. Current metrics:\n{}",
1052+
current_metrics
1053+
);
1054+
}
1055+
tokio::time::sleep(Duration::from_secs(1)).await;
1056+
}
1057+
1058+
let invoice_resp = server_b
1059+
.client()
1060+
.bolt11_receive(Bolt11ReceiveRequest {
1061+
amount_msat: Some(10_000_000),
1062+
description: Some(Bolt11InvoiceDescription {
1063+
kind: Some(bolt11_invoice_description::Kind::Direct("metrics test".to_string())),
1064+
}),
1065+
expiry_secs: 3600,
1066+
})
1067+
.await
1068+
.unwrap();
1069+
1070+
run_cli(&server_a, &["bolt11-send", &invoice_resp.invoice]);
1071+
1072+
// Wait to receive the PaymentSuccessful event and update metrics
1073+
let timeout = Duration::from_secs(30);
1074+
let start = std::time::Instant::now();
1075+
loop {
1076+
let metrics = client.get_metrics().await.unwrap();
1077+
if metrics.contains("ldk_server_total_successful_payments_count 1")
1078+
&& !metrics.contains("ldk_server_total_lightning_balance_sats 0")
1079+
&& !metrics.contains("ldk_server_total_onchain_balance_sats 0")
1080+
&& !metrics.contains("ldk_server_spendable_onchain_balance_sats 0")
1081+
&& !metrics.contains("ldk_server_total_anchor_channels_reserve_sats 0")
1082+
{
1083+
break;
1084+
}
1085+
if start.elapsed() > timeout {
1086+
panic!("Timed out waiting for payment metrics to update");
1087+
}
1088+
tokio::time::sleep(Duration::from_millis(500)).await;
1089+
}
1090+
}
1091+
1092+
#[tokio::test]
1093+
async fn test_metrics_endpoint_with_auth() {
1094+
let bitcoind = TestBitcoind::new();
1095+
1096+
let username = "admin";
1097+
let password = "password123";
1098+
1099+
let config =
1100+
LdkServerConfig { metrics_auth: Some((username.to_string(), password.to_string())) };
1101+
1102+
let server = LdkServerHandle::start_with_config(&bitcoind, config).await;
1103+
let client = server.client();
1104+
1105+
// Should fail because auth is provided in the config
1106+
let result = client.get_metrics().await;
1107+
assert!(result.is_err(), "Expected failure without credentials");
1108+
1109+
// Request has the correct auth, so it should succeed
1110+
let result = client.get_metrics_with_auth(Some(username), Some(password)).await;
1111+
1112+
assert!(result.is_ok(), "Expected success with correct credentials");
1113+
let metrics = result.unwrap();
1114+
1115+
assert!(metrics.contains("ldk_server_total_peers_count 0"));
1116+
assert!(metrics.contains("ldk_server_total_payments_count 0"));
1117+
assert!(metrics.contains("ldk_server_total_successful_payments_count 0"));
1118+
assert!(metrics.contains("ldk_server_total_pending_payments_count 0"));
1119+
assert!(metrics.contains("ldk_server_total_failed_payments_count 0"));
1120+
assert!(metrics.contains("ldk_server_total_channels_count 0"));
1121+
assert!(metrics.contains("ldk_server_total_public_channels_count 0"));
1122+
assert!(metrics.contains("ldk_server_total_private_channels_count 0"));
1123+
assert!(metrics.contains("ldk_server_total_onchain_balance_sats 0"));
1124+
assert!(metrics.contains("ldk_server_spendable_onchain_balance_sats 0"));
1125+
assert!(metrics.contains("ldk_server_total_anchor_channels_reserve_sats 0"));
1126+
assert!(metrics.contains("ldk_server_total_lightning_balance_sats 0"));
1127+
}

ldk-server-client/src/client.rs

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ use ldk_server_protos::endpoints::{
4040
BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH,
4141
CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DECODE_INVOICE_PATH, DECODE_OFFER_PATH,
4242
DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH,
43-
GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, GRAPH_GET_CHANNEL_PATH,
44-
GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, LIST_CHANNELS_PATH,
45-
LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH, ONCHAIN_RECEIVE_PATH,
46-
ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH, SPLICE_OUT_PATH,
47-
SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH, VERIFY_SIGNATURE_PATH,
43+
GET_BALANCES_PATH, GET_METRICS_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH,
44+
GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH,
45+
LIST_CHANNELS_PATH, LIST_FORWARDED_PAYMENTS_PATH, LIST_PAYMENTS_PATH, LIST_PEERS_PATH,
46+
ONCHAIN_RECEIVE_PATH, ONCHAIN_SEND_PATH, OPEN_CHANNEL_PATH, SIGN_MESSAGE_PATH, SPLICE_IN_PATH,
47+
SPLICE_OUT_PATH, SPONTANEOUS_SEND_PATH, UNIFIED_SEND_PATH, UPDATE_CHANNEL_CONFIG_PATH,
48+
VERIFY_SIGNATURE_PATH,
4849
};
4950
use ldk_server_protos::error::{ErrorCode, ErrorResponse};
51+
use prost::bytes::Bytes;
5052
use prost::Message;
5153
use reqwest::header::CONTENT_TYPE;
5254
use reqwest::{Certificate, Client};
@@ -70,6 +72,11 @@ pub struct LdkServerClient {
7072
api_key: String,
7173
}
7274

75+
enum RequestType {
76+
Get,
77+
Post,
78+
}
79+
7380
impl LdkServerClient {
7481
/// Constructs a [`LdkServerClient`] using `base_url` as the ldk-server endpoint.
7582
///
@@ -115,6 +122,27 @@ impl LdkServerClient {
115122
self.post_request(&request, &url).await
116123
}
117124

125+
/// Retrieve the node metrics in Prometheus format.
126+
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
127+
self.get_metrics_with_auth(None, None).await
128+
}
129+
130+
/// Retrieve the node metrics in Prometheus format using Basic Auth.
131+
pub async fn get_metrics_with_auth(
132+
&self, username: Option<&str>, password: Option<&str>,
133+
) -> Result<String, LdkServerError> {
134+
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
135+
let payload =
136+
self.make_request(&url, RequestType::Get, None, false, username, password).await?;
137+
138+
String::from_utf8(payload.to_vec()).map_err(|e| {
139+
LdkServerError::new(
140+
InternalError,
141+
format!("Failed to decode metrics response as string: {}", e),
142+
)
143+
})
144+
}
145+
118146
/// Retrieves an overview of all known balances.
119147
/// For API contract/usage, refer to docs for [`GetBalancesRequest`] and [`GetBalancesResponse`].
120148
pub async fn get_balances(
@@ -450,31 +478,60 @@ impl LdkServerClient {
450478
&self, request: &Rq, url: &str,
451479
) -> Result<Rs, LdkServerError> {
452480
let request_body = request.encode_to_vec();
453-
let auth_header = self.compute_auth_header(&request_body);
454-
let response_raw = self
455-
.client
456-
.post(url)
457-
.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM)
458-
.header("X-Auth", auth_header)
459-
.body(request_body)
460-
.send()
461-
.await
462-
.map_err(|e| {
463-
LdkServerError::new(InternalError, format!("HTTP request failed: {}", e))
464-
})?;
481+
let payload =
482+
self.make_request(url, RequestType::Post, Some(request_body), true, None, None).await?;
483+
Rs::decode(&payload[..]).map_err(|e| {
484+
LdkServerError::new(InternalError, format!("Failed to decode success response: {}", e))
485+
})
486+
}
487+
488+
async fn make_request(
489+
&self, url: &str, request_type: RequestType, body: Option<Vec<u8>>,
490+
hmac_authenticated: bool, metrics_username: Option<&str>, metrics_password: Option<&str>,
491+
) -> Result<Bytes, LdkServerError> {
492+
let builder = match request_type {
493+
RequestType::Get => self.client.get(url),
494+
RequestType::Post => self.client.post(url),
495+
};
496+
497+
let builder = if hmac_authenticated {
498+
let body_for_auth = body.as_deref().unwrap_or(&[]);
499+
let auth_header = self.compute_auth_header(body_for_auth);
500+
builder.header("X-Auth", auth_header)
501+
} else {
502+
builder
503+
};
504+
505+
let builder = if let Some(body_content) = body {
506+
builder.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM).body(body_content)
507+
} else {
508+
builder
509+
};
510+
511+
let builder = if let (Some(username), Some(password)) = (metrics_username, metrics_password)
512+
{
513+
builder.basic_auth(username, Some(password))
514+
} else {
515+
builder
516+
};
517+
518+
let response_raw = builder.send().await.map_err(|e| {
519+
LdkServerError::new(InternalError, format!("HTTP request failed: {}", e))
520+
})?;
465521

522+
self.handle_response(response_raw).await
523+
}
524+
525+
async fn handle_response(
526+
&self, response_raw: reqwest::Response,
527+
) -> Result<Bytes, LdkServerError> {
466528
let status = response_raw.status();
467529
let payload = response_raw.bytes().await.map_err(|e| {
468530
LdkServerError::new(InternalError, format!("Failed to read response body: {}", e))
469531
})?;
470532

471533
if status.is_success() {
472-
Ok(Rs::decode(&payload[..]).map_err(|e| {
473-
LdkServerError::new(
474-
InternalError,
475-
format!("Failed to decode success response: {}", e),
476-
)
477-
})?)
534+
Ok(payload)
478535
} else {
479536
let error_response = ErrorResponse::decode(&payload[..]).map_err(|e| {
480537
LdkServerError::new(

ldk-server-protos/src/endpoints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ pub const GRAPH_LIST_NODES_PATH: &str = "GraphListNodes";
4545
pub const GRAPH_GET_NODE_PATH: &str = "GraphGetNode";
4646
pub const DECODE_INVOICE_PATH: &str = "DecodeInvoice";
4747
pub const DECODE_OFFER_PATH: &str = "DecodeOffer";
48+
pub const GET_METRICS_PATH: &str = "metrics";

ldk-server/ldk-server-config.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,11 @@ client_trusts_lsp = false
8888
## A token we may require to be sent by the clients.
8989
## If set, only requests matching this token will be accepted. (uncomment and set if required)
9090
# require_token = ""
91+
92+
# Metrics settings
93+
[metrics]
94+
enabled = false
95+
poll_metrics_interval = 60 # The polling interval for metrics in seconds. Defaults to 60secs if unset and metrics enabled.
96+
# The auth details below are optional, but uncommenting the fields means enabling basic auth, so valid fields must be supplied.
97+
#username = "" # The username required to access the metrics endpoint (Basic Auth).
98+
#password = "" # The password required to access the metrics endpoint (Basic Auth).

0 commit comments

Comments
 (0)