adding node stats

This commit is contained in:
aglkm
2024-09-19 01:05:33 +03:00
parent 45c6678c70
commit 1d2ab82daf
10 changed files with 738 additions and 361 deletions

700
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "grin-explorer" name = "grin-explorer"
version = "0.1.5" version = "0.1.6"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,5 +1,5 @@
# Node address. # Node address.
ip = "127.0.0.1" host = "127.0.0.1"
# Node port. # Node port.
port = "3413" port = "3413"
@@ -25,9 +25,12 @@ coingecko_api = "enabled"
# Enable or disable node POST API public access. # Enable or disable node POST API public access.
public_api = "enabled" public_api = "enabled"
# List of external node endpoints, used for peer stats.
# Comment out if you wish, only local peer stats will be used then.
# external_nodes = ["https://grinnode.live:3413", "https://grincoin.org"]
# Grinnode config # Grinnode config
# ip = "grinnode.live" # host = "grinnode.live"
# port = "3413" # port = "3413"
# proto = "https" # proto = "https"
# coingecko_api = "enabled" # coingecko_api = "enabled"
@@ -35,14 +38,14 @@ public_api = "enabled"
# Grincoin config # Grincoin config
# ip = "grincoin.org" # host = "grincoin.org"
# proto = "https" # proto = "https"
# coingecko_api = "enabled" # coingecko_api = "enabled"
# public_api = "enabled" # public_api = "enabled"
# Testnet config # Testnet config
# ip = "127.0.0.1" # host = "127.0.0.1"
# port = "13413" # port = "13413"
# proto = "http" # proto = "http"
# user = "grin" # user = "grin"

View File

