Skip to content

Commit

Permalink
Merge pull request #224 from bitfinity-network/maxim/logger_canister_…
Browse files Browse the repository at this point in the history
…trait

Add common logger canister trait
  • Loading branch information
Maximkaaa authored Jul 23, 2024
2 parents e01274b + 8676e45 commit d4f5b55
Show file tree
Hide file tree
Showing 13 changed files with 1,340 additions and 112 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ async-recursion = "1.0.2"
async-trait = "0.1"
auto_ops = "0.3"
bincode = "1.3"
cfg-if = "1.0"
criterion = "0.5.1"
crypto-bigint = { version = "0.5", features = ["serde"] }
dirs = "5.0"
Expand Down
10 changes: 10 additions & 0 deletions ic-log/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ edition.workspace = true
anyhow = { workspace = true }
arc-swap = { workspace = true }
candid = { workspace = true }
cfg-if = { workspace = true, optional = true }
env_filter = { workspace = true }
humantime = { workspace = true }
ic-canister = { path = "../ic-canister/ic-canister", optional = true }
ic-stable-structures = { path = "../ic-stable-structures", optional = true }
ic-storage = { path = "../ic-storage", optional = true }
ic-exports = { path = "../ic-exports" }
log = { workspace = true }
ringbuffer = { workspace = true }
Expand All @@ -20,8 +24,14 @@ serde = { workspace = true }
ic-canister = { path = "../ic-canister/ic-canister" }

[features]
canister = ["export-api", "ic-canister", "ic-storage", "ic-stable-structures", "cfg-if"]
export-api = []

[[example]]
name = "log_canister"
path = "examples/log_canister.rs"
required-features = ["canister"]

[[test]]
name = "in_memory_logger"
required-features = ["canister"]
109 changes: 35 additions & 74 deletions ic-log/examples/log_canister.rs
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);
}
198 changes: 198 additions & 0 deletions ic-log/src/canister.rs
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())
}
}
Loading

0 comments on commit d4f5b55

Please sign in to comment.