From 01087eed208b6edb4d8b167001a0c3def8276cf2 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 12:05:57 +0200 Subject: [PATCH 1/2] docs: canister logs guide --- docs/guides/canister-management/logs.md | 407 +++++++++++++++++++++++- 1 file changed, 396 insertions(+), 11 deletions(-) diff --git a/docs/guides/canister-management/logs.md b/docs/guides/canister-management/logs.md index 11c2d9b3..c32d3858 100644 --- a/docs/guides/canister-management/logs.md +++ b/docs/guides/canister-management/logs.md @@ -1,20 +1,405 @@ --- title: "Canister Logs" -description: "Debug and monitor canisters using the logging API and query statistics" +description: "Debug and monitor canisters using the logging API, query statistics, and access log streaming" sidebar: order: 3 --- -TODO: Write content for this page. +Canister logs help you understand what your canister is doing at runtime, including during traps. The Internet Computer captures log output from update calls, timers, heartbeats, and lifecycle hooks — even when the canister traps mid-execution. Logs are retrievable by canister controllers and optionally by other principals. - -Debug canisters using the logging API. Cover log levels, structured logging patterns, viewing logs via icp-cli, query statistics for monitoring call patterns, and access logs. Include backtrace debugging for traps. Show examples for both Rust (ic_cdk::println!) and Motoko (Debug.print). +## Writing log messages - -- Portal: building-apps/canister-management/logs.mdx, backtraces.mdx, advanced/canister-access-logs.mdx -- Examples: canister_logs (both), query_stats (both) +Both Rust and Motoko support printing messages to the canister log. - -- guides/testing/strategies -- logs as part of debugging -- guides/canister-management/lifecycle -- log visibility settings -- icp-cli docs: https://cli.internetcomputer.org/ +**Rust** — use `ic_cdk::println!`: + +```rust +use ic_cdk::{init, update}; + +#[init] +fn init() { + ic_cdk::println!("Canister initialized"); +} + +#[update] +fn process(value: u64) -> u64 { + ic_cdk::println!("Processing value: {}", value); + value * 2 +} +``` + +The `ic_cdk::println!` macro formats a string and writes it to the canister log on the IC. Outside of Wasm (for example in unit tests), it falls back to `std::println!`. + +**Motoko** — use `Debug.print` from `mo:core/Debug`: + +```motoko +import Debug "mo:core/Debug"; + +persistent actor { + public func process(value : Nat) : async Nat { + Debug.print("Processing value: " # debug_show(value)); + value * 2 + }; +}; +``` + +`Debug.print` writes to the canister log when running on the IC. In other environments such as the Motoko interpreter, it writes to standard output. + +### What logging captures + +Log messages are recorded for: + +- Update calls +- Timer and heartbeat executions +- `canister_init`, `canister_pre_upgrade`, and `canister_post_upgrade` hooks +- Query calls executed in **replicated mode** (non-replicated queries are not logged) + +Log storage is capped at 4096 bytes by default. When the log buffer is full, the oldest entries are purged. You can increase this limit up to 2 MiB (see [Log memory limit](#log-memory-limit)). + +## Viewing canister logs + +To fetch and display the logs for a canister: + +```bash +icp canister logs -e local +``` + +To follow logs in real time (polls every 2 seconds by default): + +```bash +icp canister logs -e local --follow +``` + +To adjust the polling interval: + +```bash +icp canister logs -e local --follow --interval 5 +``` + +To fetch logs on mainnet, use `-e ic`: + +```bash +icp canister logs -e ic +``` + +### Filtering by timestamp or index + +You can scope log output to a specific time range or index range. + +By timestamp (RFC3339 or nanoseconds since Unix epoch): + +```bash +icp canister logs -e ic \ + --since 2024-01-01T00:00:00Z \ + --until 2024-01-02T00:00:00Z +``` + +By log entry index: + +```bash +icp canister logs -e ic --since-index 100 --until-index 200 +``` + +Timestamp and index filters cannot be combined with `--follow`. + +To output logs as JSON for programmatic processing: + +```bash +icp canister logs -e ic --json +``` + +## Log visibility + +By default, only the canister's controllers can read its logs. You can make logs visible to everyone, or grant read access to specific principals. + +### Making logs public + +```bash +icp canister update-settings -e ic --log-visibility public +``` + +To revert to controller-only visibility: + +```bash +icp canister update-settings -e ic --log-visibility controllers +``` + +### Granting specific principals access + +To allow a principal to view logs without making them public: + +```bash +icp canister update-settings -e ic \ + --add-log-viewer +``` + +To replace the current set of allowed viewers with a single principal: + +```bash +icp canister update-settings -e ic \ + --set-log-viewer +``` + +To revoke access for a principal: + +```bash +icp canister update-settings -e ic \ + --remove-log-viewer +``` + +### Setting log visibility in icp.yaml + +You can configure log visibility per canister in `icp.yaml` so it is applied on every `icp deploy`: + +```yaml +canisters: + - name: backend + recipe: + type: "@dfinity/rust@v3.2.0" + configuration: + package: backend + settings: + log_visibility: controllers # "controllers" | "public" | allowed_viewers object +``` + +To grant access to specific principals in the config: + +```yaml + settings: + log_visibility: + allowed_viewers: + - "aaaaa-aa" + - "2vxsx-fae" +``` + +## Log memory limit + +The default log buffer size is 4096 bytes. When the buffer fills up, older log entries are automatically purged to make room for new ones. You can increase the limit up to 2 MiB: + +```bash +icp canister update-settings -e ic --log-memory-limit 2mib +``` + +Supported suffixes: `kb` (1,000 bytes), `kib` (1,024 bytes), `mb` (1,000,000 bytes), `mib` (1,048,576 bytes). In `icp.yaml`: + +```yaml + settings: + log_memory_limit: 2mib +``` + +## Backtrace debugging + +When a canister traps, ICP records a **backtrace** — the function call stack at the point of the trap — and appends it to the canister logs. If the caller has [log access](#log-visibility), the backtrace also appears in the error response they receive. + +For example, if a Rust canister performs an out-of-bounds stable memory write: + +```rust +#[update] +fn outer() { + inner(); +} + +fn inner() { + inner_2(); +} + +fn inner_2() { + ic_cdk::api::stable::stable_write(0xdeadbeef, b"foo"); +} +``` + +The log will contain output similar to: + +```text +Canister Backtrace: +ic0::ic0::stable64_write +_wasm_backtrace_canister::inner_2 +_wasm_backtrace_canister::inner +_wasm_backtrace_canister::outer +``` + +This pinpoints that the trap occurred in `inner_2`, called via `outer` → `inner`. + +### Verifying backtrace support + +Backtraces require function names to be stored in the Wasm `name` custom section. Any canister built with the standard icp-cli recipes includes this section automatically. + +If you post-process the Wasm with `ic-wasm` (for example to shrink or optimize it), pass `--keep-name-section` to preserve function names: + +```bash +ic-wasm canister.wasm -o canister.wasm shrink --keep-name-section +ic-wasm canister.wasm -o canister.wasm optimize O2 --keep-name-section +``` + +This requires `ic-wasm` version 0.8.6 or later. + +To verify the `name` section is present in a Wasm binary, use [`wasm-objdump`](https://github.com/WebAssembly/wabt) and look for a `Custom` section named `"name"`: + +```bash +wasm-objdump -h canister.wasm +``` + +You should see a line like: + +```text +Custom start=0x001e3467 end=0x001e60a6 (size=0x00002c3f) "name" +``` + +If the `"name"` section is absent, backtraces will not be available. + +## Query statistics + +Each canister exposes cumulative statistics about its query call traffic. These are available through the [management canister](../../reference/management-canister.md)'s `canister_status` method. + +The statistics are cumulative since the canister was last installed or upgraded. They are updated approximately once per epoch (roughly every 10 minutes) rather than in real time. + +**Rust** — read query stats from `canister_status`: + +```rust +use ic_cdk::{management_canister, update}; +use ic_cdk::management_canister::{CanisterStatusArgs, CanisterIdRecord}; + +#[update] +async fn print_query_stats() -> String { + let status = management_canister::canister_status( + &CanisterIdRecord { canister_id: ic_cdk::id() } + ) + .await + .expect("canister_status failed"); + + let qs = &status.query_stats; + format!( + "calls: {} | instructions: {} | request bytes: {} | response bytes: {}", + qs.num_calls_total, + qs.num_instructions_total, + qs.request_payload_bytes_total, + qs.response_payload_bytes_total, + ) +} +``` + +**Motoko** — call `canister_status` on the management canister: + +```motoko +import Principal "mo:core/Principal"; + +persistent actor QueryStats { + + transient let IC = actor "aaaaa-aa" : actor { + canister_status : { canister_id : Principal } -> async { + query_stats : { + num_calls_total : Nat; + num_instructions_total : Nat; + request_payload_bytes_total : Nat; + response_payload_bytes_total : Nat; + }; + }; + }; + + public func get_query_stats() : async Text { + let stats = await IC.canister_status({ + canister_id = Principal.fromActor(QueryStats); + }); + let qs = stats.query_stats; + "calls: " # debug_show(qs.num_calls_total) + # " | instructions: " # debug_show(qs.num_instructions_total) + # " | request bytes: " # debug_show(qs.request_payload_bytes_total) + # " | response bytes: " # debug_show(qs.response_payload_bytes_total) + }; +}; +``` + +### Query statistics fields + +| Field | Description | +|-------|-------------| +| `num_calls_total` | Total number of query calls made to the canister | +| `num_instructions_total` | Total instructions executed across all query calls | +| `request_payload_bytes_total` | Total bytes of query call request payloads | +| `response_payload_bytes_total` | Total bytes of query call response payloads | + +These cumulative totals reset to zero when the canister is reinstalled or upgraded. + +## Streaming access logs from API boundary nodes + +API boundary nodes (API BNs) handle all incoming requests and log every request they process. You can stream these access logs in real time for a canister. This is especially useful for observing query call traffic, which is otherwise not visible in canister logs. + +A complete working implementation in Rust is available at [dfinity/ic-bn-logs](https://github.com/dfinity/ic-bn-logs). + +### Access log format + +Each access log entry is a JSON object. Key fields: + +| Field | Description | +|-------|-------------| +| `ic_canister_id` | Principal ID of the target canister | +| `ic_method` | Canister method that was called | +| `request_type` | `query`, `call`, `sync_call`, or `read_state` | +| `http_status` | HTTP response code | +| `duration` | Request processing time in seconds | +| `timestamp` | UTC timestamp (ISO 8601 with nanosecond precision) | +| `cache_status` | `HIT`, `MISS`, `BYPASS`, or `DISABLED` | +| `error_cause` | Error category if the request failed | +| `client_id` | Salted hash of client IP + sender principal | + +Example entry: + +```json +{ + "cache_status": "DISABLED", + "client_id": "ab6e7b821eb97295e3d20cec94160288", + "duration": 0.028693668, + "http_status": 200, + "ic_canister_id": "qoctq-giaaa-aaaaa-aaaea-cai", + "ic_method": "http_request", + "request_type": "query", + "response_size": 2818, + "timestamp": "2025-07-17T08:12:39.964131788Z" +} +``` + +### Connecting to the WebSocket endpoint + +API BNs expose access logs over WebSocket. The URL format is: + +```text +wss://{api_bn_domain}/logs/canister/{canister_id} +``` + +For full coverage, connect to **all** API BNs — each node only streams the requests it handles, and traffic is distributed across nodes. + +To discover the current list of API BN domains, fetch them from the IC's certified state using `agent-rs`: + +```rust +use candid::Principal; +use ic_agent::Agent; +use anyhow::Result; + +#[tokio::main] +async fn main() -> Result<()> { + let agent = Agent::builder() + .with_url("https://icp-api.io") + .build()?; + + let subnet_id = Principal::from_text( + "tdb26-jop6k-aogll-7ltgs-eruif-6kk7m-qpktf-gdiqx-mxtrf-vb5e6-eqe" + )?; + let api_bns = agent + .fetch_api_boundary_nodes_by_subnet_id(subnet_id) + .await?; + + for node in &api_bns { + println!("wss://{}/logs/canister/", node.domain); + } + + Ok(()) +} +``` + +## Next steps + +- [Canister lifecycle](lifecycle.md) — configure log visibility and memory limits when creating or deploying a canister +- [Testing strategies](../testing/strategies.md) — use canister logs as part of your debugging workflow +- [CLI reference](https://cli.internetcomputer.org/) — full documentation for `icp canister logs` and `icp canister update-settings` + + From 0dd3b8578164dde347f3329895a571586fcaffe7 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 16 Apr 2026 13:53:38 +0200 Subject: [PATCH 2/2] docs(canister-logs): address PR #64 feedback - fix CLI command, resolve stats contradiction, clean up imports - Replace all occurrences of `icp canister update-settings` with the correct `icp canister settings update` command (4 occurrences including the CLI reference link text at the bottom of the page) - Resolve contradictory query stats reset claim: per IC interface spec, query_stats is only initialized on canister creation and is never reset on reinstall or upgrade; update description to "since the canister was created" and add TODO comments for human verification - Remove unused `CanisterStatusArgs` import from Rust query stats snippet; `CanisterStatusArgs = CanisterIdRecord` is a type alias and only `CanisterIdRecord` is used in the struct literal - Add deprecation note to backtrace example: `ic_cdk::api::stable` was deprecated since ic-cdk 0.18.0; suggest `ic_cdk::stable` as replacement - Add TODO flag for epoch duration claim (unverified in source material) --- docs/guides/canister-management/logs.md | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/guides/canister-management/logs.md b/docs/guides/canister-management/logs.md index c32d3858..fbf3b1c1 100644 --- a/docs/guides/canister-management/logs.md +++ b/docs/guides/canister-management/logs.md @@ -115,13 +115,13 @@ By default, only the canister's controllers can read its logs. You can make logs ### Making logs public ```bash -icp canister update-settings -e ic --log-visibility public +icp canister settings update -e ic --log-visibility public ``` To revert to controller-only visibility: ```bash -icp canister update-settings -e ic --log-visibility controllers +icp canister settings update -e ic --log-visibility controllers ``` ### Granting specific principals access @@ -129,21 +129,21 @@ icp canister update-settings -e ic --log-visibility controllers To allow a principal to view logs without making them public: ```bash -icp canister update-settings -e ic \ +icp canister settings update -e ic \ --add-log-viewer ``` To replace the current set of allowed viewers with a single principal: ```bash -icp canister update-settings -e ic \ +icp canister settings update -e ic \ --set-log-viewer ``` To revoke access for a principal: ```bash -icp canister update-settings -e ic \ +icp canister settings update -e ic \ --remove-log-viewer ``` @@ -177,7 +177,7 @@ To grant access to specific principals in the config: The default log buffer size is 4096 bytes. When the buffer fills up, older log entries are automatically purged to make room for new ones. You can increase the limit up to 2 MiB: ```bash -icp canister update-settings -e ic --log-memory-limit 2mib +icp canister settings update -e ic --log-memory-limit 2mib ``` Supported suffixes: `kb` (1,000 bytes), `kib` (1,024 bytes), `mb` (1,000,000 bytes), `mib` (1,048,576 bytes). In `icp.yaml`: @@ -204,6 +204,8 @@ fn inner() { } fn inner_2() { + // Note: `ic_cdk::api::stable` is deprecated since ic-cdk 0.18.0. + // Use `ic_cdk::stable::stable_write` instead. ic_cdk::api::stable::stable_write(0xdeadbeef, b"foo"); } ``` @@ -251,13 +253,16 @@ If the `"name"` section is absent, backtraces will not be available. Each canister exposes cumulative statistics about its query call traffic. These are available through the [management canister](../../reference/management-canister.md)'s `canister_status` method. -The statistics are cumulative since the canister was last installed or upgraded. They are updated approximately once per epoch (roughly every 10 minutes) rather than in real time. +The statistics are cumulative since the canister was created. They are updated approximately once per epoch rather than in real time. + + + **Rust** — read query stats from `canister_status`: ```rust use ic_cdk::{management_canister, update}; -use ic_cdk::management_canister::{CanisterStatusArgs, CanisterIdRecord}; +use ic_cdk::management_canister::CanisterIdRecord; #[update] async fn print_query_stats() -> String { @@ -318,7 +323,7 @@ persistent actor QueryStats { | `request_payload_bytes_total` | Total bytes of query call request payloads | | `response_payload_bytes_total` | Total bytes of query call response payloads | -These cumulative totals reset to zero when the canister is reinstalled or upgraded. +These cumulative totals accumulate since the canister was created. ## Streaming access logs from API boundary nodes @@ -400,6 +405,6 @@ async fn main() -> Result<()> { - [Canister lifecycle](lifecycle.md) — configure log visibility and memory limits when creating or deploying a canister - [Testing strategies](../testing/strategies.md) — use canister logs as part of your debugging workflow -- [CLI reference](https://cli.internetcomputer.org/) — full documentation for `icp canister logs` and `icp canister update-settings` +- [CLI reference](https://cli.internetcomputer.org/) — full documentation for `icp canister logs` and `icp canister settings update`