Skip to content

Commit e0f8536

Browse files
Add Finalized Block Height Monitoring to Exporter (#69)
* Add Finalized Block Height Monitoring to Exporter * docker-compose * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests * fix tests
1 parent 48415ae commit e0f8536

File tree

6 files changed

+115
-95
lines changed

6 files changed

+115
-95
lines changed

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+
"""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/registries.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ def get_collector_registry(self) -> list:
8888
collector = collectors.StarknetCollector
8989
case "aptos", "aptos":
9090
collector = collectors.AptosCollector
91-
case "tron", "tron":
92-
collector = collectors.TronCollector
9391
case "xrpl", "xrpl":
9492
collector = collectors.XRPLCollector
9593
case "evmhttp", other: # pylint: disable=unused-variable

src/test_collectors.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,44 @@ 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 that finalized_block_height uses correct call and args to get finalized block"""
67+
# Mock with hex string, not integer
68+
mock_block_response = {"number": "0x1a2b3c"}
69+
self.mocked_websocket.return_value.query.return_value = mock_block_response
70+
71+
payload = {
72+
"jsonrpc": "2.0",
73+
"method": "eth_getBlockByNumber",
74+
"params": ["finalized", False],
75+
"id": self.chain_id
76+
}
77+
self.evm_collector.finalized_block_height()
78+
self.mocked_websocket.return_value.query.assert_called_once_with(payload)
79+
80+
def test_finalized_block_height_return_none_when_query_none(self):
81+
"""Tests that finalized_block_height returns None if the query returns None"""
82+
self.mocked_websocket.return_value.query.return_value = None
83+
result = self.evm_collector.finalized_block_height()
84+
self.assertEqual(None, result)
85+
86+
def test_finalized_block_height_return_none_when_no_number_field(self):
87+
"""Tests that finalized_block_height returns None if the response has no 'number' field"""
88+
self.mocked_websocket.return_value.query.return_value = {"hash": "0x123"}
89+
result = self.evm_collector.finalized_block_height()
90+
self.assertEqual(None, result)
91+
92+
def test_finalized_block_height_return(self):
93+
"""Tests that finalized_block_height converts hex block number to integer correctly"""
94+
mock_block_response = {
95+
"number": "0x1a2b3c", # Hex string as your code expects
96+
"hash": "0x456def"
97+
}
98+
self.mocked_websocket.return_value.query.return_value = mock_block_response
99+
result = self.evm_collector.finalized_block_height()
100+
# 0x1a2b3c = 1715004 in decimal
101+
self.assertEqual(1715004, result)
102+
65103
def test_client_version(self):
66104
"""Tests the client_version function uses the correct call and args to get client version"""
67105
payload = {
@@ -735,8 +773,8 @@ def test_latency(self):
735773
self.mocked_connection.return_value.latest_query_latency = 0.123
736774
self.assertEqual(0.123, self.aptos_collector.latency())
737775

738-
class TestTronCollector(TestCase):
739-
"""Tests the Tron collector class"""
776+
class TestEvmHttpCollector(TestCase):
777+
"""Tests the EvmHttp collector class"""
740778

741779
def setUp(self):
742780
self.url = "https://test.com"
@@ -747,70 +785,73 @@ def setUp(self):
747785
self.client_params = {
748786
"open_timeout": self.open_timeout, "ping_timeout": self.ping_timeout}
749787
with mock.patch('collectors.HttpsInterface') as mocked_connection:
750-
self.tron_collector = collectors.TronCollector(
788+
self.evmhttp_collector = collectors.EvmHttpCollector(
751789
self.url, self.labels, self.chain_id, **self.client_params)
752790
self.mocked_connection = mocked_connection
753791

754792
def test_logger_metadata(self):
755793
"""Validate logger metadata. Makes sure url is stripped by helpers.strip_url function."""
756794
expected_metadata = {
757-
'component': 'TronCollector', 'url': 'test.com'}
795+
'component': 'EvmHttpCollector', 'url': 'test.com'}
758796
self.assertEqual(expected_metadata,
759-
self.tron_collector._logger_metadata)
797+
self.evmhttp_collector._logger_metadata)
760798

761799
def test_https_interface_created(self):
762-
"""Tests that the Tron collector calls the https interface with the correct args"""
800+
"""Tests that the EvmHttp collector calls the https interface with the correct args"""
763801
self.mocked_connection.assert_called_once_with(
764802
self.url, self.open_timeout, self.ping_timeout)
765803

766804
def test_interface_attribute_exists(self):
767805
"""Tests that the interface attribute exists."""
768-
self.assertTrue(hasattr(self.tron_collector, 'interface'))
806+
self.assertTrue(hasattr(self.evmhttp_collector, 'interface'))
769807

770808
def test_alive_call(self):
771809
"""Tests the alive function uses the correct call"""
772-
self.tron_collector.alive()
810+
self.evmhttp_collector.alive()
773811
self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with(
774-
self.tron_collector.client_version_payload)
812+
self.evmhttp_collector.client_version_payload)
775813

