1use crate::{LogFormat, LogLevel, datetime};
7use euxis_commons::counter::Counter;
8use serde::{Deserialize, Serialize};
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::fmt;
12use std::sync::LazyLock;
13use std::sync::atomic::Ordering;
14
15mod write;
16use write::Part::{Map, Num, Raw, Str, Value};
17use write::{write_logfmt_value, write_parts};
18
19#[cfg(not(miri))]
23pub(crate) fn caller_string(
24 caller: &std::panic::Location<'_>,
25) -> String {
26 let mut line = itoa::Buffer::new();
27 let line = line.format(caller.line());
28 let file = caller.file();
29 let mut out = String::with_capacity(file.len() + 1 + line.len());
30 out.push_str(file);
31 out.push(':');
32 out.push_str(line);
33 out
34}
35
36static SESSION_COUNTER: Counter = Counter::new(1);
38
39static CACHED_HOSTNAME: LazyLock<String> =
41 LazyLock::new(|| resolve_hostname(hostname::get()));
42
43fn resolve_hostname(
46 raw: std::io::Result<std::ffi::OsString>,
47) -> String {
48 raw.map_or_else(
49 |_| "localhost".to_string(),
50 |h| h.to_string_lossy().to_string(),
51 )
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq)]
62pub struct Log {
63 pub session_id: u64,
65 pub time: Cow<'static, str>,
67 pub level: LogLevel,
69 pub component: Cow<'static, str>,
71 pub description: String,
73 pub format: LogFormat,
75 pub attributes: BTreeMap<String, serde_json::Value>,
77}
78
79impl Default for Log {
80 fn default() -> Self {
81 Self {
82 session_id: 0,
83 time: Cow::Borrowed(""),
84 level: LogLevel::INFO,
85 component: Cow::Borrowed(""),
86 description: String::default(),
87 format: LogFormat::CLF,
88 attributes: BTreeMap::new(),
89 }
90 }
91}
92
93impl Log {
94 #[track_caller]
99 pub fn log(&self) {
100 crate::engine::ENGINE
101 .ingest(crate::engine::LogEvent::new(self.clone()));
102 }
103
104 #[must_use]
106 pub fn info(description: &str) -> Self {
107 Self::build(LogLevel::INFO, description)
108 }
109
110 #[must_use]
112 pub fn warn(description: &str) -> Self {
113 Self::build(LogLevel::WARN, description)
114 }
115
116 #[must_use]
118 pub fn error(description: &str) -> Self {
119 Self::build(LogLevel::ERROR, description)
120 }
121
122 #[must_use]
124 pub fn debug(description: &str) -> Self {
125 Self::build(LogLevel::DEBUG, description)
126 }
127
128 #[must_use]
130 pub fn trace(description: &str) -> Self {
131 Self::build(LogLevel::TRACE, description)
132 }
133
134 #[must_use]
136 pub fn verbose(description: &str) -> Self {
137 Self::build(LogLevel::VERBOSE, description)
138 }
139
140 #[must_use]
142 pub fn fatal(description: &str) -> Self {
143 Self::build(LogLevel::FATAL, description)
144 }
145
146 #[must_use]
148 pub fn critical(description: &str) -> Self {
149 Self::build(LogLevel::CRITICAL, description)
150 }
151
152 #[must_use]
157 pub fn build(level: LogLevel, description: &str) -> Self {
158 Self {
159 session_id: SESSION_COUNTER.fetch_add(1, Ordering::Relaxed),
160 time: Cow::Owned(datetime::now_iso8601()),
161 level,
162 component: Cow::Borrowed("default"),
163 description: description.to_string(),
164 format: LogFormat::MCP,
165 attributes: BTreeMap::new(),
166 }
167 }
168
169 #[must_use]
171 pub fn time(mut self, time: &str) -> Self {
172 self.time = Cow::Owned(time.to_string());
173 self
174 }
175
176 #[must_use]
178 pub const fn session_id(mut self, session_id: u64) -> Self {
179 self.session_id = session_id;
180 self
181 }
182
183 #[must_use]
185 pub fn with<T: Serialize>(mut self, key: &str, value: T) -> Self {
186 if let Ok(val) = serde_json::to_value(value) {
187 self.attributes.insert(key.to_string(), val);
188 }
189 self
190 }
191
192 #[must_use]
194 pub fn component(mut self, component: &str) -> Self {
195 self.component = Cow::Owned(component.to_string());
196 self
197 }
198
199 #[must_use]
201 pub const fn format(mut self, format: LogFormat) -> Self {
202 self.format = format;
203 self
204 }
205
206 #[track_caller]
212 pub fn fire(self) {
213 crate::engine::ENGINE.ingest(self.into_fired_event());
214 }
215
216 #[track_caller]
219 fn into_fired_event(self) -> crate::engine::LogEvent {
220 crate::engine::LogEvent {
221 caller: Some(std::panic::Location::caller()),
222 ..crate::engine::LogEvent::new(self)
223 }
224 }
225
226 fn write_logfmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(
228 f,
229 "level={} msg=\"{}\" session_id={} component=\"{}\"",
230 self.level.as_str_lowercase(),
231 self.description.replace('"', "\\\""),
232 self.session_id,
233 self.component,
234 )?;
235 for (key, value) in &self.attributes {
236 write!(f, " {key}=")?;
237 write_logfmt_value(f, value)?;
238 }
239 Ok(())
240 }
241}
242
243impl Log {
245 fn fmt_clf(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 write!(
247 f,
248 "SessionID={} Timestamp={} Description={} Level={} Component={}",
249 self.session_id,
250 self.time,
251 self.description,
252 self.level,
253 self.component
254 )
255 }
256
257 fn fmt_cef(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 write!(
259 f,
260 "CEF:0|{}|{}|{}|{}|{}|CEF",
261 self.session_id,
262 self.time,
263 self.level,
264 self.component,
265 self.description
266 )
267 }
268
269 fn fmt_elf(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 write!(
271 f,
272 "ELF:0|{}|{}|{}|{}|{}|ELF",
273 self.session_id,
274 self.time,
275 self.level,
276 self.component,
277 self.description
278 )
279 }
280
281 fn fmt_w3c(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282 write!(
283 f,
284 "W3C:0|{}|{}|{}|{}|{}|W3C",
285 self.session_id,
286 self.time,
287 self.level,
288 self.component,
289 self.description
290 )
291 }
292
293 fn fmt_apache(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 write!(
295 f,
296 "{} - - [{}] \"{}\" {} {}",
297 *CACHED_HOSTNAME,
298 self.time,
299 self.description,
300 self.level,
301 self.component
302 )
303 }
304
305 fn fmt_log4j_xml(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 write!(
307 f,
308 r#"<log4j:event logger="{}" timestamp="{}" level="{}" thread="{}"><log4j:message>{}</log4j:message></log4j:event>"#,
309 self.component,
310 self.time,
311 self.level,
312 self.session_id,
313 self.description
314 )
315 }
316
317 fn fmt_json(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318 write_parts(
319 f,
320 &[
321 Raw("{\"Attributes\":"),
322 Map(&self.attributes),
323 Raw(",\"Component\":"),
324 Str(&self.component),
325 Raw(",\"Description\":"),
326 Str(&self.description),
327 Raw(",\"Format\":\"JSON\",\"Level\":"),
328 Str(self.level.as_str()),
329 Raw(",\"SessionID\":"),
330 Num(self.session_id),
331 Raw(",\"Timestamp\":"),
332 Str(&self.time),
333 Raw("}"),
334 ],
335 )
336 }
337
338 fn fmt_gelf(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 write_parts(
340 f,
341 &[
342 Raw("{\"_attributes\":"),
343 Map(&self.attributes),
344 Raw(",\"_session_id\":"),
345 Num(self.session_id),
346 Raw(",\"full_message\":"),
347 Str(&self.description),
348 Raw(",\"host\":"),
349 Str(&self.component),
350 Raw(",\"level\":"),
351 Num(u64::from(self.level.to_numeric())),
352 Raw(",\"short_message\":"),
353 Str(&self.description),
354 Raw(",\"timestamp\":"),
355 Str(&self.time),
356 Raw(",\"version\":\"1.1\"}"),
357 ],
358 )
359 }
360
361 fn fmt_logstash(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 write_parts(
363 f,
364 &[
365 Raw("{\"@timestamp\":"),
366 Str(&self.time),
367 Raw(",\"attributes\":"),
368 Map(&self.attributes),
369 Raw(",\"component\":"),
370 Str(&self.component),
371 Raw(",\"level\":"),
372 Str(self.level.as_str()),
373 Raw(",\"message\":"),
374 Str(&self.description),
375 Raw(",\"session_id\":"),
376 Num(self.session_id),
377 Raw("}"),
378 ],
379 )
380 }
381
382 fn fmt_ndjson(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383 write_parts(
384 f,
385 &[
386 Raw("{\"attributes\":"),
387 Map(&self.attributes),
388 Raw(",\"component\":"),
389 Str(&self.component),
390 Raw(",\"level\":"),
391 Str(self.level.as_str()),
392 Raw(",\"message\":"),
393 Str(&self.description),
394 Raw(",\"timestamp\":"),
395 Str(&self.time),
396 Raw("}"),
397 ],
398 )
399 }
400
401 fn fmt_mcp(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 write_parts(
403 f,
404 &[
405 Raw(
406 "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/log\",\"params\":{\"data\":{\"attributes\":",
407 ),
408 Map(&self.attributes),
409 Raw(",\"component\":"),
410 Str(&self.component),
411 Raw(",\"description\":"),
412 Str(&self.description),
413 Raw(",\"session_id\":"),
414 Num(self.session_id),
415 Raw(",\"time\":"),
416 Str(&self.time),
417 Raw("},\"level\":"),
418 Str(self.level.as_str_lowercase()),
419 Raw("}}"),
420 ],
421 )
422 }
423
424 fn fmt_otlp(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 let empty = serde_json::Value::String(String::new());
426 let trace_id =
427 self.attributes.get("trace_id").unwrap_or(&empty);
428 let span_id = self.attributes.get("span_id").unwrap_or(&empty);
429 write_parts(
430 f,
431 &[
432 Raw("{\"attributes\":"),
433 Map(&self.attributes),
434 Raw(",\"body\":{\"stringValue\":"),
435 Str(&self.description),
436 Raw("},\"severityNumber\":"),
437 Num(u64::from(self.level.to_numeric())),
438 Raw(",\"severityText\":"),
439 Str(self.level.as_str()),
440 Raw(",\"spanId\":"),
441 Value(span_id),
442 Raw(",\"timeUnixNano\":"),
443 Str(&self.time),
444 Raw(",\"traceId\":"),
445 Value(trace_id),
446 Raw("}"),
447 ],
448 )
449 }
450
451 fn fmt_ecs(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 write_parts(
453 f,
454 &[
455 Raw("{\"@timestamp\":"),
456 Str(&self.time),
457 Raw(",\"labels\":"),
458 Map(&self.attributes),
459 Raw(",\"log.level\":"),
460 Str(self.level.as_str_lowercase()),
461 Raw(",\"log.logger\":\"rlg\",\"message\":"),
462 Str(&self.description),
463 Raw(",\"process.name\":"),
464 Str(&self.component),
465 Raw("}"),
466 ],
467 )
468 }
469}
470
471impl fmt::Display for Log {
472 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473 match self.format {
474 LogFormat::CLF => self.fmt_clf(f),
475 LogFormat::CEF => self.fmt_cef(f),
476 LogFormat::ELF => self.fmt_elf(f),
477 LogFormat::W3C => self.fmt_w3c(f),
478 LogFormat::ApacheAccessLog => self.fmt_apache(f),
479 LogFormat::Log4jXML => self.fmt_log4j_xml(f),
480 LogFormat::JSON => self.fmt_json(f),
481 LogFormat::GELF => self.fmt_gelf(f),
482 LogFormat::Logstash => self.fmt_logstash(f),
483 LogFormat::NDJSON => self.fmt_ndjson(f),
484 LogFormat::MCP => self.fmt_mcp(f),
485 LogFormat::OTLP => self.fmt_otlp(f),
486 LogFormat::Logfmt => self.write_logfmt(f),
487 LogFormat::ECS => self.fmt_ecs(f),
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests;