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