Skip to content

Commit 4df75c2

Browse files
committed
Add Finalized Block Height Monitoring to Exporter
1 parent 48415ae commit 4df75c2

File tree

4 files changed

+114
-74
lines changed

4 files changed

+114
-74
lines changed

docker-compose.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ services:
1919
- monitoring
2020
volumes:
2121
- type: bind
22-
source: ${PWD}/${CONFIG_FILE_PATH}
22+
source: ./config.yml
2323
target: /config/config.yml
24-
- type: bind
25-
source: ${PWD}/${VALIDATION_FILE_PATH}
26-
target: /config/validation.yml
24+
# - type: bind
25+
# source: ${PWD}/${VALIDATION_FILE_PATH}
26+
# target: /config/validation.yml
2727
prometheus:
2828
image: prom/prometheus:latest
2929
logging:

src/collectors.py

Lines changed: 33 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ def block_height(self):
2828
"""Returns latest block height."""
2929
return self.interface.get_message_property_to_hex('number')
3030

31+
def finalized_block_height(self):
32+
"""Runs a query to return finalized block height"""
33+
payload = {
34+
"jsonrpc": "2.0",
35+
"method": "eth_getBlockByNumber",
36+
"params": ["finalized", False],
37+
"id": self.chain_id
38+
}
39+
40+
finalized_block = self.interface.query(payload)
41+
if finalized_block is None:
42+
return None
43+
block_number_hex = finalized_block.get('number')
44+
if block_number_hex is None:
45+
return None
46+
return int(block_number_hex, 16)
47+
3148
def heads_received(self):
3249
"""Returns amount of received messages from the subscription."""
3350
return self.interface.heads_received
@@ -54,7 +71,6 @@ def client_version(self):
5471
client_version = {"client_version": version}
5572
return client_version
5673

57-
5874
class ConfluxCollector():
5975
"""A collector to fetch information about conflux RPC endpoints."""
6076

@@ -394,60 +410,6 @@ def latency(self):
394410
"""Returns connection latency."""
395411
return self.interface.latest_query_latency
396412

397-
class TronCollector():
398-
"""A collector to fetch information from Tron endpoints."""
399-
400-
def __init__(self, url, labels, chain_id, **client_parameters):
401-
402-
self.labels = labels
403-
self.chain_id = chain_id
404-
self.interface = HttpsInterface(url, client_parameters.get('open_timeout'),
405-
client_parameters.get('ping_timeout'))
406-
407-
self._logger_metadata = {
408-
'component': 'TronCollector',
409-
'url': strip_url(url)
410-
}
411-
self.client_version_payload = {
412-
'jsonrpc': '2.0',
413-
'method': "web3_clientVersion",
414-
'id': 1
415-
}
416-
self.block_height_payload = {
417-
'jsonrpc': '2.0',
418-
'method': "eth_blockNumber",
419-
'id': 1
420-
}
421-
422-
def alive(self):
423-
"""Returns true if endpoint is alive, false if not."""
424-
# Run cached query because we can also fetch client version from this
425-
# later on. This will save us an RPC call per run.
426-
return self.interface.cached_json_rpc_post(
427-
self.client_version_payload) is not None
428-
429-
def block_height(self):
430-
"""Cached query and returns blockheight after converting hex string value to an int"""
431-
result = self.interface.cached_json_rpc_post(self.block_height_payload)
432-
433-
if result and isinstance(result, str) and result.startswith('0x'):
434-
return int(result, 16)
435-
raise ValueError(f"Invalid block height result: {result}")
436-
437-
def client_version(self):
438-
"""Runs a cached query to return client version."""
439-
version = self.interface.cached_json_rpc_post(
440-
self.client_version_payload)
441-
if version is None:
442-
return None
443-
client_version = {"client_version": version}
444-
return client_version
445-
446-
def latency(self):
447-
"""Returns connection latency."""
448-
return self.interface.latest_query_latency
449-
450-
451413
class EvmHttpCollector():
452414
"""A collector to fetch information from EVM HTTPS endpoints."""
453415

@@ -472,6 +434,12 @@ def __init__(self, url, labels, chain_id, **client_parameters):
472434
'method': "eth_blockNumber",
473435
'id': 1
474436
}
437+
self.finalized_block_height_payload = {
438+
"jsonrpc": "2.0",
439+
"method": "eth_getBlockByNumber",
440+
"params": ["finalized", False],
441+
"id": 1
442+
}
475443

476444
def alive(self):
477445
"""Returns true if endpoint is alive, false if not."""
@@ -488,6 +456,16 @@ def block_height(self):
488456
return int(result, 16)
489457
raise ValueError(f"Invalid block height result: {result}")
490458

