3.2 Canister history & logging
The Internet Computer tracks the history of a deployed canister through changes in its Wasm module hashes and controller settings. This allows users to view a canister’s history, including which code was used and which controller deployed it.
Reviewing a canister’s history can be useful in several scenarios, such as:
Ensuring that a controller has not maliciously altered the canister's code.
Confirming that the list of controllers has not been tampered with. For instance, if the controller list only shows the governance canister, users can have confidence that the canister’s code remains trustworthy.
This feature is accessed via the canister_info
method on the IC management canister, using the target canister's ID. The system records the 20 most recent changes, including:
- Canister creation.
- Wasm module installations, reinstalls, and upgrades.
- Changes to the controller list.
Only the 20 latest changes are stored.
To retrieve a canister’s history, call the canister_info
method on the IC management canister with the canister ID as the argument.
use candid::{CandidType, Principal};
use ic_cdk::api::management_canister::main::{
canister_info, CanisterChange,
CanisterChangeDetails::{CodeDeployment, CodeUninstall, ControllersChange, Creation},
CanisterChangeOrigin::{FromCanister, FromUser},
CanisterInfoRequest, CanisterInfoResponse,
};
use serde::Deserialize;
/// Returns canister info with all available canister changes for a canister characterized by a given principal.
/// Traps if the canister_info management call is rejected (in particular, if the principal does not characterize a canister).
#[ic_cdk::update]
async fn info(canister_id: Principal) -> CanisterInfoResponse {
let request = CanisterInfoRequest {
canister_id,
num_requested_changes: Some(20),
};
canister_info(request).await.unwrap().0
}
/// Returns all (reflexive and transitive) controllers of a canister characterized by a given principal
/// by implementing BFS over the controllers.
#[ic_cdk::update]
async fn reflexive_transitive_controllers(canister_id: Principal) -> Vec<Principal> {
let mut ctrls = vec![canister_id];
let mut queue = vec![canister_id];
while !queue.is_empty() {
let cur = queue.pop().unwrap();
// check if the principal characterizes a canister by determining if it is an opaque principal
if cur.as_slice().last() == Some(&0x01) {
let info = info(cur).await;
for c in info.controllers {
if !ctrls.contains(&c) {
ctrls.push(c);
queue.push(c);
}
}
}
}
ctrls
}
/// Specifies a canister snapshot by providing a Unix timestamp in nanoseconds
/// or a canister version.
#[derive(CandidType, Deserialize, Clone)]
pub enum CanisterSnapshot {
#[serde(rename = "at_timestamp")]
AtTimestamp(u64),
#[serde(rename = "at_version")]
AtVersion(u64),
}
/// Maps the latest canister change of the canister characterized by a given principal before or at the given `CanisterSnapshot`
/// and returns an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnet hosting the canister and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// Returns `None` if no change to map can be determined due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
async fn map_canister_change<T>(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
f: impl Fn(&CanisterChange) -> Option<T>,
) -> Option<(T, Option<u64>)> {
let info = info(canister_id).await;
let mut map_change = None;
let mut skew = None;
for c in info.recent_changes {
if let Some(x) = f(&c) {
match &canister_deployment {
CanisterSnapshot::AtTimestamp(t) => {
if *t == c.timestamp_nanos {
return None;
}
if *t >= c.timestamp_nanos {
map_change = Some(x);
}
let d = if *t >= c.timestamp_nanos {
*t - c.timestamp_nanos
} else {
c.timestamp_nanos - *t
};
skew = Some(std::cmp::min(d, skew.unwrap_or(d)));
}
CanisterSnapshot::AtVersion(v) => {
if *v >= c.canister_version {
map_change = Some(x);
} else {
break;
}
}
};
}
}
map_change.map(|x| (x, skew))
}
/// Type for the result of `canister_controllers` calls.
#[derive(CandidType, Deserialize, Clone)]
struct CanisterControllersResult {
controllers: Vec<Principal>,
max_clock_skew: Option<u64>,
}
/// Returns the controllers of the canister characterized by a given principal and at the given `CanisterSnapshot`
/// and an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnet hosting the canister and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// Returns `None` if the controllers cannot be determined due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
#[ic_cdk::update]
async fn canister_controllers(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
) -> Option<CanisterControllersResult> {
map_canister_change(canister_id, canister_deployment, |c| match &c.details {
Creation(creation) => Some(creation.controllers.clone()),
ControllersChange(ctrls) => Some(ctrls.controllers.clone()),
_ => None,
})
.await
.map(|(controllers, max_clock_skew)| CanisterControllersResult {
controllers,
max_clock_skew,
})
}
/// Type for the result of `canister_module_hash` calls.
#[derive(CandidType, Deserialize, Clone)]
struct CanisterModuleHashResult {
module_hash: Option<Vec<u8>>,
max_clock_skew: Option<u64>,
}
/// Returns the module hash of the canister characterized by a given principal and at the given `CanisterSnapshot`
/// and an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnet hosting the canister and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// Returns `None` if the module hash cannot be determined due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
#[ic_cdk::update]
async fn canister_module_hash(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
) -> Option<CanisterModuleHashResult> {
map_canister_change(canister_id, canister_deployment, |c| match &c.details {
Creation(_) => Some(None),
CodeUninstall => Some(None),
CodeDeployment(code_deployment) => Some(Some(code_deployment.module_hash.clone())),
_ => None,
})
.await
.map(|(module_hash, max_clock_skew)| CanisterModuleHashResult {
module_hash,
max_clock_skew,
})
}
/// Type for the result of `canister_deployment_chain` calls.
#[derive(CandidType, Deserialize, Clone)]
struct CanisterDeploymentChainResult {
deployment_chain: Vec<CanisterChange>,
max_clock_skew: Option<u64>,
}
/// Returns the deployment chain of the canister characterized by a given principal and at the given `CanisterSnapshot`:
/// a list of canister changes starting with the change resulting in the canister deployment characterized by the given principal and at the given `CanisterSnapshot`
/// and with each subsequent change resulting in the canister deployment triggering the previous change,
/// and an optional integer characterizing the maximum clock skew (in nanoseconds)
/// between the subnets hosting the canisters from the deployment chain and the given `CanisterSnapshot`
/// (i.e., if this integer is `None`, then no assumptions on clock skew are needed).
/// The deployment chain stops if a canister change in the deployment chain cannot be determined (due to unavailability of canister changes in canister history
/// or due to ambiguity between canister changes with the same timestamp)
/// or upon encountering a change from a user principal or upon encountering a loop among canisters from the deployment chain.
/// Traps if a canister_info call is rejected (in particular, if the given principal does not characterize a canister).
#[ic_cdk::update]
async fn canister_deployment_chain(
canister_id: Principal,
canister_deployment: CanisterSnapshot,
) -> CanisterDeploymentChainResult {
let mut current_canister_id = canister_id;
let mut current_canister_deployment = canister_deployment;
let mut visited_canister_ids = vec![]; // canister IDs of canisters from the deployment chain
let mut deployment_chain = vec![];
let mut max_clock_skew = None;
loop {
visited_canister_ids.push(current_canister_id);
match map_canister_change(
current_canister_id,
current_canister_deployment.clone(),
|c| match &c.details {
CodeDeployment(_) => Some(c.clone()),
_ => None,
},
)
.await
{
Some((c, s)) => {
let mut done = false;
match &c.origin {
FromUser(_) => {
done = true;
}
FromCanister(o) => {
if visited_canister_ids.contains(&o.canister_id) {
done = true;
} else {
current_canister_id = o.canister_id;
current_canister_deployment = match o.canister_version {
None => CanisterSnapshot::AtTimestamp(c.timestamp_nanos),
Some(v) => CanisterSnapshot::AtVersion(v),
};
}
}
};
deployment_chain.push(c);
max_clock_skew = s
.map(|dt| Some(std::cmp::min(dt, max_clock_skew.unwrap_or(dt))))
.unwrap_or(max_clock_skew);
if done {
break;
}
}
None => {
break;
}
};
}
CanisterDeploymentChainResult {
deployment_chain,
max_clock_skew,
}
}
#[test]
fn check_candid_file() {
use candid_parser::utils::{service_equal, CandidSource};
let did_path = std::path::PathBuf::from(
std::env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env var undefined"),
)
.join("canister-info.did");
candid::export_service!();
let expected = __export_service();
service_equal(
CandidSource::Text(&expected),
CandidSource::File(did_path.as_path()),
)
.unwrap();
}
Accessing canister logs
The canister logging feature gives developers visibility into their canister’s behavior and helps diagnose issues. It supports logging for:
- Heartbeat functions
- Timers
canister_init
,pre_upgrade
, andpost_upgrade
hooks- Update calls
- Queries (only when called in replicated mode)
Canister logs can be retrieved by the controller, even if the canister traps, using the following command:
dfx canister logs <canister-name>
By default, only the canister's controllers can access its logs. To make logs public, update the canister’s settings with:
dfx canister update-settings <canister-name> --log-visibility public
However, canister log allow lists can let specific principals who are not controllers view the logs without making them public. This feature is supported in dfx
version 0.24.2
and above.
Use the following commands to manage log viewers:
- Set a log viewer (replaces any existing viewers):
dfx canister update-settings <canister-name> --set-log-viewer <principal-id>
- Add a log viewer:
dfx canister update-settings <canister-name> --add-log-viewer <principal-id>
- Remove a log viewer:
dfx canister update-settings <canister-name> --remove-log-viewer <principal-id>
You can also set log viewers during canister creation:
dfx canister create <canister-name> --log-viewer <principal-id>
Or configure allowed log viewers in your dfx.json
:
{
"canisters": {
"<canister-name>": {
"initialization_values": {
"log_visibility": {
"allowed_viewers": ["<principal-id>"]
}
}
}
}
}

Did you get stuck somewhere in this tutorial, or do you feel like you need additional help understanding some of the concepts? The ICP community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:
- Developer Discord
- Developer Liftoff forum discussion
- Developer tooling working group
- Motoko Bootcamp - The DAO Adventure
- Motoko Bootcamp - Discord community
- Motoko developer working group
- Upcoming events and conferences
- Upcoming hackathons
- Weekly developer office hours to ask questions, get clarification, and chat with other developers live via voice chat.
- Submit your feedback to the ICP Developer feedback board