mirror of
https://github.com/transatoshi-mw/grin-explorer.git
synced 2025-10-22 05:43:42 +00:00
adding node stats
This commit is contained in:
26
src/data.rs
26
src/data.rs
@@ -155,7 +155,7 @@ impl Transactions {
|
||||
// Explorer configuration
|
||||
#[derive(Debug)]
|
||||
pub struct ExplorerConfig {
|
||||
pub ip: String,
|
||||
pub host: String,
|
||||
pub port: String,
|
||||
pub proto: String,
|
||||
pub user: String,
|
||||
@@ -166,12 +166,13 @@ pub struct ExplorerConfig {
|
||||
pub foreign_api_secret: String,
|
||||
pub coingecko_api: String,
|
||||
pub public_api: String,
|
||||
pub external_nodes: Vec<String>,
|
||||
}
|
||||
|
||||
impl ExplorerConfig {
|
||||
pub fn new() -> ExplorerConfig {
|
||||
ExplorerConfig {
|
||||
ip: String::new(),
|
||||
host: String::new(),
|
||||
port: String::new(),
|
||||
proto: String::new(),
|
||||
user: String::new(),
|
||||
@@ -182,10 +183,12 @@ impl ExplorerConfig {
|
||||
foreign_api_secret: String::new(),
|
||||
coingecko_api: String::new(),
|
||||
public_api: String::new(),
|
||||
external_nodes: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Output data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
23
src/main.rs
23
src/main.rs
@@ -17,6 +17,7 @@ use crate::data::Block;
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Kernel;
|
||||
use crate::data::Output;
|
||||
use crate::data::Statistics;
|
||||
use crate::data::Transactions;
|
||||
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.
|
||||
// Whitelisted methods: get_connected_peers, get_peers, get_status.
|
||||
#[post("/v2/owner", data="<data>")]
|
||||
@@ -649,13 +665,15 @@ async fn main() {
|
||||
let blocks_clone = blocks.clone();
|
||||
let txns = Arc::new(Mutex::new(Transactions::new()));
|
||||
let txns_clone = txns.clone();
|
||||
let stats = Arc::new(Mutex::new(Statistics::new()));
|
||||
let stats_clone = stats.clone();
|
||||
let mut ready = false;
|
||||
|
||||
// Starting the Worker
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let result = worker::run(dash_clone.clone(), blocks_clone.clone(),
|
||||
txns_clone.clone()).await;
|
||||
txns_clone.clone(), stats_clone.clone()).await;
|
||||
|
||||
match result {
|
||||
Ok(_v) => {
|
||||
@@ -679,6 +697,7 @@ async fn main() {
|
||||
.manage(dash)
|
||||
.manage(blocks)
|
||||
.manage(txns)
|
||||
.manage(stats)
|
||||
.mount("/", routes![index, peers_inbound, peers_outbound, sync_status, market_supply,
|
||||
inflation_rate, volume_usd, volume_btc, price_usd, price_btc,
|
||||
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,
|
||||
soft_supply, production_cost, reward_ratio, breakeven_cost,
|
||||
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"))
|
||||
.attach(Template::fairing())
|
||||
.launch()
|
||||
|
156
src/requests.rs
156
src/requests.rs
@@ -11,36 +11,60 @@ use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Block;
|
||||
use crate::data::Transactions;
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::ExplorerConfig;
|
||||
use crate::data::Kernel;
|
||||
use crate::data::Output;
|
||||
use crate::data::Statistics;
|
||||
use crate::data::Transactions;
|
||||
|
||||
|
||||
// Static explorer config structure
|
||||
lazy_static! {
|
||||
pub static ref CONFIG: ExplorerConfig = {
|
||||
let mut cfg = ExplorerConfig::new();
|
||||
let settings = Config::builder().add_source(config::File::with_name("Explorer"))
|
||||
.build().unwrap();
|
||||
let mut cfg = ExplorerConfig::new();
|
||||
let toml = Config::builder().add_source(config::File::with_name("Explorer")).build().unwrap();
|
||||
|
||||
let settings: HashMap<String, String> = settings.try_deserialize().unwrap();
|
||||
|
||||
for (name, value) in settings {
|
||||
match name.as_str() {
|
||||
"ip" => cfg.ip = value,
|
||||
"port" => cfg.port = value,
|
||||
"proto" => cfg.proto = value,
|
||||
"user" => cfg.user = value,
|
||||
"api_secret_path" => cfg.api_secret_path = value,
|
||||
"foreign_api_secret_path" => cfg.foreign_api_secret_path = value,
|
||||
"grin_dir" => cfg.grin_dir = value,
|
||||
"coingecko_api" => cfg.coingecko_api = value,
|
||||
"public_api" => cfg.public_api = value,
|
||||
_ => error!("unknown config setting '{}'.", name),
|
||||
}
|
||||
// Mandatory settings
|
||||
cfg.host = toml.get_string("host").unwrap();
|
||||
cfg.proto = toml.get_string("proto").unwrap();
|
||||
cfg.coingecko_api = toml.get_string("coingecko_api").unwrap();
|
||||
cfg.public_api = toml.get_string("public_api").unwrap();
|
||||
|
||||
// Optional settings
|
||||
match toml.get_string("port") {
|
||||
Ok(v) => cfg.port = v,
|
||||
Err(_e) => {},
|
||||
}
|
||||
|
||||
match toml.get_string("user") {
|
||||
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 {
|
||||
@@ -66,9 +90,9 @@ pub async fn call(method: &str, params: &str, id: &str, rpc_type: &str) -> Resul
|
||||
let secret;
|
||||
|
||||
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 {
|
||||
rpc_url = format!("{}://{}/v2/{}", CONFIG.proto, CONFIG.ip, rpc_type);
|
||||
rpc_url = format!("{}://{}/v2/{}", CONFIG.proto, CONFIG.host, rpc_type);
|
||||
}
|
||||
|
||||
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.
|
||||
pub async fn get_status(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), anyhow::Error> {
|
||||
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.
|
||||
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>)
|
||||
-> Result<(), anyhow::Error> {
|
||||
let resp = call("get_connected_peers", "[]", "1", "owner").await?;
|
||||
// Collecting: inbound, outbound, user_agent.
|
||||
pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>, statistics: Arc<Mutex<Statistics>>) -> Result<(), anyhow::Error> {
|
||||
let mut peers = HashMap::new();
|
||||
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 {
|
||||
let mut inbound = 0;
|
||||
let mut outbound = 0;
|
||||
|
||||
for peer in resp["result"]["Ok"].as_array().unwrap() {
|
||||
if peer["direction"] == "Inbound" {
|
||||
@@ -147,11 +203,47 @@ pub async fn get_connected_peers(dashboard: Arc<Mutex<Dashboard>>)
|
||||
if peer["direction"] == "Outbound" {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -220,7 +312,7 @@ pub fn get_disk_usage(dashboard: Arc<Mutex<Dashboard>>) -> Result<(), Error> {
|
||||
match get_size(chain_dir.clone()) {
|
||||
Ok(chain_size) => data.disk_usage = format!("{:.2}", (chain_size as f64) / 1000.0 / 1000.0 / 1000.0),
|
||||
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);
|
||||
} else {
|
||||
// Ignore error for external node connection
|
||||
|
@@ -1,18 +1,20 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Block;
|
||||
use crate::data::Dashboard;
|
||||
use crate::data::Statistics;
|
||||
use crate::data::Transactions;
|
||||
|
||||
use crate::requests;
|
||||
|
||||
|
||||
// Tokio Runtime Worker.
|
||||
// Collecting all the data.
|
||||
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_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?;
|
||||
requests::get_disk_usage(dash.clone())?;
|
||||
let _ = requests::get_mining_stats(dash.clone()).await?;
|
||||
|
Reference in New Issue
Block a user