use std::{
collections::{hash_map::DefaultHasher, BTreeMap},
fmt,
hash::{Hash, Hasher},
};
use chrono::{DateTime, SecondsFormat, Utc};
use serde::{Serialize, Serializer};
use types::SemVer;
use crate::logging::{
log_level::{LogLevel, LogPriority},
log_settings::{HostName, LogSettingsProvider, ProcessId, ProcessName},
};
const MESSAGE_TYPE: &str = "ee-structured";
#[derive(Clone, Debug, Serialize)]
pub struct LogMessage {
pub timestamp: TimestampRfc3999,
pub process_id: ProcessId,
pub process_name: ProcessName,
pub host_name: HostName,
pub log_level: LogLevel,
pub priority: LogPriority,
pub message_type: MessageType,
pub message_type_version: MessageTypeVersion,
pub message_id: MessageId,
pub description: String,
pub properties: MessageProperties,
}
impl LogMessage {
pub fn new_props<T: ?Sized>(
log_settings_provider: &T,
log_level: LogLevel,
message_template: String,
mut properties: BTreeMap<String, String>,
) -> LogMessage
where
T: LogSettingsProvider,
{
const MESSAGE_TEMPLATE_KEY: &str = "message_template";
properties
.entry(MESSAGE_TEMPLATE_KEY.to_string())
.or_insert_with(|| message_template.clone());
let message_type = MessageType::new(MESSAGE_TYPE.to_string());
let message_type_version: MessageTypeVersion = Default::default();
let process_id = log_settings_provider.get_process_id();
let process_name = log_settings_provider.get_process_name();
let host_name = log_settings_provider.get_host_name();
let timestamp = TimestampRfc3999::default();
let priority = LogPriority::new(log_level);
let properties = MessageProperties::new(properties);
let description = properties.get_formatted_message(&message_template);
let hash = {
let mut state = DefaultHasher::new();
message_type_version.hash(&mut state);
process_id.hash(&mut state);
process_name.hash(&mut state);
host_name.hash(&mut state);
timestamp.hash(&mut state);
log_level.hash(&mut state);
priority.hash(&mut state);
description.hash(&mut state);
properties.hash(&mut state);
state.finish()
};
let message_id = MessageId::new(hash.to_string());
LogMessage {
timestamp,
process_id,
process_name,
host_name,
log_level,
priority,
message_type,
message_type_version,
message_id,
description,
properties,
}
}
pub fn new_msg<T: ?Sized>(
log_settings_provider: &T,
log_level: LogLevel,
message: String,
) -> LogMessage
where
T: LogSettingsProvider,
{
let mut properties = BTreeMap::new();
properties.insert("message".to_owned(), message);
Self::new_props(
log_settings_provider,
log_level,
"{message}".to_owned(),
properties,
)
}
}
impl fmt::Display for LogMessage {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{timestamp} priority:{priority}|{level} {description}",
timestamp = self.timestamp,
priority = self.priority,
level = self.log_level,
description = self.description
)
}
}
#[derive(Debug, Copy, Clone)]
pub struct MessageTypeVersion(SemVer);
impl MessageTypeVersion {
pub fn new(major: u32, minor: u32, patch: u32) -> MessageTypeVersion {
MessageTypeVersion(SemVer::new(major, minor, patch))
}
}
impl fmt::Display for MessageTypeVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
impl Hash for MessageTypeVersion {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl Default for MessageTypeVersion {
fn default() -> Self {
MessageTypeVersion(SemVer::V1_0_0)
}
}
impl Serialize for MessageTypeVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!("{}.{}.{}", self.0.major, self.0.minor, self.0.patch);
serializer.serialize_str(&s)
}
}
#[derive(Clone, Debug, Hash, Serialize)]
pub struct TimestampRfc3999(String);
impl Default for TimestampRfc3999 {
fn default() -> Self {
let now: DateTime<Utc> = Utc::now();
TimestampRfc3999(now.to_rfc3339_opts(SecondsFormat::Millis, true))
}
}
impl fmt::Display for TimestampRfc3999 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{timestamp}", timestamp = self.0.to_string())
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct MessageId(String);
impl MessageId {
pub fn new(hash: String) -> MessageId {
MessageId(hash)
}
#[allow(dead_code)]
pub(crate) fn value(&self) -> String {
self.0.to_owned()
}
}
#[derive(Clone, Debug, Hash, Serialize)]
pub struct MessageType(String);
impl MessageType {
pub fn new(message_type: String) -> MessageType {
MessageType(message_type)
}
#[allow(dead_code)]
pub(crate) fn value(&self) -> String {
self.0.to_owned()
}
}
#[derive(Clone, Debug, Hash, Serialize)]
pub struct MessageTemplate(String);
impl MessageTemplate {
pub fn new(fmt_template: String) -> Self {
MessageTemplate(fmt_template)
}
#[allow(dead_code)]
pub(crate) fn value(&self) -> String {
self.0.to_owned()
}
}
#[derive(Clone, Debug, Hash, Serialize)]
pub struct MessageProperties(BTreeMap<String, String>);
impl MessageProperties {
pub fn new(properties: BTreeMap<String, String>) -> MessageProperties {
MessageProperties(properties)
}
#[allow(dead_code)]
pub(crate) fn value(&self) -> BTreeMap<String, String> {
self.0.to_owned()
}
pub fn get_formatted_message(&self, message_template: &str) -> String {
if message_template.is_empty() {
return String::new();
}
let mut buf = String::new();
let mut candidate_key = String::new();
let mut key_seek = false;
let properties = &self.0;
for c in message_template.chars() {
match c {
'{' => {
key_seek = true;
candidate_key.clear();
}
'}' if key_seek => {
key_seek = false;
if let Some(v) = properties.get(&candidate_key) {
buf.push_str(v);
}
}
'}' => (),
c if key_seek => candidate_key.push(c),
c => buf.push(c),
}
}
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::logging::log_settings::{self, LogLevelFilter};
#[test]
fn should_format_message_template_default_use_case() {
let mut properties: BTreeMap<String, String> = BTreeMap::new();
properties.insert("message".to_string(), "i am a log message".to_string());
let props = MessageProperties::new(properties);
let template = "{message}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(
formatted,
"i am a log message".to_string(),
"message malformed"
)
}
#[test]
fn should_format_message_template_starting_and_ending_with_braces() {
let mut properties: BTreeMap<String, String> = BTreeMap::new();
properties.insert("message".to_string(), "i convey meaning".to_string());
properties.insert("abc".to_string(), "some text".to_string());
properties.insert("some-hash".to_string(), "A@#$!@#".to_string());
properties.insert("byz".to_string(), "".to_string());
let props = MessageProperties::new(properties);
let template =
"{abc} i'm a message temp{byz}late some-hash:{some-hash} msg:{message}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(
formatted,
"some text i\'m a message template some-hash:A@#$!@# msg:i convey meaning".to_string(),
"message malformed"
)
}
#[test]
fn should_format_message_template_with_escaped_braces() {
let mut properties: BTreeMap<String, String> = BTreeMap::new();
properties.insert("message".to_string(), "a message".to_string());
properties.insert("more-data".to_string(), "some additional data".to_string());
let props = MessageProperties::new(properties);
let template = "this is {{message}} with {{{more-data}}}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(
formatted,
"this is a message with some additional data".to_string(),
"message malformed"
)
}
#[test]
fn should_format_message_template_with_no_properties() {
let properties: BTreeMap<String, String> = BTreeMap::new();
let props = MessageProperties::new(properties);
let template = "{message}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(formatted, "".to_string(), "message malformed")
}
#[test]
fn should_format_message_template_with_unclosed_brace() {
let properties: BTreeMap<String, String> = BTreeMap::new();
let props = MessageProperties::new(properties);
let template = "{message".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(formatted, "".to_string(), "message malformed")
}
#[test]
fn should_format_message_template_with_unopened_brace() {
let properties: BTreeMap<String, String> = BTreeMap::new();
let props = MessageProperties::new(properties);
let template = "message}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(formatted, "message".to_string(), "message malformed")
}
#[test]
fn should_format_message_template_with_mismatched_braces_left() {
let properties: BTreeMap<String, String> = BTreeMap::new();
let props = MessageProperties::new(properties);
let template = "{{message}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(formatted, "".to_string(), "message malformed")
}
#[test]
fn should_format_message_template_with_mismatched_braces_right() {
let properties: BTreeMap<String, String> = BTreeMap::new();
let props = MessageProperties::new(properties);
let template = "{message}}".to_string();
let formatted = props.get_formatted_message(&template);
assert_eq!(formatted, "".to_string(), "message malformed")
}
#[test]
fn should_validate_log_message() {
let settings = log_settings::LogSettings::new(
"log_message_tests",
LogLevelFilter::new(LogLevel::Error),
);
let l = super::LogMessage::new_msg(&settings, LogLevel::Error, "test msg".to_owned());
assert!(
should_have_rfc3339_timestamp(&l),
"rfc3339 timestamp required"
);
assert!(
should_have_message_dedup_hash(&l),
"dedup mechanism required"
);
assert!(should_have_log_level(&l), "log level required");
assert!(should_have_process_id(&l), "process id required");
assert!(should_have_process_name(&l), "process name required");
assert!(should_have_host_name(&l), "host name required");
assert!(should_have_at_least_one_property(&l), "properties required");
assert!(should_have_description(&l), "description required");
}
fn should_have_rfc3339_timestamp(l: &super::LogMessage) -> bool {
match DateTime::parse_from_rfc3339(&l.timestamp.0) {
Ok(_d) => true,
Err(_) => false,
}
}
fn should_have_message_dedup_hash(l: &super::LogMessage) -> bool {
!l.message_id.0.is_empty()
}
fn should_have_log_level(l: &super::LogMessage) -> bool {
l.log_level <= LogLevel::Fatal && l.log_level >= LogLevel::Debug
}
fn should_have_description(l: &super::LogMessage) -> bool {
!l.description.is_empty()
}
fn should_have_process_id(l: &super::LogMessage) -> bool {
l.process_id.value() > 0
}
fn should_have_process_name(l: &super::LogMessage) -> bool {
!l.process_name.value().is_empty()
}
fn should_have_host_name(l: &super::LogMessage) -> bool {
!l.host_name.value().is_empty()
}
fn should_have_at_least_one_property(l: &super::LogMessage) -> bool {
l.properties.value().keys().len() > 0
}
}