Skip to content

Commit

Permalink
Add metrics collector to proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
dmytrotkk committed Jun 5, 2024
1 parent f2e6ebf commit 62ee39d
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __pycache__

abi.json
chains.json
metrics.json

conf/upstreams/*.conf
conf/chains/*.conf
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@ services:
options:
max-file: "200"
max-size: "500m"
metrics:
environment:
ETH_ENDPOINT: ${ETH_ENDPOINT}
NETWORK_NAME: ${NETWORK_NAME}
image: metrics:latest
container_name: metrics
build:
context: ./metrics
dockerfile: Dockerfile
volumes:
- ./data:/data
logging:
driver: "json-file"
options:
max-file: "5"
max-size: "50m"
restart: unless-stopped
16 changes: 16 additions & 0 deletions metrics/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.12.3-bookworm

RUN apt-get update

RUN mkdir /usr/src/metrics /data
WORKDIR /usr/src/metrics

COPY requirements.txt ./
RUN pip3 install --no-cache-dir -r requirements.txt

COPY . .

ENV PYTHONPATH="/usr/src/metrics"
ENV COLUMNS=80

CMD python /usr/src/metrics/src/main.py
2 changes: 2 additions & 0 deletions metrics/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web3==6.19.0
requests==2.32.3
Empty file added metrics/src/__init__.py
Empty file.
114 changes: 114 additions & 0 deletions metrics/src/collector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
#
# This file is part of portal-metrics
#
# Copyright (C) 2024 SKALE Labs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import json
import logging
import asyncio
import aiohttp
from datetime import datetime
from typing import Any, Dict, List, Tuple

import requests

from explorer import get_address_counters_url, get_chain_stats
from gas import calc_avg_gas_price
from config import METRICS_FILEPATH

logger = logging.getLogger(__name__)


def get_metadata_url(network_name: str):
# return f'https://raw.githubusercontent.com/skalenetwork/skale-network/master/metadata/{network_name}/chains.json' # noqa
return f'https://raw.githubusercontent.com/skalenetwork/skale-network/update-mainnet-chains-metadata/metadata/{network_name}/chains.json' # noqa


def download_metadata(network_name: str):
url = get_metadata_url(network_name)
response = requests.get(url)
response.raise_for_status()
return response.json()


async def get_address_counters(session, network, chain_name, address):
url = get_address_counters_url(network, chain_name, address)
async with session.get(url) as response:
return await response.json()


async def get_all_address_counters(network, chain_name, addresses):
results = {}
async with aiohttp.ClientSession() as session:
tasks = []
for address in addresses:
tasks.append(get_address_counters(session, network, chain_name, address))

responses = await asyncio.gather(*tasks)

for address, response in zip(addresses, responses):
results[address] = response

return results


async def _fetch_counters_for_app(network_name, chain_name, app_name, app_info):
logger.info(f'fetching counters for app {app_name}')
if 'contracts' in app_info:
counters = await get_all_address_counters(network_name, chain_name, app_info['contracts'])
return app_name, counters
return app_name, None


async def fetch_counters_for_apps(chain_info, network_name, chain_name):
tasks = []
for app_name, app_info in chain_info['apps'].items():
task = _fetch_counters_for_app(network_name, chain_name, app_name, app_info)
tasks.append(task)
return await asyncio.gather(*tasks)


def transform_to_dict(apps_counters: List[Tuple[str, Any]] | None) -> Dict[str, Any]:
if not apps_counters:
return {}
results = {}
for app_name, counters in apps_counters:
results[app_name] = counters
return results


def collect_metrics(network_name: str):
metadata = download_metadata(network_name)
metrics = {}
for chain_name, chain_info in metadata.items():
apps_counters = None
chain_stats = get_chain_stats(network_name, chain_name)
if 'apps' in chain_info:
apps_counters = asyncio.run(
fetch_counters_for_apps(chain_info, network_name, chain_name))
metrics[chain_name] = {
'chain_stats': chain_stats,
'apps_counters': transform_to_dict(apps_counters),
}
data = {
'metrics': metrics,
'gas': int(calc_avg_gas_price()),
'last_updated': int(datetime.now().timestamp())
}
logger.info(f'Saving metrics to {METRICS_FILEPATH}')
with open(METRICS_FILEPATH, 'w') as f:
json.dump(data, f, indent=4, sort_keys=True)
50 changes: 50 additions & 0 deletions metrics/src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
#
# This file is part of portal-metrics
#
# Copyright (C) 2024 SKALE Labs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import os

ERROR_TIMEOUT = os.getenv('MONITOR_INTERVAL', 60)
MONITOR_INTERVAL = os.getenv('MONITOR_INTERVAL', 10800)
NETWORK_NAME = os.getenv('NETWORK_NAME', 'mainnet')
ENDPOINT = os.environ['ETH_ENDPOINT']

PROXY_ENDPOINTS = {
'mainnet': 'mainnet.skalenodes.com',
'legacy': 'legacy-proxy.skaleserver.com',
'regression': 'regression-proxy.skalenodes.com',
'testnet': 'testnet.skalenodes.com'
}

BASE_EXPLORER_URLS = {
'mainnet': 'explorer.mainnet.skalenodes.com',
'legacy': 'legacy-explorer.skaleserver.com',
'regression': 'regression-explorer.skalenodes.com',
'testnet': 'explorer.testnet.skalenodes.com'
}

STATS_API = {
'mainnet': 'https://stats.explorer.mainnet.skalenodes.com/v2/stats',
}

HTTPS_PREFIX = "https://"

BLOCK_SAMPLING = 100
GAS_ESTIMATION_ITERATIONS = 300

METRICS_FILEPATH = os.path.join('/', 'data', 'metrics.json')
46 changes: 46 additions & 0 deletions metrics/src/explorer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# This file is part of portal-metrics
#
# Copyright (C) 2024 SKALE Labs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import logging
import requests
from typing import Any

from config import BASE_EXPLORER_URLS, HTTPS_PREFIX

logger = logging.getLogger(__name__)


def _get_explorer_url(network, chain_name):
explorer_base_url = BASE_EXPLORER_URLS[network]
return HTTPS_PREFIX + chain_name + '.' + explorer_base_url


def get_chain_stats(network: str, chain_name: str) -> Any:
try:
explorer_url = _get_explorer_url(network, chain_name)
response = requests.get(f'{explorer_url}/api/v2/stats')
return response.json()
except Exception as e:
logger.error(f'Failed to get chain stats: {e}')
return None


def get_address_counters_url(network: str, chain_name: str, address: str) -> str:
explorer_url = _get_explorer_url(network, chain_name)
return f'{explorer_url}/api/v2/addresses/{address}/counters'
48 changes: 48 additions & 0 deletions metrics/src/gas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
#
# This file is part of portal-metrics
#
# Copyright (C) 2024 SKALE Labs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import logging
from web3 import Web3
from config import ENDPOINT, GAS_ESTIMATION_ITERATIONS, BLOCK_SAMPLING

logger = logging.getLogger(__name__)


def init_w3():
logger.info(f'Connecting to {ENDPOINT}...')
return Web3(Web3.HTTPProvider(ENDPOINT))


def calc_avg_gas_price():
block_num = GAS_ESTIMATION_ITERATIONS * BLOCK_SAMPLING
logger.info(f'Calculating average gas price for the last {block_num} blocks')
w3 = init_w3()
block_number = w3.eth.block_number
total_gas_used = 0

logger.info(f'Getting historic block gas prices...')
for index in range(GAS_ESTIMATION_ITERATIONS):
block_number = block_number - BLOCK_SAMPLING * index
block = w3.eth.get_block(block_number)
total_gas_used += block['baseFeePerGas']

avg_gas_price = total_gas_used / GAS_ESTIMATION_ITERATIONS
avg_gas_price_gwei = Web3.from_wei(avg_gas_price, 'gwei')
logger.info(f'avg_gas_price_gwei: {avg_gas_price_gwei}')
return avg_gas_price_gwei
53 changes: 53 additions & 0 deletions metrics/src/logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#
# This file is part of portal-metrics
#
# Copyright (C) 2024 SKALE Labs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import os
import sys
import logging
import logging.handlers as py_handlers
from logging import Formatter, StreamHandler


LOG_FORMAT = '[%(asctime)s %(levelname)s] %(name)s:%(lineno)d - %(threadName)s - %(message)s'
LOG_FILEPATH = os.path.join(os.getcwd(), 'portal-metrics.log')

LOG_FILE_SIZE_MB = 300
LOG_FILE_SIZE_BYTES = LOG_FILE_SIZE_MB * 1000000
LOG_BACKUP_COUNT = 3

MONITOR_INTERVAL = 600


def get_file_handler(log_filepath, log_level):
formatter = Formatter(LOG_FORMAT)
f_handler = py_handlers.RotatingFileHandler(
log_filepath, maxBytes=LOG_FILE_SIZE_BYTES,
backupCount=LOG_BACKUP_COUNT)
f_handler.setFormatter(formatter)
f_handler.setLevel(log_level)
return f_handler


def init_default_logger():
formatter = Formatter(LOG_FORMAT)
f_handler = get_file_handler(LOG_FILEPATH, logging.INFO)
stream_handler = StreamHandler(sys.stderr)
stream_handler.setFormatter(formatter)
stream_handler.setLevel(logging.INFO)
logging.basicConfig(level=logging.DEBUG, handlers=[f_handler, stream_handler])
Loading

0 comments on commit 62ee39d

Please sign in to comment.