1use 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#[derive(clap::Parser, Debug)]
22pub struct CmdAppLogs {
23 #[clap(flatten)]
24 env: WasmerEnv,
25
26 #[clap(flatten)]
27 fmt: ListFormatOpts,
28
29 #[clap(long, value_parser = parse_timestamp_or_relative_time_negative_offset, conflicts_with = "request_id")]
41 from: Option<OffsetDateTime>,
42
43 #[clap(long, value_parser = parse_timestamp_or_relative_time_negative_offset, conflicts_with = "request_id")]
52 until: Option<OffsetDateTime>,
53
54 #[clap(long, default_value = "1000")]
57 max: usize,
58
59 #[clap(long, default_value = "false")]
61 watch: bool,
62
63 #[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 #[clap(long)]
73 pub request_id: Option<String>,
74
75 #[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 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, 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, 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, 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 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.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}