Skip to content

Commit 0e3f55c

Browse files
committed
add health check to db connection
1 parent f51f5ba commit 0e3f55c

5 files changed

Lines changed: 181 additions & 28 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/connections.rs

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,37 @@
11
use crate::api::ApiClient;
22
use serde::{Deserialize, Serialize};
33

4+
#[derive(Deserialize, Serialize)]
5+
struct HealthResponse {
6+
#[allow(dead_code)]
7+
connection_id: String,
8+
healthy: bool,
9+
#[serde(default, skip_serializing_if = "Option::is_none")]
10+
latency_ms: Option<u64>,
11+
#[serde(default, skip_serializing_if = "Option::is_none")]
12+
error: Option<String>,
13+
}
14+
15+
fn fetch_health(api: &ApiClient, connection_id: &str, show_spinner: bool) -> HealthResponse {
16+
let spinner = show_spinner.then(|| crate::util::spinner("Checking connection health..."));
17+
let health: HealthResponse = api.get(&format!("/connections/{connection_id}/health"));
18+
if let Some(s) = spinner { s.finish_and_clear(); }
19+
health
20+
}
21+
22+
fn format_health(health: &HealthResponse) -> String {
23+
use crossterm::style::Stylize;
24+
if health.healthy {
25+
match health.latency_ms {
26+
Some(ms) => format!("{} {}", "healthy".green(), format!("({ms}ms)").dark_grey()),
27+
None => "healthy".green().to_string(),
28+
}
29+
} else {
30+
let err = health.error.as_deref().unwrap_or("unknown error");
31+
format!("{} — {}", "unhealthy".red(), err)
32+
}
33+
}
34+
435
#[derive(Deserialize, Serialize)]
536
struct ConnectionType {
637
name: String,
@@ -88,23 +119,60 @@ struct ListResponse {
88119

89120
pub fn get(workspace_id: &str, connection_id: &str, format: &str) {
90121
let api = ApiClient::new(Some(workspace_id));
122+
let is_table = format == "table";
123+
124+
let spinner = is_table.then(|| crate::util::spinner("Fetching connection..."));
91125
let detail: ConnectionDetail = api.get(&format!("/connections/{connection_id}"));
126+
if let Some(s) = spinner { s.finish_and_clear(); }
127+
128+
let health = fetch_health(&api, connection_id, is_table);
92129

93130
match format {
94-
"json" => println!("{}", serde_json::to_string_pretty(&detail).unwrap()),
95-
"yaml" => print!("{}", serde_yaml::to_string(&detail).unwrap()),
131+
"json" => {
132+
let combined = serde_json::json!({
133+
"id": detail.id,
134+
"name": detail.name,
135+
"source_type": detail.source_type,
136+
"table_count": detail.table_count,
137+
"synced_table_count": detail.synced_table_count,
138+
"health": &health,
139+
});
140+
println!("{}", serde_json::to_string_pretty(&combined).unwrap());
141+
}
142+
"yaml" => {
143+
let combined = serde_json::json!({
144+
"id": detail.id,
145+
"name": detail.name,
146+
"source_type": detail.source_type,
147+
"table_count": detail.table_count,
148+
"synced_table_count": detail.synced_table_count,
149+
"health": &health,
150+
});
151+
print!("{}", serde_yaml::to_string(&combined).unwrap());
152+
}
96153
"table" => {
97154
use crossterm::style::Stylize;
98155
let label = |l: &str| format!("{:<16}", l).dark_grey().to_string();
99156
println!("{}{}", label("id:"), detail.id.dark_cyan());
100157
println!("{}{}", label("name:"), detail.name.white());
101158
println!("{}{}", label("source_type:"), detail.source_type.green());
102159
println!("{}{}", label("tables:"), format!("{} synced / {} total", detail.synced_table_count.to_string().cyan(), detail.table_count.to_string().cyan()));
160+
println!("{}{}", label("health:"), format_health(&health));
103161
}
104162
_ => unreachable!(),
105163
}
106164
}
107165

166+
#[derive(Deserialize, Serialize)]
167+
struct CreateResponse {
168+
id: String,
169+
name: String,
170+
source_type: String,
171+
tables_discovered: u64,
172+
discovery_status: String,
173+
discovery_error: Option<String>,
174+
}
175+
108176
pub fn create(
109177
workspace_id: &str,
110178
name: &str,
@@ -127,22 +195,53 @@ pub fn create(
127195
});
128196

129197
let api = ApiClient::new(Some(workspace_id));
198+
let is_table = format == "table";
130199

131-
#[derive(Deserialize, Serialize)]
132-
struct CreateResponse {
133-
id: String,
134-
name: String,
135-
source_type: String,
136-
tables_discovered: u64,
137-
discovery_status: String,
138-
discovery_error: Option<String>,
200+
let spinner = is_table.then(|| crate::util::spinner("Creating connection..."));
201+
let (status, resp_body) = api.post_raw("/connections", &body);
202+
if let Some(s) = &spinner { s.finish_and_clear(); }
203+
204+
if !status.is_success() {
205+
use crossterm::style::Stylize;
206+
eprintln!("{}", crate::util::api_error(resp_body).red());
207+
std::process::exit(1);
139208
}
140209

141-
let result: CreateResponse = api.post("/connections", &body);
210+
let result: CreateResponse = match serde_json::from_str(&resp_body) {
211+
Ok(v) => v,
212+
Err(e) => {
213+
eprintln!("error parsing response: {e}");
214+
std::process::exit(1);
215+
}
216+
};
217+
218+
let health = fetch_health(&api, &result.id, is_table);
142219

143220
match format {
144-
"json" => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
145-
"yaml" => print!("{}", serde_yaml::to_string(&result).unwrap()),
221+
"json" => {
222+
let combined = serde_json::json!({
223+
"id": result.id,
224+
"name": result.name,
225+
"source_type": result.source_type,
226+
"tables_discovered": result.tables_discovered,
227+
"discovery_status": result.discovery_status,
228+
"discovery_error": result.discovery_error,
229+
"health": &health,
230+
});
231+
println!("{}", serde_json::to_string_pretty(&combined).unwrap());
232+
}
233+
"yaml" => {
234+
let combined = serde_json::json!({
235+
"id": result.id,
236+
"name": result.name,
237+
"source_type": result.source_type,
238+
"tables_discovered": result.tables_discovered,
239+
"discovery_status": result.discovery_status,
240+
"discovery_error": result.discovery_error,
241+
"health": &health,
242+
});
243+
print!("{}", serde_yaml::to_string(&combined).unwrap());
244+
}
146245
"table" => {
147246
use crossterm::style::Stylize;
148247
println!("{}", "Connection created".green());
@@ -156,9 +255,14 @@ pub fn create(
156255
_ => result.discovery_status.yellow().to_string(),
157256
};
158257
println!("discovery_status: {status_colored}");
258+
println!("health: {}", format_health(&health));
159259
}
160260
_ => unreachable!(),
161261
}
262+
263+
if !health.healthy {
264+
std::process::exit(1);
265+
}
162266
}
163267

164268
pub fn list(workspace_id: &str, format: &str) {

src/connections_new.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,9 +268,37 @@ pub fn run(workspace_id: &str) {
268268
discovery_error: Option<String>,
269269
}
270270

271-
let result: CreateResponse = api.post("/connections", &body);
271+
#[derive(serde::Deserialize)]
272+
struct HealthResponse {
273+
healthy: bool,
274+
#[serde(default)]
275+
latency_ms: Option<u64>,
276+
#[serde(default)]
277+
error: Option<String>,
278+
}
279+
280+
let create_spinner = crate::util::spinner("Creating connection...");
281+
let (status_code, resp_body) = api.post_raw("/connections", &body);
282+
create_spinner.finish_and_clear();
272283

273284
use crossterm::style::Stylize;
285+
if !status_code.is_success() {
286+
eprintln!("{}", crate::util::api_error(resp_body).red());
287+
std::process::exit(1);
288+
}
289+
290+
let result: CreateResponse = match serde_json::from_str(&resp_body) {
291+
Ok(v) => v,
292+
Err(e) => {
293+
eprintln!("error parsing response: {e}");
294+
std::process::exit(1);
295+
}
296+
};
297+
298+
let health_spinner = crate::util::spinner("Checking connection health...");
299+
let health: HealthResponse = api.get(&format!("/connections/{}/health", result.id));
300+
health_spinner.finish_and_clear();
301+
274302
println!("{}", "Connection created".green());
275303
println!("id: {}", result.id);
276304
println!("name: {}", result.name);
@@ -282,4 +310,18 @@ pub fn run(workspace_id: &str) {
282310
_ => result.discovery_status.yellow().to_string(),
283311
};
284312
println!("discovery_status: {status}");
313+
let health_str = if health.healthy {
314+
match health.latency_ms {
315+
Some(ms) => format!("{} {}", "healthy".green(), format!("({ms}ms)").dark_grey()),
316+
None => "healthy".green().to_string(),
317+
}
318+
} else {
319+
let err = health.error.as_deref().unwrap_or("unknown error");
320+
format!("{} — {}", "unhealthy".red(), err)
321+
};
322+
println!("health: {health_str}");
323+
324+
if !health.healthy {
325+
std::process::exit(1);
326+
}
285327
}

src/query.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,7 @@ pub fn execute(sql: &str, workspace_id: &str, connection: Option<&str>, format:
5757
body["connection_id"] = Value::String(conn.to_string());
5858
}
5959

60-
let spinner = indicatif::ProgressBar::new_spinner();
61-
spinner.set_style(
62-
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}")
63-
.unwrap(),
64-
);
65-
spinner.set_message("running query...");
66-
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
60+
let spinner = crate::util::spinner("running query...");
6761

6862
let (status, resp_body) = api.post_raw("/query", &body);
6963
spinner.finish_and_clear();

src/util.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
use std::sync::atomic::{AtomicBool, Ordering};
2+
use std::time::Duration;
3+
4+
/// Create a steady-ticking spinner with a cyan glyph and the given message.
5+
/// Writes to stderr so stdout (json/yaml output) stays clean.
6+
pub fn spinner(msg: &str) -> indicatif::ProgressBar {
7+
let pb = indicatif::ProgressBar::new_spinner();
8+
pb.set_style(
9+
indicatif::ProgressStyle::with_template("{spinner:.cyan} {msg}").unwrap(),
10+
);
11+
pb.set_message(msg.to_string());
12+
pb.enable_steady_tick(Duration::from_millis(80));
13+
pb
14+
}
215

316
static DEBUG: AtomicBool = AtomicBool::new(false);
417

0 commit comments

Comments
 (0)