1#![allow(clippy::cast_possible_truncation)]
2use crate::{
3 Error, ThisError,
4 cdk::structures::{
5 DefaultMemoryImpl,
6 log::{Log as StableLogImpl, WriteError},
7 memory::VirtualMemory,
8 },
9 config::{Config, schema::LogConfig},
10 eager_static, ic_memory, impl_storable_unbounded,
11 log::Level,
12 model::memory::{
13 MemoryError,
14 id::log::{LOG_DATA_ID, LOG_INDEX_ID},
15 },
16 utils::{
17 case::{Case, Casing},
18 time,
19 },
20};
21
22use candid::CandidType;
23use serde::{Deserialize, Serialize};
24use std::cell::RefCell;
25
26type StableLogStorage =
31 StableLogImpl<LogEntry, VirtualMemory<DefaultMemoryImpl>, VirtualMemory<DefaultMemoryImpl>>;
32
33struct LogIndexMemory;
35struct LogDataMemory;
36
37fn create_log() -> StableLogStorage {
38 StableLogImpl::new(
39 ic_memory!(LogIndexMemory, LOG_INDEX_ID),
40 ic_memory!(LogDataMemory, LOG_DATA_ID),
41 )
42}
43
44eager_static! {
45 static LOG: RefCell<StableLogStorage> = RefCell::new(create_log());
46}
47
48fn with_log<R>(f: impl FnOnce(&StableLogStorage) -> R) -> R {
50 LOG.with_borrow(|l| f(l))
51}
52
53fn with_log_mut<R>(f: impl FnOnce(&mut StableLogStorage) -> R) -> R {
54 LOG.with_borrow_mut(|l| f(l))
55}
56
57pub(crate) fn log_config() -> LogConfig {
58 Config::try_get().map(|c| c.log.clone()).unwrap_or_default()
59}
60
61#[derive(Debug, ThisError)]
68pub enum LogError {
69 #[error("log write failed: current_size={current_size}, delta={delta}")]
70 WriteFailed { current_size: u64, delta: u64 },
71}
72
73impl From<WriteError> for LogError {
74 fn from(err: WriteError) -> Self {
75 match err {
76 WriteError::GrowFailed {
77 current_size,
78 delta,
79 } => Self::WriteFailed {
80 current_size,
81 delta,
82 },
83 }
84 }
85}
86
87impl From<LogError> for Error {
88 fn from(err: LogError) -> Self {
89 MemoryError::from(err).into()
90 }
91}
92
93#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
98pub struct LogEntry {
99 pub crate_name: String,
100 pub created_at: u64,
101 pub level: Level,
102 pub topic: Option<String>,
103 pub message: String,
104}
105
106impl LogEntry {
107 pub fn new(crate_name: &str, level: Level, topic: Option<&str>, msg: &str) -> Self {
108 Self {
109 crate_name: crate_name.to_string(),
110 created_at: time::now_secs(),
111 level,
112 topic: topic.map(ToString::to_string),
113 message: msg.to_string(),
114 }
115 }
116}
117
118impl_storable_unbounded!(LogEntry);
119
120pub(crate) struct StableLog;
125
126impl StableLog {
127 pub fn append<T, M>(
130 crate_name: &str,
131 topic: Option<T>,
132 level: Level,
133 message: M,
134 ) -> Result<u64, Error>
135 where
136 T: ToString,
137 M: AsRef<str>,
138 {
139 let topic_normalized = Self::normalize_topic(topic);
140 let entry = LogEntry::new(
141 crate_name,
142 level,
143 topic_normalized.as_deref(),
144 message.as_ref(),
145 );
146
147 Self::append_entry(entry)
148 }
149
150 pub fn append_entry(entry: LogEntry) -> Result<u64, Error> {
151 let cfg = log_config();
152
153 if cfg.max_entries == 0 {
154 return Ok(0);
155 }
156
157 with_log(|log| log.append(&entry))
158 .map_err(LogError::from)
159 .map_err(Error::from)
160 }
161
162 fn normalize_topic<T: ToString>(topic: Option<T>) -> Option<String> {
165 topic.as_ref().map(|t| t.to_string().to_case(Case::Snake))
166 }
167
168 #[must_use]
169 pub fn entries_page_filtered(
170 crate_name: Option<&str>,
171 topic: Option<&str>,
172 min_level: Option<Level>,
173 offset: u64,
174 limit: u64,
175 ) -> (Vec<(usize, LogEntry)>, u64) {
176 let offset = offset as usize;
177 let limit = limit as usize;
178 let topic_norm: Option<String> = Self::normalize_topic(topic);
179 let topic_norm = topic_norm.as_deref();
180
181 with_log(|log| {
182 let items: Vec<(usize, LogEntry)> =
184 iter_filtered(log, crate_name, topic_norm, min_level).collect();
185
186 let total = items.len() as u64;
187
188 let entries = items.into_iter().skip(offset).take(limit).collect();
190
191 (entries, total)
192 })
193 }
194}
195
196pub(crate) fn apply_retention() -> Result<(), Error> {
199 let cfg = log_config();
200
201 if cfg.max_entries == 0 {
202 with_log_mut(|log| *log = create_log());
203 return Ok(());
204 }
205
206 let now = time::now_secs();
207 let max_entries = cfg.max_entries as usize;
208
209 let mut retained: Vec<LogEntry> = with_log(|log| log.iter().collect());
211
212 if let Some(age) = cfg.max_age_secs {
214 retained.retain(|e| now.saturating_sub(e.created_at) <= age);
215 }
216
217 if retained.len() > max_entries {
219 let drop = retained.len() - max_entries;
220 retained.drain(0..drop);
221 }
222
223 let original_len = with_log(|log| log.len() as usize);
225 if retained.len() == original_len {
226 return Ok(());
227 }
228
229 with_log_mut(|log| *log = create_log());
231 for entry in retained {
232 with_log(|log| log.append(&entry))
233 .map_err(LogError::from)
234 .map_err(Error::from)?;
235 }
236
237 Ok(())
238}
239
240fn iter_filtered<'a>(
241 log: &'a StableLogStorage,
242 crate_name: Option<&'a str>, topic: Option<&'a str>, min_level: Option<Level>, ) -> impl Iterator<Item = (usize, LogEntry)> + 'a {
246 log.iter().enumerate().filter(move |(_, e)| {
247 crate_name.is_none_or(|name| e.crate_name == name)
248 && topic.is_none_or(|t| e.topic.as_deref() == Some(t))
249 && min_level.is_none_or(|lvl| e.level >= lvl)
250 })
251}