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