ic_canister_log/
lib.rs

1use std::cell::RefCell;
2use std::collections::VecDeque;
3use std::fmt;
4use std::thread::LocalKey;
5
6/// Declares a new canister log buffer.
7#[macro_export]
8macro_rules! declare_log_buffer {
9    (name = $name:ident, capacity = $cap:expr) => {
10        thread_local! {
11            pub static $name: std::cell::RefCell<$crate::LogBuffer> =
12                std::cell::RefCell::new($crate::LogBuffer::with_capacity($cap));
13        }
14    };
15}
16
17pub mod entry_counter {
18    thread_local! {
19        static ENTRY_COUNTER: std::cell::Cell<u64> = Default::default();
20    }
21    pub fn increment() -> u64 {
22        ENTRY_COUNTER.with(|cell| {
23            cell.set(cell.get() + 1);
24            cell.get()
25        })
26    }
27    pub fn set(value: u64) {
28        ENTRY_COUNTER.with(|cell| cell.set(value));
29    }
30    pub fn get() -> u64 {
31        ENTRY_COUNTER.with(|cell| cell.get())
32    }
33}
34
35/// Adds a new record to a canister log buffer.
36///
37/// ```
38/// use ic_canister_log::{declare_log_buffer, log};
39///
40/// // Keep up to 100 last messages.
41/// declare_log_buffer!(name = LOG, capacity = 100);
42///
43/// fn sum_and_log(x: u64, y: u64) -> u64 {
44///    let result = x.saturating_add(y);
45///    log!(LOG, "{} + {} = {}", x, y, result);
46///    result
47/// }
48/// ```
49#[macro_export]
50macro_rules! log {
51    ($sink:expr, $message:expr $(,$args:expr)* $(,)*) => {{
52        use $crate::Sink;
53        let message = std::format!($message $(,$args)*);
54        // Print the message for convenience for local development (e.g. integration tests)
55        println!("{}", &message);
56        (&$sink).append($crate::LogEntry {
57            timestamp: $crate::now(),
58            message,
59            file: std::file!(),
60            line: std::line!(),
61            counter: $crate::entry_counter::increment()
62        });
63    }}
64}
65
66pub trait Sink {
67    fn append(&self, entry: LogEntry);
68}
69
70/// An entry in the canister log.
71#[derive(Debug, serde::Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
72pub struct LogEntry {
73    pub timestamp: u64,
74    // The index of this entry starting from the last canister upgrade.
75    pub counter: u64,
76    pub message: String,
77    pub file: &'static str,
78    pub line: u32,
79}
80
81impl fmt::Display for LogEntry {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(
84            f,
85            "[{}] {}:{} {}",
86            self.timestamp, self.file, self.line, self.message
87        )
88    }
89}
90
91#[derive(Clone)]
92pub struct DevNull;
93
94impl Sink for DevNull {
95    fn append(&self, _: LogEntry) {}
96}
97
98/// A circular buffer for log messages.
99pub struct LogBuffer {
100    max_capacity: usize,
101    entries: VecDeque<LogEntry>,
102}
103
104impl LogBuffer {
105    /// Creates a new buffer of the specified max capacity.
106    pub fn with_capacity(max_capacity: usize) -> Self {
107        Self {
108            max_capacity,
109            entries: VecDeque::with_capacity(max_capacity),
110        }
111    }
112
113    /// Adds a new entry to the buffer, potentially evicting older entries.
114    pub fn append(&mut self, entry: LogEntry) {
115        if self.entries.len() >= self.max_capacity {
116            self.entries.pop_front();
117        }
118        self.entries.push_back(entry);
119    }
120
121    /// Returns an iterator over entries in the order of their insertion.
122    pub fn iter(&self) -> impl Iterator<Item = &LogEntry> {
123        self.entries.iter()
124    }
125
126    /// Returns the first iterator for which p returns false (or past the end
127    /// iterator if they all return false).
128    ///
129    /// Warning: Entries MUST be partitioned by p. That is, p returns true for
130    /// all elements in the "front" of the entries list, and false for all
131    /// elements in the "back". Otherwise, behavior is not defined. This is
132    /// because binary search is used.
133    ///
134    /// For example,
135    ///
136    ///   log_buffer.skip_old_entries(|log_entry| log_entry.timestamp <= T)
137    ///
138    /// In practice, p only uses the timestamp field, because you can partition
139    /// on that (since entries are in chronological order, assuming the clock is
140    /// monotonic, and the IC, it is).
141    ///
142    /// If you want an iterator to the first iterator that returns true, but p
143    /// does not partition, do this instead:
144    ///
145    ///    log_buffer.iter().skip_while(opposite_of_p)
146    pub fn entries_partition_point<P>(&self, p: P) -> impl Iterator<Item = &LogEntry>
147    where
148        P: Fn(&LogEntry) -> bool,
149    {
150        let head_len = self.entries.partition_point(p);
151        self.iter().skip(head_len)
152    }
153}
154
155pub type GlobalBuffer = LocalKey<RefCell<LogBuffer>>;
156
157impl Sink for &'static GlobalBuffer {
158    fn append(&self, entry: LogEntry) {
159        self.with(|cell| cell.borrow_mut().append(entry))
160    }
161}
162
163mod private {
164    #[cfg(target_arch = "wasm32")]
165    #[link(wasm_import_module = "ic0")]
166    extern "C" {
167        pub fn time() -> u64;
168    }
169
170    #[cfg(target_arch = "wasm32")]
171    pub fn timestamp() -> u64 {
172        unsafe { time() }
173    }
174
175    #[cfg(not(target_arch = "wasm32"))]
176    pub fn timestamp() -> u64 {
177        use std::time::SystemTime;
178
179        match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
180            Ok(d) => d.as_nanos() as u64,
181            Err(_) => panic!("SystemTime before UNIX EPOCH!"),
182        }
183    }
184}
185
186/// Returns the current time as a number of nanoseconds passed since the Unix
187/// epoch.
188#[doc(hidden)]
189pub fn now() -> u64 {
190    private::timestamp()
191}
192
193/// Exports the contents of a buffer as a vector of entries in the order of
194/// insertion.
195///
196/// ```
197/// use ic_canister_log::{declare_log_buffer, export, log};
198///
199/// // Keep up to 100 last messages.
200/// declare_log_buffer!(name = LOG, capacity = 100);
201///
202/// log!(LOG, "Hello, {}!", "world");
203/// let entries = export(&LOG);
204/// assert_eq!(entries.len(), 1);
205/// assert_eq!(entries[0].message, "Hello, world!");
206/// ```
207pub fn export(buf: &'static GlobalBuffer) -> Vec<LogEntry> {
208    buf.with(|cell| cell.borrow().iter().cloned().collect())
209}