canic_core/ops/model/memory/
log.rs

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