1use alloc::{collections::VecDeque, string::String, vec::Vec};
6use core::num::NonZeroUsize;
7
8const TRUNCATION_SUFFIX: &str = "...[truncated]";
9
10#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
12pub enum Level {
13 Debug,
15 Info,
17 Ok,
19 Warn,
21 Error,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub struct Limits {
28 pub max_entries: NonZeroUsize,
30 pub max_message_bytes: usize,
32 pub max_label_bytes: usize,
34}
35
36#[derive(Clone, Copy, Debug)]
38pub struct EntryInput<'a> {
39 pub created_at_secs: u64,
41 pub level: Level,
43 pub source: &'a str,
45 pub topic: Option<&'a str>,
47 pub message: &'a str,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct Entry {
54 pub sequence: u64,
56 pub created_at_secs: u64,
58 pub level: Level,
60 pub source: String,
62 pub topic: Option<String>,
64 pub message: String,
66 pub truncated: bool,
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum Label {
73 Source,
75 Topic,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum AppendError {
82 LabelTooLong {
84 label: Label,
86 actual_bytes: usize,
88 max_bytes: usize,
90 },
91 TimestampRegressed {
93 previous: u64,
95 current: u64,
97 },
98 SequenceExhausted,
100}
101
102impl core::fmt::Display for AppendError {
103 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104 match self {
105 Self::LabelTooLong {
106 label,
107 actual_bytes,
108 max_bytes,
109 } => {
110 let label = match label {
111 Label::Source => "source",
112 Label::Topic => "topic",
113 };
114 write!(
115 formatter,
116 "log {label} label exceeds {max_bytes} bytes ({actual_bytes} supplied)"
117 )
118 }
119 Self::TimestampRegressed { previous, current } => write!(
120 formatter,
121 "log timestamp regressed from {previous} to {current} seconds"
122 ),
123 Self::SequenceExhausted => formatter.write_str("log sequence identities are exhausted"),
124 }
125 }
126}
127
128impl core::error::Error for AppendError {}
129
130#[derive(Clone, Copy, Debug, Default)]
132pub struct Filter<'a> {
133 pub source: Option<&'a str>,
135 pub topic: Option<&'a str>,
137 pub min_level: Option<Level>,
139}
140
141impl Filter<'_> {
142 fn matches(self, entry: &Entry) -> bool {
143 let source_matches = self.source.is_none_or(|source| source == entry.source);
144 let topic_matches = self
145 .topic
146 .is_none_or(|topic| Some(topic) == entry.topic.as_deref());
147 let level_matches = self.min_level.is_none_or(|level| entry.level >= level);
148 source_matches && topic_matches && level_matches
149 }
150}
151
152#[derive(Clone, Copy, Debug)]
154pub struct PageRequest {
155 pub before: Option<u64>,
157 pub max_results: NonZeroUsize,
159 pub max_scanned: NonZeroUsize,
161}
162
163#[derive(Debug)]
165pub struct Page<'a> {
166 pub entries: Vec<&'a Entry>,
168 pub scanned: usize,
170 pub next_before: Option<u64>,
172}
173
174#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176pub struct RetentionBatch {
177 pub removed: usize,
179 pub more_due: bool,
181}
182
183#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct LogSnapshot {
189 pub limits: Limits,
191 pub entries: Vec<Entry>,
193 pub next_sequence: Option<u64>,
195 pub last_created_at_secs: Option<u64>,
197}
198
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub enum RestoreError {
202 TooManyEntries,
204 InvalidSequence,
206 InvalidTimestamp,
208 InvalidEntry,
210}
211
212impl core::fmt::Display for RestoreError {
213 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
214 formatter.write_str(match self {
215 Self::TooManyEntries => "log checkpoint exceeds its entry limit",
216 Self::InvalidSequence => "invalid log checkpoint sequence",
217 Self::InvalidTimestamp => "invalid log checkpoint timestamp",
218 Self::InvalidEntry => "invalid bounded log checkpoint entry",
219 })
220 }
221}
222
223impl core::error::Error for RestoreError {}
224
225#[derive(Debug)]
227pub struct Log {
228 limits: Limits,
229 entries: VecDeque<Entry>,
230 next_sequence: Option<u64>,
231 last_created_at_secs: Option<u64>,
232}
233
234impl Log {
235 #[must_use]
237 pub const fn new(limits: Limits) -> Self {
238 Self {
239 limits,
240 entries: VecDeque::new(),
241 next_sequence: Some(0),
242 last_created_at_secs: None,
243 }
244 }
245
246 #[must_use]
248 pub const fn limits(&self) -> Limits {
249 self.limits
250 }
251
252 #[must_use]
255 pub fn snapshot(&self) -> LogSnapshot {
256 LogSnapshot {
257 limits: self.limits,
258 entries: self.entries.iter().cloned().collect(),
259 next_sequence: self.next_sequence,
260 last_created_at_secs: self.last_created_at_secs,
261 }
262 }
263
264 #[must_use]
268 pub fn into_snapshot(self) -> LogSnapshot {
269 LogSnapshot {
270 limits: self.limits,
271 entries: self.entries.into(),
272 next_sequence: self.next_sequence,
273 last_created_at_secs: self.last_created_at_secs,
274 }
275 }
276
277 pub fn restore(snapshot: LogSnapshot) -> Result<Self, RestoreError> {
287 validate_snapshot(&snapshot)?;
288 let LogSnapshot {
289 limits,
290 entries,
291 next_sequence,
292 last_created_at_secs,
293 } = snapshot;
294 Ok(Self {
295 limits,
296 entries: entries.into(),
297 next_sequence,
298 last_created_at_secs,
299 })
300 }
301
302 #[must_use]
304 pub fn len(&self) -> usize {
305 self.entries.len()
306 }
307
308 #[must_use]
310 pub fn is_empty(&self) -> bool {
311 self.entries.is_empty()
312 }
313
314 pub fn append(&mut self, input: EntryInput<'_>) -> Result<u64, AppendError> {
320 self.validate_label(Label::Source, input.source)?;
321 if let Some(topic) = input.topic {
322 self.validate_label(Label::Topic, topic)?;
323 }
324 if let Some(previous) = self.last_created_at_secs
325 && input.created_at_secs < previous
326 {
327 return Err(AppendError::TimestampRegressed {
328 previous,
329 current: input.created_at_secs,
330 });
331 }
332 let sequence = self.next_sequence.ok_or(AppendError::SequenceExhausted)?;
333 let entry = Entry {
334 sequence,
335 created_at_secs: input.created_at_secs,
336 level: input.level,
337 source: String::from(input.source),
338 topic: input.topic.map(String::from),
339 message: bounded_message(input.message, self.limits.max_message_bytes),
340 truncated: input.message.len() > self.limits.max_message_bytes,
341 };
342 if self.entries.len() == self.limits.max_entries.get() {
343 self.entries.pop_front();
344 }
345 self.entries.push_back(entry);
346 self.next_sequence = sequence.checked_add(1);
347 self.last_created_at_secs = Some(input.created_at_secs);
348 Ok(sequence)
349 }
350
351 pub fn prune_expired(
354 &mut self,
355 now_secs: u64,
356 max_age_secs: u64,
357 max_remove: usize,
358 ) -> RetentionBatch {
359 let cutoff = now_secs.saturating_sub(max_age_secs);
360 let mut removed = 0;
361 while removed < max_remove && self.expired_before(cutoff) {
362 self.entries.pop_front();
363 removed += 1;
364 }
365 RetentionBatch {
366 removed,
367 more_due: self.expired_before(cutoff),
368 }
369 }
370
371 #[must_use]
373 pub fn next_expiry_secs(&self, max_age_secs: u64) -> Option<u64> {
374 self.entries
375 .front()?
376 .created_at_secs
377 .checked_add(max_age_secs)?
378 .checked_add(1)
379 }
380
381 #[must_use]
384 pub fn page(&self, filter: Filter<'_>, request: PageRequest) -> Page<'_> {
385 let mut cursor = request.before.map_or(self.entries.len(), |sequence| {
386 self.entries
387 .binary_search_by_key(&sequence, |entry| entry.sequence)
388 .unwrap_or_else(|index| index)
389 });
390 let mut entries = Vec::new();
391 let mut scanned = 0;
392 let mut last_scanned = None;
393 while cursor > 0
394 && scanned < request.max_scanned.get()
395 && entries.len() < request.max_results.get()
396 {
397 cursor -= 1;
398 let entry = &self.entries[cursor];
399 scanned += 1;
400 last_scanned = Some(entry.sequence);
401 if filter.matches(entry) {
402 entries.push(entry);
403 }
404 }
405 Page {
406 entries,
407 scanned,
408 next_before: if cursor > 0 { last_scanned } else { None },
409 }
410 }
411
412 const fn validate_label(&self, label: Label, text: &str) -> Result<(), AppendError> {
413 if text.len() > self.limits.max_label_bytes {
414 return Err(AppendError::LabelTooLong {
415 label,
416 actual_bytes: text.len(),
417 max_bytes: self.limits.max_label_bytes,
418 });
419 }
420 Ok(())
421 }
422
423 fn expired_before(&self, cutoff: u64) -> bool {
424 self.entries
425 .front()
426 .is_some_and(|entry| entry.created_at_secs < cutoff)
427 }
428}
429
430fn validate_snapshot(snapshot: &LogSnapshot) -> Result<(), RestoreError> {
431 let LogSnapshot {
432 limits,
433 entries,
434 next_sequence,
435 last_created_at_secs,
436 } = snapshot;
437 let (next_sequence, last_created_at_secs) = (*next_sequence, *last_created_at_secs);
438 if entries.len() > limits.max_entries.get() {
439 return Err(RestoreError::TooManyEntries);
440 }
441 if (next_sequence == Some(0)) != last_created_at_secs.is_none() {
442 return Err(RestoreError::InvalidTimestamp);
443 }
444 if entries.is_empty() && last_created_at_secs == Some(u64::MAX) {
447 return Err(RestoreError::InvalidTimestamp);
448 }
449 let mut previous: Option<&Entry> = None;
450 for entry in entries {
451 if entry.source.len() > limits.max_label_bytes
452 || entry
453 .topic
454 .as_ref()
455 .is_some_and(|topic| topic.len() > limits.max_label_bytes)
456 || !valid_retained_message(entry, limits.max_message_bytes)
457 {
458 return Err(RestoreError::InvalidEntry);
459 }
460 if let Some(previous) = previous {
461 if previous.sequence.checked_add(1) != Some(entry.sequence) {
462 return Err(RestoreError::InvalidSequence);
463 }
464 if entry.created_at_secs < previous.created_at_secs {
465 return Err(RestoreError::InvalidTimestamp);
466 }
467 }
468 previous = Some(entry);
469 }
470 if let Some(last) = previous {
471 if last.sequence.checked_add(1) != next_sequence {
472 return Err(RestoreError::InvalidSequence);
473 }
474 if Some(last.created_at_secs) != last_created_at_secs {
475 return Err(RestoreError::InvalidTimestamp);
476 }
477 }
478 Ok(())
479}
480
481const fn truncation_suffix(max_bytes: usize) -> &'static str {
482 if max_bytes > TRUNCATION_SUFFIX.len() {
483 TRUNCATION_SUFFIX
484 } else {
485 ""
486 }
487}
488
489fn valid_retained_message(entry: &Entry, max_bytes: usize) -> bool {
490 if entry.message.len() > max_bytes {
491 return false;
492 }
493 if !entry.truncated {
494 return true;
495 }
496 let suffix = truncation_suffix(max_bytes);
497 let Some(prefix) = entry.message.strip_suffix(suffix) else {
498 return false;
499 };
500 let budget = max_bytes - suffix.len();
501 budget.saturating_sub(prefix.len()) < 4
502}
503
504fn bounded_message(message: &str, max_bytes: usize) -> String {
505 if message.len() <= max_bytes {
506 return String::from(message);
507 }
508 let suffix = truncation_suffix(max_bytes);
509 let mut end = max_bytes - suffix.len();
510 while !message.is_char_boundary(end) {
511 end -= 1;
512 }
513 let mut retained = String::with_capacity(end + suffix.len());
515 retained.push_str(&message[..end]);
516 retained.push_str(suffix);
517 retained
518}
519
520#[cfg(test)]
521mod tests;