canic_core/ops/runtime/
log.rs

1use crate::{
2    Error,
3    dto::Page,
4    log,
5    log::{Level, Topic},
6    model::memory::log::{LogEntry, StableLog, apply_retention},
7    ops::{
8        OPS_INIT_DELAY, OPS_LOG_RETENTION_INTERVAL,
9        ic::timer::{TimerId, TimerOps},
10    },
11    types::PageRequest,
12};
13use candid::CandidType;
14use serde::{Deserialize, 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, Deserialize, 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_indexed_entry(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/// LogOps
53///
54
55pub struct LogOps;
56
57impl LogOps {
58    /// Start periodic log retention sweeps. Safe to call multiple times.
59    pub fn start_retention() {
60        RETENTION_TIMER.with_borrow_mut(|slot| {
61            if slot.is_some() {
62                return;
63            }
64
65            let init = TimerOps::set(OPS_INIT_DELAY, "log_retention:init", async {
66                let _ = Self::retain();
67
68                let interval = TimerOps::set_interval(
69                    RETENTION_INTERVAL,
70                    "log_retention:interval",
71                    || async {
72                        let _ = Self::retain();
73                    },
74                );
75
76                RETENTION_TIMER.with_borrow_mut(|slot| *slot = Some(interval));
77            });
78
79            *slot = Some(init);
80        });
81    }
82
83    /// Stop periodic retention sweeps.
84    pub fn stop_retention() {
85        RETENTION_TIMER.with_borrow_mut(|slot| {
86            if let Some(id) = slot.take() {
87                TimerOps::clear(id);
88            }
89        });
90    }
91
92    /// Run a retention sweep immediately.
93    /// This enforces configured retention limits on stable logs.
94    #[must_use]
95    pub fn retain() -> bool {
96        match apply_retention() {
97            Ok(()) => true,
98            Err(err) => {
99                log!(Topic::Memory, Warn, "log retention failed: {err}");
100                false
101            }
102        }
103    }
104
105    /// Append a log entry to stable storage.
106    pub fn append<T, M>(
107        crate_name: &str,
108        topic: Option<T>,
109        level: Level,
110        message: M,
111    ) -> Result<u64, Error>
112    where
113        T: ToString,
114        M: AsRef<str>,
115    {
116        StableLog::append(crate_name, topic, level, message)
117    }
118
119    ///
120    /// Export a page of log entries and the total count.
121    ///
122    #[must_use]
123    pub fn page(
124        crate_name: Option<String>,
125        topic: Option<String>,
126        min_level: Option<Level>,
127        request: PageRequest,
128    ) -> Page<LogEntryDto> {
129        let request = request.clamped();
130
131        let (raw_entries, total) = StableLog::entries_page_filtered(
132            crate_name.as_deref(),
133            topic.as_deref(),
134            min_level,
135            request,
136        );
137
138        let entries = raw_entries
139            .into_iter()
140            .map(|(i, entry)| LogEntryDto::from_indexed_entry(i, entry))
141            .collect();
142
143        Page { entries, total }
144    }
145}