Skip to main content

links_logs_vault/
model.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7/// Monolog-compatible severity levels, ordered from least to most severe.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10pub enum LogLevel {
11    /// Detailed diagnostic information.
12    Debug,
13    /// Informational events.
14    Info,
15    /// Normal but significant events.
16    Notice,
17    /// Exceptional events that do not stop processing.
18    Warning,
19    /// Runtime errors.
20    Error,
21    /// Critical conditions.
22    Critical,
23    /// Conditions requiring immediate action.
24    Alert,
25    /// System-wide emergencies.
26    Emergency,
27}
28
29impl fmt::Display for LogLevel {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(formatter, "{}", format!("{self:?}").to_lowercase())
32    }
33}
34
35impl FromStr for LogLevel {
36    type Err = String;
37
38    fn from_str(value: &str) -> Result<Self, Self::Err> {
39        match value.to_ascii_lowercase().as_str() {
40            "debug" => Ok(Self::Debug),
41            "info" => Ok(Self::Info),
42            "notice" => Ok(Self::Notice),
43            "warning" | "warn" => Ok(Self::Warning),
44            "error" => Ok(Self::Error),
45            "critical" => Ok(Self::Critical),
46            "alert" => Ok(Self::Alert),
47            "emergency" => Ok(Self::Emergency),
48            _ => Err(format!("unknown log level: {value}")),
49        }
50    }
51}
52
53/// Text or arbitrary binary log content.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(tag = "kind", content = "value", rename_all = "lowercase")]
56pub enum LogPayload {
57    /// UTF-8 text suitable for full-text matching.
58    Text(String),
59    /// Lossless arbitrary bytes.
60    Binary(Vec<u8>),
61}
62
63impl LogPayload {
64    /// Returns the payload without changing its representation.
65    #[must_use]
66    pub fn as_bytes(&self) -> &[u8] {
67        match self {
68            Self::Text(value) => value.as_bytes(),
69            Self::Binary(value) => value,
70        }
71    }
72
73    /// Returns text payloads and leaves binary payloads opaque.
74    #[must_use]
75    pub fn as_text(&self) -> Option<&str> {
76        match self {
77            Self::Text(value) => Some(value),
78            Self::Binary(_) => None,
79        }
80    }
81}
82
83/// A normalized event accepted by the vault.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct LogRecord {
86    /// Unix timestamp in seconds. Producers may use another documented epoch.
87    pub timestamp: u64,
88    /// Event severity.
89    pub level: LogLevel,
90    /// Producer or logical logger name.
91    pub channel: String,
92    /// Text or binary event body.
93    pub payload: LogPayload,
94    /// Structured, searchable event context.
95    pub fields: BTreeMap<String, String>,
96}
97
98impl LogRecord {
99    /// Creates a text event.
100    #[must_use]
101    pub fn text(
102        timestamp: u64,
103        level: LogLevel,
104        channel: impl Into<String>,
105        message: impl Into<String>,
106    ) -> Self {
107        Self {
108            timestamp,
109            level,
110            channel: channel.into(),
111            payload: LogPayload::Text(message.into()),
112            fields: BTreeMap::new(),
113        }
114    }
115
116    /// Creates a binary event without attempting UTF-8 conversion.
117    #[must_use]
118    pub fn binary(
119        timestamp: u64,
120        level: LogLevel,
121        channel: impl Into<String>,
122        payload: Vec<u8>,
123    ) -> Self {
124        Self {
125            timestamp,
126            level,
127            channel: channel.into(),
128            payload: LogPayload::Binary(payload),
129            fields: BTreeMap::new(),
130        }
131    }
132
133    /// Adds or replaces one structured field.
134    #[must_use]
135    pub fn with_field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
136        self.fields.insert(key.into(), value.into());
137        self
138    }
139
140    /// Returns the message when this is a text record.
141    #[must_use]
142    pub fn message(&self) -> Option<&str> {
143        self.payload.as_text()
144    }
145}
146
147/// A durable record and its monotonically increasing identifier.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct StoredRecord {
150    /// Vault-local identifier.
151    pub id: u64,
152    /// Root link in the associative graph.
153    pub graph_id: u32,
154    /// Original event.
155    pub record: LogRecord,
156}
157
158/// Filters supported by the initial search engine.
159#[derive(Debug, Clone, Default, PartialEq, Eq)]
160pub struct Query {
161    /// Exact channel match.
162    pub channel: Option<String>,
163    /// Inclusive severity floor.
164    pub min_level: Option<LogLevel>,
165    /// Inclusive lower timestamp bound.
166    pub from_timestamp: Option<u64>,
167    /// Inclusive upper timestamp bound.
168    pub to_timestamp: Option<u64>,
169    /// Case-insensitive substring for text messages.
170    pub contains: Option<String>,
171    /// Exact structured-field matches.
172    pub fields: BTreeMap<String, String>,
173    /// Maximum number of results, in insertion order.
174    pub limit: Option<usize>,
175}
176
177/// Durable snapshot encoding. Both formats share the same associative graph.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum StorageFormat {
180    /// Line-oriented Links Notation containing readable JSON records.
181    Text,
182    /// Length-prefixed binary snapshot.
183    Binary,
184}
185
186impl fmt::Display for StorageFormat {
187    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
188        match self {
189            Self::Text => formatter.write_str("text"),
190            Self::Binary => formatter.write_str("binary"),
191        }
192    }
193}
194
195impl FromStr for StorageFormat {
196    type Err = String;
197
198    fn from_str(value: &str) -> Result<Self, Self::Err> {
199        match value.to_ascii_lowercase().as_str() {
200            "text" | "lino" => Ok(Self::Text),
201            "binary" | "bin" | "llvb" => Ok(Self::Binary),
202            _ => Err(format!("unknown storage format: {value}")),
203        }
204    }
205}