1use crate::{LogFormat, LogLevel};
7use dtt::datetime::DateTime;
8use serde::{Deserialize, Serialize};
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::fmt;
12use std::sync::LazyLock;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1);
17
18static CACHED_HOSTNAME: LazyLock<String> = LazyLock::new(|| {
20 hostname::get().map_or_else(
21 |_| "localhost".to_string(),
22 |h| h.to_string_lossy().to_string(),
23 )
24});
25
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq)]
34pub struct Log {
35 pub session_id: u64,
37 pub time: Cow<'static, str>,
39 pub level: LogLevel,
41 pub component: Cow<'static, str>,
43 pub description: String,
45 pub format: LogFormat,
47 pub attributes: BTreeMap<String, serde_json::Value>,
49}
50
51impl Default for Log {
52 fn default() -> Self {
53 Self {
54 session_id: 0,
55 time: Cow::Borrowed(""),
56 level: LogLevel::INFO,
57 component: Cow::Borrowed(""),
58 description: String::default(),
59 format: LogFormat::CLF,
60 attributes: BTreeMap::new(),
61 }
62 }
63}
64
65impl Log {
66 #[track_caller]
71 pub fn log(&self) {
72 crate::engine::ENGINE.inc_format(self.format);
73 let event = crate::engine::LogEvent {
74 level: self.level,
75 level_num: self.level.to_numeric(),
76 log: self.clone(),
77 };
78 crate::engine::ENGINE.ingest(event);
79 }
80
81 #[must_use]
83 pub fn info(description: &str) -> Self {
84 Self::build(LogLevel::INFO, description)
85 }
86
87 #[must_use]
89 pub fn warn(description: &str) -> Self {
90 Self::build(LogLevel::WARN, description)
91 }
92
93 #[must_use]
95 pub fn error(description: &str) -> Self {
96 Self::build(LogLevel::ERROR, description)
97 }
98
99 #[must_use]
101 pub fn debug(description: &str) -> Self {
102 Self::build(LogLevel::DEBUG, description)
103 }
104
105 #[must_use]
107 pub fn trace(description: &str) -> Self {
108 Self::build(LogLevel::TRACE, description)
109 }
110
111 #[must_use]
113 pub fn verbose(description: &str) -> Self {
114 Self::build(LogLevel::VERBOSE, description)
115 }
116
117 #[must_use]
119 pub fn fatal(description: &str) -> Self {
120 Self::build(LogLevel::FATAL, description)
121 }
122
123 #[must_use]
125 pub fn critical(description: &str) -> Self {
126 Self::build(LogLevel::CRITICAL, description)
127 }
128
129 #[must_use]
134 pub fn build(level: LogLevel, description: &str) -> Self {
135 Self {
136 session_id: SESSION_COUNTER.fetch_add(1, Ordering::Relaxed),
137 time: Cow::Owned(DateTime::new().to_string()),
138 level,
139 component: Cow::Borrowed("default"),
140 description: description.to_string(),
141 format: LogFormat::MCP,
142 attributes: BTreeMap::new(),
143 }
144 }
145
146 #[must_use]
148 pub fn time(mut self, time: &str) -> Self {
149 self.time = Cow::Owned(time.to_string());
150 self
151 }
152
153 #[must_use]
155 pub const fn session_id(mut self, session_id: u64) -> Self {
156 self.session_id = session_id;
157 self
158 }
159
160 #[must_use]
162 pub fn with<T: Serialize>(mut self, key: &str, value: T) -> Self {
163 if let Ok(val) = serde_json::to_value(value) {
164 self.attributes.insert(key.to_string(), val);
165 }
166 self
167 }
168
169 #[must_use]
171 pub fn component(mut self, component: &str) -> Self {
172 self.component = Cow::Owned(component.to_string());
173 self
174 }
175
176 #[must_use]
178 pub const fn format(mut self, format: LogFormat) -> Self {
179 self.format = format;
180 self
181 }
182
183 #[track_caller]
188 pub fn fire(mut self) {
189 let caller = std::panic::Location::caller();
190 self.attributes.insert(
191 "caller".to_string(),
192 serde_json::Value::String(format!(
193 "{}:{}",
194 caller.file(),
195 caller.line()
196 )),
197 );
198 crate::engine::ENGINE.inc_format(self.format);
199 let event = crate::engine::LogEvent {
200 level: self.level,
201 level_num: self.level.to_numeric(),
202 log: self,
203 };
204 crate::engine::ENGINE.ingest(event);
205 }
206
207 fn write_logfmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 f.write_str("level=")?;
209 f.write_str(self.level.as_str_lowercase())?;
210 f.write_str(" msg=\"")?;
211 f.write_str(&self.description.replace('"', "\\\""))?;
212 write!(f, "\" session_id={}", self.session_id)?;
213 f.write_str(" component=\"")?;
214 f.write_str(&self.component)?;
215 f.write_str("\"")?;
216
217 for (key, value) in &self.attributes {
218 write!(f, " {key}=")?;
219 match value {
220 serde_json::Value::String(s) => {
221 if s.contains(' ')
222 || s.contains('"')
223 || s.is_empty()
224 {
225 write!(f, "\"{0}\"", s.replace('"', "\\\""))?;
226 } else {
227 write!(f, "{s}")?;
228 }
229 }
230 _ => write!(f, "{value}")?,
231 }
232 }
233 Ok(())
234 }
235}
236
237fn write_json_str(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
239 f.write_str("\"")?;
240 for c in s.chars() {
241 match c {
242 '"' => f.write_str("\\\"")?,
243 '\\' => f.write_str("\\\\")?,
244 '\n' => f.write_str("\\n")?,
245 '\r' => f.write_str("\\r")?,
246 '\t' => f.write_str("\\t")?,
247 c if c.is_control() => write!(f, "\\u{:04x}", c as u32)?,
248 c => write!(f, "{c}")?,
249 }
250 }
251 f.write_str("\"")
252}
253
254fn write_json_map(
256 f: &mut fmt::Formatter<'_>,
257 map: &BTreeMap<String, serde_json::Value>,
258) -> fmt::Result {
259 f.write_str("{")?;
260 let mut first = true;
261 for (key, value) in map {
262 if !first {
263 f.write_str(",")?;
264 }
265 first = false;
266 write_json_str(f, key)?;
267 write!(f, ":{value}")?;
269 }
270 f.write_str("}")
271}
272
273impl Log {
275 fn fmt_clf(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 write!(
277 f,
278 "SessionID={} Timestamp={} Description={} Level={} Component={}",
279 self.session_id,
280 self.time,
281 self.description,
282 self.level,
283 self.component
284 )
285 }
286
287 fn fmt_cef(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288 write!(
289 f,
290 "CEF:0|{}|{}|{}|{}|{}|CEF",
291 self.session_id,
292 self.time,
293 self.level,
294 self.component,
295 self.description
296 )
297 }
298
299 fn fmt_elf(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 write!(
301 f,
302 "ELF:0|{}|{}|{}|{}|{}|ELF",
303 self.session_id,
304 self.time,
305 self.level,
306 self.component,
307 self.description
308 )
309 }
310
311 fn fmt_w3c(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(
313 f,
314 "W3C:0|{}|{}|{}|{}|{}|W3C",
315 self.session_id,
316 self.time,
317 self.level,
318 self.component,
319 self.description
320 )
321 }
322
323 fn fmt_apache(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 write!(
325 f,
326 "{} - - [{}] \"{}\" {} {}",
327 &*CACHED_HOSTNAME,
328 self.time,
329 self.description,
330 self.level,
331 self.component
332 )
333 }
334
335 fn fmt_log4j_xml(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 write!(
337 f,
338 r#"<log4j:event logger="{}" timestamp="{}" level="{}" thread="{}"><log4j:message>{}</log4j:message></log4j:event>"#,
339 self.component,
340 self.time,
341 self.level,
342 self.session_id,
343 self.description
344 )
345 }
346
347 fn fmt_json(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 f.write_str("{\"Attributes\":")?;
349 write_json_map(f, &self.attributes)?;
350 f.write_str(",\"Component\":")?;
351 write_json_str(f, &self.component)?;
352 f.write_str(",\"Description\":")?;
353 write_json_str(f, &self.description)?;
354 f.write_str(",\"Format\":\"JSON\",\"Level\":")?;
355 write_json_str(f, self.level.as_str())?;
356 write!(f, ",\"SessionID\":{}", self.session_id)?;
357 f.write_str(",\"Timestamp\":")?;
358 write_json_str(f, &self.time)?;
359 f.write_str("}")
360 }
361
362 fn fmt_gelf(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363 f.write_str("{\"_attributes\":")?;
364 write_json_map(f, &self.attributes)?;
365 write!(f, ",\"_session_id\":{}", self.session_id)?;
366 f.write_str(",\"full_message\":")?;
367 write_json_str(f, &self.description)?;
368 f.write_str(",\"host\":")?;
369 write_json_str(f, &self.component)?;
370 write!(f, ",\"level\":{}", self.level.to_numeric())?;
371 f.write_str(",\"short_message\":")?;
372 write_json_str(f, &self.description)?;
373 f.write_str(",\"timestamp\":")?;
374 write_json_str(f, &self.time)?;
375 f.write_str(",\"version\":\"1.1\"}")
376 }
377
378 fn fmt_logstash(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 f.write_str("{\"@timestamp\":")?;
380 write_json_str(f, &self.time)?;
381 f.write_str(",\"attributes\":")?;
382 write_json_map(f, &self.attributes)?;
383 f.write_str(",\"component\":")?;
384 write_json_str(f, &self.component)?;
385 f.write_str(",\"level\":")?;
386 write_json_str(f, self.level.as_str())?;
387 f.write_str(",\"message\":")?;
388 write_json_str(f, &self.description)?;
389 write!(f, ",\"session_id\":{}", self.session_id)?;
390 f.write_str("}")
391 }
392
393 fn fmt_ndjson(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
394 f.write_str("{\"attributes\":")?;
395 write_json_map(f, &self.attributes)?;
396 f.write_str(",\"component\":")?;
397 write_json_str(f, &self.component)?;
398 f.write_str(",\"level\":")?;
399 write_json_str(f, self.level.as_str())?;
400 f.write_str(",\"message\":")?;
401 write_json_str(f, &self.description)?;
402 f.write_str(",\"timestamp\":")?;
403 write_json_str(f, &self.time)?;
404 f.write_str("}")
405 }
406
407 fn fmt_mcp(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 f.write_str("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/log\",\"params\":{\"data\":{\"attributes\":")?;
409 write_json_map(f, &self.attributes)?;
410 f.write_str(",\"component\":")?;
411 write_json_str(f, &self.component)?;
412 f.write_str(",\"description\":")?;
413 write_json_str(f, &self.description)?;
414 write!(f, ",\"session_id\":{}", self.session_id)?;
415 f.write_str(",\"time\":")?;
416 write_json_str(f, &self.time)?;
417 f.write_str("},\"level\":")?;
418 write_json_str(f, self.level.as_str_lowercase())?;
419 f.write_str("}}")
420 }
421
422 fn fmt_otlp(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 let empty = serde_json::Value::String(String::new());
424 let trace_id =
425 self.attributes.get("trace_id").unwrap_or(&empty);
426 let span_id = self.attributes.get("span_id").unwrap_or(&empty);
427 f.write_str("{\"attributes\":")?;
428 write_json_map(f, &self.attributes)?;
429 f.write_str(",\"body\":{\"stringValue\":")?;
430 write_json_str(f, &self.description)?;
431 write!(f, "}},\"severityNumber\":{}", self.level.to_numeric())?;
432 f.write_str(",\"severityText\":")?;
433 write_json_str(f, self.level.as_str())?;
434 write!(f, ",\"spanId\":{span_id}")?;
435 f.write_str(",\"timeUnixNano\":")?;
436 write_json_str(f, &self.time)?;
437 write!(f, ",\"traceId\":{trace_id}}}")
438 }
439
440 fn fmt_ecs(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 f.write_str("{\"@timestamp\":")?;
442 write_json_str(f, &self.time)?;
443 f.write_str(",\"labels\":")?;
444 write_json_map(f, &self.attributes)?;
445 f.write_str(",\"log.level\":")?;
446 write_json_str(f, self.level.as_str_lowercase())?;
447 f.write_str(",\"log.logger\":\"rlg\",\"message\":")?;
448 write_json_str(f, &self.description)?;
449 f.write_str(",\"process.name\":")?;
450 write_json_str(f, &self.component)?;
451 f.write_str("}")
452 }
453}
454
455impl fmt::Display for Log {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 match self.format {
458 LogFormat::CLF => self.fmt_clf(f),
459 LogFormat::CEF => self.fmt_cef(f),
460 LogFormat::ELF => self.fmt_elf(f),
461 LogFormat::W3C => self.fmt_w3c(f),
462 LogFormat::ApacheAccessLog => self.fmt_apache(f),
463 LogFormat::Log4jXML => self.fmt_log4j_xml(f),
464 LogFormat::JSON => self.fmt_json(f),
465 LogFormat::GELF => self.fmt_gelf(f),
466 LogFormat::Logstash => self.fmt_logstash(f),
467 LogFormat::NDJSON => self.fmt_ndjson(f),
468 LogFormat::MCP => self.fmt_mcp(f),
469 LogFormat::OTLP => self.fmt_otlp(f),
470 LogFormat::Logfmt => self.write_logfmt(f),
471 LogFormat::ECS => self.fmt_ecs(f),
472 }
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use crate::log_format::LogFormat;
480
481 #[test]
482 #[cfg_attr(miri, ignore)]
483 fn test_log_write_logfmt_with_attributes() {
484 let mut log = Log::build(LogLevel::INFO, "desc")
485 .session_id(99)
486 .time("ts")
487 .component("comp")
488 .format(LogFormat::Logfmt);
489 log.attributes
490 .insert("key".to_string(), serde_json::json!("value"));
491 log.attributes.insert(
492 "space".to_string(),
493 serde_json::json!("has space"),
494 );
495 log.attributes
496 .insert("num".to_string(), serde_json::json!(42));
497 log.attributes
498 .insert("empty".to_string(), serde_json::json!(""));
499
500 let output = format!("{log}");
501 assert!(output.contains("key=value"));
502 assert!(output.contains("space=\"has space\""));
503 assert!(output.contains("num=42"));
504 assert!(output.contains("empty=\"\""));
505
506 let log_no_attr = Log::build(LogLevel::INFO, "desc")
508 .session_id(100)
509 .time("ts")
510 .component("comp")
511 .format(LogFormat::Logfmt);
512 let output_no = format!("{log_no_attr}");
513 assert!(!output_no.contains(" key="));
514 }
515}