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