canic_core/ops/runtime/
log.rs

1use crate::{
2    Error,
3    dto::page::{Page, PageRequest},
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};
12use candid::CandidType;
13use serde::{Deserialize, Serialize};
14use std::{cell::RefCell, time::Duration};
15
16thread_local! {
17    static RETENTION_TIMER: RefCell<Option<TimerId>> = const { RefCell::new(None) };
18}
19
20/// How often to enforce retention after the first sweep.
21const RETENTION_INTERVAL: Duration = OPS_LOG_RETENTION_INTERVAL;
22
23///
24/// LogEntryDto
25///
26
27#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
28pub struct LogEntryDto {
29    pub index: u64,
30    pub created_at: u64,
31    pub crate_name: String,
32    pub level: Level,
33    pub topic: Option<String>,
34    pub message: String,
35}
36
37impl LogEntryDto {
38    fn from_indexed_entry(index: usize, entry: LogEntry) -> Self {
39        Self {
40            index: index as u64,
41            created_at: entry.created_at,
42            crate_name: entry.crate_name,
43            level: entry.level,
44            topic: entry.topic,
45            message: entry.message,
46        }
47    }
48}
49
50///
51/// LogOps
52///
53
54pub struct LogOps;
55
56impl LogOps {
57    /// Start periodic log retention sweeps. Safe to call multiple times.
58    pub fn start_retention() {
59        let _ = TimerOps::set_guarded_interval(
60            &RETENTION_TIMER,
61            OPS_INIT_DELAY,
62            "log_retention:init",
63            || async {
64                let _ = Self::retain();
65            },
66            RETENTION_INTERVAL,
67            "log_retention:interval",
68            || async {
69                let _ = Self::retain();
70            },
71        );
72    }
73
74    /// Stop periodic retention sweeps.
75    pub fn stop_retention() {
76        let _ = TimerOps::clear_guarded(&RETENTION_TIMER);
77    }
78
79    /// Run a retention sweep immediately.
80    /// This enforces configured retention limits on stable logs.
81    #[must_use]
82    pub fn retain() -> bool {
83        match apply_retention() {
84            Ok(summary) => {
85                let dropped = summary.dropped_total();
86                if dropped > 0 {
87                    let before = summary.before;
88                    let retained = summary.retained;
89                    let dropped_by_age = summary.dropped_by_age;
90                    let dropped_by_limit = summary.dropped_by_limit;
91                    log!(
92                        Topic::Memory,
93                        Info,
94                        "log retention: dropped={dropped} (age={dropped_by_age}, limit={dropped_by_limit}), before={before}, retained={retained}"
95                    );
96                }
97                true
98            }
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, M>(
108        crate_name: &str,
109        topic: Option<T>,
110        level: Level,
111        message: M,
112    ) -> Result<u64, Error>
113    where
114        T: ToString,
115        M: AsRef<str>,
116    {
117        StableLog::append(crate_name, topic, level, message)
118    }
119
120    ///
121    /// Export a page of log entries and the total count.
122    ///
123    #[must_use]
124    pub fn page(
125        crate_name: Option<String>,
126        topic: Option<String>,
127        min_level: Option<Level>,
128        request: PageRequest,
129    ) -> Page<LogEntryDto> {
130        let request = request.clamped();
131
132        let (raw_entries, total) = StableLog::entries_page_filtered(
133            crate_name.as_deref(),
134            topic.as_deref(),
135            min_level,
136            request,
137        );
138
139        let entries = raw_entries
140            .into_iter()
141            .map(|(i, entry)| LogEntryDto::from_indexed_entry(i, entry))
142            .collect();
143
144        Page { entries, total }
145    }
146}