use std::sync::atomic::Ordering;
use std::time::Instant;
use crate::mining::MiningState;
const DEFAULT_PUSHGATEWAY: &str = "https://cybernode.ai/pushgateway/";
/// Push aggregated metrics every 5 minutes (driven by mining batches, not wall clock)
const PUSH_INTERVAL_SECS: f64 = 300.0;
pub struct MetricsReporter {
endpoint: String,
instance: String,
auth: Option<(String, String)>,
last_push: Instant,
session_start: Instant,
}
impl MetricsReporter {
pub fn new(miner_address: &str) -> Self {
let instance = if miner_address.len() >= 12 {
&miner_address[..12]
} else {
miner_address
};
let endpoint = std::env::var("CYB_PUSHGATEWAY_URL")
.unwrap_or_else(|_| DEFAULT_PUSHGATEWAY.to_string());
let auth = match (
std::env::var("CYB_PUSHGATEWAY_USER"),
std::env::var("CYB_PUSHGATEWAY_PASS"),
) {
(Ok(user), Ok(pass)) if !user.is_empty() => Some((user, pass)),
_ => None,
};
let now = Instant::now();
Self {
endpoint,
instance: instance.to_string(),
auth,
last_push: now,
session_start: now,
}
}
/// Called from the mining loop after each batch. Pushes if enough time has elapsed.
/// Returns true if a push was triggered.
pub fn maybe_push(&mut self, state: &MiningState, backend: &str) -> bool {
let elapsed_since_push = self.last_push.elapsed().as_secs_f64();
if elapsed_since_push < PUSH_INTERVAL_SECS {
return false;
}
self.do_push(state, backend);
true
}
/// Force-push current metrics (call on mining stop to flush final state)
pub fn flush(&mut self, state: &MiningState, backend: &str) {
self.do_push(state, backend);
}
fn do_push(&mut self, state: &MiningState, backend: &str) {
let body = self.build_metrics(state, backend);
self.push(body);
self.last_push = Instant::now();
}
/// Build Prometheus text format metrics body
fn build_metrics(&self, state: &MiningState, backend: &str) -> String {
let session_secs = self.session_start.elapsed().as_secs_f64();
let total_hashes = state.hash_count.load(Ordering::Relaxed);
let hashrate = if session_secs > 0.0 {
total_hashes as f64 / session_secs
} else {
0.0
};
let batches = state.batch_count.load(Ordering::Relaxed);
let batch_time_us = state.total_batch_time_us.load(Ordering::Relaxed);
let avg_batch_ms = if batches > 0 {
batch_time_us as f64 / batches as f64 / 1000.0
} else {
0.0
};
let proofs_submitted = state.proofs_submitted.load(Ordering::Relaxed);
let proofs_failed = state.proofs_failed.load(Ordering::Relaxed);
format!(
"# HELP cyb_mining_hashrate Current hashrate in H/s\n\
# TYPE cyb_mining_hashrate gauge\n\
cyb_mining_hashrate{{backend=\"{backend}\"}} {hashrate:.1}\n\
# HELP cyb_mining_total_hashes Total hashes computed\n\
# TYPE cyb_mining_total_hashes counter\n\
cyb_mining_total_hashes {total_hashes}\n\
# HELP cyb_mining_batch_avg_ms Average batch duration in milliseconds\n\
# TYPE cyb_mining_batch_avg_ms gauge\n\
cyb_mining_batch_avg_ms {avg_batch_ms:.2}\n\
# HELP cyb_mining_batch_count Total batches processed\n\
# TYPE cyb_mining_batch_count counter\n\
cyb_mining_batch_count {batches}\n\
# HELP cyb_mining_proofs_submitted Successfully submitted proofs\n\
# TYPE cyb_mining_proofs_submitted counter\n\
cyb_mining_proofs_submitted {proofs_submitted}\n\
# HELP cyb_mining_proofs_failed Failed proof submissions\n\
# TYPE cyb_mining_proofs_failed counter\n\
cyb_mining_proofs_failed {proofs_failed}\n\
# HELP cyb_mining_session_duration_seconds Mining session duration\n\
# TYPE cyb_mining_session_duration_seconds gauge\n\
cyb_mining_session_duration_seconds {session_secs:.1}\n",
)
}
/// Push metrics to Pushgateway (fire-and-forget in background thread)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn push(&self, body: String) {
let url = format!(
"{}metrics/job/cyb_mining/instance/{}",
self.endpoint, self.instance
);
let auth = self.auth.clone();
std::thread::spawn(move || {
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build();
let client = match client {
Ok(c) => c,
Err(e) => {
eprintln!("[Metrics] Failed to build HTTP client: {}", e);
return;
}
};
let mut req = client
.post(&url)
.header("Content-Type", "text/plain; version=0.0.4")
.body(body);
if let Some((user, pass)) = auth {
req = req.basic_auth(user, Some(pass));
}
match req.send() {
Ok(resp) => {
if !resp.status().is_success() {
eprintln!("[Metrics] Push failed: HTTP {}", resp.status());
}
}
Err(e) => {
eprintln!("[Metrics] Push error: {}", e);
}
}
});
}
#[cfg(any(target_os = "android", target_os = "ios"))]
fn push(&self, _body: String) {
// No metrics on mobile
}
}
cyb/src-tauri/src/metrics.rs
ฯ 0.0%
use Ordering;
use Instant;
use crateMiningState;
const DEFAULT_PUSHGATEWAY: &str = "https://cybernode.ai/pushgateway/";
/// Push aggregated metrics every 5 minutes (driven by mining batches, not wall clock)
const PUSH_INTERVAL_SECS: f64 = 300.0;