@@ -6,8 +6,8 @@ use crate::util::clap::parsers::{duration_parser, timestamp_parser};
6
6
use candid:: Principal ;
7
7
use clap:: Parser ;
8
8
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 } ;
11
11
use time:: format_description:: well_known:: Rfc3339 ;
12
12
use time:: OffsetDateTime ;
13
13
@@ -22,21 +22,40 @@ pub struct LogsOpts {
22
22
tail : Option < u64 > ,
23
23
24
24
/// 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) ]
26
26
since : Option < u64 > ,
27
27
28
28
/// Specifies to show the logs newer than a specific timestamp.
29
29
/// 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) ]
31
31
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 > ,
32
45
}
33
46
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 > ,
36
52
}
37
53
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 {
40
59
& logs. canister_log_records [ logs
41
60
. canister_log_records
42
61
. len ( )
@@ -57,16 +76,22 @@ fn format_canister_logs(logs: FetchCanisterLogsResponse, opts: &LogsOpts) -> Vec
57
76
. canister_log_records
58
77
. partition_point ( |r| r. timestamp_nanos <= since_time) ;
59
78
& 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..]
60
84
} else {
61
85
& logs. canister_log_records
62
- } ;
63
-
64
- if filtered_logs. is_empty ( ) {
65
- return vec ! [ "No logs" . to_string( ) ] ;
66
86
}
87
+ }
88
+
89
+ fn format_bytes ( bytes : & [ u8 ] ) -> String {
90
+ format ! ( "(bytes) 0x{}" , hex:: encode( bytes) )
91
+ }
67
92
68
- filtered_logs
69
- . iter ( )
93
+ fn format_canister_logs ( logs : & [ CanisterLogRecord ] ) -> Vec < String > {
94
+ logs . iter ( )
70
95
. map ( |r| {
71
96
let time = OffsetDateTime :: from_unix_timestamp_nanos ( r. timestamp_nanos as i128 )
72
97
. expect ( "Invalid canister log record timestamp" ) ;
@@ -91,10 +116,58 @@ fn format_canister_logs(logs: FetchCanisterLogsResponse, opts: &LogsOpts) -> Vec
91
116
. collect ( )
92
117
}
93
118
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
+
94
169
#[ test]
95
170
fn test_format_canister_logs ( ) {
96
- use ic_utils:: interfaces:: management_canister:: CanisterLogRecord ;
97
-
98
171
let logs = FetchCanisterLogsResponse {
99
172
canister_log_records : vec ! [
100
173
CanisterLogRecord {
@@ -130,16 +203,17 @@ fn test_format_canister_logs() {
130
203
] ,
131
204
} ;
132
205
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
+ ) ;
133
215
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) ,
143
217
vec![
144
218
"[42. 2021-05-06T19:17:10.000000001Z]: Some text message" . to_string( ) ,
145
219
"[43. 2021-05-06T19:17:10.000000002Z]: (bytes) 0xc0ffee" . to_string( ) ,
@@ -151,16 +225,17 @@ fn test_format_canister_logs() {
151
225
) ;
152
226
153
227
// 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
+ ) ;
154
237
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) ,
164
239
vec![
165
240
"[44. 2021-05-06T19:17:10.000000003Z]: (bytes) 0xc0ffee" . to_string( ) ,
166
241
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later" . to_string( ) ,
@@ -175,16 +250,17 @@ fn test_format_canister_logs() {
175
250
. unwrap ( )
176
251
. as_secs ( )
177
252
- 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
+ ) ;
178
262
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) ,
188
264
vec![
189
265
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later" . to_string( ) ,
190
266
"[46. 2021-05-06T19:17:15.000000002Z]: (bytes) 0xc0ffee" . to_string( ) ,
@@ -193,35 +269,40 @@ fn test_format_canister_logs() {
193
269
) ;
194
270
195
271
// 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
+ ) ;
196
281
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) ,
206
283
vec![
207
284
"[45. 2021-05-06T19:17:15.000000001Z]: Five seconds later" . to_string( ) ,
208
285
"[46. 2021-05-06T19:17:15.000000002Z]: (bytes) 0xc0ffee" . to_string( ) ,
209
286
"[47. 2021-05-06T19:17:15.000000003Z]: (bytes) 0xc0ffee" . to_string( ) ,
210
287
]
211
288
) ;
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 ?;
222
289
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
+ ) ;
227
308
}
0 commit comments