canic_core/ops/runtime/
log.rs1use 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
21const RETENTION_INTERVAL: Duration = OPS_LOG_RETENTION_INTERVAL;
23
24#[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
51pub struct LogOps;
56
57impl LogOps {
58 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 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 #[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 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 #[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}