-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #224 from bitfinity-network/maxim/logger_canister_…
…trait Add common logger canister trait
- Loading branch information
Showing
13 changed files
with
1,340 additions
and
112 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,100 +1,61 @@ | ||
use std::cell::RefCell; | ||
use std::marker::PhantomData; | ||
use std::rc::Rc; | ||
|
||
use candid::Principal; | ||
use ic_canister::{generate_idl, init, query, update, Canister, Idl, PreUpdate}; | ||
use ic_log::writer::Logs; | ||
use ic_log::{init_log, LogSettings, LoggerConfig}; | ||
use log::{debug, error, info}; | ||
use ic_canister::{generate_idl, init, Canister, Idl, PreUpdate}; | ||
use ic_exports::ic_cdk; | ||
use ic_exports::ic_kit::ic; | ||
use ic_log::canister::inspect::logger_canister_inspect; | ||
use ic_log::canister::{LogCanister, LogState}; | ||
use ic_log::did::LogCanisterSettings; | ||
use ic_stable_structures::MemoryId; | ||
use ic_storage::IcStorage; | ||
|
||
#[derive(Canister)] | ||
pub struct LogCanister { | ||
pub struct LoggerCanister { | ||
#[id] | ||
id: Principal, | ||
} | ||
|
||
impl PreUpdate for LogCanister {} | ||
impl PreUpdate for LoggerCanister {} | ||
|
||
impl LogCanister { | ||
#[init] | ||
pub fn init(&self) { | ||
let settings = LogSettings { | ||
in_memory_records: Some(128), | ||
log_filter: Some("info".to_string()), | ||
enable_console: true, | ||
}; | ||
match init_log(&settings) { | ||
Ok(logger_config) => LoggerConfigService::default().init(logger_config), | ||
Err(err) => { | ||
ic_exports::ic_cdk::println!("error configuring the logger. Err: {:?}", err) | ||
} | ||
} | ||
info!("LogCanister initialized"); | ||
} | ||
|
||
#[query] | ||
pub fn get_log_records(&self, count: usize) -> Logs { | ||
debug!("collecting {count} log records"); | ||
ic_log::take_memory_records(count, 0) | ||
} | ||
|
||
#[update] | ||
pub async fn log_info(&self, text: String) { | ||
info!("{text}"); | ||
} | ||
|
||
#[update] | ||
pub async fn log_debug(&self, text: String) { | ||
debug!("{text}"); | ||
} | ||
|
||
#[update] | ||
pub async fn log_error(&self, text: String) { | ||
error!("{text}"); | ||
} | ||
|
||
#[update] | ||
pub async fn set_logger_filter(&self, filter: String) { | ||
LoggerConfigService::default().set_logger_filter(&filter); | ||
debug!("log filter set to {filter}"); | ||
} | ||
|
||
pub fn idl() -> Idl { | ||
generate_idl!() | ||
impl LogCanister for LoggerCanister { | ||
fn log_state(&self) -> Rc<RefCell<LogState>> { | ||
LogState::get() | ||
} | ||
} | ||
|
||
type ForceNotSendAndNotSync = PhantomData<Rc<()>>; | ||
|
||
thread_local! { | ||
static LOGGER_CONFIG: RefCell<Option<LoggerConfig>> = const { RefCell::new(None) }; | ||
#[ic_cdk::inspect_message] | ||
fn inspect() { | ||
logger_canister_inspect() | ||
} | ||
|
||
#[derive(Debug, Default)] | ||
/// Handles the runtime logger configuration | ||
pub struct LoggerConfigService(ForceNotSendAndNotSync); | ||
impl LoggerCanister { | ||
#[init] | ||
pub fn init(&self) { | ||
let settings = LogCanisterSettings { | ||
log_filter: Some("trace".into()), | ||
in_memory_records: Some(128), | ||
..Default::default() | ||
}; | ||
|
||
impl LoggerConfigService { | ||
/// Sets a new LoggerConfig | ||
pub fn init(&self, logger_config: LoggerConfig) { | ||
LOGGER_CONFIG.with(|config| config.borrow_mut().replace(logger_config)); | ||
self.log_state() | ||
.borrow_mut() | ||
.init(ic::caller(), MemoryId::new(1), settings) | ||
.expect("error configuring the logger"); | ||
} | ||
|
||
/// Changes the logger filter at runtime | ||
pub fn set_logger_filter(&self, filter: &str) { | ||
LOGGER_CONFIG.with(|config| match *config.borrow_mut() { | ||
Some(ref logger_config) => { | ||
logger_config.update_filters(filter); | ||
} | ||
None => panic!("LoggerConfig not initialized"), | ||
}); | ||
pub fn get_idl() -> Idl { | ||
generate_idl!() | ||
} | ||
} | ||
|
||
fn main() { | ||
let canister_e_idl = LogCanister::idl(); | ||
let idl = candid::pretty::candid::compile(&canister_e_idl.env.env, &Some(canister_e_idl.actor)); | ||
let canister_idl = LoggerCanister::get_idl(); | ||
let mut idl = <LoggerCanister as LogCanister>::get_idl(); | ||
idl.merge(&canister_idl); | ||
|
||
let idl = candid::pretty::candid::compile(&idl.env.env, &Some(idl.actor)); | ||
|
||
println!("{}", idl); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
use std::cell::RefCell; | ||
use std::rc::Rc; | ||
|
||
use candid::Principal; | ||
use ic_canister::{ | ||
generate_exports, generate_idl, query, state_getter, update, Canister, Idl, PreUpdate, | ||
}; | ||
use ic_exports::ic_kit::ic; | ||
|
||
pub use crate::canister::state::LogState; | ||
use crate::did::{LogCanisterError, LogCanisterSettings, LoggerPermission, Pagination}; | ||
use crate::writer::Logs; | ||
|
||
pub mod inspect; | ||
mod state; | ||
|
||
/// Canister trait that provides common method for configuring and using canister logger. | ||
/// | ||
/// Check out the `log_canister` example in the `examples` directory for a guide on how to add these | ||
/// methods to you canister. In short, to use this implementation of the logger, you need to: | ||
/// | ||
/// * implement `LogCanister` trait for your type | ||
/// * call [`LogState::init`] method from the `#[init]` method of your canister. | ||
/// * call [`inspect::logger_canister_inspect`] function from the `#[inspect_message]` method of | ||
/// your canister. | ||
/// | ||
/// # Permissions | ||
/// | ||
/// Most operations in the `LogCanister` require the caller to have [`LoggerPermission`]s assigned | ||
/// to them. | ||
/// | ||
/// * `Read` permission allows a principal to get the logs with `ic_logs` method. | ||
/// * `Configure` permission allows changing the logger configuration and manager logger permissions. | ||
/// If a principal has `Configure` permission, `Read` permission is also assumed for that | ||
/// principal. | ||
/// | ||
/// # Configuration and ways to get logs | ||
/// | ||
/// There are two ways to get the logs from the logger canister: | ||
/// | ||
/// 1. Using IC management canister `get_canister_logs` method. To make the canister write logs | ||
/// with the IC API, [`LogCanisterSettings::enable_console`] must be set to `true` (it | ||
/// is enabled by default, so if `None` is given at the canister initialization, it will also | ||
/// be considered as `true`). The logs written by this method are not affected by other | ||
/// settings, such as number of in-memory logs and max log entry length (but they do apply the | ||
/// logging filter). Also, they use native IC approach for checking permissions to get the logs | ||
/// (it can be configured to allow access to the logs only to the canister controllers or to | ||
/// anyone using the canister settings). This method can be used to get logs from trapped | ||
/// operations. | ||
/// | ||
/// 2. Using canister `ic_logs` method. Logs returned by this method are stored in the canister | ||
/// memory. To limit the size of the memory that can be dedicated to the logs, configure | ||
/// max number of entries to store and max size of a single entry. This method cannot store | ||
/// logs from operations that trapped, and the logs storage is reset when the canister is | ||
/// upgraded. | ||
pub trait LogCanister: Canister + PreUpdate { | ||
/// State of the logger. Usually the implementation of this method would look like: | ||
/// | ||
/// ```ignore | ||
/// use ic_storage::IcStorage; | ||
/// fn log_state(&self) -> Rc<RefCell<LogState>> { | ||
/// LogState::get() | ||
/// } | ||
/// ``` | ||
#[state_getter] | ||
fn log_state(&self) -> Rc<RefCell<LogState>>; | ||
|
||
/// Returns canister logs. | ||
/// | ||
/// To use this method the caller must have [`LoggerPermission::Read`] permission. | ||
/// | ||
/// `pagination.offset` value specifies an absolute identifier of the first log entry to be | ||
/// returned. If the given offset is larger than the max id of the logs in the canister, | ||
/// an empty response will be returned. | ||
/// | ||
/// To get the maximum identifier of the logs currently stored in the canister, this method | ||
/// can be used with `pagination.count == 0`. | ||
/// | ||
/// # Traps | ||
/// | ||
/// Traps if the caller does not have [`LoggerPermission::Read`] permission. | ||
#[query(trait = true)] | ||
fn ic_logs(&self, pagination: Pagination) -> Logs { | ||
self.log_state() | ||
.borrow() | ||
.get_logs(ic::caller(), pagination) | ||
.expect("Failed to get logs.") | ||
} | ||
|
||
/// Sets the logger filter string. | ||
/// | ||
/// To call this method, the caller must have [`LoggerPermission::Configure`] permission. | ||
/// | ||
/// To turn off logging for the canister, use `filter == "off"`. | ||
/// | ||
/// # Traps | ||
/// | ||
/// Traps if the caller doesn't have [`LoggerPermission::Configure`] permission. | ||
#[update(trait = true)] | ||
fn set_logger_filter(&mut self, filter: String) -> Result<(), LogCanisterError> { | ||
self.log_state() | ||
.borrow_mut() | ||
.set_logger_filter(ic::caller(), filter) | ||
} | ||
|
||
/// Updates the maximum number of log entries stored in the canister memory. | ||
/// | ||
/// To call this method, the caller must have [`LoggerPermission::Configure`] permission. | ||
/// | ||
/// # Traps | ||
/// | ||
/// Traps if the caller doesn't have [`LoggerPermission::Configure`] permission. | ||
#[update(trait = true)] | ||
fn set_logger_in_memory_records( | ||
&mut self, | ||
max_log_count: usize, | ||
) -> Result<(), LogCanisterError> { | ||
self.log_state() | ||
.borrow_mut() | ||
.set_in_memory_records(ic::caller(), max_log_count) | ||
} | ||
|
||
/// Returns the current logger settings. | ||
#[query(trait = true)] | ||
fn get_logger_settings(&self) -> LogCanisterSettings { | ||
self.log_state().borrow().get_settings().clone().into() | ||
} | ||
|
||
/// Add the given `permission` to the `to` principal. | ||
/// | ||
/// To call this method, the caller must have [`LoggerPermission::Configure`] permission. | ||
/// | ||
/// # Traps | ||
/// | ||
/// Traps if the caller doesn't have [`LoggerPermission::Configure`] permission. | ||
#[update(trait = true)] | ||
fn add_logger_permission(&mut self, to: Principal, permission: LoggerPermission) { | ||
self.log_state() | ||
.borrow_mut() | ||
.add_permission(ic::caller(), to, permission) | ||
.expect("Failed to add logger permission"); | ||
} | ||
|
||
/// Remove the given `permission` from the `from` principal. | ||
/// | ||
/// To call this method, the caller must have [`LoggerPermission::Configure`] permission. | ||
/// | ||
/// # Traps | ||
/// | ||
/// Traps if the caller doesn't have [`LoggerPermission::Configure`] permission. | ||
#[update(trait = true)] | ||
fn remove_logger_permission(&mut self, from: Principal, permission: LoggerPermission) { | ||
self.log_state() | ||
.borrow_mut() | ||
.remove_permission(ic::caller(), from, permission) | ||
.expect("Failed to remove logger permission"); | ||
} | ||
|
||
/// Return idl of the logger canister. | ||
fn get_idl() -> Idl { | ||
generate_idl!() | ||
} | ||
} | ||
|
||
generate_exports!(LogCanister); | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
struct LogTestImpl {} | ||
impl Canister for LogTestImpl { | ||
fn init_instance() -> Self { | ||
todo!() | ||
} | ||
|
||
fn from_principal(_principal: Principal) -> Self { | ||
todo!() | ||
} | ||
|
||
fn principal(&self) -> Principal { | ||
todo!() | ||
} | ||
} | ||
|
||
impl PreUpdate for LogTestImpl {} | ||
impl LogCanister for LogTestImpl { | ||
fn log_state(&self) -> Rc<RefCell<LogState>> { | ||
todo!() | ||
} | ||
} | ||
|
||
#[test] | ||
fn generates_idl() { | ||
let idl = LogTestImpl::get_idl(); | ||
assert!(!format!("{idl}").is_empty()) | ||
} | ||
} |
Oops, something went wrong.