#![allow(clippy::cast_possible_truncation)]
use crate::{
cdk::structures::{
DefaultMemoryImpl,
log::{Log as StableLogImpl, WriteError},
memory::VirtualMemory,
},
eager_static, ic_memory,
log::{Level, Topic},
memory::impl_storable_unbounded,
storage::{
prelude::*,
stable::{
StableMemoryError,
memory::observability::{LOG_DATA_ID, LOG_INDEX_ID},
},
},
};
use serde::{Deserialize, Serialize};
use std::{cell::RefCell, collections::VecDeque};
type StableLog = StableLogImpl<
LogEntryRecord,
VirtualMemory<DefaultMemoryImpl>,
VirtualMemory<DefaultMemoryImpl>,
>;
struct LogIndexMemory;
struct LogDataMemory;
#[derive(Clone)]
struct LogMemory {
index: VirtualMemory<DefaultMemoryImpl>,
data: VirtualMemory<DefaultMemoryImpl>,
}
impl LogMemory {
fn new() -> Self {
Self {
index: ic_memory!(LogIndexMemory, LOG_INDEX_ID),
data: ic_memory!(LogDataMemory, LOG_DATA_ID),
}
}
}
eager_static! {
static LOG_MEMORY: LogMemory = LogMemory::new();
}
fn create_log() -> StableLog {
LOG_MEMORY.with(|mem| StableLogImpl::new(mem.index.clone(), mem.data.clone()))
}
eager_static! {
static LOG: RefCell<StableLog> = RefCell::new(create_log());
}
fn with_log<R>(f: impl FnOnce(&StableLog) -> R) -> R {
LOG.with_borrow(f)
}
fn with_log_mut<R>(f: impl FnOnce(&mut StableLog) -> R) -> R {
LOG.with_borrow_mut(f)
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LogEntryRecord {
pub crate_name: String,
pub created_at: u64,
pub level: Level,
pub topic: Option<Topic>,
pub message: String,
}
impl_storable_unbounded!(LogEntryRecord);
pub struct Log;
impl Log {
pub(crate) fn append(
max_entries: usize,
max_entry_bytes: u32,
entry: LogEntryRecord,
) -> Result<u64, StorageError> {
if max_entries == 0 {
return Ok(0);
}
let entry = truncate_entry(entry, max_entry_bytes);
let id = append_raw(&entry)?;
Ok(id)
}
#[must_use]
pub(crate) fn snapshot() -> Vec<LogEntryRecord> {
let mut out = Vec::new();
with_log(|log| {
for entry in log.iter() {
out.push(entry);
}
});
out
}
}
#[derive(Clone, Debug, Default)]
pub struct RetentionSummary {
pub before: u64,
pub retained: u64,
pub dropped_by_age: u64,
pub dropped_by_limit: u64,
}
impl RetentionSummary {
#[must_use]
pub const fn dropped_total(&self) -> u64 {
self.dropped_by_age + self.dropped_by_limit
}
}
pub fn apply_retention(
cutoff: Option<u64>,
max_entries: usize,
max_entry_bytes: u32,
) -> Result<RetentionSummary, StorageError> {
let before = with_log(StableLog::len);
if max_entries == 0 {
with_log_mut(|log| *log = create_log());
return Ok(RetentionSummary {
before,
retained: 0,
dropped_by_age: 0,
dropped_by_limit: before,
});
}
if before == 0 {
return Ok(RetentionSummary::default());
}
let mut retained = VecDeque::new();
let mut eligible = 0u64;
with_log(|log| {
for entry in log.iter() {
if let Some(cutoff) = cutoff
&& entry.created_at < cutoff
{
continue;
}
eligible += 1;
retained.push_back(entry);
if retained.len() > max_entries {
retained.pop_front();
}
}
});
let retained_len = retained.len() as u64;
let dropped_by_age = if cutoff.is_some() {
before.saturating_sub(eligible)
} else {
0
};
let dropped_by_limit = eligible.saturating_sub(retained_len);
if dropped_by_age == 0 && dropped_by_limit == 0 {
return Ok(RetentionSummary {
before,
retained: retained_len,
dropped_by_age: 0,
dropped_by_limit: 0,
});
}
with_log_mut(|log| *log = create_log());
for entry in retained {
let entry = truncate_entry(entry, max_entry_bytes);
append_raw(&entry)?;
}
Ok(RetentionSummary {
before,
retained: retained_len,
dropped_by_age,
dropped_by_limit,
})
}
const TRUNCATION_SUFFIX: &str = "...[truncated]";
fn append_raw(entry: &LogEntryRecord) -> Result<u64, StorageError> {
with_log(|log| log.append(entry)).map_err(|e| StorageError::from(map_write_error(e)))
}
const fn map_write_error(err: WriteError) -> StableMemoryError {
match err {
WriteError::GrowFailed {
current_size,
delta,
} => StableMemoryError::LogWriteFailed {
current_size,
delta,
},
}
}
fn truncate_entry(mut entry: LogEntryRecord, max_bytes: u32) -> LogEntryRecord {
if let Some(msg) = truncate_message(&entry.message, max_bytes) {
entry.message = msg;
}
entry
}
fn truncate_message(message: &str, max_entry_bytes: u32) -> Option<String> {
let max_entry_bytes = usize::try_from(max_entry_bytes).unwrap_or(usize::MAX);
if message.len() <= max_entry_bytes {
return None;
}
if max_entry_bytes == 0 {
return Some(String::new());
}
if max_entry_bytes <= TRUNCATION_SUFFIX.len() {
return Some(truncate_to_boundary(message, max_entry_bytes).to_string());
}
let keep_len = max_entry_bytes - TRUNCATION_SUFFIX.len();
let mut truncated = truncate_to_boundary(message, keep_len).to_string();
truncated.push_str(TRUNCATION_SUFFIX);
Some(truncated)
}
fn truncate_to_boundary(message: &str, max_bytes: usize) -> &str {
if message.len() <= max_bytes {
return message;
}
let mut end = max_bytes;
while end > 0 && !message.is_char_boundary(end) {
end = end.saturating_sub(1);
}
&message[..end]
}