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}