for CollectedStatistics {
+ fn into(self) -> FormattedStatistics {
+ let rx_mbits = self.bytes_received_per_second * 8.0 / 1_000_000.0;
+ let tx_mbits = self.bytes_sent_per_second * 8.0 / 1_000_000.0;
+
+ FormattedStatistics {
+ requests_per_second: (self.requests_per_second as usize)
+ .to_formatted_string(&Locale::en),
+ responses_per_second: (self.responses_per_second as usize)
+ .to_formatted_string(&Locale::en),
+ rx_mbits: format!("{:.2}", rx_mbits),
+ tx_mbits: format!("{:.2}", tx_mbits),
+ num_torrents: self.num_torrents.to_formatted_string(&Locale::en),
+ num_peers: self.num_peers.to_formatted_string(&Locale::en),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Serialize)]
+struct FormattedStatistics {
+ requests_per_second: String,
+ responses_per_second: String,
+ rx_mbits: String,
+ tx_mbits: String,
+ num_torrents: String,
+ num_peers: String,
+}
+
+#[derive(Debug, Serialize)]
+struct TemplateData {
+ stylesheet: String,
+ ipv4_active: bool,
+ ipv6_active: bool,
+ ipv4: FormattedStatistics,
+ ipv6: FormattedStatistics,
+ last_updated: String,
+ peer_update_interval: String,
+}
+
pub fn run_statistics_worker(config: Config, state: State) {
- let ipv4_active = config.network.address.is_ipv4() || !config.network.only_ipv6;
- let ipv6_active = config.network.address.is_ipv6();
+ let 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 last_ipv4 = Instant::now();
let mut last_ipv6 = Instant::now();
@@ -14,60 +121,74 @@ pub fn run_statistics_worker(config: Config, state: State) {
loop {
::std::thread::sleep(Duration::from_secs(config.statistics.interval));
- println!("General:");
- println!(" access list entries: {}", state.access_list.load().len());
+ let statistics_ipv4 =
+ CollectedStatistics::from_shared(&state.statistics_ipv4, &mut last_ipv4).into();
+ let statistics_ipv6 =
+ CollectedStatistics::from_shared(&state.statistics_ipv6, &mut last_ipv6).into();
- if ipv4_active {
- println!("IPv4:");
- gather_and_print_for_protocol(&config, &state.statistics_ipv4, &mut last_ipv4);
- }
- if ipv6_active {
- println!("IPv6:");
- gather_and_print_for_protocol(&config, &state.statistics_ipv6, &mut last_ipv6);
+ if config.statistics.print_to_stdout {
+ println!("General:");
+ println!(" access list entries: {}", 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!();
}
- println!();
+ if let Some(tt) = tt.as_ref() {
+ let template_data = TemplateData {
+ stylesheet: STYLESHEET_CONTENTS.to_string(),
+ ipv4_active: config.network.ipv4_active(),
+ ipv6_active: config.network.ipv6_active(),
+ ipv4: statistics_ipv4,
+ ipv6: statistics_ipv6,
+ last_updated: Utc::now().to_rfc2822(),
+ peer_update_interval: format!("{}", config.cleaning.torrent_cleaning_interval),
+ };
+
+ if let Err(err) = save_html_to_file(&config, tt, &template_data) {
+ ::log::error!("Couldn't save statistics to file: {:#}", err)
+ }
+ }
}
}
-fn gather_and_print_for_protocol(config: &Config, statistics: &Statistics, last: &mut Instant) {
- let requests_received: f64 = statistics.requests_received.fetch_and(0, Ordering::AcqRel) as f64;
- let responses_sent: f64 = statistics.responses_sent.fetch_and(0, Ordering::AcqRel) as f64;
- let bytes_received: f64 = statistics.bytes_received.fetch_and(0, Ordering::AcqRel) as f64;
- let bytes_sent: f64 = statistics.bytes_sent.fetch_and(0, Ordering::AcqRel) as f64;
-
- let now = Instant::now();
-
- let elapsed = (now - *last).as_secs_f64();
-
- *last = now;
-
- let requests_per_second = requests_received / elapsed;
- let responses_per_second: f64 = responses_sent / elapsed;
- let bytes_received_per_second: f64 = bytes_received / elapsed;
- let bytes_sent_per_second: f64 = bytes_sent / elapsed;
-
- let num_torrents: usize = sum_atomic_usizes(&statistics.torrents);
- let num_peers = sum_atomic_usizes(&statistics.peers);
-
+fn print_to_stdout(config: &Config, statistics: &FormattedStatistics) {
println!(
- " requests/second: {:10.2}, responses/second: {:10.2}",
- requests_per_second, responses_per_second
+ " requests/second: {:>10}, responses/second: {:>10}",
+ statistics.requests_per_second, statistics.responses_per_second
);
-
println!(
- " bandwidth: {:7.2} Mbit/s in, {:7.2} Mbit/s out",
- bytes_received_per_second * 8.0 / 1_000_000.0,
- bytes_sent_per_second * 8.0 / 1_000_000.0,
+ " bandwidth: {:>7} Mbit/s in, {:7} Mbit/s out",
+ statistics.rx_mbits, statistics.tx_mbits,
);
-
- println!(" number of torrents: {}", num_torrents);
+ println!(" number of torrents: {}", statistics.num_torrents);
println!(
" number of peers: {} (updated every {} seconds)",
- num_peers, config.cleaning.torrent_cleaning_interval
+ statistics.num_peers, config.cleaning.torrent_cleaning_interval
);
}
-fn sum_atomic_usizes(values: &[AtomicUsize]) -> usize {
- values.iter().map(|n| n.load(Ordering::Acquire)).sum()
+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(())
}
diff --git a/aquatic_udp/templates/statistics.css b/aquatic_udp/templates/statistics.css
new file mode 100644
index 0000000..ea8872a
--- /dev/null
+++ b/aquatic_udp/templates/statistics.css
@@ -0,0 +1,22 @@
+body {
+ font-family: arial, sans-serif;
+ font-size: 16px;
+}
+
+table {
+ border-collapse: collapse
+}
+
+caption {
+ caption-side: bottom;
+ padding-top: 0.5rem;
+}
+
+th, td {
+ padding: 0.5rem 2rem;
+ border: 1px solid #ccc;
+}
+
+th {
+ background-color: #eee;
+}
\ No newline at end of file
diff --git a/aquatic_udp/templates/statistics.html b/aquatic_udp/templates/statistics.html
new file mode 100644
index 0000000..8dfa380
--- /dev/null
+++ b/aquatic_udp/templates/statistics.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+ UDP BitTorrent tracker statistics
+
+ {#- Include stylesheet like this to prevent code editor syntax warnings #}
+ { stylesheet | unescaped }
+
+
+
+ BitTorrent tracker statistics
+
+
+ Tracker software: aquatic_udp
+
+
+
+ Updated: { last_updated } (UTC)
+
+
+ {{ if ipv4_active }}
+
+ IPv4
+
+
+ * Peer count is updated every { peer_update_interval } seconds
+
+ | Number of torrents |
+ { ipv4.num_torrents } |
+
+
+ | Number of peers |
+ { ipv4.num_peers } * |
+
+
+ | Requests / second |
+ { ipv4.requests_per_second } |
+
+
+ | Responses / second |
+ { ipv4.responses_per_second } |
+
+
+ | Bandwidth (RX) |
+ { ipv4.rx_mbits } mbit/s |
+
+
+ | Bandwidth (TX) |
+ { ipv4.tx_mbits } mbit/s |
+
+
+
+ {{ endif }}
+
+ {{ if ipv6_active }}
+
+ IPv6
+
+
+ * Peer count is updated every { peer_update_interval } seconds
+
+ | Number of torrents |
+ { ipv6.num_torrents } |
+
+
+ | Number of peers |
+ { ipv6.num_peers } * |
+
+
+ | Requests / second |
+ { ipv6.requests_per_second } |
+
+
+ | Responses / second |
+ { ipv6.responses_per_second } |
+
+
+ | Bandwidth (RX) |
+ { ipv6.rx_mbits } mbit/s |
+
+
+ | Bandwidth (TX) |
+ { ipv6.tx_mbits } mbit/s |
+
+
+
+ {{ endif }}
+
+