use std::net::IpAddr;
use std::time::Duration;
use tracing::{info, instrument, warn};
use crate::adapters::ssh::SshClient;
use crate::adapters::ssh::SshConfig;
use crate::infrastructure::remote_actions::{RemoteAction, RemoteActionError};
const DEFAULT_GRAFANA_PORT: u16 = 3000;
const MAX_RETRIES: u32 = 30;
const RETRY_DELAY_SECS: u64 = 2;
pub struct GrafanaValidator {
ssh_client: SshClient,
grafana_port: u16,
}
impl GrafanaValidator {
#[must_use]
pub fn new(ssh_config: SshConfig, grafana_port: Option<u16>) -> Self {
let ssh_client = SshClient::new(ssh_config);
Self {
ssh_client,
grafana_port: grafana_port.unwrap_or(DEFAULT_GRAFANA_PORT),
}
}
}
impl RemoteAction for GrafanaValidator {
fn name(&self) -> &'static str {
"grafana-smoke-test"
}
#[instrument(
name = "grafana_smoke_test",
skip(self),
fields(
action_type = "validation",
component = "grafana",
server_ip = %server_ip,
grafana_port = self.grafana_port
)
)]
async fn execute(&self, server_ip: &IpAddr) -> Result<(), RemoteActionError> {
info!(
action = "grafana_smoke_test",
grafana_port = self.grafana_port,
"Running Grafana smoke test with retry logic (Grafana may take time to start)"
);
for attempt in 1..=MAX_RETRIES {
let command = format!(
"curl -f -s -o /dev/null http://localhost:{} && echo 'success'",
self.grafana_port
);
match self.ssh_client.execute(&command) {
Ok(output) if output.trim().contains("success") => {
info!(
action = "grafana_smoke_test",
status = "success",
attempt = attempt,
"Grafana is running and responding to HTTP requests"
);
return Ok(());
}
Ok(_) | Err(_) => {
if attempt < MAX_RETRIES {
warn!(
action = "grafana_smoke_test",
attempt = attempt,
max_retries = MAX_RETRIES,
retry_delay_secs = RETRY_DELAY_SECS,
"Grafana not ready yet, retrying..."
);
std::thread::sleep(Duration::from_secs(RETRY_DELAY_SECS));
}
}
}
}
Err(RemoteActionError::ValidationFailed {
action_name: self.name().to_string(),
message: format!(
"Grafana smoke test failed after {} retries. Grafana may not be running or accessible on port {}. \
Check that 'docker compose ps' shows Grafana container as running and healthy. \
Grafana can take 30-60 seconds to fully start.",
MAX_RETRIES,
self.grafana_port
),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
mod grafana_validator {
use super::*;
use std::path::PathBuf;
#[test]
fn it_should_have_correct_name() {
use crate::adapters::ssh::SshCredentials;
use crate::shared::Username;
use std::net::SocketAddr;
let credentials = SshCredentials::new(
PathBuf::from("test_key"),
PathBuf::from("test_key.pub"),
Username::new("test").unwrap(),
);
let ssh_config = SshConfig::new(credentials, SocketAddr::from(([127, 0, 0, 1], 22)));
let validator = GrafanaValidator::new(ssh_config, None);
assert_eq!(validator.name(), "grafana-smoke-test");
}
#[test]
fn it_should_use_default_port_when_none_provided() {
use crate::adapters::ssh::SshCredentials;
use crate::shared::Username;
use std::net::SocketAddr;
let credentials = SshCredentials::new(
PathBuf::from("test_key"),
PathBuf::from("test_key.pub"),
Username::new("test").unwrap(),
);
let ssh_config = SshConfig::new(credentials, SocketAddr::from(([127, 0, 0, 1], 22)));
let validator = GrafanaValidator::new(ssh_config, None);
assert_eq!(validator.grafana_port, DEFAULT_GRAFANA_PORT);
}
#[test]
fn it_should_use_custom_port_when_provided() {
use crate::adapters::ssh::SshCredentials;
use crate::shared::Username;
use std::net::SocketAddr;
let credentials = SshCredentials::new(
PathBuf::from("test_key"),
PathBuf::from("test_key.pub"),
Username::new("test").unwrap(),
);
let ssh_config = SshConfig::new(credentials, SocketAddr::from(([127, 0, 0, 1], 22)));
let custom_port = 4000;
let validator = GrafanaValidator::new(ssh_config, Some(custom_port));
assert_eq!(validator.grafana_port, custom_port);
}
}
}