diff --git a/README.md b/README.md index 201c084..3a0b31d 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,13 @@ Community curated plugins for c-lightning. ## Available plugins -| Name | Short description | -|-----------------------------------|--------------------------------------------| -| [persistent-channels][pers-chans] | Maintains a number of channels to peers | -| [probe](probe/) | Regularly probes the network for stability | +| Name | Short description | +|-----------------------------------|--------------------------------------------------------------| +| [persistent-channels][pers-chans] | Maintains a number of channels to peers | +| [probe][probe] | Regularly probes the network for stability | +| [prometheus][prometheus] | Lightning node exporter for the prometheus timeseries server | [pers-chans]: https://github.com/lightningd/plugins/tree/master/persistent-channels +[probe]: https://github.com/lightningd/plugins/tree/master/probe +[prometheus]: https://github.com/lightningd/plugins/tree/master/prometheus diff --git a/prometheus/README.md b/prometheus/README.md new file mode 100644 index 0000000..b05a7f2 --- /dev/null +++ b/prometheus/README.md @@ -0,0 +1,18 @@ +# Prometheus plugin for c-lightning + +This plugin exposes some key metrics from c-lightning in the prometheus format +so it can be scraped, plotted and alerts can be created on it. The plugin adds +the following command line arguments: + + - `prometheus-listen`: the IP address and port to bind the HTTP server to + (default: `0.0.0.0:9900`) + +Exposed variables include: + + - `node`: ID, version, ... + - `peers`: whether they are connected, and how many channels are currently + open + - `channels`: fund allocations, spendable funds, and how many unresolved + HTLCs are currently attached to the channel + - `funds`: satoshis in on-chain outputs, satoshis allocated to channels and + total sum (may be inaccurate during channel resolution). diff --git a/prometheus/prometheus.py b/prometheus/prometheus.py new file mode 100755 index 0000000..9f1c50a --- /dev/null +++ b/prometheus/prometheus.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +from lightning import Plugin +from prometheus_client import start_http_server, CollectorRegistry +from prometheus_client.core import InfoMetricFamily, GaugeMetricFamily +from sys import exit + +plugin = Plugin() + + +class BaseLnCollector(object): + def __init__(self, rpc, registry): + self.rpc = rpc + self.registry = registry + + +class NodeCollector(BaseLnCollector): + def collect(self): + info = self.rpc.getinfo() + info_labels = {k: v for k, v in info.items() if isinstance(v, str)} + node_info_fam = InfoMetricFamily( + 'node', + 'Static node information', + labels=info_labels.keys(), + ) + node_info_fam.add_metric(info_labels, info_labels) + yield node_info_fam + + +class FundsCollector(BaseLnCollector): + def collect(self): + funds = self.rpc.listfunds() + print(funds['outputs']) + output_funds = sum( + [o['amount_msat'].to_satoshi() for o in funds['outputs']] + ) + channel_funds = sum( + [c['our_amount_msat'].to_satoshi() for c in funds['channels']] + ) + total = output_funds + channel_funds + + yield GaugeMetricFamily( + 'total_funds', + "Total satoshis we own on this node.", + value=total, + ) + yield GaugeMetricFamily( + 'output_funds', + "On-chain satoshis at our disposal", + value=output_funds, + ) + yield GaugeMetricFamily( + 'channel_funds', + "Satoshis in channels.", + value=channel_funds, + ) + + +class PeerCollector(BaseLnCollector): + def collect(self): + peers = self.rpc.listpeers()['peers'] + + connected = GaugeMetricFamily( + 'connected', + 'Is the peer currently connected?', + labels=['id'], + ) + count = GaugeMetricFamily( + 'num_channels', + "The number of channels with the peer", + labels=['id'], + ) + + for p in peers: + labels = [p['id']] + count.add_metric(labels, len(p['channels'])) + connected.add_metric(labels, int(p['connected'])) + + return [count, connected] + + +class ChannelsCollector(BaseLnCollector): + def collect(self): + balance_gauge = GaugeMetricFamily( + 'channel_balance', + 'How many funds are at our disposal?', + labels=['id', 'scid'], + ) + spendable_gauge = GaugeMetricFamily( + 'channel_spendable', + 'How much can we currently send over this channel?', + labels=['id', 'scid'] + ) + total_gauge = GaugeMetricFamily( + 'channel_total', + 'How many funds are in this channel in total?', + labels=['id', 'scid'], + ) + htlc_gauge = GaugeMetricFamily( + 'channel_htlcs', + 'How many HTLCs are currently active on this channel?', + labels=['id', 'scid'], + ) + + peers = self.rpc.listpeers()['peers'] + for p in peers: + for c in p['channels']: + labels = [p['id'], c['short_channel_id']] + balance_gauge.add_metric(labels, c['to_us_msat'].to_satoshi()) + spendable_gauge.add_metric(labels, + c['spendable_msat'].to_satoshi()) + total_gauge.add_metric(labels, c['total_msat'].to_satoshi()) + htlc_gauge.add_metric(labels, len(c['htlcs'])) + + return [htlc_gauge, total_gauge, spendable_gauge, balance_gauge] + + +@plugin.init() +def init(options, configuration, plugin): + s = options['prometheus-listen'].rpartition(':') + if len(s) != 3 or s[1] != ':': + print("Could not parse prometheus-listen address") + exit(1) + ip, port = s[0], int(s[2]) + + registry = CollectorRegistry() + start_http_server(addr=ip, port=port, registry=registry) + registry.register(NodeCollector(plugin.rpc, registry)) + registry.register(FundsCollector(plugin.rpc, registry)) + registry.register(PeerCollector(plugin.rpc, registry)) + registry.register(ChannelsCollector(plugin.rpc, registry)) + + +plugin.add_option( + 'prometheus-listen', + '0.0.0.0:9900', + 'Address and port to bind to' +) + + +plugin.run() diff --git a/prometheus/requirements.txt b/prometheus/requirements.txt new file mode 100644 index 0000000..9ace978 --- /dev/null +++ b/prometheus/requirements.txt @@ -0,0 +1,2 @@ +prometheus-client==0.6.0 +pylightning>=0.0.6