canic_core/ops/model/memory/
log.rs

1use crate::{
2    Error,
3    interface::ic::timer::{Timer, TimerId},
4    log,
5    log::{Level, Topic},
6    model::memory::log::{LogEntry, StableLog, apply_retention},
7    ops::model::{OPS_INIT_DELAY, OPS_LOG_RETENTION_INTERVAL},
8    types::PageRequest,
9};
10use candid::CandidType;
11use serde::Serialize;
12use std::{cell::RefCell, time::Duration};
13
14thread_local! {
15    static RETENTION_TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
16}
17
18/// How often to enforce retention after the first sweep.
19const RETENTION_INTERVAL: Duration = OPS_LOG_RETENTION_INTERVAL;
20
21///
22/// LogEntryDto
23///
24
25#[derive(CandidType, Clone, Debug, Serialize)]
26pub struct LogEntryDto {
27    pub index: u64,
28    pub created_at: u64,
29    pub crate_name: String,
30    pub level: Level,
31    pub topic: Option<String>,
32    pub message: String,
33}
34
35impl LogEntryDto {
36    fn from_pair(index: usize, entry: LogEntry) -> Self {
37        Self {
38            index: index as u64,
39            created_at: entry.created_at,
40            crate_name: entry.crate_name,
41            level: entry.level,
42            topic: entry.topic,
43            message: entry.message,
44        }
45    }
46}
47
48///
49/// LogPageDto
50///
51
52#[derive(CandidType, Serialize)]
53pub struct LogPageDto {
54    pub entries: Vec<LogEntryDto>,
55    pub total: u64,
56}
57
58///
59/// LogOps
60///
61
62pub struct LogOps;
63
64impl LogOps {
65    /// Start periodic log retention sweeps. Safe to call multiple times.
66    pub fn start_retention() {
67        RETENTION_TIMER.with_borrow_mut(|slot| {
68            if slot.is_some() {
69                return;
70            }
71
72            let init = Timer::set(OPS_INIT_DELAY, "log_retention:init", async {
73                let _ = Self::retain();
74
75                let interval =
76                    Timer::set_interval(RETENTION_INTERVAL, "log_retention:interval", || async {
77                        let _ = Self::retain();
78                    });
79
80                RETENTION_TIMER.with_borrow_mut(|slot| *slot = Some(interval));
81            });
82
83            *slot = Some(init);
84        });
85    }
86
87    /// Stop periodic retention sweeps.
88    pub fn stop_retention() {
89        RETENTION_TIMER.with_borrow_mut(|slot| {
90            if let Some(id) = slot.take() {
91                Timer::clear(id);
92            }
93        });
94    }
95
96    /// Run a retention sweep immediately.
97    #[must_use]
98    pub fn retain() -> bool {
99        match apply_retention() {
100            Ok(()) => true,
101            Err(err) => {
102                log!(Topic::Memory, Warn, "log retention failed: {err}");
103                false
104            }
105        }
106    }
107
108    /// Append a log entry to stable storage.
109    pub fn append<T: ToString, M: AsRef<str>>(
110        crate_name: &str,
111        topic: Option<T>,
112        level: Level,
113        message: M,
114    ) -> Result<u64, Error> {
115        StableLog::append(crate_name, topic, level, message)
116    }
117
118    ///
119    /// Export a page of log entries and the total count.
120    ///
121    #[must_use]
122    pub fn page(
123        crate_name: Option<String>,
124        topic: Option<String>,
125        min_level: Option<Level>,
126        request: PageRequest,
127    ) -> LogPageDto {
128        let request = request.clamped();
129
130        let (raw_entries, total) = StableLog::entries_page_filtered(
131            crate_name.as_deref(),
132            topic.as_deref(),
133            min_level,
134            request,
135        );
136
137        let entries = raw_entries
138            .into_iter()
139            .map(|(i, entry)| LogEntryDto::from_pair(i, entry))
140            .collect();
141
142        LogPageDto { entries, total }
143    }
144}