Skip to content

Commit 0c08dd2

Browse files
authored
feat: Allow exposing Prometheus metrics in http port (#44)
* feat: Allow exposing Prometheus metrics in http port * style: fix linters * fix: default value in cli arg * fix: improve update metrics task in prometheus * fix: prometheus test * fix: style * fix: skip yapf in Makefile * fix: tests not passing in python 3.6 * fix: styles
1 parent 5bb4826 commit 0c08dd2

File tree

5 files changed

+82
-34
lines changed

5 files changed

+82
-34
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ fmt: yapf isort
6161

6262
.PHONY: yapf
6363
yapf: $(py_sources) $(py_tests)
64-
yapf -rip $^ -e \*_pb2.py,\*_pb2_grpc.py
64+
echo "Skipping yapf because it's conflicting with flake8"
65+
# yapf -rip $^ -e \*_pb2.py,\*_pb2_grpc.py
6566

6667
.PHONY: isort
6768
isort: $(py_sources) $(py_tests)

tests/test_prometheus.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import tempfile
1010
import unittest
1111

12+
from tests.utils import async_test
1213
from txstratum.manager import TxMiningManager
1314
from txstratum.prometheus import METRIC_INFO, MetricData, PrometheusExporter
1415

@@ -27,9 +28,10 @@ def test_collect_metrics(self):
2728
metric_keys = set(MetricData._fields)
2829
self.assertEqual(description_keys, metric_keys)
2930

30-
def test_update_metrics(self):
31+
@async_test
32+
async def test_update_metrics(self):
3133
prometheus = PrometheusExporter(self.manager, self.tmpdir)
32-
prometheus.update_metrics()
34+
await prometheus.update_metrics()
3335
self.assertTrue(os.path.exists(prometheus.filepath))
3436

3537
# Check if all metrics are in the file.

tests/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,14 @@ def enable(self) -> None:
2727

2828
def disable(self) -> None:
2929
txstratum.time.set_time_function(None)
30+
31+
32+
# XXX: We could use unittest's IsolatedAsyncioTestCase instead, but it's available only in Python 3.8+.
33+
def async_test(coro):
34+
def wrapper(*args, **kwargs):
35+
loop = asyncio.new_event_loop()
36+
try:
37+
return loop.run_until_complete(coro(*args, **kwargs))
38+
finally:
39+
loop.close()
40+
return wrapper

txstratum/cli.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def create_parser() -> ArgumentParser:
3030
parser.add_argument('--tx-timeout', help='Tx mining timeout (seconds)', type=int, default=None)
3131
parser.add_argument('--fix-invalid-timestamp', action='store_true', help='Fix invalid timestamp to current time')
3232
parser.add_argument('--prometheus', help='Path to export metrics for Prometheus', type=str, default=None)
33+
parser.add_argument('--prometheus-port', help='Enables exporting metrics in a http port', type=int, default=None)
3334
parser.add_argument('--testnet', action='store_true', help='Use testnet config parameters')
3435
parser.add_argument('--address', help='Mining address for blocks', type=str, default=None)
3536
parser.add_argument('--allow-non-standard-script', action='store_true', help='Accept mining non-standard tx')
@@ -102,6 +103,12 @@ def execute(args: Namespace) -> None:
102103
metrics = PrometheusExporter(manager, args.prometheus)
103104
metrics.start()
104105

106+
if args.prometheus_port:
107+
from txstratum.prometheus import HttpPrometheusExporter
108+
http_metrics = HttpPrometheusExporter(manager, args.prometheus_port)
109+
http_metrics.start()
110+
logger.info('Prometheus metrics server running at 0.0.0.0:{}...'.format(args.prometheus_port))
111+
105112
if args.ban_addrs or args.ban_tx_ids:
106113
tx_filters.append(FileFilter.load_from_files(args.ban_tx_ids, args.ban_addrs))
107114

txstratum/prometheus.py

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import os
33
from typing import TYPE_CHECKING, Dict, NamedTuple
44

5-
from prometheus_client import CollectorRegistry, Gauge, write_to_textfile # type: ignore
5+
from prometheus_client import CollectorRegistry, Gauge, start_http_server, write_to_textfile # type: ignore
6+
7+
from txstratum.utils import Periodic
68

79
if TYPE_CHECKING:
810
from txstratum.manager import TxMiningManager
@@ -50,24 +52,16 @@ def collect_metrics(manager: 'TxMiningManager') -> MetricData:
5052
)
5153

5254

