use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use crate::context;
use crate::level::LogLevel;
type RecordFormatter = Box<dyn Fn(&Record) -> String + Send + Sync>;
pub struct Record {
level: LogLevel,
message: String,
module: String,
file: String,
line: u32,
timestamp: DateTime<Utc>,
metadata: HashMap<String, String>,
context: HashMap<String, serde_json::Value>,
format_fn: Option<RecordFormatter>,
}
impl Clone for Record {
fn clone(&self) -> Self {
Self {
level: self.level,
message: self.message.clone(),
module: self.module.clone(),
file: self.file.clone(),
line: self.line,
timestamp: self.timestamp,
metadata: self.metadata.clone(),
context: self.context.clone(),
format_fn: None, }
}
}
impl fmt::Debug for Record {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Record")
.field("level", &self.level)
.field("message", &self.message)
.field("module", &self.module)
.field("file", &self.file)
.field("line", &self.line)
.field("timestamp", &self.timestamp)
.field("metadata", &self.metadata)
.field("context", &self.context)
.field("format_fn", &format_args!("<function>"))
.finish()
}
}
static UNKNOWN: &str = "unknown";
impl Record {
pub(crate) fn empty() -> Self {
Self {
level: LogLevel::Info,
message: String::new(),
module: UNKNOWN.to_string(),
file: UNKNOWN.to_string(),
line: 0,
timestamp: Utc::now(),
metadata: HashMap::new(),
context: HashMap::new(),
format_fn: None,
}
}
pub fn new(
level: LogLevel,
message: impl Into<String>,
module: Option<String>,
file: Option<String>,
line: Option<u32>,
) -> Self {
let context_map = if context::has_context() {
let mut map = HashMap::new();
for (k, v) in context::current_context().into_iter() {
map.insert(k, context_value_to_json(v));
}
map
} else {
HashMap::new()
};
Self {
level,
message: message.into(),
module: module.unwrap_or_else(|| UNKNOWN.to_string()),
file: file.unwrap_or_else(|| UNKNOWN.to_string()),
line: line.unwrap_or(0),
timestamp: Utc::now(),
metadata: HashMap::new(),
context: context_map,
format_fn: None,
}
}
pub fn level(&self) -> LogLevel {
self.level
}
pub fn message(&self) -> &str {
&self.message
}
pub fn module(&self) -> &str {
&self.module
}
pub fn file(&self) -> &str {
&self.file
}
pub fn line(&self) -> u32 {
self.line
}
pub fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
pub fn metadata(&self) -> &HashMap<String, String> {
&self.metadata
}
pub fn context(&self) -> &HashMap<String, serde_json::Value> {
&self.context
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_structured_data<T: serde::Serialize + ?Sized>(
mut self,
key: &str,
value: &T,
) -> Result<Self, serde_json::Error> {
let json_value = serde_json::to_string(value)?;
self.metadata.insert(key.to_string(), json_value);
Ok(self)
}
pub fn with_context(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.context.insert(key.into(), value);
self
}
pub fn with_deferred_format<F>(mut self, format_fn: F) -> Self
where
F: Fn(&Record) -> String + Send + Sync + 'static,
{
self.format_fn = Some(Box::new(format_fn));
self
}
pub fn get_metadata(&self, key: &str) -> Option<&str> {
self.metadata.get(key).map(String::as_str)
}
pub fn get_context(&self, key: &str) -> Option<&serde_json::Value> {
self.context.get(key)
}
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
pub fn has_metadata(&self) -> bool {
!self.metadata.is_empty()
}
pub fn has_formatter(&self) -> bool {
self.format_fn.is_some()
}
pub fn context_len(&self) -> usize {
self.context.len()
}
pub fn metadata_len(&self) -> usize {
self.metadata.len()
}
pub fn location(&self) -> String {
format!("{}:{}", self.file(), self.line())
}
}
impl fmt::Display for Record {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(format_fn) = &self.format_fn {
write!(f, "{}", format_fn(self))
} else {
write!(
f,
"[{}] {} {}:{} - {}",
self.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
self.level,
self.file,
self.line,
self.message
)
}
}
}
impl Serialize for Record {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("Record", 7)?;
state.serialize_field("level", &self.level)?;
state.serialize_field("message", &self.message)?;
state.serialize_field("module", &self.module)?;
state.serialize_field("file", &self.file)?;
state.serialize_field("line", &self.line)?;
state.serialize_field("timestamp", &self.timestamp.to_rfc3339())?;
state.serialize_field("metadata", &self.metadata)?;
state.end()
}
}
impl<'de> Deserialize<'de> for Record {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Level,
Message,
Module,
File,
Line,
Timestamp,
Metadata,
}
struct RecordVisitor;
impl<'de> serde::de::Visitor<'de> for RecordVisitor {
type Value = Record;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("struct Record")
}
fn visit_map<V>(self, mut map: V) -> Result<Record, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut level = None;
let mut message = None;
let mut module = None;
let mut file = None;
let mut line = None;
let mut timestamp = None;
let mut metadata = None;
while let Some(key) = map.next_key()? {
match key {
Field::Level => {
if level.is_some() {
return Err(serde::de::Error::duplicate_field("level"));
}
level = Some(map.next_value()?);
}
Field::Message => {
if message.is_some() {
return Err(serde::de::Error::duplicate_field("message"));
}
message = Some(map.next_value()?);
}
Field::Module => {
if module.is_some() {
return Err(serde::de::Error::duplicate_field("module"));
}
module = Some(map.next_value()?);
}
Field::File => {
if file.is_some() {
return Err(serde::de::Error::duplicate_field("file"));
}
file = Some(map.next_value()?);
}
Field::Line => {
if line.is_some() {
return Err(serde::de::Error::duplicate_field("line"));
}
line = Some(map.next_value()?);
}
Field::Timestamp => {
if timestamp.is_some() {
return Err(serde::de::Error::duplicate_field("timestamp"));
}
let ts_str: String = map.next_value()?;
timestamp = Some(
DateTime::parse_from_rfc3339(&ts_str)
.map_err(serde::de::Error::custom)?
.with_timezone(&Utc),
);
}
Field::Metadata => {
if metadata.is_some() {
return Err(serde::de::Error::duplicate_field("metadata"));
}
metadata = Some(map.next_value()?);
}
}
}
let level = level.ok_or_else(|| serde::de::Error::missing_field("level"))?;
let message = message.ok_or_else(|| serde::de::Error::missing_field("message"))?;
let module = module.ok_or_else(|| serde::de::Error::missing_field("module"))?;
let file = file.ok_or_else(|| serde::de::Error::missing_field("file"))?;
let line = line.ok_or_else(|| serde::de::Error::missing_field("line"))?;
let timestamp =
timestamp.ok_or_else(|| serde::de::Error::missing_field("timestamp"))?;
let metadata = metadata.unwrap_or_default();
Ok(Record {
level,
message,
module,
file,
line,
timestamp,
metadata,
context: HashMap::new(),
format_fn: None,
})
}
}
deserializer.deserialize_struct(
"Record",
&[
"level",
"message",
"module",
"file",
"line",
"timestamp",
"metadata",
],
RecordVisitor,
)
}
}
fn context_value_to_json(val: context::ContextValue) -> serde_json::Value {
match val {
context::ContextValue::String(s) => serde_json::Value::String(s),
context::ContextValue::Integer(i) => serde_json::Value::Number(i.into()),
context::ContextValue::Float(f) => match serde_json::Number::from_f64(f) {
Some(num) => serde_json::Value::Number(num),
None => serde_json::Value::Null,
},
context::ContextValue::Bool(b) => serde_json::Value::Bool(b),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_record_creation() {
let record = Record::new(LogLevel::Info, "test message", None, None, None);
assert_eq!(record.level(), LogLevel::Info);
assert_eq!(record.message(), "test message");
assert_eq!(record.module(), "unknown");
assert_eq!(record.file(), "unknown");
assert_eq!(record.line(), 0);
}
#[test]
fn test_record_with_metadata() {
let record = Record::new(LogLevel::Info, "test message", None, None, None)
.with_metadata("key", "value");
assert_eq!(record.metadata().get("key").unwrap(), "value");
}
#[test]
fn test_record_with_all_fields() {
let record = Record::new(
LogLevel::Info,
"test message",
Some("test_module".to_string()),
Some("test_file.rs".to_string()),
Some(42),
);
assert_eq!(record.module(), "test_module");
assert_eq!(record.file(), "test_file.rs");
assert_eq!(record.line(), 42);
}
#[test]
fn test_record_display() {
let record = Record::new(LogLevel::Error, "Test error message", None, None, None);
let display = format!("{}", record);
assert!(display.contains("ERROR"));
assert!(display.contains("unknown:0"));
assert!(display.contains("Test error message"));
}
#[test]
fn test_record_metadata_overwrite() {
let record = Record::new(
LogLevel::Info,
"Test message",
Some("test_module".to_string()),
Some("test.rs".to_string()),
Some(42),
)
.with_metadata("key", "value1")
.with_metadata("key", "value2");
assert_eq!(record.metadata().get("key"), Some(&"value2".to_string()));
}
#[test]
fn test_record_structured_data() {
let record = Record::new(
LogLevel::Info,
"test message",
Some("test_module".to_string()),
Some("test.rs".to_string()),
Some(42),
);
let record = record.with_structured_data("key", &"value").unwrap();
assert_eq!(record.metadata().get("key"), Some(&"\"value\"".to_string()));
}
#[test]
fn test_record_timestamp() {
let record = Record::new(
LogLevel::Info,
"Test message",
Some("test_module".to_string()),
Some("test.rs".to_string()),
Some(42),
);
let now = chrono::Utc::now();
assert!(record.timestamp() <= now);
}
#[test]
fn test_record_context() {
let record = Record::new(LogLevel::Info, "test message", None, None, None).with_context(
"user",
json!({
"id": 123,
"name": "test"
}),
);
let user = record.get_context("user").unwrap();
assert_eq!(user["id"], 123);
assert_eq!(user["name"], "test");
}
#[test]
fn test_record_deferred_format() {
let record = Record::new(LogLevel::Info, "test message", None, None, None)
.with_deferred_format(|r| {
format!("[{}] {} - {}", r.timestamp(), r.level(), r.message())
});
let display = format!("{}", record);
assert!(display.contains("INFO"));
assert!(display.contains("test message"));
}
#[test]
fn test_record_serialization() {
let record = Record::new(
LogLevel::Info,
"test message",
Some("test_module".to_string()),
Some("test.rs".to_string()),
Some(42),
);
let serialized = serde_json::to_string(&record).unwrap();
let deserialized: Record = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.level(), record.level());
assert_eq!(deserialized.message(), record.message());
assert_eq!(deserialized.module(), record.module());
assert_eq!(deserialized.file(), record.file());
assert_eq!(deserialized.line(), record.line());
}
#[test]
fn test_record_state_checks() {
let record = Record::new(LogLevel::Info, "test message", None, None, None);
assert!(!record.has_context());
assert!(!record.has_metadata());
assert!(!record.has_formatter());
assert_eq!(record.context_len(), 0);
assert_eq!(record.metadata_len(), 0);
let record = record
.with_metadata("key", "value")
.with_context("user", json!({"id": 1}))
.with_deferred_format(|r| format!("{}", r.message()));
assert!(record.has_context());
assert!(record.has_metadata());
assert!(record.has_formatter());
assert_eq!(record.context_len(), 1);
assert_eq!(record.metadata_len(), 1);
}
}