Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add server diagnostics and print on every CLI command #428

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cli/src/config.rs
Original file line number Diff line number Diff line change
@@ -103,6 +103,9 @@ pub struct Advanced {
#[serde_as(as = "Option<DurationSecondsWithFrac>")]
pub build_dequeue_timeout: Option<Duration>,

#[serde(default, skip_serializing_if = "Option::is_none")]
pub collect_diagnostics: Option<bool>,

/// Options for rendering error traces.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_trace_options: Option<tg::error::TraceOptions>,
48 changes: 43 additions & 5 deletions packages/cli/src/health.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,56 @@
use crate::Cli;
use crossterm::style::Stylize as _;
use tangram_client::{self as tg, Handle as _};

/// Get the server's health.
#[derive(Clone, Debug, clap::Args)]
#[group(skip)]
pub struct Args {}
pub struct Args {
// Format as JSON.
#[arg(long)]
pub json: bool,
}

impl Cli {
pub async fn command_health(&self, _args: Args) -> tg::Result<()> {
pub async fn command_health(&self, args: Args) -> tg::Result<()> {
let handle = self.handle().await?;
let health = handle.health().await?;
let health = serde_json::to_string_pretty(&health)
.map_err(|source| tg::error!(!source, "failed to serialize"))?;
println!("{health}");
if args.json {
let health = serde_json::to_string_pretty(&health)
.map_err(|source| tg::error!(!source, "failed to serialize"))?;
println!("{health}");
return Ok(());
}

if let Some(version) = &health.version {
println!("{}: {version}", "version".blue());
}
if let Some(builds) = &health.builds {
println!(
"{}: created:{} dequeued:{} started:{}",
"builds".blue(),
builds.created,
builds.dequeued,
builds.started
);
}
if let Some(database) = &health.database {
println!(
"{}: available_connections:{}",
"database".blue(),
database.available_connections
);
}
if let Some(file_descriptor_semaphore) = &health.file_descriptor_semaphore {
println!(
"{}: available_permits:{}",
"files".blue(),
file_descriptor_semaphore.available_permits
);
}
for diagnostic in &health.diagnostics {
Self::print_diagnostic(diagnostic);
}
Ok(())
}
}
29 changes: 15 additions & 14 deletions packages/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ use crossterm::{style::Stylize as _, tty::IsTty as _};
use futures::FutureExt as _;
use num::ToPrimitive as _;
use std::{fmt::Write as _, path::PathBuf, sync::Mutex, time::Duration};
use tangram_client::{self as tg, Client};
use tangram_client::{self as tg, Client, Handle};
use tangram_either::Either;
use tangram_server::Server;
use tokio::io::AsyncWriteExt as _;
@@ -52,7 +52,7 @@ pub struct Cli {
before_help = before_help(),
disable_help_subcommand = true,
name = "tangram",
version = version(),
version = tg::health::version(),
)]
struct Args {
#[command(subcommand)]
@@ -76,20 +76,11 @@ struct Args {
}

fn before_help() -> String {
let version = version();
let version = tg::health::version();
let logo = include_str!("tangram.ascii").trim_end();
format!("Tangram {version}\n\n{logo}")
}

fn version() -> String {
let mut version = env!("CARGO_PKG_VERSION").to_owned();
if let Some(commit) = option_env!("TANGRAM_CLI_COMMIT_HASH") {
version.push('+');
version.push_str(commit);
}
version
}

#[derive(Clone, Copy, Debug, Default, clap::ValueEnum, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum Mode {
@@ -830,6 +821,7 @@ impl Cli {

// Run the command.
async fn command(&self, command: Command) -> tg::Result<()> {
self.show_diagnostics().await.ok();
match command {
Command::Artifact(args) => self.command_artifact(args).boxed(),
Command::Blob(args) => self.command_blob(args).boxed(),
@@ -872,6 +864,15 @@ impl Cli {
.await
}

async fn show_diagnostics(&self) -> tg::Result<()> {
let handle = self.handle().await?;
let health = handle.health().await?;
for diagnostic in &health.diagnostics {
Self::print_diagnostic(diagnostic);
}
Ok(())
}

fn read_config(path: Option<PathBuf>) -> tg::Result<Option<Config>> {
let path = path.unwrap_or_else(|| {
PathBuf::from(std::env::var("HOME").unwrap()).join(".config/tangram/config.json")
@@ -951,9 +952,9 @@ impl Cli {
tg::diagnostic::Severity::Info => "info".blue().bold(),
tg::diagnostic::Severity::Hint => "hint".cyan().bold(),
};
let mut string = String::new();
write!(string, "{title}: ").unwrap();
eprint!("{title}: {}", diagnostic.message);
if let Some(location) = &diagnostic.location {
let mut string = String::new();
let path = location
.module
.referent
13 changes: 13 additions & 0 deletions packages/client/src/health.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ pub struct Health {
pub database: Option<Database>,
pub file_descriptor_semaphore: Option<FileDescriptorSemaphore>,
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub diagnostics: Vec<tg::Diagnostic>,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
@@ -44,3 +46,14 @@ impl tg::Client {
Ok(output)
}
}

/// Return the compiled version string.
#[must_use]
pub fn version() -> String {
let mut version = env!("CARGO_PKG_VERSION").to_owned();
if let Some(commit) = option_env!("TANGRAM_CLI_COMMIT_HASH") {
version.push('+');
version.push_str(commit);
}
version
}
2 changes: 2 additions & 0 deletions packages/server/src/config.rs
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ pub struct Config {
#[derive(Clone, Debug)]
pub struct Advanced {
pub build_dequeue_timeout: Duration,
pub collect_diagnostics: bool,
pub error_trace_options: tg::error::TraceOptions,
pub file_descriptor_semaphore_size: usize,
pub preserve_temp_directories: bool,
@@ -174,6 +175,7 @@ impl Default for Advanced {
fn default() -> Self {
Self {
build_dequeue_timeout: Duration::from_secs(3600),
collect_diagnostics: true,
error_trace_options: tg::error::TraceOptions {
internal: true,
reverse: false,
3 changes: 3 additions & 0 deletions packages/server/src/health.rs
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ use tangram_database::{self as db, prelude::*};
use tangram_either::Either;
use tangram_http::{outgoing::response::Ext as _, Incoming, Outgoing};

mod diagnostics;

impl Server {
pub async fn health(&self) -> tg::Result<tg::Health> {
// Get a database connection.
@@ -70,6 +72,7 @@ impl Server {
database: Some(database),
file_descriptor_semaphore: Some(file_descriptor_semaphore),
version: self.config.version.clone(),
diagnostics: self.diagnostics(),
};

Ok(health)
52 changes: 52 additions & 0 deletions packages/server/src/health/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::Server;
use tangram_client as tg;

impl Server {
pub(crate) fn diagnostics(&self) -> Vec<tg::Diagnostic> {
self.diagnostics.lock().unwrap().clone()
}

pub(crate) async fn diagnostics_task(&self) -> tg::Result<()> {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
loop {
interval.tick().await;
let mut diagnostics = Vec::new();

if let Some(diagnostic) = self.try_check_latest_version().await {
diagnostics.push(diagnostic);
}

*self.diagnostics.lock().unwrap() = diagnostics;
}
}

async fn try_check_latest_version(&self) -> Option<tg::Diagnostic> {
let version = tg::health::version();
#[derive(serde::Deserialize)]
struct Response {
name: String,
}
let response: Response = reqwest::Client::new()
.request(
http::Method::GET,
"https://api.github.com/repos/tangramdotdev/tangram/releases/latest",
)
.header("Accept", "application/vnd.github+json")
.header("User-Agent", "tangram")
.send()
.await
.inspect_err(|error| tracing::warn!(%error, "failed to get response from github"))
.ok()?
.json()
.await
.inspect_err(
|error| tracing::warn!(%error, "failed to deserialize response from github"),
)
.ok()?;
(response.name != version).then(|| tg::Diagnostic {
location: None,
severity: tg::diagnostic::Severity::Warning,
message: format!("A new version of tangram is available. Current is {version}, but the latest is {}.", response.name)
})
}
}
22 changes: 22 additions & 0 deletions packages/server/src/lib.rs
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ pub struct Inner {
compilers: RwLock<Vec<Compiler>>,
config: Config,
database: Database,
diagnostics: Mutex<Vec<tg::Diagnostic>>,
file_descriptor_semaphore: tokio::sync::Semaphore,
local_pool_handle: tokio_util::task::LocalPoolHandle,
lock_file: Mutex<Option<tokio::fs::File>>,
@@ -235,6 +236,9 @@ impl Server {
},
};

// Create the diagnostics.
let diagnostics = Mutex::new(Vec::new());

// Create the file system semaphore.
let file_descriptor_semaphore =
tokio::sync::Semaphore::new(config.advanced.file_descriptor_semaphore_size);
@@ -287,6 +291,7 @@ impl Server {
compilers,
config,
database,
diagnostics,
file_descriptor_semaphore,
local_pool_handle,
lock_file,
@@ -463,6 +468,18 @@ impl Server {
None
};

// Spawn the diagnostics task.
let diagnostics_task = server.config.advanced.collect_diagnostics.then(|| {
let server = server.clone();
tokio::spawn(async move {
server
.diagnostics_task()
.await
.inspect_err(|error| tracing::error!(?error))
.ok();
})
});

// Spawn the object indexer task.
let object_indexer_task = if server.config.object_indexer.is_some() {
Some(tokio::spawn({
@@ -539,6 +556,11 @@ impl Server {
}
}

// Abort the diagnostics task.
if let Some(task) = diagnostics_task {
task.abort();
}

// Abort the object index task.
if let Some(task) = object_indexer_task {
task.abort();