#[macro_use]
extern crate prettytable;
extern crate textwrap;
use chrono::prelude::*;
use grep::cli::DecompressionReader;
use grep::searcher::sinks::UTF8;
use grep::searcher::Searcher;
use prettytable::{format, Cell, Row, Table};
use regex::Regex;
use std::process;
use grep_pcre2::RegexMatcher;
use std::collections::BTreeMap;
use std::error::Error;
use std::io::Read;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(query: &str, filename: &str) -> Result<Config, &'static str> {
let query = String::from(query);
let filename = String::from(filename);
Ok(Config { query, filename })
}
}
pub fn create_query(filter: &str, user_filter: &str, _verbose: bool) -> String {
let mut query = String::from("^(?=.*\\bCRON\\b)");
if user_filter.is_empty() {
let user_filter = format!("(?=.*\\b{}\\b)", user_filter);
query.push_str(&user_filter);
}
if filter.is_empty() {
let filter = format!("(?=.*\\b{}\\b)", filter);
query.push_str(&filter);
}
query.push_str(".*$");
query
}
struct Cronjob {
pid: i32,
status: JobStatus,
user: Option<String>,
hostname: Option<String>,
start_date: Option<chrono::NaiveDateTime>,
end_date: Option<chrono::NaiveDateTime>,
duration: Option<i64>,
start_line: Option<String>,
message: Option<String>,
end_line: Option<String>,
}
impl Cronjob {
fn get_user(&self) -> &str {
match &self.user {
Some(user) => user.as_str(),
_ => "",
}
}
fn get_dates(&self) -> String {
let mut dates = String::from("");
match (self.start_date, self.end_date) {
(Some(start), Some(end)) => dates = format!("{}\n{}", start, end),
(Some(start), None) => dates = format!("{}", start),
(None, Some(end)) => dates = format!("{}", end),
_ => (),
}
dates
}
fn set_duration(&mut self) {
if let (Some(start), Some(end)) = (self.start_date, self.end_date) {
let duration = end - start;
self.duration = Some(duration.num_seconds());
}
}
fn get_duration(&self) -> String {
match &self.duration {
Some(duration) => duration.to_string(),
_ => String::from("unknow"),
}
}
fn get_command(&self) -> String {
match &self.start_line {
Some(command) => command.to_owned(),
_ => String::from(""),
}
}
}
enum JobStatus {
Ok,
Failed,
Unknow,
}
fn parse_date(date: String, year: i32) -> Option<chrono::NaiveDateTime> {
let date_with_year = format!("{} {}", year, date);
let parsed = NaiveDateTime::parse_from_str(date_with_year.as_str(), "%Y %b %d %T");
match parsed {
Ok(parsed) => Some(parsed),
Err(e) => {
println!("err {}", e);
None
}
}
}
fn status_filter(ko_filter: bool, ok_filter: bool, verbose: bool) {
if verbose {
println!("DEBUG: ko:{:?} ok:{:?}", ko_filter, ok_filter);
}
}
fn create_job_if_needed(cronjobs: &mut BTreeMap<i32, Cronjob>, pid: i32) {
match cronjobs.get(&pid) {
Some(_) => (),
_ => {
let job = Cronjob {
pid,
user: None,
hostname: None,
start_date: None,
end_date: None,
start_line: None,
message: None,
end_line: None,
status: JobStatus::Unknow,
duration: None,
};
cronjobs.insert(pid, job);
}
}
}
fn create_cronjobs_list(res: &Vec<String>, verbose: bool) -> Option<BTreeMap<i32, Cronjob>> {
let re_cron_log = match Regex::new(
r"^(?P<date>.*) (?P<hostname>.*) CRON\[(?P<pid>[0-9]+)\]: \((?P<user>.*)\) (?P<logtype>(CMD|END|error)) (?P<message>.*)",
) {
Ok(re) => re,
Err(error) => {
eprintln!("Problem creating regex to parse cron log: {}", error);
process::exit(1);
}
};
if verbose {
println!("DEBUG: regex pid: {:?}", re_cron_log);
}
let mut cronjobs: BTreeMap<i32, Cronjob> = BTreeMap::new();
let current_year = Local::now().year();
for line in res {
match re_cron_log.captures(line) {
None => (),
Some(matched_line) => {
match matched_line.name("pid")?.as_str().parse() {
Ok(pid) => {
let pid = pid;
let user = matched_line.name("user")?.as_str().to_string();
let hostname = matched_line.name("hostname")?.as_str().to_string();
let date = matched_line.name("date")?.as_str().to_string();
let logtype = matched_line.name("logtype")?.as_str().to_string();
let message = matched_line.name("message")?.as_str().to_string();
create_job_if_needed(&mut cronjobs, pid);
if let Some(job) = cronjobs.get_mut(&pid) {
match logtype.as_str() {
"CMD" => {
job.start_line = Some(message);
job.user = Some(user);
job.hostname = Some(hostname);
job.start_date = parse_date(date, current_year);
}
"END" => {
job.end_line = Some(message);
job.user = Some(user);
job.end_date = parse_date(date, current_year);
Cronjob::set_duration(job);
match job.status {
JobStatus::Failed => (),
_ => job.status = JobStatus::Ok,
}
}
"error" => {
job.message = Some(message);
job.end_date = parse_date(date, current_year);
job.status = JobStatus::Failed;
}
_ => eprintln!("Some line are not CRON log."),
}
}
}
Err(_) => {
println!("Warnig, unable to parse following line : {}", line);
}
};
}
}
}
Some(cronjobs)
}
pub fn display_jobs(res: Vec<String>, ko_filter: bool, ok_filter: bool, verbose: bool) {
match create_cronjobs_list(&res, verbose) {
None => (),
Some(cronjobs) => {
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_BOX_CHARS);
table.add_row(row![
b->"PID", b->"USER", b->"STATUS", b->"DATES", b->"DURATION", b->"COMMAND"
]);
status_filter(ko_filter, ok_filter, verbose);
for (_pid, job) in cronjobs {
let mut start_of_row =
vec![Cell::new(&job.pid.to_string()), Cell::new(job.get_user())];
match job.status {
JobStatus::Ok => start_of_row.push(Cell::new("OK").style_spec("Fg")),
JobStatus::Failed => start_of_row.push(Cell::new("KO").style_spec("bFr")),
JobStatus::Unknow => start_of_row.push(Cell::new("unknow").style_spec("Fb")),
}
let mut end_of_row = vec![
(Cell::new(job.get_dates().as_str())),
(Cell::new(job.get_duration().as_str())),
(Cell::new(textwrap::fill(job.get_command().as_str(), 80).as_str())),
];
start_of_row.append(&mut end_of_row);
table.add_row(Row::new(start_of_row));
}
table.printstd();
}
}
}
pub fn grep_file(config: Config, verbose: bool) -> Result<Vec<String>, Box<dyn Error>> {
let mut reader = DecompressionReader::new(&config.filename)?;
let mut contents = vec![];
reader.read_to_end(&mut contents)?;
let matcher = RegexMatcher::new(config.query.as_str())?;
if verbose {
println!("DEBUG: regex query : {}", config.query);
}
let mut matches: Vec<String> = vec![];
Searcher::new().search_slice(
&matcher,
&contents,
UTF8(|_lnum, line| {
matches.push(line.to_string());
Ok(true)
}),
)?;
Ok(matches)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grep_file_result() {
let filename = String::from("tests/sample_cron.log");
let query = String::from("59697");
let config = Config::new(&query, &filename).unwrap();
let expected_res = Vec::from(["Mar 23 14:35:01 srv4 CRON[59697]: (_tuptime) CMD ( if [ -x /usr/bin/tuptime ]; then /usr/bin/tuptime -x > /dev/null; fi)\n".to_string()]);
match grep_file(config, false) {
Ok(res) => assert_eq!(expected_res, res),
Err(e) => eprintln!("Application error: {}", e),
};
}
}