use crate::log_settings::LogSettings;
use crate::time::try_convert_timestamp_to_readable;
use handlebars::Handlebars;
use indexmap::IndexMap;
use serde_json::{Map, Value};
use std::borrow::ToOwned;
use std::collections::BTreeMap;
use std::io::Write;
use yansi::Paint;
pub fn print_log_line(
out: &mut dyn Write,
maybe_prefix: Option<&str>,
log_entry: &Map<String, Value>,
log_settings: &LogSettings,
handlebars: &Handlebars<'static>,
) {
let string_log_entry = flatten_json(log_entry, "");
let level = {
let level = get_string_value_or_default(&string_log_entry, &log_settings.level_keys, "unknown");
log_settings.level_map.get(&level).cloned().unwrap_or(level)
};
let trimmed_prefix = maybe_prefix.map(|p| p.trim()).unwrap_or_else(|| "").to_string();
let mut message = get_string_value_or_default(&string_log_entry, &log_settings.message_keys, "");
let timestamp = try_convert_timestamp_to_readable(get_string_value_or_default(&string_log_entry, &log_settings.time_keys, ""));
if let Some(message_template) = &log_settings.substitution
&& let Some(templated_message) = message_template.apply(&message, log_entry)
{
message = templated_message;
}
let mut handle_bar_input: Map<String, Value> = log_entry.clone();
handle_bar_input.insert("fblog_timestamp".to_string(), Value::String(timestamp));
handle_bar_input.insert("fblog_level".to_string(), Value::String(level));
handle_bar_input.insert("fblog_message".to_string(), Value::String(message));
handle_bar_input.insert("fblog_prefix".to_string(), Value::String(trimmed_prefix));
let write_result = match handlebars.render("main_line", &handle_bar_input) {
Ok(string) => writeln!(out, "{string}"),
Err(e) => writeln!(out, "{} Failed to process line: {}", "??? >".red().bold(), e),
};
if write_result.is_err() {
std::process::exit(14);
}
if log_settings.dump_all {
let all_values: Vec<String> = string_log_entry
.keys()
.map(ToOwned::to_owned)
.filter(|v| !log_settings.excluded_values.contains(v))
.collect();
write_additional_values(out, &string_log_entry, &all_values, handlebars);
} else {
write_additional_values(out, &string_log_entry, &log_settings.additional_values, handlebars);
}
}
fn flatten_json(log_entry: &Map<String, Value>, prefix: &str) -> IndexMap<String, String> {
let mut flattened_json: IndexMap<String, String> = IndexMap::new();
for (key, value) in log_entry {
match value {
Value::String(string_value) => {
flattened_json.insert(format!("{prefix}{key}"), string_value.to_string());
}
Value::Bool(bool_value) => {
flattened_json.insert(format!("{prefix}{key}"), bool_value.to_string());
}
Value::Number(number_value) => {
flattened_json.insert(format!("{prefix}{key}"), number_value.to_string());
}
Value::Array(array_values) => {
for (index, array_value) in array_values.iter().enumerate() {
let key = format!("{}[{}]", key, index);
match array_value {
Value::Array(array_values) => {
flatten_array(&key, prefix, array_values, &mut flattened_json);
}
Value::Object(nested_entry) => {
flattened_json.extend(flatten_json(nested_entry, &format!("{prefix}{key} > ")));
}
_ => {
flattened_json.insert(format!("{prefix}{key}"), array_value.to_string());
}
};
}
}
Value::Object(nested_entry) => {
flattened_json.extend(flatten_json(nested_entry, &format!("{prefix}{key} > ")));
}
Value::Null => {}
};
}
flattened_json
}
fn flatten_array(key: &str, prefix: &str, array_values: &[Value], flattened_json: &mut IndexMap<String, String>) {
for (index, array_value) in array_values.iter().enumerate() {
let key = format!("{}[{}]", key, index + 1);
match array_value {
Value::Array(nested_array_values) => flatten_array(&key, prefix, nested_array_values, flattened_json),
Value::Object(nested_entry) => {
flattened_json.extend(flatten_json(nested_entry, &format!("{prefix}{key} > ")));
}
_ => {
flattened_json.insert(format!("{prefix}{key}"), array_value.to_string());
}
};
}
}
fn get_string_value(value: &IndexMap<String, String>, keys: &[String]) -> Option<String> {
keys
.iter()
.fold(None::<String>, |maybe_match, key| maybe_match.or_else(|| value.get(key).map(ToOwned::to_owned)))
}
fn get_string_value_or_default(value: &IndexMap<String, String>, keys: &[String], default: &str) -> String {
get_string_value(value, keys).unwrap_or_else(|| default.to_string())
}
fn write_additional_values(out: &mut dyn Write, log_entry: &IndexMap<String, String>, additional_values: &[String], handlebars: &Handlebars<'static>) {
for additional_value_prefix in additional_values {
for additional_value in log_entry
.keys()
.filter(|k| *k == additional_value_prefix || k.starts_with(&format!("{}{}", additional_value_prefix, " > ")))
{
if let Some(value) = get_string_value(log_entry, &[additional_value.to_string()]) {
let mut variables: BTreeMap<String, String> = BTreeMap::new();
variables.insert("key".to_string(), additional_value.to_string());
variables.insert("value".to_string(), value.to_string());
let write_result = match handlebars.render("additional_value", &variables) {
Ok(string) => writeln!(out, "{string}"),
Err(e) => writeln!(out, "{} Failed to process additional value: {}", " ??? >".red().bold(), e),
};
if write_result.is_err() {
std::process::exit(14);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::template;
fn without_style(styled: &str) -> String {
use regex::Regex;
let regex = Regex::new("\u{001B}\\[[\\d;]*[^\\d;]").expect("Regex should be valid");
regex.replace_all(styled, "").into_owned()
}
fn fblog_handlebar_registry_default_format() -> Handlebars<'static> {
let main_line_format = template::DEFAULT_MAIN_LINE_FORMAT.to_string();
let additional_value_format = template::DEFAULT_ADDITIONAL_VALUE_FORMAT.to_string();
template::fblog_handlebar_registry(main_line_format, additional_value_format)
}
fn out_to_string(out: Vec<u8>) -> String {
let out_with_style = String::from_utf8_lossy(&out).into_owned();
without_style(&out_with_style)
}
#[test]
fn write_log_entry() {
let handlebars = fblog_handlebar_registry_default_format();
let log_settings = LogSettings::new_default_settings();
let mut out: Vec<u8> = Vec::new();
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("level".to_string(), Value::String("info".to_string()));
print_log_line(&mut out, None, &log_entry, &log_settings, &handlebars);
assert_eq!(out_to_string(out), "2017-07-06T15:21:16 INFO: something happened\n");
}
#[test]
fn write_log_entry_with_mapped_level() {
let handlebars = fblog_handlebar_registry_default_format();
let mut log_settings = LogSettings::new_default_settings();
log_settings.level_map = BTreeMap::from([("30".to_string(), "info".to_string())]);
let mut out: Vec<u8> = Vec::new();
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("level".to_string(), Value::String("30".to_string()));
print_log_line(&mut out, None, &log_entry, &log_settings, &handlebars);
assert_eq!(out_to_string(out), "2017-07-06T15:21:16 INFO: something happened\n");
}
#[test]
fn write_log_entry_with_prefix() {
let handlebars = fblog_handlebar_registry_default_format();
let log_settings = LogSettings::new_default_settings();
let mut out: Vec<u8> = Vec::new();
let prefix = "abc";
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("level".to_string(), Value::String("info".to_string()));
print_log_line(&mut out, Some(prefix), &log_entry, &log_settings, &handlebars);
assert_eq!(out_to_string(out), "2017-07-06T15:21:16 INFO: abc something happened\n");
}
#[test]
fn write_log_entry_with_additional_field() {
let handlebars = fblog_handlebar_registry_default_format();
let mut out: Vec<u8> = Vec::new();
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("fu".to_string(), Value::String("bower".to_string()));
log_entry.insert("level".to_string(), Value::String("info".to_string()));
let mut log_settings = LogSettings::new_default_settings();
log_settings.add_additional_values(vec!["process".to_string(), "fu".to_string()]);
print_log_line(&mut out, None, &log_entry, &log_settings, &handlebars);
assert_eq!(
out_to_string(out),
"\
2017-07-06T15:21:16 INFO: something happened
process: rust
fu: bower
"
);
}
#[test]
fn write_log_entry_with_additional_field_and_prefix() {
let handlebars = fblog_handlebar_registry_default_format();
let mut out: Vec<u8> = Vec::new();
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("fu".to_string(), Value::String("bower".to_string()));
log_entry.insert("level".to_string(), Value::String("info".to_string()));
let prefix = "abc";
let mut log_settings = LogSettings::new_default_settings();
log_settings.add_additional_values(vec!["process".to_string(), "fu".to_string()]);
print_log_line(&mut out, Some(prefix), &log_entry, &log_settings, &handlebars);
assert_eq!(
out_to_string(out),
"\
2017-07-06T15:21:16 INFO: abc something happened
process: rust
fu: bower
"
);
}
#[test]
fn write_log_entry_dump_all() {
let handlebars = fblog_handlebar_registry_default_format();
let mut out: Vec<u8> = Vec::new();
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("fu".to_string(), Value::String("bower".to_string()));
log_entry.insert("level".to_string(), Value::String("info".to_string()));
let mut log_settings = LogSettings::new_default_settings();
log_settings.dump_all = true;
print_log_line(&mut out, None, &log_entry, &log_settings, &handlebars);
assert_eq!(
out_to_string(out),
"\
2017-07-06T15:21:16 INFO: something happened
fu: bower
level: info
message: something happened
process: rust
time: 2017-07-06T15:21:16
"
);
}
#[test]
fn write_log_entry_with_exotic_fields() {
let handlebars = fblog_handlebar_registry_default_format();
let mut log_settings = LogSettings::new_default_settings();
let mut out: Vec<u8> = Vec::new();
let mut log_entry: Map<String, Value> = Map::new();
log_entry.insert("message".to_string(), Value::String("something happened".to_string()));
log_entry.insert("time".to_string(), Value::String("2017-07-06T15:21:16".to_string()));
log_entry.insert("process".to_string(), Value::String("rust".to_string()));
log_entry.insert("moep".to_string(), Value::String("moep".to_string()));
log_entry.insert("hugo".to_string(), Value::String("hugo".to_string()));
log_entry.insert("level".to_string(), Value::String("info".to_string()));
log_settings.add_message_keys(vec!["process".to_string()]);
log_settings.add_time_keys(vec!["moep".to_string()]);
log_settings.add_level_keys(vec!["hugo".to_string()]);
print_log_line(&mut out, None, &log_entry, &log_settings, &handlebars);
assert_eq!(out_to_string(out), " moep HUGO: rust\n");
}
}