Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Introduce telemetry for observability by Anyitechs · Pull Request #117 · lightningdevkit/ldk-server · GitHub
Skip to content

Introduce telemetry for observability - #117

Merged
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry
Mar 31, 2026
Merged

Introduce telemetry for observability#117
benthecarman merged 1 commit into
lightningdevkit:mainfrom
Anyitechs:introduce-telemetry

Conversation

@Anyitechs

@AnyitechsAnyitechs commented Jan 27, 2026

Copy link
Copy Markdown
Contributor

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.

It adds a new /metrics endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.

  • Added a Metrics utility struct to hold all the metrics we need to
    expose.
  • Made Metrics configurable via config.
  • Added Prometheus basic auth support.

This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.

Related issue #38

@ldk-reviews-bot

ldk-reviews-bot commented Jan 27, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @benthecarman as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 8692c8c to 56b5e4aCompareJanuary 27, 2026 01:51
@Anyitechs
Anyitechs marked this pull request as ready for review January 27, 2026 01:59
Comment threadldk-server/Cargo.toml Outdated
chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid taking this dependency and rather use one of the std::sync primitives (Once/OnceLock/LazyLock), if we need this at all.

Comment threadldk-server/Cargo.toml Outdated
log = "0.4.28"
base64 = { version = "0.21", default-features = false, features = ["std"] }
lazy_static = "1.5.0"
prometheus = "0.14.0"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

If we need it, this should be at the very least made optional behind the metrics feature, and disable any default features.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, its a plain text file with some numbers, don't think we need to take a dep :)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this dependency, or can we just reimplement it easily locally?

We can reimplement locally, but I think the dependency already offers some benefits that will come in handy when we want to provide metrics for things like balances in different states, payments, fees, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also pulls in several solo-maintainer dependencies (okay, with relatively well-known Rust community folks like burntsushi and dtolnay but also the apparently-kinda-unmaintained fnv crate) which we very strongly try to avoid given the security risk. Unless there's something that takes many hundred to a few thousand lines of code or very complicated and hard to test code to replicate, we should absolutely avoid taking a dependency for it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I've implemented building prometheus endpoints in bash and python and....many times with probably less effort than it would have taken to figure out how to add a dependency and use it, so I'm not at all convinced that its worth it here.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, will drop the dependency and reimplement locally.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score? I would find it very hard to interpret, tbh.?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it customary to have such a score?

I think it is as some users might want to rely on that to know how their node is performing per time at a glance.

I would find it very hard to interpret, tbh.?

The current computation is pretty basic and relies on the NodeStatus informations from ldk-node. Though, we might want to refine that to include more informations and decide the best weightage value to assign for each event.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

Comment threadldk-server/src/util/metrics.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 7th Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 6bcc9f8 to 1563d7cCompareFebruary 13, 2026 14:04
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

This is ready for another look. Dropped the deps and reimplemented locally.

@Anyitechs
Anyitechs requested a review from tnullFebruary 13, 2026 14:25
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 8th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 9th Reminder

Hey @tnull@benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment threadldk-server/src/util/metrics.rs Outdated
pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

/// This represents a [`Metrics`] type that can go up and down in value.
pub struct IntGauge {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this extra newtype? Couldn't we just use a plain AtomicI64?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A plain AtomicI64 could work, but I introduce the type for better organization and to represent the Gauge metric type (this will help differentiate it from a Counter type when introduced later, which could also use a AtomicI64 type but are meant to only increase and not decrease, in the metrics world).

Happy to drop if it's too much boilerplate.

Comment threadldk-server/src/util/metrics.rs Outdated
self.service_health_score.set(score);
}

/// The health score computation is pretty basic for now and simply

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's okay as a first proof-of-concept, but IMO it would be much more useful to expose the actual underlying metrics such as peer/channel count, time since last successful chain sync/fee rate update, etc.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add the metric endpoint to the ldk-server-client as well. We don't really need a cli command for it, but would be worth at least putting in the client so we have 100% coverage

Comment threadldk-server/src/util/metrics.rs Outdated
buffer
}

