504 lines
18 KiB
Rust
504 lines
18 KiB
Rust
use crate::errors;
|
|
use crate::kafka::KafkaMessage;
|
|
use crate::merge;
|
|
use crate::parse;
|
|
use crate::rules;
|
|
use crate::settings::*;
|
|
use crate::status::{Statistic, Stats};
|
|
/**
|
|
* The connection module is responsible for handling everything pertaining to a single inbound TCP
|
|
* connection.
|
|
*/
|
|
use async_std::{
|
|
io::BufReader,
|
|
prelude::*,
|
|
sync::{Arc, Sender},
|
|
task,
|
|
};
|
|
use chrono::prelude::*;
|
|
use handlebars::Handlebars;
|
|
use log::*;
|
|
use std::collections::HashMap;
|
|
|
|
/**
|
|
* RuleState exists to help carry state into merge/replacement functions and exists only during the
|
|
* processing of rules
|
|
*/
|
|
struct RuleState<'a> {
|
|
variables: &'a HashMap<String, String>,
|
|
hb: &'a handlebars::Handlebars<'a>,
|
|
stats: Sender<Statistic>,
|
|
}
|
|
|
|
/**
|
|
* Simple type to capture a map of precompiled jmespath expressions
|
|
*/
|
|
pub type JmesPathExpressions<'a> = HashMap<String, jmespath::Expression<'a>>;
|
|
|
|
pub struct Connection {
|
|
/**
|
|
* A reference to the global Settings object for all configuration information
|
|
*/
|
|
settings: Arc<Settings>,
|
|
/**
|
|
* The sender-side of the channel to our Kafka connection, allowing the logs read in to be
|
|
* sent over to the Kafka handler
|
|
*/
|
|
sender: Sender<KafkaMessage>,
|
|
stats: Sender<Statistic>,
|
|
}
|
|
|
|
impl Connection {
|
|
pub fn new(
|
|
settings: Arc<Settings>,
|
|
sender: Sender<KafkaMessage>,
|
|
stats: Sender<Statistic>,
|
|
) -> Self {
|
|
Connection {
|
|
settings,
|
|
sender,
|
|
stats,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* connection_loop is responsible for handling incoming syslog streams connections
|
|
*
|
|
*/
|
|
pub async fn read_logs<R: async_std::io::Read + std::marker::Unpin>(
|
|
&self,
|
|
reader: BufReader<R>,
|
|
) -> Result<(), errors::HotdogError> {
|
|
let mut lines = reader.lines();
|
|
|
|
let mut hb = Handlebars::new();
|
|
let mut jmespaths = JmesPathExpressions::new();
|
|
|
|
if !precompile_templates(&mut hb, self.settings.clone()) {
|
|
error!("Failing to precompile templates is a fatal error, not going to parse logs since the configuration is broken");
|
|
// TODO fix the Err types
|
|
return Ok(());
|
|
}
|
|
|
|
if !precompile_jmespath(&mut jmespaths, self.settings.clone()) {
|
|
error!("Failing to precompile jmespaths is a fata error, not parsing this connection's logs because the configuration is broken");
|
|
// TODO fix the Err types
|
|
return Ok(());
|
|
}
|
|
|
|
while let Some(line) = lines.next().await {
|
|
let line = line?;
|
|
debug!("log: {}", line);
|
|
|
|
let parsed = parse::parse_line(line);
|
|
|
|
if let Err(e) = &parsed {
|
|
self.stats.send((Stats::LogParseError, 1)).await;
|
|
error!("failed to parse message: {:?}", e);
|
|
continue;
|
|
}
|
|
/*
|
|
* Now that we've logged the error, let's unpack and bubble the error anyways
|
|
*
|
|
* Note: msg needs to be mutable so we can fish the `msg` out within it during a
|
|
* simd_json parse
|
|
*/
|
|
let mut msg = parsed.unwrap();
|
|
self.stats.send((Stats::LineReceived, 1)).await;
|
|
let mut continue_rules = true;
|
|
debug!("parsed as: {}", msg.msg);
|
|
|
|
for rule in self.settings.rules.iter() {
|
|
/*
|
|
* If we have been told to stop processing rules, then it's time to bail on this log
|
|
* message
|
|
*/
|
|
if !continue_rules {
|
|
break;
|
|
}
|
|
|
|
// The output buffer that we will ultimately send along to the Kafka service
|
|
let mut output = String::new();
|
|
let mut rule_matches = false;
|
|
let mut hash = HashMap::new();
|
|
hash.insert("msg".to_string(), String::from(&msg.msg));
|
|
hash.insert("version".to_string(), env!["CARGO_PKG_VERSION"].to_string());
|
|
hash.insert("iso8601".to_string(), Utc::now().to_rfc3339());
|
|
|
|
match rule.field {
|
|
Field::Msg => {
|
|
rule_matches = rules::apply_rule(&rule, &msg.msg, &jmespaths, &mut hash);
|
|
}
|
|
Field::Appname => {
|
|
if let Some(appname) = &msg.appname {
|
|
rule_matches =
|
|
rules::apply_rule(&rule, &appname, &jmespaths, &mut hash);
|
|
}
|
|
}
|
|
Field::Hostname => {
|
|
if let Some(hostname) = &msg.hostname {
|
|
rule_matches =
|
|
rules::apply_rule(&rule, &hostname, &jmespaths, &mut hash);
|
|
}
|
|
}
|
|
Field::Severity => {
|
|
if let Some(severity) = &msg.severity {
|
|
rule_matches =
|
|
rules::apply_rule(&rule, &severity, &jmespaths, &mut hash);
|
|
}
|
|
}
|
|
Field::Facility => {
|
|
if let Some(facility) = &msg.facility {
|
|
rule_matches =
|
|
rules::apply_rule(&rule, &facility, &jmespaths, &mut hash);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* This specific didn't match, so onto the next one
|
|
*/
|
|
if !rule_matches {
|
|
continue;
|
|
}
|
|
|
|
let rule_state = RuleState {
|
|
hb: &hb,
|
|
variables: &hash,
|
|
stats: self.stats.clone(),
|
|
};
|
|
|
|
/*
|
|
* Process the actions one the rule has matched
|
|
*/
|
|
for index in 0..rule.actions.len() {
|
|
let action = &rule.actions[index];
|
|
/*
|
|
* @stjepang says this will fix slow future polling
|
|
*
|
|
* The underlying problem here is that this _can_ be a very tight
|
|
* and CPU-bound loop under heavy load conditions. There is nothing
|
|
* inherent in smol (under async-std 1.6.x) which will properly
|
|
* yield to other tasks in the runtime.
|
|
*/
|
|
task::yield_now().await;
|
|
|
|
match action {
|
|
Action::Forward { topic } => {
|
|
/*
|
|
* If a custom output was never defined, just take the
|
|
* raw message and pass that along.
|
|
*/
|
|
if output.is_empty() {
|
|
output = String::from(&msg.msg);
|
|
}
|
|
|
|
if let Ok(actual_topic) = hb.render_template(&topic, &hash) {
|
|
debug!("Enqueueing for topic: `{}`", actual_topic);
|
|
/*
|
|
* `output` is consumed by send_to_kafka, so the rest of the rules
|
|
* should be skipped.
|
|
*/
|
|
let kmsg = KafkaMessage::new(actual_topic, output);
|
|
self.sender.send(kmsg).await;
|
|
/*
|
|
* Ensure that we're allowing other tasks to execute when we pass
|
|
* things off to the channel
|
|
*
|
|
* See also https://github.com/stjepang/smol/issues/159
|
|
*/
|
|
task::yield_now().await;
|
|
continue_rules = false;
|
|
} else {
|
|
error!("Failed to process the configured topic: `{}`", topic);
|
|
self.stats.send((Stats::TopicParseFailed, 1)).await;
|
|
}
|
|
break;
|
|
}
|
|
|
|
Action::Merge { json, json_str: _ } => {
|
|
debug!("merging JSON content: {}", json);
|
|
if let Ok(buffer) = perform_merge(
|
|
&mut msg.msg,
|
|
&template_id_for(&rule, index),
|
|
&rule_state,
|
|
) {
|
|
output = buffer;
|
|
} else {
|
|
continue_rules = false;
|
|
}
|
|
}
|
|
|
|
Action::Replace { template } => {
|
|
let template_id = template_id_for(&rule, index);
|
|
|
|
debug!(
|
|
"replacing content with template: {} ({})",
|
|
template, template_id
|
|
);
|
|
if let Ok(rendered) = hb.render(&template_id, &hash) {
|
|
output = rendered;
|
|
}
|
|
}
|
|
|
|
Action::Stop => {
|
|
continue_rules = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a unique identifier for the given template
|
|
*/
|
|
fn template_id_for(rule: &Rule, index: usize) -> String {
|
|
format!("{}-{}", rule.uuid, index)
|
|
}
|
|
|
|
/**
|
|
* precompile_templates will register templates for all the Merge and Replace actions from the
|
|
* settings
|
|
*
|
|
* Will usually return a true, unless some setting parse failure occurred which is a critical
|
|
* failure for the daemon
|
|
*/
|
|
fn precompile_templates(hb: &mut Handlebars, settings: Arc<Settings>) -> bool {
|
|
for rule in settings.rules.iter() {
|
|
for index in 0..rule.actions.len() {
|
|
match &rule.actions[index] {
|
|
Action::Merge { json: _, json_str } => {
|
|
let template_id = template_id_for(rule, index);
|
|
|
|
if let Some(template) = json_str {
|
|
if let Err(e) = hb.register_template_string(&template_id, &template) {
|
|
error!("Failed to register template! {}\n{}", e, template);
|
|
return false;
|
|
}
|
|
} else {
|
|
error!("Could not look up the json_str for a Merge action");
|
|
return false;
|
|
}
|
|
}
|
|
Action::Replace { template } => {
|
|
let template_id = format!("{}-{}", rule.uuid, index);
|
|
if let Err(e) = hb.register_template_string(&template_id, &template) {
|
|
error!("Failed to register template! {}\n{}", e, template);
|
|
return false;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/**
|
|
* precompile_jmespath will pre-generate all the necessary JMESPath::Variable objects from the
|
|
* configuration file and shove thoe in the map given to it
|
|
*/
|
|
fn precompile_jmespath(map: &mut JmesPathExpressions, settings: Arc<Settings>) -> bool {
|
|
for rule in settings.rules.iter() {
|
|
if let Some(expression) = &rule.jmespath {
|
|
if !map.contains_key(expression) {
|
|
if let Ok(compiled) = jmespath::compile(&expression) {
|
|
map.insert(expression.to_string(), compiled);
|
|
} else {
|
|
error!("Failed to compile the JMESPath expression: {}", expression);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/**
|
|
* perform_merge will generate the buffer resulting of the JSON merge
|
|
*/
|
|
fn perform_merge(
|
|
mut buffer: &mut str,
|
|
template_id: &str,
|
|
state: &RuleState,
|
|
) -> Result<String, String> {
|
|
if let Ok(mut msg_json) = crate::json::from_str(&mut buffer) {
|
|
if let Ok(mut rendered) = state.hb.render(template_id, &state.variables) {
|
|
let to_merge: serde_json::Value = crate::json::from_str(&mut rendered)
|
|
.expect("Failed to deserialize our rendered to_merge_str");
|
|
|
|
/*
|
|
* If the administrator configured the merge incorrectly, just pass the buffer along un-merged
|
|
*/
|
|
if !to_merge.is_object() {
|
|
error!("Merge requested was not a JSON object: {}", to_merge);
|
|
state.stats.send((Stats::MergeTargetNotJsonError, 1));
|
|
return Ok(buffer.to_string());
|
|
}
|
|
|
|
merge::merge(&mut msg_json, &to_merge);
|
|
|
|
if let Ok(output) = crate::json::to_string(&msg_json) {
|
|
return Ok(output);
|
|
}
|
|
}
|
|
Err("Failed to merge and serialize".to_string())
|
|
} else {
|
|
error!("Failed to parse as JSON, stopping actions: {}", buffer);
|
|
state.stats.send((Stats::MergeInvalidJsonError, 1));
|
|
Err("Not JSON".to_string())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use async_std::sync::channel;
|
|
|
|
/**
|
|
* Generating a test RuleState for consistent states in test
|
|
*/
|
|
fn rule_state<'a>(
|
|
hb: &'a handlebars::Handlebars<'a>,
|
|
hash: &'a HashMap<String, String>,
|
|
) -> RuleState<'a> {
|
|
let (unused_sender, _) = channel(1);
|
|
RuleState {
|
|
hb: &hb,
|
|
variables: &hash,
|
|
stats: unused_sender,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn merge_with_empty() {
|
|
let mut hb = Handlebars::new();
|
|
let template_id = "1";
|
|
hb.register_template_string(&template_id, "{}");
|
|
|
|
let hash = HashMap::<String, String>::new();
|
|
let state = rule_state(&hb, &hash);
|
|
|
|
let mut buffer = "{}".to_string();
|
|
let output = perform_merge(&mut buffer, template_id, &state);
|
|
assert_eq!(output, Ok("{}".to_string()));
|
|
}
|
|
|
|
/**
|
|
* merge without a JSON object, this should return the original buffer
|
|
*/
|
|
#[test]
|
|
fn merge_with_non_object() -> std::result::Result<(), String> {
|
|
let mut hb = Handlebars::new();
|
|
let template_id = "1";
|
|
hb.register_template_string(&template_id, "[1]");
|
|
|
|
let hash = HashMap::<String, String>::new();
|
|
let state = rule_state(&hb, &hash);
|
|
|
|
let mut buffer = "{}".to_string();
|
|
let output = perform_merge(&mut buffer, template_id, &state)?;
|
|
assert_eq!(output, "{}".to_string());
|
|
Ok(())
|
|
}
|
|
|
|
/**
|
|
* merging without a JSON buffer should return an error
|
|
*/
|
|
#[test]
|
|
fn merge_without_json_buffer() {
|
|
let mut hb = Handlebars::new();
|
|
let template_id = "1";
|
|
hb.register_template_string(&template_id, "{}");
|
|
|
|
let hash = HashMap::<String, String>::new();
|
|
let state = rule_state(&hb, &hash);
|
|
|
|
let mut buffer = "invalid".to_string();
|
|
let output = perform_merge(&mut buffer, template_id, &state);
|
|
let expected = Err("Not JSON".to_string());
|
|
assert_eq!(output, expected);
|
|
}
|
|
|
|
/**
|
|
* merging with a JSON buffer should return Ok with the right result
|
|
*/
|
|
#[test]
|
|
fn merge_with_json_buffer() {
|
|
let mut hb = Handlebars::new();
|
|
let template_id = "1";
|
|
hb.register_template_string(&template_id, r#"{"hello":1}"#);
|
|
|
|
let hash = HashMap::<String, String>::new();
|
|
let state = rule_state(&hb, &hash);
|
|
|
|
let mut buffer = "{}".to_string();
|
|
let output = perform_merge(&mut buffer, template_id, &state);
|
|
assert_eq!(output, Ok("{\"hello\":1}".to_string()));
|
|
}
|
|
|
|
/**
|
|
* Ensure that merging with a JSON buffer that it renders variable substitutions
|
|
*/
|
|
#[test]
|
|
fn merge_with_json_buffer_and_vars() {
|
|
let mut hb = Handlebars::new();
|
|
let template_id = "1";
|
|
hb.register_template_string(&template_id, r#"{"hello":"{{name}}"}"#);
|
|
|
|
let mut hash = HashMap::<String, String>::new();
|
|
hash.insert("name".to_string(), "world".to_string());
|
|
let state = rule_state(&hb, &hash);
|
|
|
|
let mut buffer = "{}".to_string();
|
|
let output = perform_merge(&mut buffer, template_id, &state);
|
|
assert_eq!(output, Ok("{\"hello\":\"world\"}".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_precompile_templates_merge() {
|
|
let mut hb = Handlebars::new();
|
|
let settings = Arc::new(load("test/configs/single-rule-with-merge.yml"));
|
|
// Assuming that we're going to register the template with this id
|
|
let template_id = format!("{}-{}", settings.rules[0].uuid, 0);
|
|
|
|
let result = precompile_templates(&mut hb, settings.clone());
|
|
assert!(result);
|
|
assert!(hb.has_template(&template_id));
|
|
}
|
|
|
|
#[test]
|
|
fn test_precompile_templates_replace() {
|
|
let mut hb = Handlebars::new();
|
|
let settings = Arc::new(load("test/configs/single-rule-with-replace.yml"));
|
|
// Assuming that we're going to register the template with this id
|
|
let template_id = format!("{}-{}", settings.rules[0].uuid, 0);
|
|
|
|
let result = precompile_templates(&mut hb, settings.clone());
|
|
assert!(result);
|
|
assert!(hb.has_template(&template_id));
|
|
}
|
|
|
|
#[test]
|
|
fn test_precompile_jmespath() {
|
|
let settings = Arc::new(load("test/configs/single-rule-with-merge.yml"));
|
|
let mut map = JmesPathExpressions::new();
|
|
let result = precompile_jmespath(&mut map, settings.clone());
|
|
assert!(result);
|
|
let expected = settings.rules[0].jmespath.as_ref().unwrap();
|
|
assert!(map.contains_key(expected));
|
|
}
|
|
|
|
#[test]
|
|
fn test_precompile_jmespath_baddata() {
|
|
let settings = Arc::new(load("test/configs/single-rule-with-invalid-jmespath.yml"));
|
|
let mut map = JmesPathExpressions::new();
|
|
let result = precompile_jmespath(&mut map, settings.clone());
|
|
assert!(!result);
|
|
}
|
|
}
|