Skip to content

Commit a026b45

Browse files
feat: Support follow mode for 'dfx canister logs'. (#4215)
* Decouple log filtering and formatting. * Support follow mode in 'dfx canister logs'. * Update document and changelog. * Revert to the previous behaviour.
1 parent 4a396dd commit a026b45

File tree

3 files changed

+161
-67
lines changed

3 files changed

+161
-67
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
# UNRELEASED
44

5+
### feat: Support 'follow' mode for 'dfx canister logs'
6+
Support `follow` mode for `dfx canister logs`
7+
- `--follow` to fetch logs continuously until interrupted with `Ctrl+C`
8+
- `--interval` to specify the interval in seconds between log fetches
9+
510
### feat: Improve 'dfx canister logs' with several options
611

712
Improve `dfx canister logs` with several options

docs/cli-reference/dfx-canister.mdx

+9-1
Original file line numberDiff line numberDiff line change
@@ -639,9 +639,11 @@ You can specify the following options for the `dfx canister logs` command.
639639

640640
| Option | Description |
641641
|-----------------------------|-----------------------------------------------------------------------------------------------------|
642-
| `--tail <tail>` | Specifies to show the last number of the logs. |
642+
| `--follow` | Specifies to fetch logs continuously until interrupted with `Ctrl+C`. |
643+
| `--interval <interval>` | Specifies the interval in seconds between log fetches when following logs. Defaults to 2 seconds. |
643644
| `--since <since>` | Specifies to show the logs newer than a relative duration, with the valid units `s`, `m`, `h`, `d`. |
644645
| `--since-time <since-time>` | Specifies to show the logs newer than a specific timestamp. Required either `nanoseconds` since Unix epoch or `RFC3339` format (e.g. `2021-05-06T19:17:10.000000002Z`). |
646+
| `--tail <tail>` | Specifies to show the last number of the logs. |
645647

646648
### Examples
647649

@@ -698,6 +700,12 @@ The command displays output similar to the following:
698700
[45. 2021-05-06T19:17:15.000000002Z]: (bytes) 0xc0ffee
699701
```
700702

703+
To fetch logs continuously, you can run the following command:
704+
705+
``` bash
706+
dfx canister logs hello_world --follow
707+
```
708+
701709
## dfx canister metadata
702710

703711
Use the `dfx canister metadata` command to display metadata stored in a canister's Wasm module.

src/dfx/src/commands/canister/logs.rs

+147-66
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use crate::util::clap::parsers::{duration_parser, timestamp_parser};
66
use candid::Principal;
77
use clap::Parser;
88
use dfx_core::identity::CallSender;
9-
use ic_utils::interfaces::management_canister::FetchCanisterLogsResponse;
10-
use std::time::{SystemTime, UNIX_EPOCH};
9+
use ic_utils::interfaces::management_canister::{CanisterLogRecord, FetchCanisterLogsResponse};
10+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1111
use time::format_description::well_known::Rfc3339;
1212
use time::OffsetDateTime;
1313

@@ -22,21 +22,40 @@ pub struct LogsOpts {
2222
tail: Option<u64>,
2323

2424
/// Specifies to show the logs newer than a relative duration, with the valid units 's', 'm', 'h', 'd'.
25-
#[arg(long, conflicts_with("tail"), conflicts_with("since_time"), value_parser = duration_parser)]
25+
#[arg(long, conflicts_with("tail"), conflicts_with("since_time"), conflicts_with("follow"), value_parser = duration_parser)]
2626
since: Option<u64>,
2727

2828
/// Specifies to show the logs newer than a specific timestamp.
2929
/// Required either nanoseconds since Unix epoch or RFC3339 format (e.g. '2021-05-06T19:17:10.000000002Z').
30-
#[arg(long, conflicts_with("tail"), conflicts_with("since"), value_parser = timestamp_parser)]
30+
#[arg(long, conflicts_with("tail"), conflicts_with("since"), conflicts_with("follow"), value_parser = timestamp_parser)]
3131
since_time: Option<u64>,
32+
33+
/// Specifies to fetch logs continuously until interrupted with Ctrl+C.
34+
#[arg(
35+
long,
36+
conflicts_with("tail"),
37+
conflicts_with("since"),
38+
conflicts_with("since_time")
39+
)]
40+
follow: bool,
41+
42+
/// Specifies the interval in seconds between log fetches when following logs. Defaults to 2 seconds.
43+
#[arg(long, requires("follow"))]
44+
interval: Option<u64>,
3245
}
3346

34-
fn format_bytes(bytes: &[u8]) -> String {
35-
format!("(bytes) 0x{}", hex::encode(bytes))
47+
struct FilterOpts {
48+
tail: Option<u64>,
49+
since: Option<u64>,
50+
since_time: Option<u64>,
51+
last_idx: Option<u64>,
3652
}
3753

38-
fn format_canister_logs(logs: FetchCanisterLogsResponse, opts: &LogsOpts) -> Vec<String> {
39-
let filtered_logs = if let Some(number) = opts.tail {
54+
fn filter_canister_logs<'a>(
55+
logs: &'a FetchCanisterLogsResponse,
56+
opts: FilterOpts,
57+
) -> &'a [CanisterLogRecord] {
58+
if let Some(number) = opts.tail {
4059
&logs.canister_log_records[logs
4160
.canister_log_records
4261
.len()
@@ -57,16 +76,22 @@ fn format_canister_logs(logs: FetchCanisterLogsResponse, opts: &LogsOpts) -> Vec
5776
.canister_log_records
5877
.partition_point(|r| r.timestamp_nanos <= since_time);
5978
&logs.canister_log_records[index..]
79+
} else if let Some(last_idx) = opts.last_idx {
80+
let index = logs
81+
.canister_log_records
82+
.partition_point(|r| r.idx <= last_idx);
83+
&logs.canister_log_records[index..]
6084
} else {
6185
&logs.canister_log_records
62-
};
63-
64-
if filtered_logs.is_empty() {
65-
return vec!["No logs".to_string()];
6686
}
87+
}
88+
89+
fn format_bytes(bytes: &[u8]) -> String {
90+
format!("(bytes) 0x{}", hex::encode(bytes))
91+
}
6792

68-
filtered_logs
69-
.iter()
93+
fn format_canister_logs(logs: &[CanisterLogRecord]) -> Vec<String> {
94+
logs.iter()
7095
.map(|r| {
7196
let time = OffsetDateTime::from_unix_timestamp_nanos(r.timestamp_nanos as i128)
7297
.expect("Invalid canister log record timestamp");
@@ -91,10 +116,58 @@ fn format_canister_logs(logs: FetchCanisterLogsResponse, opts: &LogsOpts) -> Vec
91116
.collect()
92117
}
93118

119+
pub async fn exec(env: &dyn Environment, opts: LogsOpts, call_sender: &CallSender) -> DfxResult {
120+
let callee_canister = opts.canister.as_str();
121+
let canister_id_store = env.get_canister_id_store()?;
122+
123+
let canister_id = Principal::from_text(callee_canister)
124+
.or_else(|_| canister_id_store.get(callee_canister))?;
125+
126+
fetch_root_key_if_needed(env).await?;
127+
128+
if opts.follow {
129+
let interval = opts.interval.unwrap_or(2);
130+
let mut last_idx = 0u64;
131+
132+
loop {
133+
let logs = canister::get_canister_logs(env, canister_id, call_sender).await?;
134+
let filter_opts = FilterOpts {
135+
tail: None,
136+
since: None,
137+
since_time: None,
138+
last_idx: Some(last_idx),
139+
};
140+
let filtered_logs = filter_canister_logs(&logs, filter_opts);
141+
142+
if !filtered_logs.is_empty() {
143+
println!("{}", format_canister_logs(filtered_logs).join("\n"));
144+
last_idx = filtered_logs.last().unwrap().idx;
145+
}
146+
147+
tokio::select! {
148+
_ = tokio::time::sleep(Duration::from_secs(interval)) => continue,
149+
_ = tokio::signal::ctrl_c() => break,
150+
}
151+
}
152+
} else {
153+
let logs = canister::get_canister_logs(env, canister_id, call_sender).await?;
154+
155+
let filter_opts = FilterOpts {
156+
tail: opts.tail,
157+
since: opts.since,
158+
since_time: opts.since_time,
159+
last_idx: None,
160+
};
161+
let filtered_logs = filter_canister_logs(&logs, filter_opts);
162+
163+
println!("{}", format_canister_logs(filtered_logs).join("\n"));
164+
}
165+
166+
Ok(())
167+
}
168+
94169
#[test]
95170
fn test_format_canister_logs() {
96-
use ic_utils::interfaces::management_canister::CanisterLogRecord;
97-
98171
let logs = FetchCanisterLogsResponse {
99172
canister_log_records: vec![
100173
CanisterLogRecord {
@@ -130,16 +203,17 @@ fn test_format_canister_logs() {
130203
],
131204
};
132205

206+
let filtered_logs = filter_canister_logs(
207+
&logs,
208+
FilterOpts {
209+
tail: None,
210+
since: None,
211+
since_time: None,
212+
last_idx: None,
213+
},
214+
);
133215
assert_eq!(
134-
format_canister_logs(
135-
logs.clone(),
136-
&LogsOpts {
137-
canister: "2vxsx-fae".to_string(),
138-
tail: None,
139-
since: None,
140-
since_time: None,
141-
}
142-
),
216+
format_canister_logs(filtered_logs),
143217
vec![
144218
"[42. 2021-05-06T19:17:10.000000001Z]: Some text message".to_string(),
145219
"[43. 2021-05-06T19:17:10.000000002Z]: (bytes) 0xc0ffee".to_string(),
@@ -151,16 +225,17 @@ fn test_format_canister_logs() {
151225
);
152226

153227
// Test tail
228+
let filtered_logs = filter_canister_logs(
229+
&logs,
230+
FilterOpts {
231+
tail: Some(4),
232+
since: None,
233+
since_time: None,
234+
last_idx: None,
235+
},
236+
);
154237
assert_eq!(
155-
format_canister_logs(
156-
logs.clone(),
157-
&LogsOpts {
158-
canister: "2vxsx-fae".to_string(),
159-
tail: Some(4),
160-
since: None,
161-
since_time: None,
162-
}
163-
),
238+
format_canister_logs(filtered_logs),
164239
vec![
165240
"[44. 2021-05-06T19:17:10.000000003Z]: (bytes) 0xc0ffee".to_string(),
166241
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later".to_string(),
@@ -175,16 +250,17 @@ fn test_format_canister_logs() {
175250
.unwrap()
176251
.as_secs()
177252
- 1620328631; // 1 second after the first log with idx 42.
253+
let filtered_logs = filter_canister_logs(
254+
&logs,
255+
FilterOpts {
256+
tail: None,
257+
since: Some(duration_seconds),
258+
since_time: None,
259+
last_idx: None,
260+
},
261+
);
178262
assert_eq!(
179-
format_canister_logs(
180-
logs.clone(),
181-
&LogsOpts {
182-
canister: "2vxsx-fae".to_string(),
183-
tail: None,
184-
since: Some(duration_seconds),
185-
since_time: None,
186-
}
187-
),
263+
format_canister_logs(filtered_logs),
188264
vec![
189265
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later".to_string(),
190266
"[46. 2021-05-06T19:17:15.000000002Z]: (bytes) 0xc0ffee".to_string(),
@@ -193,35 +269,40 @@ fn test_format_canister_logs() {
193269
);
194270

195271
// Test since_time
272+
let filtered_logs = filter_canister_logs(
273+
&logs,
274+
FilterOpts {
275+
tail: None,
276+
since: None,
277+
since_time: Some(1_620_328_635_000_000_000),
278+
last_idx: None,
279+
},
280+
);
196281
assert_eq!(
197-
format_canister_logs(
198-
logs,
199-
&LogsOpts {
200-
canister: "2vxsx-fae".to_string(),
201-
tail: None,
202-
since: None,
203-
since_time: Some(1_620_328_635_000_000_000),
204-
}
205-
),
282+
format_canister_logs(filtered_logs),
206283
vec![
207284
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later".to_string(),
208285
"[46. 2021-05-06T19:17:15.000000002Z]: (bytes) 0xc0ffee".to_string(),
209286
"[47. 2021-05-06T19:17:15.000000003Z]: (bytes) 0xc0ffee".to_string(),
210287
]
211288
);
212-
}
213-
214-
pub async fn exec(env: &dyn Environment, opts: LogsOpts, call_sender: &CallSender) -> DfxResult {
215-
let callee_canister = opts.canister.as_str();
216-
let canister_id_store = env.get_canister_id_store()?;
217-
218-
let canister_id = Principal::from_text(callee_canister)
219-
.or_else(|_| canister_id_store.get(callee_canister))?;
220-
221-
fetch_root_key_if_needed(env).await?;
222289

223-
let logs = canister::get_canister_logs(env, canister_id, call_sender).await?;
224-
println!("{}", format_canister_logs(logs, &opts).join("\n"));
225-
226-
Ok(())
290+
// Test last index
291+
let filtered_logs = filter_canister_logs(
292+
&logs,
293+
FilterOpts {
294+
tail: None,
295+
since: None,
296+
since_time: None,
297+
last_idx: Some(44),
298+
},
299+
);
300+
assert_eq!(
301+
format_canister_logs(filtered_logs),
302+
vec![
303+
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later".to_string(),
304+
"[46. 2021-05-06T19:17:15.000000002Z]: (bytes) 0xc0ffee".to_string(),
305+
"[47. 2021-05-06T19:17:15.000000003Z]: (bytes) 0xc0ffee".to_string(),
306+
]
307+
);
227308
}

0 commit comments

Comments
 (0)