776814
def test_alive_false(self):
777815
"""Tests the alive function returns false when post returns None"""
778816
self.mocked_connection.return_value.cached_json_rpc_post.return_value = None
779-
result = self.tron_collector.alive()
817+
result = self.evmhttp_collector.alive()
780818
self.assertFalse(result)
781819

782820
def test_block_height(self):
783821
"""Tests the block_height function uses the correct call to get block height"""
784822
self.mocked_connection.return_value.cached_json_rpc_post.return_value = "0x1a2b3c"
785-
result = self.tron_collector.block_height()
823+
result = self.evmhttp_collector.block_height()
786824
self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with(
787-
self.tron_collector.block_height_payload)
825+
self.evmhttp_collector.block_height_payload)
788826
self.assertEqual(result, 1715004)
789827

790828
def test_block_height_raises_value_error(self):
791829
"""Tests that the block height raises ValueError if result is invalid"""
792830
self.mocked_connection.return_value.cached_json_rpc_post.return_value = "invalid"
793831
with self.assertRaises(ValueError):
794-
self.tron_collector.block_height()
832+
self.evmhttp_collector.block_height()
795833

796834
def test_client_version(self):
797-
"""Tests the client_version function uses the correct call to get client version"""
798-
self.mocked_connection.return_value.cached_json_rpc_post.return_value = "Tron/v1.0.0"
799-
result = self.tron_collector.client_version()
835+
"""Tests the client_version function uses the correct call and args to get client version"""
836+
payload = {
837+
"jsonrpc": "2.0",
838+
"method": "web3_clientVersion",
839+
"id": 1
840+
}
841+
self.evmhttp_collector.client_version()
800842
self.mocked_connection.return_value.cached_json_rpc_post.assert_called_once_with(
801-
self.tron_collector.client_version_payload)
802-
self.assertEqual(result, {"client_version": "Tron/v1.0.0"})
843+
payload)
803844

804845
def test_client_version_returns_none(self):
805846
"""Tests that the client_version returns None if cached_json_rpc_post returns None"""
806847
self.mocked_connection.return_value.cached_json_rpc_post.return_value = None
807-
result = self.tron_collector.client_version()
848+
result = self.evmhttp_collector.client_version()
808849
self.assertIsNone(result)
809850

810851
def test_latency(self):
811852
"""Tests that the latency is obtained from the interface based on latest_query_latency"""
812853
self.mocked_connection.return_value.latest_query_latency = 0.123
813-
self.assertEqual(0.123, self.tron_collector.latency())
854+
self.assertEqual(0.123, self.evmhttp_collector.latency())
814855

815856
class TestXRPLCollector(TestCase):
816857
"""Tests the XRPL collector class"""

