mod collector; use std::fs::File; use std::io::Write; use std::time::{Duration, Instant}; use anyhow::Context; use aquatic_common::{IndexMap, PanicSentinel}; use aquatic_udp_protocol::PeerClient; use compact_str::{CompactString, ToCompactString}; use crossbeam_channel::Receiver; use serde::Serialize; use time::format_description::well_known::Rfc2822; use time::OffsetDateTime; use tinytemplate::TinyTemplate; use collector::{CollectedStatistics, StatisticsCollector}; use crate::common::*; use crate::config::Config; const TEMPLATE_KEY: &str = "statistics"; const TEMPLATE_CONTENTS: &str = include_str!("../../../templates/statistics.html"); const STYLESHEET_CONTENTS: &str = concat!( "" ); #[derive(Debug, Serialize)] struct TemplateData { stylesheet: String, ipv4_active: bool, ipv6_active: bool, extended_active: bool, ipv4: CollectedStatistics, ipv6: CollectedStatistics, last_updated: String, peer_update_interval: String, peer_clients: Vec<(CompactString, CompactString, usize)>, } pub fn run_statistics_worker( _sentinel: PanicSentinel, config: Config, shared_state: State, statistics_receiver: Receiver, ) { let opt_tt = if config.statistics.write_html_to_file { let mut tt = TinyTemplate::new(); if let Err(err) = tt.add_template(TEMPLATE_KEY, TEMPLATE_CONTENTS) { ::log::error!("Couldn't parse statistics html template: {:#}", err); None } else { Some(tt) } } else { None }; let mut ipv4_collector = StatisticsCollector::new( shared_state.statistics_ipv4, #[cfg(feature = "prometheus")] "4".into(), ); let mut ipv6_collector = StatisticsCollector::new( shared_state.statistics_ipv6, #[cfg(feature = "prometheus")] "6".into(), ); let mut peer_clients: IndexMap = IndexMap::default(); loop { let start_time = Instant::now(); for message in statistics_receiver.try_iter() { match message { StatisticsMessage::Ipv4PeerHistogram(h) => ipv4_collector.add_histogram(&config, h), StatisticsMessage::Ipv6PeerHistogram(h) => ipv6_collector.add_histogram(&config, h), StatisticsMessage::PeerAdded(peer_id) => { peer_clients .entry(peer_id.client()) .or_insert((0, peer_id.first_8_bytes_hex())) .0 += 1; } StatisticsMessage::PeerRemoved(peer_id) => { let client = peer_id.client(); if let Some((count, _)) = peer_clients.get_mut(&client) { if *count == 1 { drop(count); peer_clients.remove(&client); } else { *count -= 1; } } } } } #[cfg(feature = "prometheus")] if config.statistics.run_prometheus_endpoint && config.statistics.extended { for (peer_client, (count, first_8_bytes)) in peer_clients.iter() { ::metrics::gauge!( "aquatic_peer_clients", *count as f64, "client" => peer_client.to_string(), "peer_id_prefix_hex" => first_8_bytes.to_string(), ); } } let statistics_ipv4 = ipv4_collector.collect_from_shared( #[cfg(feature = "prometheus")] &config, ); let statistics_ipv6 = ipv6_collector.collect_from_shared( #[cfg(feature = "prometheus")] &config, ); if config.statistics.print_to_stdout { println!("General:"); println!( " access list entries: {}", shared_state.access_list.load().len() ); if config.network.ipv4_active() { println!("IPv4:"); print_to_stdout(&config, &statistics_ipv4); } if config.network.ipv6_active() { println!("IPv6:"); print_to_stdout(&config, &statistics_ipv6); } println!(); } if let Some(tt) = opt_tt.as_ref() { let mut peer_clients = if config.statistics.extended { peer_clients .iter() .map(|(peer_client, (count, first_8_bytes))| { ( peer_client.to_compact_string(), first_8_bytes.to_owned(), *count, ) }) .collect() } else { Vec::new() }; peer_clients.sort_unstable_by(|a, b| b.2.cmp(&a.2)); let template_data = TemplateData { stylesheet: STYLESHEET_CONTENTS.to_string(), ipv4_active: config.network.ipv4_active(), ipv6_active: config.network.ipv6_active(), extended_active: config.statistics.extended, ipv4: statistics_ipv4, ipv6: statistics_ipv6, last_updated: OffsetDateTime::now_utc() .format(&Rfc2822) .unwrap_or("(formatting error)".into()), peer_update_interval: format!("{}", config.cleaning.torrent_cleaning_interval), peer_clients, }; if let Err(err) = save_html_to_file(&config, tt, &template_data) { ::log::error!("Couldn't save statistics to file: {:#}", err) } } if let Some(time_remaining) = Duration::from_secs(config.statistics.interval).checked_sub(start_time.elapsed()) { ::std::thread::sleep(time_remaining); } } } fn print_to_stdout(config: &Config, statistics: &CollectedStatistics) { println!( " bandwidth: {:>7} Mbit/s in, {:7} Mbit/s out", statistics.rx_mbits, statistics.tx_mbits, ); println!(" requests/second: {:>10}", statistics.requests_per_second); println!(" responses/second"); println!( " total: {:>10}", statistics.responses_per_second_total ); println!( " connect: {:>10}", statistics.responses_per_second_connect ); println!( " announce: {:>10}", statistics.responses_per_second_announce ); println!( " scrape: {:>10}", statistics.responses_per_second_scrape ); println!( " error: {:>10}", statistics.responses_per_second_error ); println!(" torrents: {:>10}", statistics.num_torrents); println!( " peers: {:>10} (updated every {}s)", statistics.num_peers, config.cleaning.torrent_cleaning_interval ); if config.statistics.extended { println!( " peers per torrent (updated every {}s)", config.cleaning.torrent_cleaning_interval ); println!(" min {:>10}", statistics.peer_histogram.min); println!(" p10 {:>10}", statistics.peer_histogram.p10); println!(" p20 {:>10}", statistics.peer_histogram.p20); println!(" p30 {:>10}", statistics.peer_histogram.p30); println!(" p40 {:>10}", statistics.peer_histogram.p40); println!(" p50 {:>10}", statistics.peer_histogram.p50); println!(" p60 {:>10}", statistics.peer_histogram.p60); println!(" p70 {:>10}", statistics.peer_histogram.p70); println!(" p80 {:>10}", statistics.peer_histogram.p80); println!(" p90 {:>10}", statistics.peer_histogram.p90); println!(" p95 {:>10}", statistics.peer_histogram.p95); println!(" p99 {:>10}", statistics.peer_histogram.p99); println!(" p99.9 {:>10}", statistics.peer_histogram.p999); println!(" max {:>10}", statistics.peer_histogram.max); } } fn save_html_to_file( config: &Config, tt: &TinyTemplate, template_data: &TemplateData, ) -> anyhow::Result<()> { let mut file = File::create(&config.statistics.html_file_path).with_context(|| { format!( "File path: {}", &config.statistics.html_file_path.to_string_lossy() ) })?; write!(file, "{}", tt.render(TEMPLATE_KEY, template_data)?)?; Ok(()) }