Skip to content

Commit b903183

Browse files
authored
V3.0.0 (#17)
* Labels refactoring and additional error handling. * Update README.md
1 parent 0b42f98 commit b903183

File tree

13 files changed

+243
-278
lines changed

13 files changed

+243
-278
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
The exporter is used to scrape metrics from blockchain RPC endpoints. The purpose of this exporter is to perform black-box testing on RPC endpoints.
33
## Metrics
44
Exporter currently supports all EVM-compatible chains. In addition, there is limited support for the following chains:
5-
- Cardano
6-
- Conflux
7-
- Solana
8-
- Bitcoin
9-
- Dogecoin
10-
- Filecoin
11-
- Starkware
5+
- Cardano (wss)
6+
- Conflux (wss)
7+
- Solana (https & wss)
8+
- Bitcoin (https)
9+
- Dogecoin (https)
10+
- Filecoin (https)
11+
- Starkware (https)
1212

1313
# Disclaimer
1414
Please note that this tool is in the early development stage and should not be used to influence critical business decisions.

config/exporter_example/config.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@ connection_parameters:
1010
ping_timeout: 3 # Liveness ping timeout
1111
collector: "evm" # This will load different collectors based on what mode exporter will run with Supported modes are: "evm", "solana", "conflux", "cardano", "bitcoin"
1212
endpoints: # List of endpoints with their metadata.
13-
- ws_url: wss://example-rpc-1.com/ws # RPC Endpoint websocket endpoint (Must start with wss://)
14-
https_url: https://example-rpc-1.com/rpc # RPC Endpoint https endpoint (Must be valid https:// domain)
13+
- url: wss://example-rpc-1.com/ws # RPC Endpoint websocket endpoint (Must start with wss:// or https://)
1514
provider: Provider1 # Provider (Must be present in allowed providers list. Please check src/settings.py line 24) The purpose is to make sure we do not have same providers spelled differently
16-
- ws_url: wss://example-rpc-2.com/ws
17-
https_url: https://example-rpc-2.com/rpc
15+
- url: wss://example-rpc-2.com/ws
1816
provider: Provider2
19-
- ws_url: wss://example-rpc-3.com/ws
20-
https_url: https://example-rpc-3.com/rpc
17+
- url: wss://example-rpc-3.com/ws
2118
provider: Provider3
19+
# Solana specific
20+
- url: https://example-solana-rpc-1.com/rpc
21+
subscribe_url: wss://example-solana-rpc-1.com/ws
22+
provider: Provider3
23+
24+
2225

2326

2427

28+
##

src/collectors/bitcoin.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,30 @@
11
import asyncio
22
from bitcoinrpc import BitcoinRPC
3-
from helpers import strip_url
3+
from helpers import strip_url, validate_protocol, generate_labels_from_metadata
44
from time import perf_counter
5-
from settings import cfg, logger
5+
from settings import logger
66

77

88
class bitcoin_collector():
99

10-
def __init__(self, https_url, provider):
11-
self.https_url = https_url
12-
self.labels = [
13-
'https_url', 'provider', 'blockchain', 'network_name',
14-
'network_type'
15-
]
16-
self.labels_values = [
17-
https_url, provider, cfg.blockchain, cfg.network_name,
18-
cfg.network_type
19-
]
10+
def __init__(self, rpc_metadata):
11+
validate_protocol(rpc_metadata['url'], "https")
12+
self.url = rpc_metadata['url']
13+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
2014

2115
async def _probe(self, metrics):
2216
try:
23-
async with BitcoinRPC(self.https_url, "admin", "admin") as rpc:
17+
async with BitcoinRPC(self.url, "admin", "admin") as rpc:
2418
start = perf_counter()
2519
chain_info = await rpc.getblockchaininfo()
2620
latency = (perf_counter() - start) * 1000
2721

2822
metrics['ws_rpc_health'].add_metric(self.labels_values, True)
29-
metrics['ws_rpc_latency'].add_metric(self.labels_values,
30-
latency)
31-
metrics['ws_rpc_block_height'].add_metric(
32-
self.labels_values, chain_info['headers'])
33-
metrics['ws_rpc_total_difficulty'].add_metric(
34-
self.labels_values, chain_info['difficulty'])
23+
metrics['ws_rpc_latency'].add_metric(self.labels_values, latency)
24+
metrics['ws_rpc_block_height'].add_metric(self.labels_values, chain_info['headers'])
25+
metrics['ws_rpc_total_difficulty'].add_metric(self.labels_values, chain_info['difficulty'])
3526
except Exception as exc:
36-
logger.error("Failed probing {} with error: {}".format(
37-
strip_url(self.url), exc))
27+
logger.error("Failed probing {} with error: {}".format(strip_url(self.url), exc))
3828
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
3929

4030
def probe(self, metrics):

src/collectors/cardano.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
from settings import cfg, logger
1+
from settings import logger
22
import json
33
from collectors.ws import websocket_collector
4-
from helpers import strip_url
4+
from helpers import strip_url, validate_protocol, generate_labels_from_metadata
55

66

77
class cardano_collector():
88

9-
def __init__(self, url, provider):
10-
self.labels = ['url', 'provider', 'blockchain', 'network_name', 'network_type']
11-
self.labels_values = [url, provider, cfg.blockchain, cfg.network_name, cfg.network_type]
12-
self.url = url
13-
self.ws_collector = websocket_collector(url, provider)
9+
def __init__(self, rpc_metadata):
10+
validate_protocol(rpc_metadata['url'], "wss")
11+
self.url = rpc_metadata['url']
12+
13+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
14+
self.ws_collector = websocket_collector(self.url)
1415

1516
def _get_block_height(self):
1617
blk_height_payload = {

src/collectors/conflux.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
11
from settings import cfg, logger
22
from conflux_web3 import Web3
33
import asyncio
4-
from helpers import strip_url
4+
from helpers import strip_url, validate_protocol, generate_labels_from_metadata
55
from collectors.ws import websocket_collector
66

77

88
class conflux_collector():
99

10-
def __init__(self, websocket_url, https_url, provider):
11-
self.client = Web3(Web3.WebsocketProvider(websocket_url, websocket_timeout=cfg.response_timeout))
12-
self.labels = [
13-
'websocket_url', 'https_url', 'provider', 'blockchain', 'network_name', 'network_type', 'evmChainID'
14-
]
15-
self.labels_values = [
16-
websocket_url, https_url, provider, cfg.blockchain, cfg.network_name, cfg.network_type,
17-
str(cfg.chain_id)
18-
]
19-
self.websocket_url = websocket_url
20-
self.ws_collector = websocket_collector(websocket_url,
10+
def __init__(self, rpc_metadata):
11+
validate_protocol(rpc_metadata['url'], "wss")
12+
self.url = rpc_metadata['url']
13+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
14+
self.client = Web3(Web3.WebsocketProvider(self.url, websocket_timeout=cfg.response_timeout))
15+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
16+
17+
self.ws_collector = websocket_collector(self.url,
2118
sub_payload={
2219
"method": "cfx_subscribe",
2320
"jsonrpc": "2.0",
@@ -26,7 +23,6 @@ def __init__(self, websocket_url, https_url, provider):
2623
})
2724
self.ws_collector.setDaemon(True)
2825
self.ws_collector.start()
29-
self.first_time = False
3026

3127
def probe(self, metrics):
3228
try:
@@ -37,16 +33,20 @@ def probe(self, metrics):
3733
metrics['ws_rpc_latency'].add_metric(self.labels_values, self.ws_collector.get_latency())
3834
metrics['ws_rpc_block_height'].add_metric(self.labels_values, self.client.cfx.epoch_number)
3935

40-
metrics['ws_rpc_difficulty'].add_metric(
41-
self.labels_values,
42-
self.client.cfx.get_block_by_hash(self.client.cfx.get_best_block_hash())['difficulty'])
36+
try:
37+
difficulty = self.client.cfx.get_block_by_hash(self.client.cfx.get_best_block_hash())['difficulty']
38+
metrics['ws_rpc_difficulty'].add_metric(self.labels_values, difficulty)
39+
except TypeError:
40+
logger.error(
41+
"RPC Endpoint sent faulty response type when querying for difficulty. This is most likely issue with RPC endpoint."
42+
)
4343

4444
metrics['ws_rpc_gas_price'].add_metric(self.labels_values, self.client.cfx.gas_price)
4545
else:
46-
logger.info("Client is not connected to {}".format(strip_url(self.websocket_url)))
46+
logger.info("Client is not connected to {}".format(strip_url(self.url)))
4747
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
4848
except asyncio.exceptions.TimeoutError:
49-
logger.info("Client timed out for {}".format(strip_url(self.websocket_url)))
49+
logger.info("Client timed out for {}".format(strip_url(self.url)))
5050
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
5151
except Exception as exc:
52-
logger.error("Failed probing {} with error: {}".format(strip_url(self.url), exc))
52+
logger.error("Failed probing {} with error: {}".format(strip_url(self.url), exc))

src/collectors/dogecoin.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,34 @@
1-
from helpers import strip_url
2-
from settings import cfg, logger
1+
from helpers import strip_url, validate_protocol, generate_labels_from_metadata
2+
from settings import logger
33
from time import perf_counter
44
import requests
55

66

77
class doge_collector():
88

9-
def __init__(self, https_url, provider):
10-
self.https_url = https_url
11-
self.labels = [
12-
'https_url', 'provider', 'blockchain', 'network_name',
13-
'network_type'
14-
]
15-
self.labels_values = [
16-
https_url, provider, cfg.blockchain, cfg.network_name,
17-
cfg.network_type
18-
]
9+
def __init__(self, rpc_metadata):
10+
validate_protocol(rpc_metadata['url'], "https")
11+
self.url = rpc_metadata['url']
12+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
1913

2014
def probe(self, metrics):
2115
try:
2216
payload = {'version': '1.1', 'method': "getinfo", 'id': 1}
2317
start = perf_counter()
24-
response = requests.post(self.https_url, json=payload).json()
18+
response = requests.post(self.url, json=payload).json()
2519
latency = (perf_counter() - start) * 1000
2620

2721
if response:
2822
metrics['ws_rpc_health'].add_metric(self.labels_values, True)
29-
metrics['ws_rpc_latency'].add_metric(self.labels_values,
30-
latency)
31-
metrics['ws_rpc_block_height'].add_metric(
32-
self.labels_values, response['result']['blocks'])
33-
metrics['ws_rpc_total_difficulty'].add_metric(
34-
self.labels_values, response['result']['difficulty'])
23+
metrics['ws_rpc_latency'].add_metric(self.labels_values, latency)
24+
metrics['ws_rpc_block_height'].add_metric(self.labels_values, response['result']['blocks'])
25+
metrics['ws_rpc_total_difficulty'].add_metric(self.labels_values, response['result']['difficulty'])
3526
else:
36-
logger.error("Bad response from client {}: {}".format(
37-
strip_url(self.https_url), exc))
27+
logger.error("Bad response from client {}: {}".format(strip_url(self.url), exc))
3828
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
3929
except requests.RequestException as exc:
40-
logger.error("Health check failed for {}: {}".format(
41-
strip_url(self.https_url), exc))
30+
logger.error("Health check failed for {}: {}".format(strip_url(self.url), exc))
4231
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
4332
except Exception as exc:
44-
logger.error("Health check failed for {}: {}".format(
45-
strip_url(self.https_url), exc))
33+
logger.error("Health check failed for {}: {}".format(strip_url(self.url), exc))
4634
metrics['ws_rpc_health'].add_metric(self.labels_values, False)

src/collectors/evm.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
11
from settings import cfg, logger
22
from web3 import Web3
33
import asyncio
4-
from helpers import strip_url
4+
from helpers import strip_url, validate_protocol, generate_labels_from_metadata
55
from collectors.ws import websocket_collector
66
from websockets.exceptions import WebSocketException
77

8+
89
class evm_collector():
910

10-
def __init__(self, websocket_url, https_url, provider):
11-
self.client = Web3(Web3.WebsocketProvider(websocket_url, websocket_timeout=cfg.response_timeout))
12-
self.labels = [
13-
'websocket_url', 'https_url', 'provider', 'blockchain', 'network_name', 'network_type', 'evmChainID'
14-
]
15-
self.labels_values = [
16-
websocket_url, https_url, provider, cfg.blockchain, cfg.network_name, cfg.network_type,
17-
str(cfg.chain_id)
18-
]
19-
self.websocket_url = websocket_url
20-
self.ws_collector = websocket_collector(websocket_url,
11+
def __init__(self, rpc_metadata):
12+
validate_protocol(rpc_metadata['url'], "wss")
13+
self.url = rpc_metadata['url']
14+
self.client = Web3(Web3.WebsocketProvider(self.url, websocket_timeout=cfg.response_timeout))
15+
16+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
17+
18+
self.ws_collector = websocket_collector(self.url,
2119
sub_payload={
2220
"method": "eth_subscribe",
2321
"jsonrpc": "2.0",
24-
"id": cfg.chain_id,
22+
"id": rpc_metadata['chain_id'],
2523
"params": ["newHeads"]
2624
})
25+
self.net_peer_enabled = True
2726
self.ws_collector.setDaemon(True)
2827
self.ws_collector.start()
2928

30-
def probe(self, metrics):
29+
def probe(self, metrics):
3130
try:
3231
if self.client.isConnected():
3332
metrics['ws_rpc_health'].add_metric(self.labels_values, True)
@@ -39,17 +38,26 @@ def probe(self, metrics):
3938
self.client.eth.get_block('latest')['totalDifficulty'])
4039
metrics['ws_rpc_difficulty'].add_metric(self.labels_values,
4140
self.client.eth.get_block('latest')['difficulty'])
42-
metrics['ws_rpc_net_peer_count'].add_metric(self.labels_values, self.client.net.peer_count)
41+
42+
try:
43+
if self.net_peer_enabled:
44+
metrics['ws_rpc_net_peer_count'].add_metric(self.labels_values, self.client.net.peer_count)
45+
except ValueError:
46+
logger.error(
47+
"Net peer function is not supported for this chain, the collector will ignore this from this point on."
48+
)
49+
self.net_peer_enabled = False
50+
4351
metrics['ws_rpc_gas_price'].add_metric(self.labels_values, self.client.eth.gas_price)
4452
metrics['ws_rpc_max_priority_fee'].add_metric(self.labels_values, self.client.eth.max_priority_fee)
4553
else:
46-
logger.info("Client is not connected to {}".format(strip_url(self.websocket_url)))
54+
logger.info("Client is not connected to {}".format(strip_url(self.url)))
4755
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
4856
except asyncio.exceptions.TimeoutError as exc:
49-
logger.info("Client timed out for {}: {}".format(strip_url(self.websocket_url), exc))
57+
logger.info("Client timed out for {}: {}".format(strip_url(self.url), exc))
5058
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
5159
except WebSocketException as exc:
52-
logger.info("Websocket client exception {}: {}".format(strip_url(self.websocket_url), exc))
60+
logger.info("Websocket client exception {}: {}".format(strip_url(self.url), exc))
5361
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
5462
except Exception as exc:
5563
logger.error("Failed probing {} with error: {}".format(strip_url(self.url), exc))

src/collectors/filecoin.py

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,35 @@
1-
from helpers import strip_url
2-
from settings import cfg, logger
1+
from helpers import strip_url, validate_protocol, generate_labels_from_metadata
2+
from settings import logger
33
from time import perf_counter
44
import requests
55

66

77
class filecoin_collector():
88

9-
def __init__(self, https_url, provider):
10-
self.https_url = https_url
11-
self.labels = [
12-
'https_url', 'provider', 'blockchain', 'network_name',
13-
'network_type'
14-
]
15-
self.labels_values = [
16-
https_url, provider, cfg.blockchain, cfg.network_name,
17-
cfg.network_type
18-
]
9+
def __init__(self, rpc_metadata):
10+
validate_protocol(rpc_metadata['url'], "https")
11+
self.url = rpc_metadata['url']
12+
13+
self.labels, self.labels_values = generate_labels_from_metadata(rpc_metadata)
1914

2015
def probe(self, metrics):
2116
try:
22-
payload = {
23-
'jsonrpc': '2.0',
24-
'method': "Filecoin.ChainHead",
25-
'id': 1
26-
}
17+
payload = {'jsonrpc': '2.0', 'method': "Filecoin.ChainHead", 'id': 1}
2718
start = perf_counter()
28-
response = requests.post(self.https_url, json=payload).json()
19+
response = requests.post(self.url, json=payload)
2920
latency = (perf_counter() - start) * 1000
3021

31-
if response:
22+
if response.ok:
3223
metrics['ws_rpc_health'].add_metric(self.labels_values, True)
33-
metrics['ws_rpc_latency'].add_metric(self.labels_values,
34-
latency)
35-
metrics['ws_rpc_block_height'].add_metric(
36-
self.labels_values, response['result']['Height'])
24+
metrics['ws_rpc_latency'].add_metric(self.labels_values, latency)
25+
metrics['ws_rpc_block_height'].add_metric(self.labels_values, response.json()['result']['Height'])
3726
else:
38-
logger.error("Bad response from client {}: {}".format(
39-
strip_url(self.https_url), exc))
27+
logger.error("Bad response from client while fetching Filecoin.ChainHead method for {}: {}".format(
28+
strip_url(self.url), response))
4029
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
4130
except requests.RequestException as exc:
42-
logger.error("Health check failed for {}: {}".format(
43-
strip_url(self.https_url), exc))
31+
logger.error("Health check failed for {}: {}".format(strip_url(self.url), exc))
4432
metrics['ws_rpc_health'].add_metric(self.labels_values, False)
4533
except Exception as e:
46-
logger.error("Health check failed for {}: {}".format(
47-
strip_url(self.https_url), e))
34+
logger.error("Health check failed for {}: {}".format(strip_url(self.url), e))
4835
metrics['ws_rpc_health'].add_metric(self.labels_values, False)

0 commit comments

Comments
 (0)