src/test_metrics.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class TestMetricsLoader(TestCase):
1313
def setUp(self):
1414
self.metrics_loader = MetricsLoader()
1515
self.labels = [
16-
'url', 'provider', 'blockchain', 'network_name', 'network_type',
16+
'url', 'provider', 'blockchain', 'network_name', 'network_type',
1717
'integration_maturity', 'canonical_name', 'chain_selector',
1818
'evmChainID'
1919
]
@@ -171,6 +171,7 @@ def test_collect_yields_correct_metrics(self):
171171
self.mocked_loader.return_value.heads_received_metric,
172172
self.mocked_loader.return_value.disconnects_metric,
173173
self.mocked_loader.return_value.block_height_metric,
174+
self.mocked_loader.return_value.finalized_block_height_metric,
174175
self.mocked_loader.return_value.client_version_metric,
175176
self.mocked_loader.return_value.total_difficulty_metric,
176177
self.mocked_loader.return_value.latency_metric,
@@ -184,14 +185,14 @@ def test_collect_yields_correct_metrics(self):
184185
def test_collect_number_of_yields(self):
185186
"""Tests that the collect method yields the expected number of values"""
186187
results = self.prom_collector.collect()
187-
self.assertEqual(9, len(list(results)))
188+
self.assertEqual(10, len(list(results)))
188189

189190
def test_get_thread_count(self):
190191
"""Tests get thread count returns the expected number of threads
191192
based on number of metrics and collectors"""
192193
thread_count = self.prom_collector.get_thread_count()
193-
# Total of 9 metrics times 2 items in our mocked pool should give 18
194-
self.assertEqual(18, thread_count)
194+
# Total of 10 metrics times 2 items in our mocked pool should give 20
195+
self.assertEqual(20, thread_count)
195196

196197
def test_collect_thread_max_workers(self):
197198
"""Tests the max workers is correct for the collect threads"""

src/test_registries.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,13 @@ def test_get_collector_registry_for_aptos(self):
148148
helper_test_collector_registry(self, collector)
149149

150150
@mock.patch.dict(os.environ, {
151-
"CONFIG_FILE_PATH": "tests/fixtures/configuration_tron.yaml",
151+
"CONFIG_FILE_PATH": "tests/fixtures/configuration_evmhttp.yaml",
152152
"VALIDATION_FILE_PATH": "tests/fixtures/validation.yaml"
153153
})
154-
def test_get_collector_registry_for_tron(self):
155-
"""Tests that the Tron collector is called with the correct args"""
154+
def test_get_collector_registry_for_evmhttp(self):
155+
"""Tests that the EvmHttp collector is called with the correct args"""
156156
self.collector_registry = CollectorRegistry()
157-
with mock.patch('collectors.TronCollector', new=mock.Mock()) as collector:
157+
with mock.patch('collectors.EvmHttpCollector', new=mock.Mock()) as collector:
158158
helper_test_collector_registry(self, collector)
159159

160160
@mock.patch.dict(os.environ, {
@@ -167,16 +167,6 @@ def test_get_collector_registry_for_xrpl(self):
167167
with mock.patch('collectors.XRPLCollector', new=mock.Mock()) as collector:
168168
helper_test_collector_registry(self, collector)
169169

170-
@mock.patch.dict(os.environ, {
171-
"CONFIG_FILE_PATH": "tests/fixtures/configuration_evmhttp.yaml",
172-
"VALIDATION_FILE_PATH": "tests/fixtures/validation.yaml"
173-
})
174-
def test_get_collector_registry_for_evmhttp(self):
175-
"""Tests that the EVM HTTP collector is called with the correct args"""
176-
self.collector_registry = CollectorRegistry()
177-
with mock.patch('collectors.EvmHttpCollector', new=mock.Mock()) as collector:
178-
helper_test_collector_registry(self, collector)
179-
180170
@mock.patch.dict(os.environ, {
181171
"CONFIG_FILE_PATH": "tests/fixtures/configuration_evm.yaml",
182172
"VALIDATION_FILE_PATH": "tests/fixtures/validation.yaml"

0 commit comments

Comments
 (0)