Skip to main content

rlg/
log.rs

1// log.rs
2// Copyright © 2024-2026 RustLogs (RLG). All rights reserved.
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6use 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/// `file:line` for a call site, built without the `format!` machinery.
20/// The flusher calls it once per record `fire()` sent; under Miri,
21/// which runs no flusher, nothing does.
22#[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
36/// Monotonic session ID counter. Incremented atomically per `build()` call.
37static SESSION_COUNTER: Counter = Counter::new(1);
38
39/// Hostname, resolved once and cached for the process lifetime.
40static CACHED_HOSTNAME: LazyLock<String> =
41    LazyLock::new(|| resolve_hostname(hostname::get()));
42
43/// Pure helper for [`CACHED_HOSTNAME`] — exposed so the `"localhost"`
44/// fallback branch can be unit-tested without injecting a syscall failure.
45fn 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/// A structured log entry with a chainable builder API.
55///
56/// Fields use `Cow<'static, str>` and `u64` where possible to
57/// minimize heap allocations on the ingestion hot path.
58///
59/// Construct via level shortcuts ([`Log::info`], [`Log::error`], ...)
60/// or the generic [`Log::build`]. Dispatch with [`Log::fire`].
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq)]
62pub struct Log {
63    /// Monotonic counter assigned at `build()` time.
64    pub session_id: u64,
65    /// Wall-clock timestamp. Populated at build time; override with `.time()`.
66    pub time: Cow<'static, str>,
67    /// Severity level (`INFO`, `ERROR`, etc.).
68    pub level: LogLevel,
69    /// Originating service or module name. Defaults to `"default"`.
70    pub component: Cow<'static, str>,
71    /// Human-readable message body.
72    pub description: String,
73    /// Output format applied during `Display` serialization.
74    pub format: LogFormat,
75    /// Arbitrary key-value attributes for structured context.
76    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    /// Ingest this entry into the engine by cloning it.
95    ///
96    /// **Prefer [`fire()`](Self::fire)**, which consumes `self` and avoids
97    /// the clone. Use `log()` only when you need to retain the entry.
98    #[track_caller]
99    pub fn log(&self) {
100        crate::engine::ENGINE
101            .ingest(crate::engine::LogEvent::new(self.clone()));
102    }
103
104    /// Build an INFO-level log entry.
105    #[must_use]
106    pub fn info(description: &str) -> Self {
107        Self::build(LogLevel::INFO, description)
108    }
109
110    /// Build a WARN-level log entry.
111    #[must_use]
112    pub fn warn(description: &str) -> Self {
113        Self::build(LogLevel::WARN, description)
114    }
115
116    /// Build an ERROR-level log entry.
117    #[must_use]
118    pub fn error(description: &str) -> Self {
119        Self::build(LogLevel::ERROR, description)
120    }
121
122    /// Build a DEBUG-level log entry.
123    #[must_use]
124    pub fn debug(description: &str) -> Self {
125        Self::build(LogLevel::DEBUG, description)
126    }
127
128    /// Build a TRACE-level log entry.
129    #[must_use]
130    pub fn trace(description: &str) -> Self {
131        Self::build(LogLevel::TRACE, description)
132    }
133
134    /// Build a VERBOSE-level log entry.
135    #[must_use]
136    pub fn verbose(description: &str) -> Self {
137        Self::build(LogLevel::VERBOSE, description)
138    }
139
140    /// Build a FATAL-level log entry.
141    #[must_use]
142    pub fn fatal(description: &str) -> Self {
143        Self::build(LogLevel::FATAL, description)
144    }
145
146    /// Build a CRITICAL-level log entry.
147    #[must_use]
148    pub fn critical(description: &str) -> Self {
149        Self::build(LogLevel::CRITICAL, description)
150    }
151
152    /// Build a log entry with an explicit level and description.
153    ///
154    /// Assigns a monotonic `session_id` and captures the current wall-clock
155    /// time. Defaults to `LogFormat::MCP` and component `"default"`.
156    #[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    /// Override the timestamp for this entry.
170    #[must_use]
171    pub fn time(mut self, time: &str) -> Self {
172        self.time = Cow::Owned(time.to_string());
173        self
174    }
175
176    /// Override the auto-assigned session ID.
177    #[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    /// Attach a key-value attribute. Accepts any `T: Serialize`.
184    #[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    /// Tag the originating service or module.
193    #[must_use]
194    pub fn component(mut self, component: &str) -> Self {
195        self.component = Cow::Owned(component.to_string());
196        self
197    }
198
199    /// Set the output format for this entry.
200    #[must_use]
201    pub const fn format(mut self, format: LogFormat) -> Self {
202        self.format = format;
203        self
204    }
205
206    /// Consume this entry and push it into the ring buffer.
207    ///
208    /// Cost: one `Log` move (~128 bytes). Serialization is deferred.
209    /// Automatically captures `file:line` via `#[track_caller]`; the
210    /// flusher adds it as the `caller` attribute.
211    #[track_caller]
212    pub fn fire(self) {
213        crate::engine::ENGINE.ingest(self.into_fired_event());
214    }
215
216    /// The event `fire()` ingests: this entry plus its call site,
217    /// which stays a `&'static Location` until the flusher renders it.
218    #[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
243// --- Per-format serialization methods ---
244impl 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;