@@ -155,7 +155,7 @@ impl Transactions {
// Explorer configuration // Explorer configuration
#[derive(Debug)] #[derive(Debug)]
pub struct ExplorerConfig { pub struct ExplorerConfig {
pub ip: String, pub host: String,
pub port: String, pub port: String,
pub proto: String, pub proto: String,
pub user: String, pub user: String,
@@ -166,12 +166,13 @@ pub struct ExplorerConfig {
pub foreign_api_secret: String, pub foreign_api_secret: String,
pub coingecko_api: String, pub coingecko_api: String,
pub public_api: String, pub public_api: String,
pub external_nodes: Vec<String>,
} }
impl ExplorerConfig { impl ExplorerConfig {
pub fn new() -> ExplorerConfig { pub fn new() -> ExplorerConfig {
ExplorerConfig { ExplorerConfig {
ip: String::new(), host: String::new(),
port: String::new(), port: String::new(),
proto: String::new(), proto: String::new(),
user: String::new(), user: String::new(),
@@ -182,10 +183,12 @@ impl ExplorerConfig {
foreign_api_secret: String::new(), foreign_api_secret: String::new(),
coingecko_api: String::new(), coingecko_api: String::new(),
public_api: String::new(), public_api: String::new(),
external_nodes: Vec::new(),
} }
} }
} }
// Output data // Output data
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Output { pub struct Output {
@@ -208,3 +211,22 @@ impl Output {
} }
} }
// Statistics data
#[derive(Debug)]
pub struct Statistics {
pub user_agent: Vec<String>,
pub count: Vec<String>,
pub total: u32,
}
impl Statistics {
pub fn new() -> Statistics {
Statistics {
user_agent: Vec::new(),
count: Vec::new(),
total: 0,
}
}
}

View File

@@ -17,6 +17,7 @@ use crate::data::Block;
use crate::data::Dashboard; use crate::data::Dashboard;
use crate::data::Kernel; use crate::data::Kernel;
use crate::data::Output; use crate::data::Output;
use crate::data::Statistics;
use crate::data::Transactions; use crate::data::Transactions;
use crate::requests::CONFIG; use crate::requests::CONFIG;
@@ -225,6 +226,21 @@ pub async fn search(input: Option<&str>) -> Either<Template, Redirect> {
} }
// Rendering Statistics page.
#[get("/stats")]
fn stats(statistics: &State<Arc<Mutex<Statistics>>>) -> Template {
let data = statistics.lock().unwrap();
Template::render("stats", context! {
route: "stats",
user_agent: data.user_agent.clone(),
count: data.count.clone(),
total: data.total,
cg_api: CONFIG.coingecko_api.clone(),
})
}
// Owner API. // Owner API.
// Whitelisted methods: get_connected_peers, get_peers, get_status. // Whitelisted methods: get_connected_peers, get_peers, get_status.
#[post("/v2/owner", data="<data>")] #[post("/v2/owner", data="<data>")]
@@ -649,13 +665,15 @@ async fn main() {
let blocks_clone = blocks.clone(); let blocks_clone = blocks.clone();
let txns = Arc::new(Mutex::new(Transactions::new())); let txns = Arc::new(Mutex::new(Transactions::new()));
let txns_clone = txns.clone(); let txns_clone = txns.clone();
let stats = Arc::new(Mutex::new(Statistics::new()));
let stats_clone = stats.clone();
let mut ready = false; let mut ready = false;
// Starting the Worker // Starting the Worker
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
let result = worker::run(dash_clone.clone(), blocks_clone.clone(), let result = worker::run(dash_clone.clone(), blocks_clone.clone(),
txns_clone.clone()).await; txns_clone.clone(), stats_clone.clone()).await;
match result { match result {
Ok(_v) => { Ok(_v) => {
@@ -679,6 +697,7 @@ async fn main() {
.manage(dash) .manage(dash)
.manage(blocks) .manage(blocks)
.manage(txns) .manage(txns)
.manage(stats)
.mount("/", routes![index, peers_inbound, peers_outbound, sync_status, market_supply, .mount("/", routes![index, peers_inbound, peers_outbound, sync_status, market_supply,
inflation_rate, volume_usd, volume_btc, price_usd, price_btc, inflation_rate, volume_usd, volume_btc, price_usd, price_btc,
mcap_usd, mcap_btc,latest_height, disk_usage, network_hashrate, mcap_usd, mcap_btc,latest_height, disk_usage, network_hashrate,
@@ -688,7 +707,7 @@ async fn main() {
block_weight, block_details_by_height, block_header_by_hash, block_weight, block_details_by_height, block_header_by_hash,
soft_supply, production_cost, reward_ratio, breakeven_cost, soft_supply, production_cost, reward_ratio, breakeven_cost,
last_block_age, block_list_by_height, block_list_index, search, kernel, last_block_age, block_list_by_height, block_list_index, search, kernel,
output, api_owner, api_foreign]) output, api_owner, api_foreign, stats])
.mount("/static", FileServer::from("static")) .mount("/static", FileServer::from("static"))
.attach(Template::fairing()) .attach(Template::fairing())
.launch() .launch()

View File

@@ -11,36 +11,60 @@ use std::collections::HashMap;
use std::fs; use std::fs;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use crate::data::Dashboard;
use crate::data::Block; use crate::data::Block;
use crate::data::Transactions; use crate::data::Dashboard;
use crate::data::ExplorerConfig; use crate::data::ExplorerConfig;
use crate::data::Kernel; use crate::data::Kernel;
use crate::data::Output; use crate::data::Output;
use crate::data::Statistics;
use crate::data::Transactions;
// Static explorer config structure // Static explorer config structure
lazy_static! { lazy_static! {
pub static ref CONFIG: ExplorerConfig = { pub static ref CONFIG: ExplorerConfig = {
let mut cfg = ExplorerConfig::new(); let mut cfg = ExplorerConfig::new();
let settings = Config::builder().add_source(config::File::with_name("Explorer")) let toml = Config::builder().add_source(config::File::with_name("Explorer")).build().unwrap();
.build().unwrap();
let settings: HashMap<String, String> = settings.try_deserialize().unwrap(); // Mandatory settings
cfg.host = toml.get_string("host").unwrap();
for (name, value) in settings { cfg.proto = toml.get_string("proto").unwrap();
match name.as_str() { cfg.coingecko_api = toml.get_string("coingecko_api").unwrap();
"ip" => cfg.ip = value, cfg.public_api = toml.get_string("public_api").unwrap();
"port" => cfg.port = value,
"proto" => cfg.proto = value, // Optional settings
"user" => cfg.user = value, match toml.get_string("port") {
"api_secret_path" => cfg.api_secret_path = value, Ok(v) => cfg.port = v,
"foreign_api_secret_path" => cfg.foreign_api_secret_path = value, Err(_e) => {},
"grin_dir" => cfg.grin_dir = value, }
"coingecko_api" => cfg.coingecko_api = value,
"public_api" => cfg.public_api = value, match toml.get_string("user") {
_ => error!("unknown config setting '{}'.", name), Ok(v) => cfg.user = v,
} Err(_e) => {},
}
match toml.get_string("api_secret_path") {
Ok(v) => cfg.api_secret_path = v,
Err(_e) => {},
}
match toml.get_string("foreign_api_secret_path") {
Ok(v) => cfg.foreign_api_secret_path = v,
Err(_e) => {},
}
match toml.get_string("grin_dir") {
Ok(v) => cfg.grin_dir = v,
Err(_e) => {},
}
match toml.get_array("external_nodes") {
Ok(nodes) => {
for endpoint in nodes.clone() {
cfg.external_nodes.push(endpoint.into_string().unwrap());
}
},
Err(_e) => {},
} }
if cfg.api_secret_path.is_empty() == false { if cfg.api_secret_path.is_empty() == false {
@@ -66,9 +90,9 @@ pub async fn call(method: &str, params: &str, id: &str, rpc_type: &str) -> Resul
let secret; let secret;
if CONFIG.port.is_empty() == false { if CONFIG.port.is_empty() == false {
rpc_url = format!("{}://{}:{}/v2/{}", CONFIG.proto, CONFIG.ip, CONFIG.port, rpc_type); rpc_url = format!("{}://{}:{}/v2/{}", CONFIG.proto, CONFIG.host, CONFIG.port, rpc_type);
} else { } else {
rpc_url = format!("{}://{}/v2/{}", CONFIG.proto, CONFIG.ip, rpc_type); rpc_url = format!("{}://{}/v2/{}", CONFIG.proto, CONFIG.host, rpc_type);
} }
if rpc_type == "owner" { if rpc_type == "owner" {
@@ -96,6 +120,39 @@ pub async fn call(method: &str, params: &str, id: &str, rpc_type: &str) -> Resul
} }
// RPC requests to grin node.
// The same call as above but with the option to add ip, proto and port.
pub async fn call_external(method: &str, params: &str, id: &str, rpc_type: &str, endpoint: String) -> Result<Value, anyhow::Error> {
let rpc_url;
let secret;
rpc_url = format!("{}/v2/{}", endpoint, rpc_type);
if rpc_type == "owner" {
secret = CONFIG.api_secret.clone();
} else {
secret = CONFIG.foreign_api_secret.clone();
}
let client = reqwest::Client::new();
let result = client.post(rpc_url)
.body(format!("{{\"method\": \"{}\", \"params\": {}, \"id\": {}, \"jsonrpc\": \"2.0\"}}", method, params, id))
.basic_auth(CONFIG.user.clone(), Some(secret))
.header("content-type", "application/json")
.send()
.await?;
match result.error_for_status_ref() {
Ok(_res) => (),
Err(err) => { error!("rpc failed, status code: {:?}", err.status().unwrap()); },
}
let val: Value = serde_json::from_str(&result.text().await?)?;
Ok(val)
}
// Collecting: height, sync, node_ver, proto_ver. // Collecting: height, sync, node_ver, proto_ver.
pub async fn get_status(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow::Error> { pub async fn get_status(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow::Error> {
let resp = call("get_status", "[]", "1", "owner").await?; let resp = call("get_status", "[]", "1", "owner").await?;
@@ -129,16 +186,15 @@ pub async fn get_mempool(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow:
} }
// Collecting: inbound, outbound. // Collecting: inbound, outbound, user_agent.
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>) pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>, statistics: Arc<Mutex<Statistics>>) -> Result<(), anyhow::Error> {
-> Result<(), anyhow::Error> { let mut peers = HashMap::new();
let resp = call("get_connected_peers", "[]", "1", "owner").await?; let mut inbound = 0;
let mut outbound = 0;
let mut data = dashboard.lock().unwrap(); let resp = call("get_connected_peers", "[]", "1", "owner").await?;
if resp != Value::Null { if resp != Value::Null {
let mut inbound = 0;
let mut outbound = 0;
for peer in resp["result"]["Ok"].as_array().unwrap() { for peer in resp["result"]["Ok"].as_array().unwrap() {
if peer["direction"] == "Inbound" { if peer["direction"] == "Inbound" {
@@ -147,11 +203,47 @@ pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>)
if peer["direction"] == "Outbound" { if peer["direction"] == "Outbound" {
outbound += 1; outbound += 1;
} }
// Collecting user_agent nodes stats
*peers.entry(peer["user_agent"].to_string()).or_insert(0) += 1;
} }
data.inbound = inbound;
data.outbound = outbound;
} }
// Collecting peers stats from external endpoints
for endpoint in CONFIG.external_nodes.clone() {
match call_external("get_connected_peers", "[]", "1", "owner", endpoint).await {
Ok(resp) => {
if resp != Value::Null {
for peer in resp["result"]["Ok"].as_array().unwrap() {
// Collecting user_agent nodes stats
*peers.entry(peer["user_agent"].to_string()).or_insert(0) += 1;
}
}
},
Err(e) => warn!("{}", e),
}
}
// Sort HashMap into Vec
let mut peers_vec: Vec<(&String, &u32)> = peers.iter().collect();
peers_vec.sort_by(|a, b| b.1.cmp(a.1));
let mut dash = dashboard.lock().unwrap();
let mut stats = statistics.lock().unwrap();
stats.user_agent.clear();
stats.count.clear();
stats.total = 0;
for v in peers_vec {
stats.total = stats.total + v.1;
stats.user_agent.push(v.0.to_string());
stats.count.push(v.1.to_string());
}
dash.inbound = inbound;
dash.outbound = outbound;
Ok(()) Ok(())
} }
@@ -220,7 +312,7 @@ pub fn get_disk_usage(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
match get_size(chain_dir.clone()) { match get_size(chain_dir.clone()) {
Ok(chain_size) => data.disk_usage = format!("{:.2}", (chain_size as f64) / 1000.0 / 1000.0 / 1000.0), Ok(chain_size) => data.disk_usage = format!("{:.2}", (chain_size as f64) / 1000.0 / 1000.0 / 1000.0),
Err(e) => { Err(e) => {
if CONFIG.ip == "127.0.0.1" || CONFIG.ip == "0.0.0.0" { if CONFIG.host == "127.0.0.1" || CONFIG.host == "0.0.0.0" {
error!("{}: \"{}\"", e, chain_dir); error!("{}: \"{}\"", e, chain_dir);
} else { } else {
// Ignore error for external node connection // Ignore error for external node connection

View File

@@ -1,18 +1,20 @@
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::data::Dashboard;
use crate::data::Block; use crate::data::Block;
use crate::data::Dashboard;
use crate::data::Statistics;
use crate::data::Transactions; use crate::data::Transactions;
use crate::requests; use crate::requests;
// Tokio Runtime Worker. // Tokio Runtime Worker.
// Collecting all the data. // Collecting all the data.
pub async fn run(dash: Arc<Mutex<Dashboard>>, blocks: Arc<Mutex<Vec<Block>>>, pub async fn run(dash: Arc<Mutex<Dashboard>>, blocks: Arc<Mutex<Vec<Block>>>,
txns: Arc<Mutex<Transactions>>) -> Result<(), anyhow::Error> { txns: Arc<Mutex<Transactions>>, stats: Arc<Mutex<Statistics>>) -> Result<(), anyhow::Error> {
let _ = requests::get_status(dash.clone()).await?; let _ = requests::get_status(dash.clone()).await?;
let _ = requests::get_mempool(dash.clone()).await?; let _ = requests::get_mempool(dash.clone()).await?;
let _ = requests::get_connected_peers(dash.clone()).await?; let _ = requests::get_connected_peers(dash.clone(), stats.clone()).await?;
let _ = requests::get_market(dash.clone()).await?; let _ = requests::get_market(dash.clone()).await?;
requests::get_disk_usage(dash.clone())?; requests::get_disk_usage(dash.clone())?;
let _ = requests::get_mining_stats(dash.clone()).await?; let _ = requests::get_mining_stats(dash.clone()).await?;

20
static/scripts/chart.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -44,6 +44,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a> <a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/stats"><i class="bi bi-calculator"></i> Statistics</a>
</li>
{% elif route == "block_list" or route == "block_list_by_height" %} {% elif route == "block_list" or route == "block_list_by_height" %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a> <a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a>
@@ -51,6 +54,19 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/block_list"><div class="darkorange-text"><i class="bi bi-box"></i> Blocks</div></a> <a class="nav-link" href="/block_list"><div class="darkorange-text"><i class="bi bi-box"></i> Blocks</div></a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/stats"><i class="bi bi-calculator"></i> Statistics</a>
</li>
{% elif route == "stats" %}
<li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/stats"><div class="darkorange-text"><i class="bi bi-calculator"></i> Statistics</div></a>
</li>
{% else %} {% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a> <a class="nav-link" href="/"><i class="bi bi-speedometer"></i> Dashboard</a>
@@ -58,6 +74,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a> <a class="nav-link" href="/block_list"><i class="bi bi-box"></i> Blocks</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/stats"><i class="bi bi-calculator"></i> Statistics</a>
</li>
{% endif %} {% endif %}
</ul> </ul>
@@ -220,7 +239,7 @@
<div class="row mb-2"> <div class="row mb-2">
<div class="col d-flex justify-content-center"> <div class="col d-flex justify-content-center">
<a class="text-decoration-none me-2" href="https://github.com/aglkm/grin-explorer"> <a class="text-decoration-none me-2" href="https://github.com/aglkm/grin-explorer">
<span style="color:grey"><i class="bi bi-github me-1"></i>v.0.1.5</span> <span style="color:grey"><i class="bi bi-github me-1"></i>v.0.1.6</span>
</a> </a>
<a class="text-decoration-none" href="/search"> <a class="text-decoration-none" href="/search">
<span style="color:grey"><i class="bi bi-search me-1"></i>Search</span> <span style="color:grey"><i class="bi bi-search me-1"></i>Search</span>

132
templates/stats.html.tera Normal file
View File

@@ -0,0 +1,132 @@
{% extends "base" %}
{% block content %}
<script src="/static/scripts/chart.js"></script>
<code>
<div class="card border-bottom-0 border-start-0 border-end-0 rounded-0">
<div class="card-body" align="center">
<div class="value-text">
<div class="darkorange-text"><i class="bi bi-pc-display-horizontal"></i> NODE VERSION STATS ({{ total }})</div>
<div style="position: relative; height:60vh; width:90vw"><canvas id="1"></canvas></div>
</div>
</div>
</div>
<div class="d-none d-sm-block"> <!-- Show on >= sm screens -->
<div class="card-group">
<div class="card border-start-0 rounded-0">
<div class="card-body">
<div class="darkorange-text">
#
</div>
</div>
</div>
<div class="card rounded-0">
<div class="card-body">
<div class="darkorange-text">
VERSION
</div>
</div>
</div>
<div class="card border-end-0 rounded-0">
<div class="card-body">
<div class="darkorange-text">
COUNT
</div>
</div>
</div>
</div>
{% for v in user_agent %}
<div class="card-group rounded-0">
<div class="card border-top-0 border-start-0 rounded-0">
<div class="card-body">
<div class="value-text">
{{ loop.index }}
</div>
</div>
</div>
<div class="card border-top-0 rounded-0">
<div class="card-body">
<div class="value-text">
{{ v }}
</div>
</div>
</div>
<div class="card border-top-0 border-end-0 rounded-0">
<div class="card-body">
<div class="value-text">
{{ count[loop.index0] }}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="d-sm-none"> <!-- Show on < sm screens-->
{% for v in user_agent %}
{% if loop.index0 == 0 %}
<div class="card border-start-0 rounded-0">
{% else %}
<div class="card border-top-0 border-start-0 rounded-0">
{% endif %}
<div class="card-body">
<div class="d-flex justify-content-start">
<div class="darkorange-text">
#{{ loop.index }}
</div>
</div>
<div class="d-flex justify-content-between">
<div class="value-text">
{{ v }}
</div>
<div class="value-text">
{{ count[loop.index0] }}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</code>
<script>
var ctx = document.getElementById('1').getContext('2d');
var options = {
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: 'gray',
},
position: 'top'
},
},
};
var data = {
labels: {{ user_agent }},
datasets: [{
label: " Count",
borderWidth: 1,
data: {{ count }}
}]
};
new Chart(document.getElementById("1"), {
type: 'pie',
data: data,
options: options
});
</script>
{% endblock %}