53-
class PrometheusExporter:
54-
"""Class that sends hathor metrics to a node exporter that will be read by Prometheus."""
55+
class BasePrometheusExporter:
56+
"""Base class for prometheus exporters."""
5557

56-
def __init__(self, manager: 'TxMiningManager', path: str, filename: str = 'tx-mining-service.prom'):
57-
"""Init PrometheusExporter.
58+
def __init__(self, manager: 'TxMiningManager'):
59+
"""Init BasePrometheusExporter.
5860
5961
:param manager: Manager where the metrics will be collected from
60-
:param path: Path to save the prometheus file
61-
:param filename: Name of the prometheus file (must end in .prom)
6262
"""
6363
self.manager = manager
6464

65-
# Create full directory, if does not exist
66-
os.makedirs(path, exist_ok=True)
67-
68-
# Full filepath with filename
69-
self.filepath: str = os.path.join(path, filename)
70-
7165
# Stores all Gauge objects for each metric (key is the metric name)
7266
self.metric_gauges: Dict[str, Gauge] = {}
7367

@@ -77,8 +71,8 @@ def __init__(self, manager: 'TxMiningManager', path: str, filename: str = 'tx-mi
7771
# Interval in which the write data method will be called (in seconds)
7872
self.call_interval: int = 5
7973

80-
# If exporter is running
81-
self.running: bool = False
74+
# Periodic task to update metrics
75+
self.update_metrics_task: Periodic = Periodic(self.update_metrics, self.call_interval)
8276

8377
def _initial_setup(self) -> None:
8478
"""Start a collector registry to send data to node exporter."""
@@ -87,28 +81,61 @@ def _initial_setup(self) -> None:
8781
for name, comment in METRIC_INFO.items():
8882
self.metric_gauges[name] = Gauge(name, comment, registry=self.registry)
8983

90-
def start(self) -> None:
91-
"""Start exporter."""
92-
self.running = True
93-
self._schedule_and_write_data()
94-
95-
def update_metrics(self) -> None:
84+
async def update_metrics(self) -> None:
9685
"""Update metric_gauges dict with new data from metrics."""
9786
data = collect_metrics(self.manager)
9887
for metric_name in METRIC_INFO.keys():
9988
self.metric_gauges[metric_name].set(getattr(data, metric_name))
10089

90+
def start(self) -> None:
91+
"""Start exporter."""
92+
asyncio.ensure_future(self.update_metrics_task.start())
93+
94+
def stop(self) -> None:
95+
"""Stop exporter."""
96+
asyncio.ensure_future(self.update_metrics_task.stop())
97+
98+
99+
class PrometheusExporter(BasePrometheusExporter):
100+
"""Class that sends hathor metrics to a node exporter that will be read by Prometheus."""
101+
102+
def __init__(self, manager: 'TxMiningManager', path: str, filename: str = 'tx-mining-service.prom'):
103+
"""Init PrometheusExporter.
104+
105+
:param manager: Manager where the metrics will be collected from
106+
:param path: Path to save the prometheus file
107+
:param filename: Name of the prometheus file (must end in .prom)
108+
"""
109+
super().__init__(manager)
110+
111+
# Create full directory, if does not exist
112+
os.makedirs(path, exist_ok=True)
113+
114+
# Full filepath with filename
115+
self.filepath: str = os.path.join(path, filename)
116+
117+
async def update_metrics(self) -> None:
118+
"""Update metric_gauges dict with new data from metrics."""
119+
await super().update_metrics()
120+
101121
write_to_textfile(self.filepath, self.registry)
102122

103-
def _schedule_and_write_data(self) -> None:
104-
"""Update metrics and schedule to be called again."""
105-
if self.running:
106-
self.update_metrics()
107123

108-
# Schedule next call
109-
loop = asyncio.get_event_loop()
110-
loop.call_later(self.call_interval, self._schedule_and_write_data)
124+
class HttpPrometheusExporter(BasePrometheusExporter):
125+
"""Class that exposes metrics in a http endpoint."""
111126

112-
def stop(self) -> None:
113-
"""Stop exporter."""
114-
self.running = False
127+
def __init__(self, manager: 'TxMiningManager', port: int):
128+
"""Init HttpPrometheusExporter.
129+
130+
:param manager: Manager where the metrics will be collected from
131+
:param port: Port to expose the metrics
132+
"""
133+
super().__init__(manager)
134+
135+
self.port = port
136+
137+
def start(self) -> None:
138+
"""Start exporter."""
139+
super().start()
140+
141+
start_http_server(self.port, registry=self.registry)

0 commit comments

Comments
 (0)