diff --git a/Cargo.lock b/Cargo.lock index 9237208..bd1057a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,7 @@ checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" dependencies = [ "async-channel", "async-executor", - "async-io", + "async-io 1.13.0", "async-lock", "blocking", "futures-lite", @@ -125,13 +125,33 @@ dependencies = [ "futures-lite", "log", "parking", - "polling", - "rustix 0.37.26", + "polling 2.8.0", + "rustix 0.37.27", "slab", "socket2 0.4.10", "waker-fn", ] +[[package]] +name = "async-io" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da8f3146014722c89e7859e1d7bb97873125d7346d10ca642ffab794355828" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling 3.3.0", + "rustix 0.38.21", + "slab", + "tracing", + "waker-fn", + "windows-sys 0.48.0", +] + [[package]] name = "async-lock" version = "2.8.0" @@ -147,30 +167,30 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" dependencies = [ - "async-io", + "async-io 1.13.0", "async-lock", "async-signal", "blocking", "cfg-if", - "event-listener 3.0.0", + "event-listener 3.0.1", "futures-lite", - "rustix 0.38.20", + "rustix 0.38.21", "windows-sys 0.48.0", ] [[package]] name = "async-signal" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a5415b7abcdc9cd7d63d6badba5288b2ca017e3fbd4173b8f405449f1a2399" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" dependencies = [ - "async-io", + "async-io 2.1.0", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.20", + "rustix 0.38.21", "signal-hook-registry", "slab", "windows-sys 0.48.0", @@ -184,7 +204,7 @@ checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-channel", "async-global-executor", - "async-io", + "async-io 1.13.0", "async-lock", "async-process", "crossbeam-utils", @@ -452,7 +472,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-util 0.7.9", + "tokio-util 0.7.10", "tracing", "uuid", ] @@ -460,7 +480,7 @@ dependencies = [ [[package]] name = "cloud-openapi" version = "0.1.0" -source = "git+https://github.com/fermyon/cloud-openapi?rev=3e70369e3fc9574e262827332157da40da0a4f66#3e70369e3fc9574e262827332157da40da0a4f66" +source = "git+https://github.com/fermyon/cloud-openapi?rev=ce1e916110b9a9e59a1171ac364f0b6e23908428#ce1e916110b9a9e59a1171ac364f0b6e23908428" dependencies = [ "reqwest", "serde", @@ -562,9 +582,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -830,9 +850,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e56284f00d94c1bc7fd3c77027b4623c88c1f53d8d2394c6199f2921dea325" +checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1" dependencies = [ "concurrent-queue", "parking", @@ -923,9 +943,9 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -938,9 +958,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -948,15 +968,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -965,9 +985,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-lite" @@ -986,9 +1006,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -997,21 +1017,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -1085,7 +1105,7 @@ dependencies = [ "indexmap 1.9.3", "slab", "tokio", - "tokio-util 0.7.9", + "tokio-util 0.7.10", "tracing", ] @@ -1627,7 +1647,7 @@ dependencies = [ "sha2", "thiserror", "tokio", - "tokio-util 0.7.9", + "tokio-util 0.7.10", "tracing", "unicase", ] @@ -1862,6 +1882,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "polling" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53b6af1f60f36f8c2ac2aad5459d75a5a9b4be1e8cdd40264f315d78193e531" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.21", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2075,7 +2109,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-util 0.7.9", + "tokio-util 0.7.10", "tower-service", "url", "wasm-bindgen", @@ -2120,9 +2154,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.37.26" +version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f3f8f960ed3b5a59055428714943298bf3fa2d4a1d53135084e0544829d995" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", @@ -2134,9 +2168,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ "bitflags 2.4.1", "errno", @@ -2212,18 +2246,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", @@ -2241,9 +2275,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -2486,7 +2520,7 @@ dependencies = [ "spin-manifest", "tempfile", "tokio", - "tokio-util 0.7.9", + "tokio-util 0.7.10", "tracing", "walkdir", ] @@ -2623,14 +2657,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand 2.0.1", - "redox_syscall 0.3.5", - "rustix 0.38.20", + "redox_syscall 0.4.1", + "rustix 0.38.21", "windows-sys 0.48.0", ] @@ -2797,9 +2831,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2812,9 +2846,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef75d881185fd2df4a040793927c153d863651108a93c7e17a9e591baa95cc6" +checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc" dependencies = [ "indexmap 2.0.2", "serde", @@ -2834,9 +2868,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.20.4" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380f9e8120405471f7c9ad1860a713ef5ece6a670c7eae39225e477340f32fc4" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap 2.0.2", "serde", @@ -3282,9 +3316,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c" +checksum = "176b6138793677221d420fd2f0aeeced263f197688b36484660da767bca2fa32" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 20d5aba..16aadac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ openssl = { version = "0.10" } [workspace.dependencies] tracing = { version = "0.1", features = ["log"] } -cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi", rev = "3e70369e3fc9574e262827332157da40da0a4f66" } +cloud-openapi = { git = "https://github.com/fermyon/cloud-openapi", rev = "ce1e916110b9a9e59a1171ac364f0b6e23908428" } [build-dependencies] vergen = { version = "^8.2.1", default-features = false, features = [ diff --git a/crates/cloud/src/client.rs b/crates/cloud/src/client.rs index e7e996e..71c3def 100644 --- a/crates/cloud/src/client.rs +++ b/crates/cloud/src/client.rs @@ -3,11 +3,14 @@ use async_trait::async_trait; use cloud_openapi::{ apis::{ self, - apps_api::{api_apps_get, api_apps_id_delete, api_apps_id_get, api_apps_post}, + apps_api::{ + api_apps_get, api_apps_id_delete, api_apps_id_get, api_apps_id_logs_get, + api_apps_id_logs_raw_get, api_apps_post, + }, auth_tokens_api::api_auth_tokens_refresh_post, channels_api::{ - api_channels_get, api_channels_id_delete, api_channels_id_get, - api_channels_id_logs_get, api_channels_post, ApiChannelsIdPatchError, + api_channels_get, api_channels_id_delete, api_channels_id_get, api_channels_post, + ApiChannelsIdPatchError, }, configuration::{ApiKey, Configuration}, device_codes_api::api_device_codes_post, @@ -28,8 +31,9 @@ use cloud_openapi::{ CreateAppCommand, CreateChannelCommand, CreateDeviceCodeCommand, CreateKeyValuePairCommand, CreateSqlDatabaseCommand, CreateVariablePairCommand, Database, DeleteSqlDatabaseCommand, DeleteVariablePairCommand, DeviceCodeItem, EnvironmentVariableItem, - ExecuteSqlStatementCommand, GetChannelLogsVm, GetSqlDatabasesQuery, GetVariablesQuery, - RefreshTokenCommand, RegisterRevisionCommand, ResourceLabel, RevisionItemPage, TokenInfo, + ExecuteSqlStatementCommand, GetAppLogsVm, GetAppRawLogsVm, GetSqlDatabasesQuery, + GetVariablesQuery, RefreshTokenCommand, RegisterRevisionCommand, ResourceLabel, + RevisionItemPage, TokenInfo, }, }; use reqwest::header; @@ -173,11 +177,29 @@ impl CloudClientInterface for Client { None, None, None, + None, ) .await .map_err(format_response_error) } + async fn app_logs(&self, id: String) -> Result { + api_apps_id_logs_get(&self.configuration, &id, None, None, None) + .await + .map_err(format_response_error) + } + + async fn app_logs_raw( + &self, + id: String, + max_lines: Option, + since: Option, + ) -> Result { + api_apps_id_logs_raw_get(&self.configuration, &id, max_lines, since.as_deref(), None) + .await + .map_err(format_response_error) + } + async fn get_channel_by_id(&self, id: &str) -> Result { api_channels_id_get(&self.configuration, id, None) .await @@ -302,12 +324,6 @@ impl CloudClientInterface for Client { .map_err(format_response_error) } - async fn channel_logs(&self, id: String) -> Result { - api_channels_id_logs_get(&self.configuration, &id, None, None) - .await - .map_err(format_response_error) - } - async fn add_revision( &self, app_storage_id: String, diff --git a/crates/cloud/src/client_interface.rs b/crates/cloud/src/client_interface.rs index 2152a21..96e4b89 100644 --- a/crates/cloud/src/client_interface.rs +++ b/crates/cloud/src/client_interface.rs @@ -2,8 +2,8 @@ use anyhow::Result; use async_trait::async_trait; use cloud_openapi::models::{ AppItem, AppItemPage, ChannelItem, ChannelItemPage, ChannelRevisionSelectionStrategy, Database, - DeviceCodeItem, EnvironmentVariableItem, GetChannelLogsVm, ResourceLabel, RevisionItemPage, - TokenInfo, + DeviceCodeItem, EnvironmentVariableItem, GetAppLogsVm, GetAppRawLogsVm, ResourceLabel, + RevisionItemPage, TokenInfo, }; use std::string::String; @@ -26,6 +26,15 @@ pub trait CloudClientInterface: Send + Sync { async fn list_apps(&self, page_size: i32, page_index: Option) -> Result; + async fn app_logs(&self, id: String) -> Result; + + async fn app_logs_raw( + &self, + id: String, + max_lines: Option, + since: Option, + ) -> Result; + async fn get_channel_by_id(&self, id: &str) -> Result; async fn list_channels(&self) -> Result; @@ -53,8 +62,6 @@ pub trait CloudClientInterface: Send + Sync { async fn remove_channel(&self, id: String) -> Result<()>; - async fn channel_logs(&self, id: String) -> Result; - async fn add_revision( &self, app_storage_id: String, diff --git a/src/commands/deploy.rs b/src/commands/deploy.rs index 433aa05..2983a74 100644 --- a/src/commands/deploy.rs +++ b/src/commands/deploy.rs @@ -38,6 +38,8 @@ use crate::{ use super::sqlite::database_has_link; const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy"; +pub const DEVELOPER_CLOUD_FAQ: &str = "https://developer.fermyon.com/cloud/faq"; + const SPIN_DEFAULT_KV_STORE: &str = "default"; /// Package and upload an application to the Fermyon Cloud. @@ -118,8 +120,6 @@ impl DeployCommand { let login_connection = login_connection(self.deployment_env_id.as_deref()).await?; - const DEVELOPER_CLOUD_FAQ: &str = "https://developer.fermyon.com/cloud/faq"; - self.deploy_cloud(login_connection) .await .map_err(|e| anyhow!("{:?}\n\nLearn more at {}", e, DEVELOPER_CLOUD_FAQ)) diff --git a/src/commands/logs.rs b/src/commands/logs.rs new file mode 100644 index 0000000..3a0979e --- /dev/null +++ b/src/commands/logs.rs @@ -0,0 +1,192 @@ +use std::ops::Sub; +use std::time::Duration; + +use anyhow::{bail, Context, Result}; +use chrono::Utc; +use cloud::{CloudClientExt, CloudClientInterface}; +use cloud_openapi::models::Entry; +use std::option::Option; + +use crate::commands::create_cloud_client; +use crate::opts::*; +use clap::Parser; +use uuid::Uuid; + +/// fetch logs for an app from Fermyon Cloud +#[derive(Parser, Debug)] +pub struct LogsCommand { + /// Use the Fermyon instance saved under the specified name. + /// If omitted, Spin looks for app in default unnamed instance. + #[clap( + name = "environment-name", + long = "environment-name", + env = DEPLOYMENT_ENV_NAME_ENV + )] + pub deployment_env_id: Option, + + /// App name + pub app: String, + + /// Follow logs output + #[clap(name = "follow", long = "follow")] + pub follow: bool, + + /// Number of lines to show from the end of the logs + #[clap(name = "tail", long = "tail", default_value = "10")] + pub max_lines: i32, + + /// Interval in seconds to refresh logs from cloud + #[clap(parse(try_from_str = parse_interval), name="interval", long="interval", default_value = "2")] + pub interval_secs: std::time::Duration, + + /// Only return logs newer than a relative duration. The duration format is a number + /// and a unit, where the unit is 's' for seconds, 'm' for minutes, 'h' for hours + /// or 'd' for days (e.g. "30m" for 30 minutes ago). The default it 7 days. + #[clap(parse(try_from_str = parse_duration), name="since", long="since", default_value = "7d")] + pub since: std::time::Duration, + + /// Show timestamps + #[clap( + name = "show-timestamps", + long = "show-timestamps", + default_value = "true", + action = clap::ArgAction::Set + )] + pub show_timestamp: bool, +} + +impl LogsCommand { + pub async fn run(self) -> Result<()> { + let client = create_cloud_client(self.deployment_env_id.as_deref()).await?; + self.logs(&client).await + } + + async fn logs(self, client: &impl CloudClientInterface) -> Result<()> { + let app_id = client + .get_app_id(&self.app) + .await + .with_context(|| format!("failed to find app with name {:?}", &self.app))? + .with_context(|| format!("app with name {:?} not found", &self.app))?; + + fetch_logs_and_print_loop( + client, + app_id, + self.follow, + self.interval_secs, + self.max_lines, + self.since, + self.show_timestamp, + ) + .await?; + + Ok(()) + } +} + +async fn fetch_logs_and_print_loop( + client: &impl CloudClientInterface, + app_id: Uuid, + follow: bool, + interval: Duration, + max_lines: i32, + since: Duration, + show_timestamp: bool, +) -> Result<()> { + let mut curr_since = Utc::now().sub(since).to_rfc3339(); + curr_since = + fetch_logs_and_print_once(client, app_id, Some(max_lines), curr_since, show_timestamp) + .await?; + + if !follow { + return Ok(()); + } + + loop { + tokio::time::sleep(interval).await; + curr_since = + fetch_logs_and_print_once(client, app_id, None, curr_since, show_timestamp).await?; + } +} + +async fn fetch_logs_and_print_once( + client: &impl CloudClientInterface, + app_id: Uuid, + max_lines: Option, + since: String, + show_timestamp: bool, +) -> Result { + let entries = client + .app_logs_raw(app_id.to_string(), max_lines, Some(since.to_string())) + .await? + .entries; + + if entries.is_empty() { + return Ok(since.to_owned()); + } + + let updated_since = print_logs(&entries, show_timestamp); + if let Some(u) = updated_since { + return Ok(u.to_owned()); + } + + Ok(since) +} + +fn print_logs(entries: &[Entry], show_timestamp: bool) -> Option<&str> { + let mut since = None; + for entry in entries.iter().rev() { + let Some(log_lines) = entry.log_lines.as_ref() else { + continue; + }; + + for log_entry in log_lines { + let Some(log) = log_entry.line.as_ref() else { + continue; + }; + + match &log_entry.time { + Some(time) => { + if show_timestamp { + println!("[{time}] {log}"); + } else { + println!("{log}"); + } + + since = Some(time.as_str()); + } + None => (), + } + } + } + + since +} + +fn parse_duration(arg: &str) -> anyhow::Result { + let duration = if let Some(parg) = arg.strip_suffix('s') { + let value = parg.parse()?; + std::time::Duration::from_secs(value) + } else if let Some(parg) = arg.strip_suffix('m') { + let value: u64 = parg.parse()?; + std::time::Duration::from_secs(value * 60) + } else if let Some(parg) = arg.strip_suffix('h') { + let value: u64 = parg.parse()?; + std::time::Duration::from_secs(value * 60 * 60) + } else if let Some(parg) = arg.strip_suffix('d') { + let value: u64 = parg.parse()?; + std::time::Duration::from_secs(value * 24 * 60 * 60) + } else { + bail!(r#"since must be a number followed by an allowed unit ("300s", "5m", "4h" or "1d")"#); + }; + + Ok(duration) +} + +fn parse_interval(arg: &str) -> anyhow::Result { + let value = arg.parse()?; + if value < 2 { + bail!("interval cannot be less than 2 seconds") + } + + Ok(std::time::Duration::from_secs(value)) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index fae1c36..04b4c56 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,6 +2,7 @@ pub mod apps; pub mod deploy; pub mod link; pub mod login; +pub mod logs; pub mod sqlite; pub mod variables; diff --git a/src/main.rs b/src/main.rs index c07d024..23c151b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use commands::{ deploy::DeployCommand, link::{LinkCommand, UnlinkCommand}, login::LoginCommand, + logs::LogsCommand, sqlite::SqliteCommand, variables::VariablesCommand, }; @@ -35,6 +36,8 @@ enum CloudCli { Deploy(DeployCommand), /// Login to Fermyon Cloud Login(LoginCommand), + /// Fetch logs for an app from Fermyon Cloud + Logs(LogsCommand), /// Manage Spin application variables #[clap(subcommand, alias = "vars")] Variables(VariablesCommand), @@ -61,6 +64,7 @@ async fn main() -> Result<(), Error> { CloudCli::Apps(cmd) => cmd.run().await, CloudCli::Deploy(cmd) => cmd.run().await, CloudCli::Login(cmd) => cmd.run().await, + CloudCli::Logs(cmd) => cmd.run().await, CloudCli::Variables(cmd) => cmd.run().await, CloudCli::Sqlite(cmd) => cmd.run().await, CloudCli::Link(cmd) => cmd.run().await,