Skip to content

Commit

Permalink
implement logs command
Browse files Browse the repository at this point in the history
Signed-off-by: Rajat Jindal <[email protected]>
  • Loading branch information
rajatjindal committed Oct 17, 2023
1 parent a33d7d3 commit 6a7cfbc
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 9 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,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 = "236278f45b5afe5d1e575b3e4882a3fb93ee41d9" }

[build-dependencies]
vergen = { version = "^8.2.1", default-features = false, features = [
Expand Down
21 changes: 17 additions & 4 deletions crates/cloud/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use cloud_openapi::{
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_id_logs_get, api_channels_id_logs_raw_get, api_channels_post,
ApiChannelsIdPatchError,
},
configuration::{ApiKey, Configuration},
device_codes_api::api_device_codes_post,
Expand All @@ -28,8 +29,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, GetChannelLogsVm, GetChannelRawLogsVm, GetSqlDatabasesQuery,
GetVariablesQuery, RefreshTokenCommand, RegisterRevisionCommand, ResourceLabel,
RevisionItemPage, TokenInfo,
},
};
use reqwest::header;
Expand Down Expand Up @@ -303,7 +305,18 @@ impl CloudClientInterface for Client {
}

async fn channel_logs(&self, id: String) -> Result<GetChannelLogsVm> {
api_channels_id_logs_get(&self.configuration, &id, None, None)
api_channels_id_logs_get(&self.configuration, &id, None, None, None)
.await
.map_err(format_response_error)
}

async fn channel_logs_raw(
&self,
id: String,
max_lines: Option<i32>,
since: Option<String>,
) -> Result<GetChannelRawLogsVm> {
api_channels_id_logs_raw_get(&self.configuration, &id, max_lines, since.as_deref(), None)
.await
.map_err(format_response_error)
}
Expand Down
11 changes: 9 additions & 2 deletions crates/cloud/src/client_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, GetChannelLogsVm, GetChannelRawLogsVm, ResourceLabel,
RevisionItemPage, TokenInfo,
};

use std::string::String;
Expand Down Expand Up @@ -55,6 +55,13 @@ pub trait CloudClientInterface: Send + Sync {

async fn channel_logs(&self, id: String) -> Result<GetChannelLogsVm>;

async fn channel_logs_raw(
&self,
id: String,
max_lines: Option<i32>,
since: Option<String>,
) -> Result<GetChannelRawLogsVm>;

async fn add_revision(
&self,
app_storage_id: String,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use crate::{

use super::sqlite::database_has_link;

const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy";
pub const SPIN_DEPLOY_CHANNEL_NAME: &str = "spin-deploy";
const SPIN_DEFAULT_KV_STORE: &str = "default";

/// Package and upload an application to the Fermyon Cloud.
Expand Down
168 changes: 168 additions & 0 deletions src/commands/logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::ops::Sub;
use std::time::Duration;

use anyhow::{anyhow, Result};
use chrono::Utc;
use cloud_openapi::models::Entry;

use cloud::{client::Client as CloudClient, CloudClientExt, CloudClientInterface};

use crate::commands::create_cloud_client;
use crate::commands::deploy::SPIN_DEPLOY_CHANNEL_NAME;
use crate::opts::*;
use clap::Parser;
use uuid::Uuid;

/// Fetch and tail logs of an application deployed to the Fermyon Cloud.
#[derive(Parser, Debug)]
#[clap(about = "fetch logs for an app from Fermyon Cloud")]
pub struct LogsCommand {
/// Find app to fetch logs for in 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<String>,

/// App name
#[clap(name = "app-name", long = "app-name")]
pub app_name: 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 secs to refresh logs from cloud
#[clap(parse(try_from_str = parse_interval), name="interval", long="interval", default_value = "2")]
pub interval_secs: u64,

/// fetch logs since
#[clap(parse(try_from_str = parse_duration), name="since", long="since", default_value = "7d")]
pub since: std::time::Duration,
}

impl LogsCommand {
pub async fn run(self) -> Result<()> {
let client = create_cloud_client(self.deployment_env_id.as_deref()).await?;
let app_name: String = self.app_name.clone();

self.logs(client, app_name.as_str())
.await
.map_err(|e| anyhow!("{:?}\n\nLearn more at {}", e, "DEVELOPER_CLOUD_FAQ"))
}

async fn logs(self, client: CloudClient, app_name: &str) -> Result<()> {
let app_id = match client
.get_app_id(app_name)
.await
.map_err(|_e| anyhow!("app with name {:?} not found", app_name))?
{
Some(x) => x,
None => return Err(anyhow!("app with name {:?} not found", app_name)),
};

let channel_id = client
.get_channel_id(app_id, SPIN_DEPLOY_CHANNEL_NAME)
.await?;

fetch_logs_and_print_loop(
&client,
channel_id,
self.follow,
self.interval_secs,
Some(self.max_lines),
self.since,
)
.await?;

Ok(())
}
}

async fn fetch_logs_and_print_loop(
client: &CloudClient,
channel_id: Uuid,
follow: bool,
interval: u64,
max_lines: Option<i32>,
since: Duration,
) -> Result<()> {
let mut new_since = Utc::now().sub(since).to_rfc3339();
new_since =
fetch_logs_and_print_once(&client, channel_id, max_lines, new_since.to_owned()).await?;

if !follow {
return Ok(());
}

loop {
tokio::time::sleep(tokio::time::Duration::from_secs(interval)).await;
new_since =
fetch_logs_and_print_once(&client, channel_id, None, new_since.to_owned()).await?;
}
}

async fn fetch_logs_and_print_once(
client: &CloudClient,
channel_id: Uuid,
max_lines: Option<i32>,
since: String,
) -> Result<String> {
let entries = client
.channel_logs_raw(channel_id.to_string(), max_lines, Some(since.to_string()))
.await?
.entries;

if entries.len() == 0 {
return Ok(since.to_owned());
}

Ok(print_lastn_logs(&entries).to_owned())
}

fn print_lastn_logs(entries: &[Entry]) -> &str {
let mut new_since: &str = "";
for entry in entries.iter().rev() {
for line in entry.log_lines.as_ref().unwrap() {
println!("{}", line.line.as_ref().unwrap());
new_since = line.time.as_ref().unwrap().as_str()
}
}

return new_since;
}

fn parse_duration(arg: &str) -> Result<std::time::Duration, anyhow::Error> {
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 {
return Err(anyhow!("invalid duration"));
};

Ok(duration)
}

fn parse_interval(arg: &str) -> Result<u64, anyhow::Error> {
let interval = arg.parse()?;
if interval < 2 {
return Err(anyhow!("interval cannot be less than 2 seconds"));
}

Ok(interval)
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use commands::{
deploy::DeployCommand,
link::{LinkCommand, UnlinkCommand},
login::LoginCommand,
logs::LogsCommand,
sqlite::SqliteCommand,
variables::VariablesCommand,
};
Expand All @@ -35,6 +36,8 @@ enum CloudCli {
Deploy(DeployCommand),
/// Login to Fermyon Cloud
Login(LoginCommand),
/// Fetches app logs from Fermyon Cloud
Logs(LogsCommand),
/// Manage Spin application variables
#[clap(subcommand, alias = "vars")]
Variables(VariablesCommand),
Expand All @@ -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,
Expand Down

0 comments on commit 6a7cfbc

Please sign in to comment.