mirror of
https://git.mentality.rip/numas13/xash3d-master.git
synced 2025-02-07 12:44:14 +00:00
master: collect and log stats
This commit is contained in:
parent
4a3b165781
commit
d0bf04e177
@ -18,6 +18,26 @@
|
|||||||
## Time in seconds before next admin request is allowed after wrong password
|
## Time in seconds before next admin request is allowed after wrong password
|
||||||
#admin = 10
|
#admin = 10
|
||||||
|
|
||||||
|
#[stat]
|
||||||
|
# Interval in seconds, set zero to disable
|
||||||
|
#interval = 0
|
||||||
|
# Format string description
|
||||||
|
#
|
||||||
|
# %s - servers count
|
||||||
|
#
|
||||||
|
# Requests per second
|
||||||
|
# %a - add server
|
||||||
|
# %d - remove server
|
||||||
|
# %q - query servers
|
||||||
|
# %e - errors
|
||||||
|
#
|
||||||
|
# Requests per interval time
|
||||||
|
# %A - add server
|
||||||
|
# %D - remove server
|
||||||
|
# %Q - query servers
|
||||||
|
# %E - errors
|
||||||
|
#format = "stats: %s servers, %a add/s, %d del/s, %q query/s, %e error/s"
|
||||||
|
|
||||||
#[client]
|
#[client]
|
||||||
## If client version is less then show update message
|
## If client version is less then show update message
|
||||||
#version = "0.19"
|
#version = "0.19"
|
||||||
|
@ -20,6 +20,8 @@ pub enum Error {
|
|||||||
InvalidIp(String),
|
InvalidIp(String),
|
||||||
#[error("Invalid port number \"{0}\"")]
|
#[error("Invalid port number \"{0}\"")]
|
||||||
InvalidPort(String),
|
InvalidPort(String),
|
||||||
|
#[error("Invalid stats interval \"{0}\"")]
|
||||||
|
InvalidStatsInterval(String),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Options(#[from] getopts::Fail),
|
Options(#[from] getopts::Fail),
|
||||||
}
|
}
|
||||||
@ -30,6 +32,8 @@ pub struct Cli {
|
|||||||
pub listen_ip: Option<IpAddr>,
|
pub listen_ip: Option<IpAddr>,
|
||||||
pub listen_port: Option<u16>,
|
pub listen_port: Option<u16>,
|
||||||
pub config_path: Option<Box<str>>,
|
pub config_path: Option<Box<str>>,
|
||||||
|
pub stats_format: Option<Box<str>>,
|
||||||
|
pub stats_interval: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_usage(opts: Options) {
|
fn print_usage(opts: Options) {
|
||||||
@ -59,6 +63,8 @@ pub fn parse() -> Result<Cli, Error> {
|
|||||||
);
|
);
|
||||||
opts.optopt("p", "port", &port_help, "PORT");
|
opts.optopt("p", "port", &port_help, "PORT");
|
||||||
opts.optopt("c", "config", "config path", "PATH");
|
opts.optopt("c", "config", "config path", "PATH");
|
||||||
|
opts.optopt("s", "stats-format", "stats format string", "FMT");
|
||||||
|
opts.optopt("I", "stats-interval", "stats interval", "SECONDS");
|
||||||
|
|
||||||
let matches = opts.parse(&args[1..])?;
|
let matches = opts.parse(&args[1..])?;
|
||||||
|
|
||||||
@ -94,5 +100,13 @@ pub fn parse() -> Result<Cli, Error> {
|
|||||||
cli.config_path = Some(s.into_boxed_str());
|
cli.config_path = Some(s.into_boxed_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(s) = matches.opt_str("stats-format") {
|
||||||
|
cli.stats_format = Some(s.into_boxed_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = matches.opt_str("stats-interval") {
|
||||||
|
cli.stats_interval = Some(s.parse().map_err(|_| Error::InvalidStatsInterval(s))?);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(cli)
|
Ok(cli)
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,8 @@ pub struct Config {
|
|||||||
#[serde(rename = "admin")]
|
#[serde(rename = "admin")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub admin_list: Box<[AdminConfig]>,
|
pub admin_list: Box<[AdminConfig]>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub stat: StatConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@ -173,6 +175,23 @@ pub struct AdminConfig {
|
|||||||
pub password: Box<str>,
|
pub password: Box<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct StatConfig {
|
||||||
|
pub interval: u32,
|
||||||
|
#[serde(default = "default_stats_format")]
|
||||||
|
pub format: Box<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StatConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
interval: 0,
|
||||||
|
format: default_stats_format(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn default_log_level() -> LevelFilter {
|
fn default_log_level() -> LevelFilter {
|
||||||
LevelFilter::Warn
|
LevelFilter::Warn
|
||||||
}
|
}
|
||||||
@ -189,6 +208,10 @@ fn default_hash_personal() -> Box<str> {
|
|||||||
Box::from(admin::HASH_PERSONAL)
|
Box::from(admin::HASH_PERSONAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_stats_format() -> Box<str> {
|
||||||
|
Box::from("stats: %s servers, %a add/s, %d del/s, %q query/s, %e error/s")
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_log_level<'de, D>(de: D) -> Result<LevelFilter, D::Error>
|
fn deserialize_log_level<'de, D>(de: D) -> Result<LevelFilter, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
|
@ -7,6 +7,7 @@ mod cli;
|
|||||||
mod config;
|
mod config;
|
||||||
mod logger;
|
mod logger;
|
||||||
mod master_server;
|
mod master_server;
|
||||||
|
mod stats;
|
||||||
|
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@ -35,6 +36,12 @@ fn load_config(cli: &Cli) -> Result<Config, config::Error> {
|
|||||||
if let Some(port) = cli.listen_port {
|
if let Some(port) = cli.listen_port {
|
||||||
cfg.server.port = port;
|
cfg.server.port = port;
|
||||||
}
|
}
|
||||||
|
if let Some(format) = &cli.stats_format {
|
||||||
|
cfg.stat.format = format.clone();
|
||||||
|
}
|
||||||
|
if let Some(interval) = cli.stats_interval {
|
||||||
|
cfg.stat.interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
log::set_max_level(cfg.log.level);
|
log::set_max_level(cfg.log.level);
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ use xash3d_protocol::wrappers::Str;
|
|||||||
use xash3d_protocol::{admin, game, master, server, Error as ProtocolError, ServerInfo};
|
use xash3d_protocol::{admin, game, master, server, Error as ProtocolError, ServerInfo};
|
||||||
|
|
||||||
use crate::config::{self, Config};
|
use crate::config::{self, Config};
|
||||||
|
use crate::stats::Stats;
|
||||||
|
|
||||||
/// The maximum size of UDP packets.
|
/// The maximum size of UDP packets.
|
||||||
const MAX_PACKET_SIZE: usize = 512;
|
const MAX_PACKET_SIZE: usize = 512;
|
||||||
@ -129,6 +130,8 @@ pub struct MasterServer {
|
|||||||
hash: config::HashConfig,
|
hash: config::HashConfig,
|
||||||
|
|
||||||
blocklist: HashSet<Ipv4Addr>,
|
blocklist: HashSet<Ipv4Addr>,
|
||||||
|
|
||||||
|
stats: Stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_socket_addr<A>(addr: A) -> io::Result<Option<SocketAddrV4>>
|
fn resolve_socket_addr<A>(addr: A) -> io::Result<Option<SocketAddrV4>>
|
||||||
@ -196,6 +199,7 @@ impl MasterServer {
|
|||||||
admin_limit_counter: Counter::new(ADMIN_LIMIT_CLEANUP_MAX),
|
admin_limit_counter: Counter::new(ADMIN_LIMIT_CLEANUP_MAX),
|
||||||
hash: cfg.hash,
|
hash: cfg.hash,
|
||||||
blocklist: Default::default(),
|
blocklist: Default::default(),
|
||||||
|
stats: Stats::new(cfg.stat),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +222,7 @@ impl MasterServer {
|
|||||||
self.update_map = cfg.client.update_map;
|
self.update_map = cfg.client.update_map;
|
||||||
self.admin_list = cfg.admin_list;
|
self.admin_list = cfg.admin_list;
|
||||||
self.hash = cfg.hash;
|
self.hash = cfg.hash;
|
||||||
|
self.stats.update_config(cfg.stat);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -245,6 +250,7 @@ impl MasterServer {
|
|||||||
let src = &buf[..n];
|
let src = &buf[..n];
|
||||||
if let Err(e) = self.handle_packet(from, src) {
|
if let Err(e) = self.handle_packet(from, src) {
|
||||||
debug!("{}: {}: \"{}\"", from, e, Str(src));
|
debug!("{}: {}: \"{}\"", from, e, Str(src));
|
||||||
|
self.stats.on_error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -255,6 +261,7 @@ impl MasterServer {
|
|||||||
self.challenges.clear();
|
self.challenges.clear();
|
||||||
self.servers.clear();
|
self.servers.clear();
|
||||||
self.admin_challenges.clear();
|
self.admin_challenges.clear();
|
||||||
|
self.stats.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_server_packet(&mut self, from: SocketAddrV4, p: server::Packet) -> Result<(), Error> {
|
fn handle_server_packet(&mut self, from: SocketAddrV4, p: server::Packet) -> Result<(), Error> {
|
||||||
@ -290,11 +297,13 @@ impl MasterServer {
|
|||||||
}
|
}
|
||||||
if self.challenges.remove(&from).is_some() {
|
if self.challenges.remove(&from).is_some() {
|
||||||
self.add_server(from, ServerInfo::new(&p));
|
self.add_server(from, ServerInfo::new(&p));
|
||||||
|
self.stats.on_server_add();
|
||||||
|
self.stats.servers_count(self.servers.len());
|
||||||
}
|
}
|
||||||
self.remove_outdated_servers();
|
self.remove_outdated_servers();
|
||||||
}
|
}
|
||||||
server::Packet::ServerRemove => {
|
server::Packet::ServerRemove => {
|
||||||
// ignore
|
self.stats.on_server_del();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(Error::UnexpectedPacket);
|
return Err(Error::UnexpectedPacket);
|
||||||
@ -326,6 +335,8 @@ impl MasterServer {
|
|||||||
if p.filter.flags.contains(FilterFlags::NAT) {
|
if p.filter.flags.contains(FilterFlags::NAT) {
|
||||||
self.send_client_to_nat_servers(from, iter)?;
|
self.send_client_to_nat_servers(from, iter)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.stats.on_query_servers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
game::Packet::GetServerInfo(_) => {
|
game::Packet::GetServerInfo(_) => {
|
||||||
@ -522,6 +533,7 @@ impl MasterServer {
|
|||||||
let now = self.now();
|
let now = self.now();
|
||||||
self.servers
|
self.servers
|
||||||
.retain(|_, v| v.is_valid(now, self.timeout.server));
|
.retain(|_, v| v.is_valid(now, self.timeout.server));
|
||||||
|
self.stats.servers_count(self.servers.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
155
master/src/stats.rs
Normal file
155
master/src/stats.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
use std::fmt::{self, Write};
|
||||||
|
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering::Relaxed};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::config::StatConfig;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Counters {
|
||||||
|
servers: AtomicUsize,
|
||||||
|
server_add: AtomicU32,
|
||||||
|
server_del: AtomicU32,
|
||||||
|
query_servers: AtomicU32,
|
||||||
|
errors: AtomicU32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counters {
|
||||||
|
fn print(&self, mut format: &str, buf: &mut String, time: Duration) -> fmt::Result {
|
||||||
|
let time = time.as_secs_f64();
|
||||||
|
let servers = self.servers.load(Relaxed);
|
||||||
|
let server_add = self.server_add.swap(0, Relaxed);
|
||||||
|
let server_del = self.server_del.swap(0, Relaxed);
|
||||||
|
let query_servers = self.query_servers.swap(0, Relaxed);
|
||||||
|
let errors = self.errors.swap(0, Relaxed);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// TODO: precompile format string
|
||||||
|
match format.find('%').map(|i| format.split_at(i)) {
|
||||||
|
Some((head, tail)) => {
|
||||||
|
format = &tail[1..];
|
||||||
|
write!(buf, "{}", head)?;
|
||||||
|
let mut chars = format.char_indices();
|
||||||
|
match chars.next().map(|(_, c)| c) {
|
||||||
|
Some('s') => write!(buf, "{}", servers)?,
|
||||||
|
Some('A') => write!(buf, "{}", server_add)?,
|
||||||
|
Some('D') => write!(buf, "{}", server_del)?,
|
||||||
|
Some('Q') => write!(buf, "{}", query_servers)?,
|
||||||
|
Some('E') => write!(buf, "{}", errors)?,
|
||||||
|
Some('a') => write!(buf, "{:.1}", server_add as f64 / time)?,
|
||||||
|
Some('d') => write!(buf, "{:.1}", server_del as f64 / time)?,
|
||||||
|
Some('q') => write!(buf, "{:.1}", query_servers as f64 / time)?,
|
||||||
|
Some('e') => write!(buf, "{:.1}", errors as f64 / time)?,
|
||||||
|
Some(c) => write!(buf, "%{}", c)?,
|
||||||
|
None => write!(buf, "%")?,
|
||||||
|
}
|
||||||
|
match chars.next() {
|
||||||
|
Some((i, _)) => format = &format[i..],
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
write!(buf, "{}", format)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&self) {
|
||||||
|
self.servers.store(0, Relaxed);
|
||||||
|
self.server_add.store(0, Relaxed);
|
||||||
|
self.server_del.store(0, Relaxed);
|
||||||
|
self.query_servers.store(0, Relaxed);
|
||||||
|
self.errors.store(0, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Stats {
|
||||||
|
enabled: bool,
|
||||||
|
tx: mpsc::Sender<StatConfig>,
|
||||||
|
counters: Arc<Counters>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stats {
|
||||||
|
pub fn new(mut config: StatConfig) -> Self {
|
||||||
|
let counters_ = Arc::new(Counters::default());
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
|
||||||
|
let enabled = config.interval != 0;
|
||||||
|
let counters = counters_.clone();
|
||||||
|
thread::spawn(move || -> fmt::Result {
|
||||||
|
let buf = &mut String::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if config.interval == 0 {
|
||||||
|
config = rx.recv().unwrap();
|
||||||
|
counters.clear();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = Duration::from_secs(config.interval as u64);
|
||||||
|
let start = Instant::now();
|
||||||
|
if let Ok(new_config) = rx.recv_timeout(duration) {
|
||||||
|
config = new_config;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.clear();
|
||||||
|
counters.print(&config.format, buf, start.elapsed())?;
|
||||||
|
info!("{}", buf);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
enabled,
|
||||||
|
tx,
|
||||||
|
counters: counters_,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_config(&mut self, config: StatConfig) {
|
||||||
|
self.enabled = config.interval != 0;
|
||||||
|
self.tx.send(config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&self) {
|
||||||
|
self.counters.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn servers_count(&self, n: usize) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.servers.store(n, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_server_add(&self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.server_add.fetch_add(1, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_server_del(&self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.server_del.fetch_add(1, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_query_servers(&self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.query_servers.fetch_add(1, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_error(&self) {
|
||||||
|
if self.enabled {
|
||||||
|
self.counters.errors.fetch_add(1, Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user