459+
def finalized_block_height(self):
460+
"""Cached query and returns finalized blockheight after converting hex string value to an int"""
461+
finalized_block = self.interface.json_rpc_post(self.finalized_block_height_payload)
462+
if finalized_block is None:
463+
return None
464+
block_number_hex = finalized_block.get('number')
465+
if block_number_hex is None:
466+
return None
467+
return int(block_number_hex, 16)
468+
491469
def client_version(self):
492470
"""Runs a cached query to return client version."""
493471
version = self.interface.cached_json_rpc_post(

src/metrics.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ def block_height_metric(self):
4545
'Latest observed block_height.',
4646
labels=self._labels)
4747

48+
@property
49+
def finalized_block_height_metric(self):
50+
"""Returns instantiated finalized block height metric"""
51+
return GaugeMetricFamily(
52+
'brpc_finalized_block_height',
53+
'Latest finalized block height',
54+
labels=self._labels)
55+
4856
@property
4957
def client_version_metric(self):
5058
"""Returns instantiated client version metric."""
@@ -126,6 +134,7 @@ def collect(self):
126134
heads_received_metric = self._metrics_loader.heads_received_metric
127135
disconnects_metric = self._metrics_loader.disconnects_metric
128136
block_height_metric = self._metrics_loader.block_height_metric
137+
finalized_block_height_metric = self._metrics_loader.finalized_block_height_metric
129138
client_version_metric = self._metrics_loader.client_version_metric
130139
total_difficulty_metric = self._metrics_loader.total_difficulty_metric
131140
latency_metric = self._metrics_loader.latency_metric
@@ -142,6 +151,8 @@ def collect(self):
142151
client_version_metric, 'client_version')
143152
executor.submit(self._write_metric, collector,
144153
block_height_metric, 'block_height')
154+
executor.submit(self._write_metric, collector,
155+
finalized_block_height_metric, 'finalized_block_height')
145156
executor.submit(self._write_metric, collector,
146157
heads_received_metric, 'heads_received')
147158
executor.submit(self._write_metric, collector,
@@ -159,6 +170,7 @@ def collect(self):
159170
yield heads_received_metric
160171
yield disconnects_metric
161172
yield block_height_metric
173+
yield finalized_block_height_metric
162174
yield client_version_metric
163175
yield total_difficulty_metric
164176
yield latency_metric

src/test_collectors.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,56 @@ def test_block_height(self):
6262
self.mocked_websocket.return_value.get_message_property_to_hex.assert_called_once_with(
6363
'number')
6464

65+
def test_finalized_block_height(self):
66+
"""Tests the finalized_block_height function uses the correct call and args to get finalized block height"""
67+
payload = {
68+
"jsonrpc": "2.0",
69+
"method": "eth_getBlockByNumber",
70+
"params": ["finalized", False],
71+
"id": self.chain_id
72+
}
73+
self.evm_collector.finalized_block_height()
74+
self.mocked_websocket.return_value.query.assert_called_once_with(payload)
75+
76+
def test_finalized_block_height_return_none_when_query_none(self):
77+
"""Tests that finalized_block_height returns None if the query returns None"""
78+
self.mocked_websocket.return_value.query.return_value = None
79+
result = self.evm_collector.finalized_block_height()
80+
self.assertEqual(None, result)
81+
82+
def test_finalized_block_height_return_none_when_no_number_field(self):
83+
"""Tests that finalized_block_height returns None if the response has no 'number' field"""
84+
self.mocked_websocket.return_value.query.return_value = {"hash": "0x123"}
85+
result = self.evm_collector.finalized_block_height()
86+
self.assertEqual(None, result)
87+
88+
def test_finalized_block_height_return(self):
89+
"""Tests that finalized_block_height converts hex block number to integer correctly"""
90+
mock_block_response = {
91+
"number": "0x1a2b3c",
92+
"hash": "0x456def"
93+
}
94+
self.mocked_websocket.return_value.query.return_value = mock_block_response
95+
result = self.evm_collector.finalized_block_height()
96+
# 0x1a2b3c = 1715004 in decimal
97+
self.assertEqual(1715004, result)
98+
99+
def test_finalized_block_height_hex_conversion(self):
100+
"""Tests that finalized_block_height handles various hex values correctly"""
101+
test_cases = [
102+
("0x0", 0),
103+
("0x1", 1),
104+
("0xff", 255),
105+
("0x1000", 4096)
106+
]
107+
108+
for hex_value, expected_int in test_cases:
109+
with self.subTest(hex_value=hex_value):
110+
mock_block_response = {"number": hex_value}
111+
self.mocked_websocket.return_value.query.return_value = mock_block_response
112+
result = self.evm_collector.finalized_block_height()
113+
self.assertEqual(expected_int, result)
114+
65115
def test_client_version(self):
66116
"""Tests the client_version function uses the correct call and args to get client version"""
67117
payload = {
@@ -735,8 +785,8 @@ def test_latency(self):
735785
self.mocked_connection.return_value.latest_query_latency = 0.123
736786
self.assertEqual(0.123, self.aptos_collector.latency())
737787

738-
class TestTronCollector(TestCase):
739-
"""Tests the Tron collector class"""
788+
class TestEvmHttpCollector(TestCase):
789+
"""Tests the EvmHttp collector class"""
740790

741791
def setUp(self):
742792
self.url = "https://test.com"
@@ -747,7 +797,7 @@ def setUp(self):
747797
self.client_params = {
748798
"open_timeout": self.open_timeout, "ping_timeout": self.ping_timeout}
749799
with mock.patch('collectors.HttpsInterface') as mocked_connection:
750-
self.tron_collector = collectors.TronCollector(
800+
self.evmhttp_collector = collectors.EvmHttpCollector(
751801
self.url, self.labels, self.chain_id, **self.client_params)
752802
self.mocked_connection = mocked_connection
753803

@@ -756,7 +806,7 @@ def test_logger_metadata(self):
756806
expected_metadata = {
757807
'component': 'TronCollector', 'url': 'test.com'}
758808
self.assertEqual(expected_metadata,
759-
self.tron_collector._logger_metadata)
809+
self.evmhttp_collector._logger_metadata)
760810

761811
def test_https_interface_created(self):
762812
"""Tests that the Tron collector calls the https interface with the correct args"""
@@ -765,52 +815,52 @@ def test_https_interface_created(self):
765815

766816
def test_interface_attribute_exists(self):
767817
"""Tests that the interface attribute exists."""
768-
self.assertTrue(hasattr(self.tron_collector, 'interface'))
818+
self.assertTrue(hasattr(self.evmhttp_collector, 'interface'))
769819

770820
def test_alive_call(self):
771821
"""Tests the alive function uses the correct call"""
772-
self.tron_collector.alive()
822+
self.evmhttp_collector.alive()
773823
self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with(
774-
self.tron_collector.client_version_payload)
824+
self.evmhttp_collector.client_version_payload)
775825

776826
def test_alive_false(self):
777827
"""Tests the alive function returns false when post returns None"""
778828
self.mocked_connection.return_value.cached_json_rpc_post.return_value = None
779-
result = self.tron_collector.alive()
829+
result = self.evmhttp_collector.alive()
780830
self.assertFalse(result)
781831

782832
def test_block_height(self):
783833
"""Tests the block_height function uses the correct call to get block height"""
784834
self.mocked_connection.return_value.cached_json_rpc_post.return_value = "0x1a2b3c"
785-
result = self.tron_collector.block_height()
835+
result = self.evmhttp_collector.block_height()
786836
self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with(
787-
self.tron_collector.block_height_payload)
837+
self.evmhttp_collector.block_height_payload)
788838
self.assertEqual(result, 1715004)
789839

