Skip to main content

borderless_runtime/db/
logger.rs

1use std::cmp::min;
2
3use borderless::{
4    __private::storage_keys::{StorageKey, BASE_KEY_LOGS},
5    http::{queries::Pagination, PaginatedElements},
6    log::{LogLevel, LogLine},
7    prelude::Id,
8};
9use borderless_kv_store::*;
10use serde::{Deserialize, Serialize};
11
12use crate::log_shim::{debug, error, info, trace, warn};
13use crate::{Result, AGENT_SUB_DB, CONTRACT_SUB_DB};
14
15/// Storage key, where the meta-information about the buffer is saved
16const SUB_KEY_META: u64 = u64::MAX;
17
18/// We keep a maximum of 32k log-lines ( which should be sufficient for debugging )
19const MAX_LOG_BUFFER_SIZE: u64 = 32 * 1024;
20
21#[derive(Serialize, Deserialize, Default)]
22struct BufferMeta {
23    start: u64,
24    end: u64,
25    /// Absolute index at which the last flush started.
26    last_flush_start: u64,
27    /// Number of log lines flushed in the last flush.
28    last_flush_count: u64,
29}
30
31/// Logger instance that is created over a key-value storage for a given contract-id
32///
33/// The logger is essentially a ring-buffer with a fixed size, that uses a specific key-space.
34pub struct Logger<'a, S: Db> {
35    db: &'a S,
36    id: Id,
37}
38
39impl<'a, S: Db> Logger<'a, S> {
40    pub fn new(db: &'a S, id: impl Into<Id>) -> Self {
41        Self { db, id: id.into() }
42    }
43
44    /// Flushes the given log lines into the ring-buffer.
45    ///
46    /// This function writes a batch of log lines into the underlying key-value storage. It performs the following steps:
47    ///
48    /// 1. Reads the current buffer metadata, which includes the logical start and end indices of the stored log lines.
49    /// 2. Determines if adding the new log lines would exceed the fixed capacity (`MAX_LOG_BUFFER_SIZE`). If so,
50    ///    it advances the start index to overwrite the oldest entries.
51    /// 3. Records the flush metadata (`last_flush_start` and `last_flush_count`) to track the range of log lines added in this flush.
52    /// 4. Writes the new log lines to storage using modulo arithmetic to map the logical indices to physical storage keys.
53    /// 5. Updates and persists the modified metadata.
54    ///
55    /// # Arguments
56    ///
57    /// * `lines` - A slice of `LogLine` objects to be flushed into the buffer.
58    ///
59    /// # Returns
60    ///
61    /// * `Ok(())` if the flush is successful.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if any database operation fails or if serialization/deserialization of log lines or metadata fails.
66    pub fn flush_lines(
67        &self,
68        lines: &[LogLine],
69        db_ptr: &S::Handle,
70        txn: &mut <S as Db>::RwTx<'_>,
71    ) -> Result<()> {
72        // Retrieve meta info, or initialize it if not present.
73        let meta_key = StorageKey::system_key(self.id, BASE_KEY_LOGS, SUB_KEY_META);
74        let mut meta = match txn.read(db_ptr, &meta_key)? {
75            Some(bytes) => postcard::from_bytes(bytes)?,
76            None => {
77                // Initialize with flush info set to 0.
78                let meta = BufferMeta::default();
79                let bytes = postcard::to_allocvec(&meta)?;
80                txn.write(db_ptr, &meta_key, &bytes)?;
81                meta
82            }
83        };
84
85        let new_line_count = lines.len() as u64;
86        let current_count = meta.end - meta.start;
87
88        // If adding new lines would overflow the ring buffer, adjust the start index.
89        if current_count + new_line_count > MAX_LOG_BUFFER_SIZE {
90            let drop_count = current_count + new_line_count - MAX_LOG_BUFFER_SIZE;
91            meta.start += drop_count;
92        }
93
94        // Record the flush information: where the flush starts and how many lines are flushed.
95        meta.last_flush_start = meta.end;
96        meta.last_flush_count = new_line_count;
97
98        // Write each new log line using modulo arithmetic to wrap-around.
99        for (i, line) in lines.iter().enumerate() {
100            let index = (meta.end + i as u64) % MAX_LOG_BUFFER_SIZE;
101            let key = StorageKey::system_key(self.id, BASE_KEY_LOGS, index);
102            let bytes = postcard::to_allocvec(line)?;
103            txn.write(db_ptr, &key, &bytes)?;
104        }
105
106        // Update meta with the new end.
107        meta.end += new_line_count;
108        let meta_bytes = postcard::to_allocvec(&meta)?;
109        txn.write(db_ptr, &meta_key, &meta_bytes)?;
110        Ok(())
111    }
112
113    /// Retrieves the full log from the buffer in chronological order.
114    pub fn get_full_log(&self) -> Result<Vec<LogLine>> {
115        self.get_log_lines(0, MAX_LOG_BUFFER_SIZE)
116    }
117
118    /// Retrieves a range of log lines from the buffer in chronological order.
119    ///
120    /// # Arguments
121    ///
122    /// * `start_offset` - The number of log lines to skip from the oldest entry.
123    /// * `count` - The maximum number of log lines to retrieve.
124    ///
125    /// For example, to get the 100 oldest log lines, call with start_offset = 0 and count = 100.
126    pub fn get_log_lines(&self, start_offset: u64, count: u64) -> Result<Vec<LogLine>> {
127        let db_ptr = match self.id {
128            Id::Contract { .. } => self.db.open_sub_db(CONTRACT_SUB_DB)?,
129            Id::Agent { .. } => self.db.open_sub_db(AGENT_SUB_DB)?,
130        };
131        let txn = self.db.begin_ro_txn()?;
132        let meta_key = StorageKey::system_key(self.id, BASE_KEY_LOGS, SUB_KEY_META);
133        // Read meta info; if missing, assume an empty buffer.
134        let meta = match txn.read(&db_ptr, &meta_key)? {
135            Some(bytes) => postcard::from_bytes(bytes)?,
136            None => BufferMeta::default(),
137        };
138
139        let total_count = meta.end - meta.start;
140        // If the requested start offset is beyond the current log count, return an empty Vec.
141        if start_offset >= total_count {
142            return Ok(Vec::new());
143        }
144        // Determine the absolute indices in the logical log buffer.
145        let range_start = meta.start + start_offset;
146        let range_end = min(range_start + count, meta.end);
147
148        let mut logs = Vec::new();
149        // Iterate over the specified range and fetch each log line.
150        for i in range_start..range_end {
151            // Compute the physical index using modulo arithmetic.
152            let index = i % MAX_LOG_BUFFER_SIZE;
153            let key = StorageKey::system_key(self.id, BASE_KEY_LOGS, index);
154            if let Some(bytes) = txn.read(&db_ptr, &key)? {
155                let log_line: LogLine = postcard::from_bytes(bytes)?;
156                logs.push(log_line);
157            }
158        }
159        Ok(logs)
160    }
161
162    /// Retrieves the log lines that were flushed in the last call to `flush_lines`.
163    pub fn get_last_log(&self) -> Result<Vec<LogLine>> {
164        let db_ptr = match self.id {
165            Id::Contract { .. } => self.db.open_sub_db(CONTRACT_SUB_DB)?,
166            Id::Agent { .. } => self.db.open_sub_db(AGENT_SUB_DB)?,
167        };
168        let txn = self.db.begin_ro_txn()?;
169        let meta_key = StorageKey::system_key(self.id, BASE_KEY_LOGS, SUB_KEY_META);
170
171        let meta: BufferMeta = match txn.read(&db_ptr, &meta_key)? {
172            Some(bytes) => postcard::from_bytes(bytes)?,
173            None => return Ok(Vec::new()),
174        };
175
176        let mut logs = Vec::new();
177        let flush_start = meta.last_flush_start;
178        let flush_count = meta.last_flush_count;
179
180        // Iterate over the range corresponding to the last flush.
181        for i in flush_start..(flush_start + flush_count) {
182            // Compute the physical index using modulo arithmetic.
183            let index = i % MAX_LOG_BUFFER_SIZE;
184            let key = StorageKey::system_key(self.id, BASE_KEY_LOGS, index);
185            if let Some(bytes) = txn.read(&db_ptr, &key)? {
186                let log_line: LogLine = postcard::from_bytes(bytes)?;
187                logs.push(log_line);
188            }
189        }
190        Ok(logs)
191    }
192
193    /// Returns the total number of log lines ever flushed.
194    ///
195    /// Note that this number is the absolute index of the last flushed log line,
196    /// so if logs have been overwritten in the ring-buffer, the current log count
197    /// (meta.end - meta.start) may be lower.
198    pub fn total_log_lines(&self) -> Result<u64> {
199        let db_ptr = match self.id {
200            Id::Contract { .. } => self.db.open_sub_db(CONTRACT_SUB_DB)?,
201            Id::Agent { .. } => self.db.open_sub_db(AGENT_SUB_DB)?,
202        };
203        let txn = self.db.begin_ro_txn()?;
204        let meta_key = StorageKey::system_key(self.id, BASE_KEY_LOGS, SUB_KEY_META);
205        // If meta is missing, we assume no logs have been flushed yet.
206        let meta = match txn.read(&db_ptr, &meta_key)? {
207            Some(bytes) => postcard::from_bytes(bytes)?,
208            None => BufferMeta::default(),
209        };
210        Ok(meta.end)
211    }
212
213    // TODO: Add 'reverse' option
214    /// Retrieves log lines for the given page and the total number of pages.
215    pub fn get_logs_paginated(&self, pagination: Pagination) -> Result<PaginatedElements<LogLine>> {
216        let page = pagination.page as u64;
217        let per_page = pagination.per_page as u64;
218        let db_ptr = match self.id {
219            Id::Contract { .. } => self.db.open_sub_db(CONTRACT_SUB_DB)?,
220            Id::Agent { .. } => self.db.open_sub_db(AGENT_SUB_DB)?,
221        };
222        let txn = self.db.begin_ro_txn()?;
223        let meta_key = StorageKey::system_key(self.id, BASE_KEY_LOGS, SUB_KEY_META);
224
225        // Retrieve meta information. If not found, assume an empty buffer.
226        let meta = match txn.read(&db_ptr, &meta_key)? {
227            Some(bytes) => postcard::from_bytes(bytes)?,
228            None => BufferMeta {
229                start: 0,
230                end: 0,
231                last_flush_start: 0,
232                last_flush_count: 0,
233            },
234        };
235
236        // Calculate the total number of log lines currently in the ring-buffer.
237        let total_count = meta.end - meta.start;
238        // Calculate total pages using ceiling division.
239        let total_pages = if total_count == 0 {
240            0
241        } else {
242            (total_count + per_page - 1) / per_page
243        };
244        let total_elements = (total_pages * per_page) as usize;
245
246        // Calculate the logical start and end indices for the requested page.
247        let page_start = meta.start + page.saturating_sub(1) * per_page;
248        // If the start index is beyond the end of the stored logs, return an empty Vec.
249        if page_start >= meta.end {
250            return Ok(PaginatedElements {
251                elements: Vec::new(),
252                total_elements,
253                pagination,
254            });
255        }
256        let page_end = std::cmp::min(meta.start + page * per_page, meta.end);
257
258        // Retrieve the logs for the calculated range.
259        let mut logs = Vec::new();
260        for i in page_start..page_end {
261            // Map the logical index to the physical index in the ring-buffer.
262            let physical_index = i % MAX_LOG_BUFFER_SIZE;
263            let key = StorageKey::system_key(self.id, BASE_KEY_LOGS, physical_index);
264            if let Some(bytes) = txn.read(&db_ptr, &key)? {
265                let log_line: LogLine = postcard::from_bytes(bytes)?;
266                logs.push(log_line);
267            }
268        }
269        Ok(PaginatedElements {
270            elements: logs,
271            total_elements,
272            pagination,
273        })
274    }
275}
276
277/// Just prints a log line to stdout
278///
279/// Ignores the timestamp
280pub fn print_log_line(line: LogLine) {
281    let msg = line.msg;
282    match line.level {
283        LogLevel::Trace => trace!("{msg}"),
284        LogLevel::Debug => debug!("{msg}"),
285        LogLevel::Info => info!("{msg}"),
286        LogLevel::Warn => warn!("{msg}"),
287        LogLevel::Error => error!("{msg}"),
288    }
289}