fn compute_health_score(is_running: bool, has_peers: bool, is_wallet_synced: bool) -> i64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score. I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lnd has a prometheus integration. Would be good to look at and see what they are exposing in theirs

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is such a weird health score.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I think it'd be better to give stats about the node (balance, num peers, num channels, etc) and let the user make their own guidelines

Right, I had earlier intended to do this in a follow-up. While this PR sets the structure/foundation, a follow-up will focus more on the metrics we need to expose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to give users an indication on how their node is performing per time without having to look at other individual metrics. I've seen similar metric for other services, but will need to refine this the more.

I'm not entirely opposed to have such an aggregated metric (and we can still discuss what factors to include with what weight, etc), but I agree it only makes sense in addition to exposing the values it's based on.

Comment threadldk-server/src/main.rs Outdated
Comment on lines +266 to +272
runtime.spawn(async move {
loop {
interval.tick().await;
metrics_bg.update_service_health_score(&metrics_node);
}
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of every minute updating this, we should be able to do it real time and just update it when we get a relevant event from ldk-node. ie we get a channel closed event so we update the metrics immediately to reflect that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intend to maintain a hybrid approach for this, real time update for the Node events but still maintain the polling for metrics like channel/peer/payment count, balances, etc.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 262569f to 226b86cCompareFebruary 25, 2026 00:41
Comment threadldk-server-client/src/client.rs Outdated
/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {
let url = format!("https://{}/{GET_METRICS_PATH}", self.base_url);
let response = self.client.get(&url).send().await.map_err(|e| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor post_request to do be able to do GET requests and use that. That has a lot of this logic already and would be better for future use

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment threadldk-server/src/service.rs Outdated
Comment on lines +161 to +168
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {
let metrics = Arc::clone(&self.metrics);
return Box::pin(async move {
Ok(Response::builder()
.header("Content-Type", "text/plain")
.body(Full::new(Bytes::from(metrics.gather_metrics())))
.unwrap())
});

@benthecarmanbenthecarmanFeb 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done. I'm not sure if that'll break the typical Prometheus flow though?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't validating auth here we are short cutting before its done.

Yes, this is intentional because Prometheus does not support the HMAC auth scheme we use. It supports only basic auth and TLS.

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from 1c27292 to 432780cCompareMarch 6, 2026 15:21

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this endpoint isn't authenticated and can't be because of the prometheus limitations, we should make it configurable and default off.

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_channels_count",
"Total number of channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be gauge because it can go down, not just up

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_public_channels_count",
"Total number of public channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
&mut buffer,
"ldk_server_total_private_channels_count",
"Total number of private channels",
"counter",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_successful_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function does a full recount, we should be able to just increase by one

Comment threadldk-server/src/main.rs Outdated
Arc::clone(&event_publisher),
Arc::clone(&paginated_store)).await;

event_metrics.update_total_failed_payments_count(&event_node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment threadldk-server/src/util/metrics.rs Outdated
Comment on lines +116 to +122
self.update_peer_count(node);
self.update_total_payments_count(node);
self.update_total_successful_payments_count(node);
self.update_total_failed_payments_count(node);
self.update_total_channels_count(node);
self.update_total_public_channels_count(node);
self.update_total_private_channels_count(node);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For these we are making multiple calls to list all channels and payments. Would be better to just do that once and then count/filter as needed for each metric

Comment threadldk-server/src/service.rs Outdated

fn call(&self, req: Request<Incoming>) -> Self::Future {
// Handle metrics endpoint separately to bypass auth and return plain text
if req.uri().path().len() > 1 && &req.uri().path()[1..] == GET_METRICS_PATH {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also require the request is a GET request

}

/// Retrieve the node metrics in Prometheus format.
pub async fn get_metrics(&self) -> Result<String, LdkServerError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a raw string, really should be decoded into the Response type

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

really should be decoded into the Response type

The Response type is protobuf, but Promotheus scrapers needs the endpoint to return plain-text

Comment threadldk-server-client/src/client.rs Outdated
RequestType::Post => self.client.post(url),
};

let body_for_auth = body.as_deref().unwrap_or(&[]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can move this into the if authenticated arm


pub const BUILD_METRICS_INTERVAL: Duration = Duration::from_secs(60);

pub struct Metrics {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some docs about how this is used for Prometheus? There's a lot of logic here that is specific to it without any context

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Improved the metrics e2e test and rebased to fix conflicts. Ready for another look!


// Set up channel and make a payment to trigger metric update
setup_funded_channel(&bitcoind, &server_a, &server_b, 100_000).await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to check that the channel (private/public) count, our LN balance, and num peers, goes up after opening a channel. It also sends funds to the nodes for anchor channels so we should be able to verify our on-chain balance numbers change.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also do a receive and check the metrics after that.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the metrics after the channel open but before the receive

We'll only be able to check the channels count metric as that is the only metric dependent on the ChannelReady event. But we can check other metrics if we poll.

Can we also do a receive and check the metrics after that.

This is what we're doing right now.

@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Addressed comment and rebased/squashed. Ready for another look!

Comment threade2e-tests/tests/e2e.rs Outdated
Comment on lines +813 to +815
// We need to poll here because the metrics below are poll-based and
// updates every 60 seconds.
let timeout = Duration::from_secs(65);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really do this. Are these not updated from the events? If we still need this even with the events updating. We should just make the interval configurable and make it like every 1 sec for the tests.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these not updated from the events?

No, the events are for payment-related actions that we emit events for. We do not emit events for the metrics covered there, so we rely on the node to get those information. Because of this, we need to poll to ensure the data is always accurate.

Arc::clone(&paginated_store)).await;

if let Some(metrics) = &metrics {
metrics.update_payments_count(true);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also update it with the channel events and their corresponding metrics

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should be able to update our balance metric here and the payment received one

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information? Same with the channel events

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how this will look, do we want to build/maintain a local balance value based on the payment received event, rather than rely on the node information?

Should be able to just use your Metrics::update_all_balances here

Same with the channel events

channel counts we can just increase/decrease like we do with the payment counts

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from d4e4f38 to c1edca2CompareMarch 20, 2026 05:42
@benthecarmanbenthecarman linked an issue Mar 20, 2026 that may be closed by this pull request
@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 4 times, most recently from d2b00f3 to 23e2f7bCompareMarch 21, 2026 23:10
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

help = "The polling interval for metrics in seconds. Required when
metrics is enabled, but defaults to 60secs if unset."
)]
poll_metrics_interval: Option<u64>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay one last thing, now that we have metrics configs, can we add an option to do the basic auth, that way we don't always have an unautheticated version

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here d4e5ea2

@Anyitechs
Anyitechsforce-pushed the introduce-telemetry branch 2 times, most recently from be0400e to d4e5ea2CompareMarch 27, 2026 19:22
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

Please excuse the delay here, wasn't feeling too great earlier.

Rebased to fix conflicts and addressed the comment on adding basic auth here d4e5ea2.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @benthecarman! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@benthecarmanbenthecarman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one nit, but lgtm on squash!

Comment threade2e-tests/src/lib.rs Outdated
Comment on lines +167 to +169
{}
"#,
metrics_auth_config,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you just put {metrics_auth_config} like we do with all the other variables

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

This introduces the foundational telemetry infrastructure to improve
the observability of LDK Server.
It adds a new `/metrics` endpoint exposed on the REST service address,
which serves Prometheus-compatible metrics. This endpoint is public and
does not require HMAC authentication, allowing for easy integration with
monitoring systems.
- Added a `Metrics` utility struct to hold all the metrics we need to
expose.
- Made `Metrics` configurable via config.
- Added Prometheus basic auth support.
This is the first step in a larger effort to provide comprehensive telemetry.
Future updates will expand this to include other detailed metrics for channels,
balances, payments, etc.
@Anyitechs

Copy link
Copy Markdown
ContributorAuthor

one nit, but lgtm on squash!

Thanks for the review. Addressed comment and rebased with no further changes.

@benthecarman
benthecarman merged commit 7431487 into lightningdevkit:mainMar 31, 2026
8 checks passed
rsafier pushed a commit to rsafier/ldk-server that referenced this pull request Apr 20, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Telemetry

5 participants

@Anyitechs@ldk-reviews-bot@tnull@TheBlueMatt@benthecarman