790840
def test_block_height_raises_value_error(self):
791841
"""Tests that the block height raises ValueError if result is invalid"""
792842
self.mocked_connection.return_value.cached_json_rpc_post.return_value = "invalid"
793843
with self.assertRaises(ValueError):
794-
self.tron_collector.block_height()
844+
self.evmhttp_collector.block_height()
795845

796846
def test_client_version(self):
797847
"""Tests the client_version function uses the correct call to get client version"""
798848
self.mocked_connection.return_value.cached_json_rpc_post.return_value = "Tron/v1.0.0"
799-
result = self.tron_collector.client_version()
849+
result = self.evmhttp_collector.client_version()
800850
self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with(
801-
self.tron_collector.client_version_payload)
851+
self.evmhttp_collector.client_version_payload)
802852
self.assertEqual(result, {"client_version": "Tron/v1.0.0"})
803853

804854
def test_client_version_returns_none(self):
805855
"""Tests that the client_version returns None if cached_json_rpc_post returns None"""
806856
self.mocked_connection.return_value.cached_json_rpc_post.return_value = None
807-
result = self.tron_collector.client_version()
857+
result = self.evmhttp_collector.client_version()
808858
self.assertIsNone(result)
809859

810860
def test_latency(self):
811861
"""Tests that the latency is obtained from the interface based on latest_query_latency"""
812862
self.mocked_connection.return_value.latest_query_latency = 0.123
813-
self.assertEqual(0.123, self.tron_collector.latency())
863+
self.assertEqual(0.123, self.evmhttp_collector.latency())
814864

815865
class TestXRPLCollector(TestCase):
816866
"""Tests the XRPL collector class"""

0 commit comments

Comments
 (0)