initial commit

This commit is contained in:
yggverse 2025-06-08 16:27:41 +03:00
parent c8d4947382
commit f56ebb6877
9 changed files with 291 additions and 0 deletions

48
src/argument.rs Normal file
View file

@ -0,0 +1,48 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Argument {
/// Debug level
///
/// * `i` - info
/// * `d` - detailed
#[arg(short, long, default_value_t = String::from("i"))]
pub debug: String,
/// Log format for given `source`
///
/// * `nginx`
#[arg(short, long, default_value_t = String::from("nginx"))]
pub format: String,
/// Export results to JSON file (e.g. `/path/to/stats.json`)
#[arg(long)]
pub export_json: Option<String>,
/// Export results to SVG file (e.g. `/path/to/badge.svg`)
///
/// * use `{hits}` / `{hosts}` pattern to replace parsed values
#[arg(long)]
pub export_svg: Option<String>,
/// Use custom SVG file template with `{hits}` / `{hosts}` placeholders
#[arg(long, default_value_t = String::from("default/counter.svg"))]
pub template_svg: String,
/// Expected memory index capacity
#[arg(short, long, default_value_t = 100)]
pub capacity: usize,
/// Exclude host(s) from index
#[arg(short, long)]
pub ignore_host: Vec<String>,
/// Access log source (e.g. `/var/nginx/access.log`)
#[arg(short, long)]
pub source: String,
/// Update delay in seconds
#[arg(short, long, default_value_t = 300)]
pub update: u64,
}

10
src/debug.rs Normal file
View file

@ -0,0 +1,10 @@
pub fn info(message: String) {
println!("[{}] [info] {message}", now())
}
fn now() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
}

94
src/main.rs Normal file
View file

@ -0,0 +1,94 @@
mod argument;
mod debug;
fn main() -> anyhow::Result<()> {
use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader, Write},
thread,
time::Duration,
};
let argument = {
use clap::Parser;
argument::Argument::parse()
};
// calculate debug level once
let is_debug_i = argument.debug.contains("i");
let is_debug_d = argument.debug.contains("d");
if !matches!(argument.format.to_lowercase().as_str(), "nginx") {
todo!("Format `{}` yet not supported!", argument.format)
}
if is_debug_i {
debug::info("Crawler started".into());
}
loop {
if is_debug_i {
debug::info("Index queue begin...".into());
}
let file = File::open(&argument.source)?;
let reader = BufReader::new(file);
let mut index: HashMap<String, usize> = HashMap::with_capacity(argument.capacity);
for line in reader.lines() {
let host = line?
.split_whitespace()
.next()
.map(|s| s.into())
.unwrap_or_default();
if argument.ignore_host.contains(&host) {
if is_debug_d {
debug::info(format!("Host `{host}` ignored by settings"))
}
continue;
}
index.entry(host).and_modify(|c| *c += 1).or_insert(1);
}
let hosts = index.len();
let hits: usize = index.values().sum();
if is_debug_i {
debug::info(format!(
"Index queue completed:\n{}\n\thosts: {} / hits: {}, await {} seconds to continue...",
if is_debug_d {
let mut b = Vec::with_capacity(hosts);
for (host, count) in &index {
b.push(format!("\t{} ({})", host, count))
}
b.join("\n")
} else {
"".into()
},
hosts,
hits,
argument.update,
));
}
if let Some(ref p) = argument.export_json {
let mut f = File::create(p)?;
f.write_all(format!("{{\"hosts\":{hosts},\"hits\":{hits}}}").as_bytes())?;
}
if let Some(ref p) = argument.export_svg {
let t = std::fs::read_to_string(&argument.template_svg)?;
let mut f = File::create(p)?;
f.write_all(
t.replace("{hosts}", &hosts.to_string())
.replace("{hits}", &hits.to_string())
.as_bytes(),
)?;
}
thread::sleep(Duration::from_secs(argument.update));
}
}