archunit/common/logging/
event.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
use crate::common::LogLevel;

/// Stable vocabulary for records emitted during architecture checks.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum LogEventKind {
    /// A terminal check began.
    StartCheck,
    /// A terminal check completed with a verdict.
    EndCheck,
    /// A check reported detailed execution progress.
    Progress,
    /// A check produced one architecture violation.
    Violation,
    /// A metric value was calculated.
    Metric,
    /// A caller emitted an ordinary debug message.
    Debug,
    /// A caller emitted an ordinary informational message.
    Info,
    /// A caller emitted an ordinary warning.
    Warn,
    /// A caller emitted an error.
    Error,
}

impl LogEventKind {
    /// Returns the stable lowercase event spelling.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::StartCheck => "start check",
            Self::EndCheck => "end check",
            Self::Progress => "log progress",
            Self::Violation => "log violation",
            Self::Metric => "log metric",
            Self::Debug => "debug",
            Self::Info => "info",
            Self::Warn => "warn",
            Self::Error => "error",
        }
    }
}

/// One immutable, single-line logging record before destination-specific timestamping.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
    level: LogLevel,
    event: LogEventKind,
    message: String,
}

impl LogRecord {
    pub(super) fn new(level: LogLevel, event: LogEventKind, message: impl AsRef<str>) -> Self {
        Self {
            level,
            event,
            message: sanitize_message(message.as_ref()),
        }
    }

    /// Returns this record's severity.
    #[must_use]
    pub const fn level(&self) -> LogLevel {
        self.level
    }

    /// Returns this record's stable event family.
    #[must_use]
    pub const fn event(&self) -> LogEventKind {
        self.event
    }

    /// Returns the sanitized single-line event details.
    #[must_use]
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Renders the deterministic destination-neutral record.
    #[must_use]
    pub fn render(&self) -> String {
        format!("[{}] {}: {}", self.level, self.event.as_str(), self.message)
    }
}

fn sanitize_message(message: &str) -> String {
    message.replace('\r', "\\r").replace('\n', "\\n")
}

#[cfg(test)]
mod tests {
    use super::{LogEventKind, LogRecord};
    use crate::common::LogLevel;

    #[test]
    fn records_use_the_fixed_vocabulary_and_remain_one_line() {
        let record = LogRecord::new(
            LogLevel::Warn,
            LogEventKind::Violation,
            "first line\r\nsecond line",
        );

        assert_eq!(record.level(), LogLevel::Warn);
        assert_eq!(record.event(), LogEventKind::Violation);
        assert_eq!(record.message(), "first line\\r\\nsecond line");
        assert_eq!(
            record.render(),
            "[WARN] log violation: first line\\r\\nsecond line"
        );
    }
}