#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: SystemTime,
pub level: TraceLevel,
pub message: String,
pub span_id: Option<String>,
pub trace_id: Option<String>,
pub attributes: HashMap<String, AttributeValue>,
}
impl LogEntry {
pub fn new(level: TraceLevel, message: &str) -> Self {
Self {
timestamp: SystemTime::now(),
level,
message: message.to_string(),
span_id: None,
trace_id: None,
attributes: HashMap::new(),
}
}
pub fn with_span(mut self, span_id: &str, trace_id: &str) -> Self {
self.span_id = Some(span_id.to_string());
self.trace_id = Some(trace_id.to_string());
self
}
pub fn with_attr(mut self, key: &str, value: AttributeValue) -> Self {
self.attributes.insert(key.to_string(), value);
self
}
pub fn to_json(&self) -> String {
let mut json = String::from("{");
json.push_str(&format!(
"\"timestamp\": \"{}\", \"level\": \"{}\", \"message\": \"{}\"",
format_timestamp(self.timestamp),
self.level.name(),
escape_json(&self.message)
));
if let Some(ref span_id) = self.span_id {
json.push_str(&format!(", \"spanId\": \"{}\"", span_id));
}
if let Some(ref trace_id) = self.trace_id {
json.push_str(&format!(", \"traceId\": \"{}\"", trace_id));
}
if !self.attributes.is_empty() {
json.push_str(", \"attributes\": {");
let attrs: Vec<String> = self
.attributes
.iter()
.map(|(k, v)| format!("\"{}\": {}", escape_json(k), v.to_json()))
.collect();
json.push_str(&attrs.join(", "));
json.push('}');
}
json.push('}');
json
}
pub fn format(&self) -> String {
format!(
"{} [{}] {}",
format_timestamp(self.timestamp),
self.level.name(),
self.message
)
}
}
#[derive(Debug)]
pub struct Logger {
context: Option<TracingContext>,
entries: Vec<LogEntry>,
min_level: TraceLevel,
}
impl Logger {
pub fn new() -> Self {
Self {
context: None,
entries: Vec::new(),
min_level: TraceLevel::Info,
}
}
pub fn with_context(mut self, context: TracingContext) -> Self {
self.context = Some(context);
self
}
pub fn with_level(mut self, level: TraceLevel) -> Self {
self.min_level = level;
self
}
pub fn log(&mut self, level: TraceLevel, message: &str) {
if !self.min_level.should_log(level) {
return;
}
let mut entry = LogEntry::new(level, message);
if let Some(ref ctx) = self.context {
if let Some(span_id) = ctx.current_span_id() {
entry = entry.with_span(&span_id, ctx.trace_id());
}
}
self.entries.push(entry);
}
pub fn error(&mut self, message: &str) {
self.log(TraceLevel::Error, message);
}
pub fn warn(&mut self, message: &str) {
self.log(TraceLevel::Warn, message);
}
pub fn info(&mut self, message: &str) {
self.log(TraceLevel::Info, message);
}
pub fn debug(&mut self, message: &str) {
self.log(TraceLevel::Debug, message);
}
pub fn entries(&self) -> &[LogEntry] {
&self.entries
}
pub fn context(&self) -> Option<&TracingContext> {
self.context.as_ref()
}
pub fn context_mut(&mut self) -> Option<&mut TracingContext> {
self.context.as_mut()
}
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
fn generate_id() -> String {
use std::time::SystemTime;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{:016x}", now ^ 0xDEADBEEF_u128)
}
fn generate_trace_id() -> String {
use std::time::SystemTime;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
format!("{:032x}", now)
}
fn time_to_nanos(time: SystemTime) -> u128 {
time.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
}
fn format_timestamp(time: SystemTime) -> String {
let duration = time.duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO);
let secs = duration.as_secs();
let millis = duration.subsec_millis();
format!("{}.{:03}", secs, millis)
}
fn escape_json(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn truncate(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
#[cfg(test)]
#[path = "tracing_tests_tracing_001.rs"]
mod tests_extracted;