wasmer_cli/commands/app/
logs.rs

1//! Show logs for an Edge app.
2
3use crate::utils::timestamp::parse_timestamp_or_relative_time_negative_offset;
4use colored::Colorize;
5use comfy_table::{Cell, Table};
6use futures::StreamExt;
7use time::{OffsetDateTime, format_description::well_known::Rfc3339};
8use wasmer_backend_api::types::{Log, LogStream};
9
10use crate::{config::WasmerEnv, opts::ListFormatOpts, utils::render::CliRender};
11
12use super::util::AppIdentOpts;
13
14#[derive(Debug, PartialEq, Eq, Clone, Copy, clap::ValueEnum)]
15pub enum LogStreamArg {
16    Stdout,
17    Stderr,
18}
19
20/// Retrieve the logs of an app
21#[derive(clap::Parser, Debug)]
22pub struct CmdAppLogs {
23    #[clap(flatten)]
24    env: WasmerEnv,
25
26    #[clap(flatten)]
27    fmt: ListFormatOpts,
28
29    /// The date of the earliest log entry.
30    ///
31    /// Defaults to the last 10 minutes.
32    ///
33    /// Format:
34    /// * RFC 3339 (`2006-01-02T03:04:05-07:00`)
35    /// * RFC 2822 (`Mon, 02 Jan 2006 03:04:05 MST`)
36    /// * Simple date (`2022-11-11`)
37    /// * Unix timestamp (`1136196245`)
38    /// * Relative time (`10m` / `-1h`, `1d1h30s`)
39    // TODO: should default to trailing logs once trailing is implemented.
40    #[clap(long, value_parser = parse_timestamp_or_relative_time_negative_offset, conflicts_with = "request_id")]
41    from: Option<OffsetDateTime>,
42
43    /// The date of the latest log entry.
44    ///
45    /// Format:
46    /// * RFC 3339 (`2006-01-02T03:04:05-07:00`)
47    /// * RFC 2822 (`Mon, 02 Jan 2006 03:04:05 MST`)
48    /// * Simple date (`2022-11-11`)
49    /// * Unix timestamp (`1136196245`)
50    /// * Relative time (`10m` / `1h`, `1d1h30s`)
51    #[clap(long, value_parser = parse_timestamp_or_relative_time_negative_offset, conflicts_with = "request_id")]
52    until: Option<OffsetDateTime>,
53
54    /// Maximum log lines to fetch.
55    /// Defaults to 1000.
56    #[clap(long, default_value = "1000")]
57    max: usize,
58
59    /// Continuously watch for new logs and display them in real-time.
60    #[clap(long, default_value = "false")]
61    watch: bool,
62
63    /// Streams of logs to display
64    #[clap(long, value_delimiter = ',', value_enum)]
65    streams: Option<Vec<LogStreamArg>>,
66
67    #[clap(flatten)]
68    #[allow(missing_docs)]
69    pub ident: AppIdentOpts,
70
71    /// The identifier of the request to show logs related to
72    #[clap(long)]
73    pub request_id: Option<String>,
74
75    /// The identifier of the app instance to show logs related to
76    #[clap(long, conflicts_with = "request_id", value_delimiter = ' ', num_args = 1..)]
77    pub instance_id: Option<Vec<String>>,
78}
79
80#[async_trait::async_trait]
81impl crate::commands::AsyncCliCommand for CmdAppLogs {
82    type Output = ();
83
84    async fn run_async(self) -> Result<(), anyhow::Error> {
85        let client = self.env.client()?;
86
87        let (_ident, app) = self.ident.load_app(&client).await?;
88
89        let from = self
90            .from
91            .unwrap_or_else(|| OffsetDateTime::now_utc() - time::Duration::minutes(10));
92
93        let version = app.active_version.as_ref().map_or("n/a", |v| &v.version);
94
95        tracing::info!(
96            app.name=%app.name,
97            app.owner=%app.owner.global_name,
98            app.version=version,
99            range.start=%from,
100            range.end=self.until.map(|ts| ts.to_string()),
101            "Fetching logs",
102        );
103
104        let (stdout, stderr) = self
105            .streams
106            .map(|s| {
107                let mut stdout = false;
108                let mut stderr = false;
109
110                for stream in s {
111                    if matches!(stream, LogStreamArg::Stdout) {
112                        stdout = true;
113                    } else if matches!(stream, LogStreamArg::Stderr) {
114                        stderr = true;
115                    }
116                }
117
118                (stdout, stderr)
119            })
120            .unwrap_or_default();
121
122        let streams = Vec::from(match (stdout, stderr) {
123            (true, true) | (false, false) => &[LogStream::Stdout, LogStream::Stderr][..],
124            (true, false) => &[LogStream::Stdout][..],
125            (false, true) => &[LogStream::Stderr][..],
126        });
127
128        // Code duplication to avoid a dependency to `OR` streams.
129        if let Some(instance_id) = &self.instance_id {
130            let logs_stream = wasmer_backend_api::query::get_app_logs_paginated_filter_instance(
131                &client,
132                app.name.clone(),
133                app.owner.global_name.to_string(),
134                None, // keep version None since we want logs from all versions atm
135                from,
136                self.until,
137                self.watch,
138                Some(streams),
139                instance_id.clone(),
140            )
141            .await;
142
143            let mut logs_stream = std::pin::pin!(logs_stream);
144            let mut rem = self.max;
145
146            while let Some(logs) = logs_stream.next().await {
147                let mut logs = logs?;
148
149                let limit = std::cmp::min(logs.len(), rem);
150
151                let logs: Vec<_> = logs.drain(..limit).collect();
152
153                if !logs.is_empty() {
154                    let rendered = self.fmt.format.render(&logs);
155                    println!("{rendered}");
156
157                    rem -= limit;
158                }
159
160                if !self.watch || rem == 0 {
161                    break;
162                }
163            }
164        } else if let Some(request_id) = &self.request_id {
165            let logs_stream = wasmer_backend_api::query::get_app_logs_paginated_filter_request(
166                &client,
167                app.name.clone(),
168                app.owner.global_name.to_string(),
169                None, // keep version None since we want logs from all versions atm
170                from,
171                self.until,
172                self.watch,
173                Some(streams),
174                request_id.clone(),
175            )
176            .await;
177
178            let mut logs_stream = std::pin::pin!(logs_stream);
179            let mut rem = self.max;
180
181            while let Some(logs) = logs_stream.next().await {
182                let mut logs = logs?;
183
184                let limit = std::cmp::min(logs.len(), rem);
185
186                let logs: Vec<_> = logs.drain(..limit).collect();
187
188                if !logs.is_empty() {
189                    let rendered = self.fmt.format.render(&logs);
190                    println!("{rendered}");
191
192                    rem -= limit;
193                }
194
195                if !self.watch || rem == 0 {
196                    break;
197                }
198            }
199        } else {
200            let logs_stream = wasmer_backend_api::query::get_app_logs_paginated(
201                &client,
202                app.name.clone(),
203                app.owner.global_name.to_string(),
204                None, // keep version None since we want logs from all versions atm
205                from,
206                self.until,
207                self.watch,
208                Some(streams),
209            )
210            .await;
211
212            let mut logs_stream = std::pin::pin!(logs_stream);
213            let mut rem = self.max;
214
215            while let Some(logs) = logs_stream.next().await {
216                let mut logs = logs?;
217
218                let limit = std::cmp::min(logs.len(), rem);
219
220                let logs: Vec<_> = logs.drain(..limit).collect();
221
222                if !logs.is_empty() {
223                    let rendered = self.fmt.format.render(&logs);
224                    println!("{rendered}");
225
226                    rem -= limit;
227                }
228
229                if !self.watch || rem == 0 {
230                    break;
231                }
232            }
233        }
234
235        Ok(())
236    }
237}
238
239impl CliRender for Log {
240    fn render_item_table(&self) -> String {
241        let mut table = Table::new();
242        // remove all borders from the table
243        let Log {
244            message, timestamp, ..
245        }: &Log = self;
246
247        table.add_rows([
248            vec![
249                "Timestamp".to_string(),
250                datetime_from_unix(*timestamp).format(&Rfc3339).unwrap(),
251            ],
252            vec!["Message".to_string(), message.to_string()],
253        ]);
254        table.to_string()
255    }
256
257    fn render_list_table(items: &[Self]) -> String {
258        let mut table = Table::new();
259        // table.set_header(vec!["Timestamp".to_string(), "Message".to_string()]);
260        table.load_preset(comfy_table::presets::NOTHING);
261        table.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
262
263        for item in items {
264            let mut message = item.message.clone().bold();
265            if let Some(stream) = item.stream {
266                message = match stream {
267                    LogStream::Stdout => message,
268                    LogStream::Stderr => message.yellow(),
269                    LogStream::Runtime => message.cyan(),
270                };
271            }
272            table.add_row([
273                Cell::new(format!(
274                    "[{}]",
275                    datetime_from_unix(item.timestamp).format(&Rfc3339).unwrap()
276                ))
277                .set_alignment(comfy_table::CellAlignment::Right),
278                Cell::new(message),
279            ]);
280        }
281        table.to_string()
282    }
283}
284
285fn datetime_from_unix(timestamp: f64) -> OffsetDateTime {
286    OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
287        .expect("Timestamp should always be valid")
288}