Skip to main content

bity_ic_canister_logger/
lib.rs

1//! Module for logging in Internet Computer canisters.
2//!
3//! This module provides a logging system for canisters that supports both regular logs
4//! and traces, with JSON formatting and circular buffer storage. It integrates with
5//! the `tracing` ecosystem for structured logging.
6//!
7//! # Example
8//! ```
9//! use bity_ic_canister_logger::{init, export_logs};
10//! use tracing::info;
11//!
12//! // Initialize the logger
13//! init(true);  // Enable tracing
14//!
15//! // Log some messages
16//! info!("Application started");
17//! tracing::trace!("Detailed trace message");
18//!
19//! // Export logs
20//! let logs = export_logs();
21//! ```
22
23use candid::CandidType;
24use serde::{Deserialize, Serialize};
25use std::cell::{Cell, RefCell};
26use std::collections::VecDeque;
27use std::io::Write;
28use tracing::Level;
29use tracing_subscriber::fmt::format::{FmtSpan, Writer};
30use tracing_subscriber::fmt::time::FormatTime;
31use tracing_subscriber::fmt::writer::MakeWriterExt;
32use tracing_subscriber::fmt::Layer;
33use tracing_subscriber::layer::SubscriberExt;
34use tracing_subscriber::util::SubscriberInitExt;
35use tracing_subscriber::Registry;
36
37thread_local! {
38    static INITIALIZED: Cell<bool> = Cell::default();
39    static LOG: RefCell<LogBuffer> = RefCell::new(LogBuffer::default());
40    static TRACE: RefCell<LogBuffer> = RefCell::new(LogBuffer::default());
41}
42
43/// Initializes the logging system.
44///
45/// This function sets up the logging infrastructure with JSON formatting,
46/// file and line number information, and optional tracing support.
47///
48/// # Arguments
49/// * `enable_trace` - Whether to enable trace-level logging
50///
51/// # Panics
52/// Panics if the logger has already been initialized
53pub fn init(enable_trace: bool) {
54    if INITIALIZED.with(|i| i.replace(true)) {
55        panic!("Logger already initialized");
56    }
57
58    let log_layer = Layer::default()
59        .with_writer((|| LogWriter::new(false)).with_max_level(Level::INFO))
60        .json()
61        .with_timer(Timer {})
62        .with_file(true)
63        .with_line_number(true)
64        .with_current_span(false)
65        .with_span_list(false);
66
67    if enable_trace {
68        let trace_layer = Layer::default()
69            .with_writer(|| LogWriter::new(true))
70            .json()
71            .with_timer(Timer {})
72            .with_file(true)
73            .with_line_number(true)
74            .with_current_span(false)
75            .with_span_events(FmtSpan::ENTER);
76
77        Registry::default().with(log_layer).with(trace_layer).init();
78    } else {
79        Registry::default().with(log_layer).init();
80    }
81}
82
83/// Initializes the logging system with pre-existing logs.
84///
85/// This function initializes the logger and populates it with existing log entries.
86///
87/// # Arguments
88/// * `enable_trace` - Whether to enable trace-level logging
89/// * `logs` - Pre-existing log entries to add
90/// * `traces` - Pre-existing trace entries to add
91pub fn init_with_logs(enable_trace: bool, logs: Vec<LogEntry>, traces: Vec<LogEntry>) {
92    init(enable_trace);
93
94    for log in logs {
95        LOG.with_borrow_mut(|l| l.append(log));
96    }
97    for trace in traces {
98        TRACE.with_borrow_mut(|t| t.append(trace));
99    }
100}
101
102/// A circular buffer for storing log messages.
103///
104/// This struct implements a fixed-size circular buffer that automatically
105/// evicts the oldest entries when full.
106///
107/// # Examples
108/// ```
109/// use bity_ic_canister_logger::LogBuffer;
110///
111/// let mut buffer = LogBuffer::with_capacity(10);
112/// buffer.append(LogEntry {
113///     timestamp: 1000,
114///     message: "Test message".to_string(),
115/// });
116/// ```
117pub struct LogBuffer {
118    max_capacity: usize,
119    entries: VecDeque<LogEntry>,
120}
121
122impl LogBuffer {
123    /// Creates a new buffer with the specified maximum capacity.
124    ///
125    /// # Arguments
126    /// * `max_capacity` - The maximum number of entries the buffer can hold
127    ///
128    /// # Returns
129    /// A new `LogBuffer` instance
130    pub fn with_capacity(max_capacity: usize) -> Self {
131        Self {
132            max_capacity,
133            entries: VecDeque::with_capacity(max_capacity),
134        }
135    }
136
137    /// Adds a new entry to the buffer.
138    ///
139    /// If the buffer is at capacity, the oldest entry is removed before adding
140    /// the new one.
141    ///
142    /// # Arguments
143    /// * `entry` - The log entry to add
144    pub fn append(&mut self, entry: LogEntry) {
145        while self.entries.len() >= self.max_capacity {
146            self.entries.pop_front();
147        }
148        self.entries.push_back(entry);
149    }
150
151    /// Returns an iterator over the entries in insertion order.
152    ///
153    /// # Returns
154    /// An iterator yielding references to `LogEntry`
155    pub fn iter(&self) -> impl Iterator<Item = &LogEntry> {
156        self.entries.iter()
157    }
158}
159
160impl Default for LogBuffer {
161    fn default() -> Self {
162        LogBuffer {
163            max_capacity: 100,
164            entries: VecDeque::new(),
165        }
166    }
167}
168
169/// Exports all current log entries.
170///
171/// # Returns
172/// A vector containing all log entries
173pub fn export_logs() -> Vec<LogEntry> {
174    LOG.with_borrow(|l| l.iter().cloned().collect())
175}
176
177/// Exports all current trace entries.
178///
179/// # Returns
180/// A vector containing all trace entries
181pub fn export_traces() -> Vec<LogEntry> {
182    TRACE.with_borrow(|t| t.iter().cloned().collect())
183}
184
185/// Represents a single log entry with timestamp and message.
186///
187/// This struct is used to store individual log messages with their
188/// associated timestamps.
189#[derive(CandidType, Serialize, Deserialize, Clone)]
190pub struct LogEntry {
191    /// The timestamp when the log entry was created (in milliseconds)
192    pub timestamp: u64,
193    /// The log message content
194    pub message: String,
195}
196
197/// A writer implementation for the logging system.
198///
199/// This struct handles the actual writing of log messages to the appropriate
200/// buffer based on whether it's handling traces or regular logs.
201struct LogWriter {
202    trace: bool,
203    buffer: Vec<u8>,
204}
205
206impl LogWriter {
207    /// Creates a new log writer.
208    ///
209    /// # Arguments
210    /// * `trace` - Whether this writer handles trace messages
211    ///
212    /// # Returns
213    /// A new `LogWriter` instance
214    fn new(trace: bool) -> LogWriter {
215        LogWriter {
216            trace,
217            buffer: Vec::new(),
218        }
219    }
220}
221
222impl Write for LogWriter {
223    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
224        self.buffer.extend(buf);
225        Ok(buf.len())
226    }
227
228    fn flush(&mut self) -> std::io::Result<()> {
229        let buffer = std::mem::take(&mut self.buffer);
230        let json = String::from_utf8(buffer).unwrap();
231
232        let log_entry = LogEntry {
233            timestamp: bity_ic_canister_time::timestamp_millis(),
234            message: json,
235        };
236
237        let sink = if self.trace { &TRACE } else { &LOG };
238        sink.with_borrow_mut(|s| s.append(log_entry));
239        Ok(())
240    }
241
242    fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
243        self.write(buf).and_then(|_| self.flush())
244    }
245}
246
247/// A timer implementation for log timestamps.
248///
249/// This struct provides timestamp formatting for log entries.
250struct Timer;
251
252impl FormatTime for Timer {
253    fn format_time(&self, w: &mut Writer) -> std::fmt::Result {
254        let now = bity_ic_canister_time::timestamp_millis();
255        w.write_str(&format!("{now}"))
256    }
257}