diff --git a/README.md b/README.md index 23b1ffa..eea4d66 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ typesense-prometheus-exporter [OPTIONS] --typesense-host --type - `--typesense-protocol `: Typesense protocol (env: TYPESENSE_PROTOCOL, default: http). - `--typesense-api-key `: Typesense API key (env: TYPESENSE_API_KEY). - `--typesense-port `: Typesense port number (env: TYPESENSE_PORT, default: 8108). +- `--typesense-timeout `: Timeout for Typesense API requests in seconds (env: TYPESENSE_TIMEOUT, default: -1 to disable). - `--exporter-bind-address `: Internal server bind address (env: EXPORTER_BIND_ADDRESS, default: 0.0.0.0). - `--exporter-bind-port `: Internal server bind port (env: EXPORTER_BIND_PORT, default: 8888). diff --git a/src/cli.rs b/src/cli.rs index 4d99090..091390b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,4 +27,8 @@ pub struct CliArgs { /// Bind port for internal server #[arg(long, env, default_value_t = 8888)] pub(crate) exporter_bind_port: u16, + + /// Timeout for Typesense API requests in seconds (-1 to disable) + #[arg(long, env, default_value_t = -1)] + pub(crate) typesense_timeout: i64, } diff --git a/src/prometheus_exp.rs b/src/prometheus_exp.rs index f0ec49f..9ef0166 100644 --- a/src/prometheus_exp.rs +++ b/src/prometheus_exp.rs @@ -4,7 +4,10 @@ use std::sync::Arc; use crate::{ cli::CliArgs, typesense::models::{ - typesense_metrics_model::TypesenseMetrics, typesense_stats_model::TypesenseStats, + typesense_metrics_model::TypesenseMetrics, + typesense_stats_model::TypesenseStats, + typesense_health_model::TypesenseHealth, + typesense_debug_model::TypesenseDebug, }, }; use prometheus::{register_gauge_vec_with_registry, Encoder, Registry, TextEncoder}; @@ -13,6 +16,8 @@ use regex::Regex; pub(crate) async fn generate_metrics( ts_metrics: TypesenseMetrics, ts_stats: TypesenseStats, + ts_health: TypesenseHealth, + ts_debug: TypesenseDebug, cli_args: Arc, ) -> String { let registry = Registry::new(); @@ -398,6 +403,42 @@ pub(crate) async fn generate_metrics( } } + let typesense_health = register_gauge_vec_with_registry!( + "typesense_health", + "Health status of Typesense instance (1 = healthy, 0 = unhealthy)", + &["host", "port"], + registry + ) + .unwrap(); + + typesense_health + .with_label_values(&[ + &cli_args.typesense_host, + &cli_args.typesense_port.to_string(), + ]) + .set(if ts_health.ok { 1.0 } else { 0.0 }); + + let typesense_raft_state = register_gauge_vec_with_registry!( + "typesense_raft_state", + "Raft state of Typesense node (1 = leader, 4 = follower, 0 = other)", + &["host", "port"], + registry + ) + .unwrap(); + + let state_value = match ts_debug.state { + 1 => 1.0, + 4 => 4.0, + _ => 0.0, + }; + + typesense_raft_state + .with_label_values(&[ + &cli_args.typesense_host, + &cli_args.typesense_port.to_string(), + ]) + .set(state_value); + let encoder = TextEncoder::new(); let metric_families = registry.gather(); let mut buffer = Vec::new(); diff --git a/src/server.rs b/src/server.rs index 05cc5b3..6c0026b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,7 +3,12 @@ use std::sync::Arc; use crate::prometheus_exp; use crate::{ cli::CliArgs, - typesense::{metrics::get_typesense_metrics, stats::get_typesense_stats}, + typesense::{ + stats::get_typesense_stats, + metrics::get_typesense_metrics, + health::get_typesense_health, + debug::get_typesense_debug, + }, }; use axum::extract::State; @@ -59,15 +64,18 @@ async fn root() -> &'static str { } async fn metrics_route_handler(State(args): State>) -> String { - let (metrics_data, stats_data) = future::join( + let (metrics_data, stats_data, health_data, debug_data) = tokio::join!( get_typesense_metrics(args.clone()), get_typesense_stats(args.clone()), - ) - .await; + get_typesense_health(args.clone()), + get_typesense_debug(args.clone()), + ); let promdata = prometheus_exp::generate_metrics( metrics_data.unwrap().clone(), stats_data.unwrap().clone(), + health_data.unwrap().clone(), + debug_data.unwrap().clone(), args.clone(), ) .await; diff --git a/src/typesense/debug.rs b/src/typesense/debug.rs new file mode 100644 index 0000000..dd96988 --- /dev/null +++ b/src/typesense/debug.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; +use std::time::Duration; + +use crate::{cli::CliArgs, typesense::models::typesense_debug_model::TypesenseDebug}; +use axum::Error; + +pub async fn get_typesense_debug(args: Arc) -> Result { + let mut debug_data: TypesenseDebug = TypesenseDebug::default(); + + let mut client_builder = reqwest::Client::builder(); + if args.typesense_timeout >= 0 { + client_builder = client_builder.timeout(Duration::from_secs(args.typesense_timeout as u64)); + } + let client = client_builder.build().unwrap(); + + let url = format!( + "{}://{}:{}/debug", + args.typesense_protocol, args.typesense_host, args.typesense_port + ); + + let res = match client + .get(url) + .header("X-TYPESENSE-API-KEY", format!("{}", args.typesense_api_key)) + .send() + .await + { + Ok(res) => res, + Err(e) => { + println!("Debug endpoint: request failed (timeout/connection error): {:?}", e); + return Ok(debug_data); + } + }; + + match res.status() { + reqwest::StatusCode::OK => { + match res.json::().await { + Ok(parsed) => { + debug_data = parsed; + } + Err(er) => println!( + "Hm, the response didn't match the shape we expected. {:?}", + er + ), + }; + } + reqwest::StatusCode::UNAUTHORIZED => { + println!("Debug endpoint: Need to grab a new token"); + } + _ => { + println!("Uh oh! Something unexpected happened."); + } + }; + + Ok(debug_data) +} + diff --git a/src/typesense/health.rs b/src/typesense/health.rs new file mode 100644 index 0000000..a063f53 --- /dev/null +++ b/src/typesense/health.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; +use std::time::Duration; + +use crate::{cli::CliArgs, typesense::models::typesense_health_model::TypesenseHealth}; +use axum::Error; + +pub async fn get_typesense_health(args: Arc) -> Result { + let mut health_data: TypesenseHealth = TypesenseHealth::default(); + + let mut client_builder = reqwest::Client::builder(); + if args.typesense_timeout >= 0 { + client_builder = client_builder.timeout(Duration::from_secs(args.typesense_timeout as u64)); + } + let client = client_builder.build().unwrap(); + + let url = format!( + "{}://{}:{}/health", + args.typesense_protocol, args.typesense_host, args.typesense_port + ); + + let res = match client + .get(url) + .send() + .await + { + Ok(res) => res, + Err(e) => { + println!("Health endpoint: request failed (timeout/connection error): {:?}", e); + return Ok(health_data); + } + }; + + match res.status() { + reqwest::StatusCode::OK => { + match res.json::().await { + Ok(parsed) => { + health_data = parsed; + } + Err(er) => println!( + "Hm, the response didn't match the shape we expected. {:?}", + er + ), + }; + } + _ => { + println!("Uh oh! Something unexpected happened."); + } + }; + + Ok(health_data) +} + diff --git a/src/typesense/metrics.rs b/src/typesense/metrics.rs index 8b37506..fb6dfc0 100644 --- a/src/typesense/metrics.rs +++ b/src/typesense/metrics.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::Duration; use crate::{cli::CliArgs, typesense::models::typesense_metrics_model::TypesenseMetrics}; use axum::Error; @@ -6,19 +7,29 @@ use axum::Error; pub async fn get_typesense_metrics(args: Arc) -> Result { let mut stats_data: TypesenseMetrics = TypesenseMetrics::default(); - let client = reqwest::Client::new(); + let mut client_builder = reqwest::Client::builder(); + if args.typesense_timeout >= 0 { + client_builder = client_builder.timeout(Duration::from_secs(args.typesense_timeout as u64)); + } + let client = client_builder.build().unwrap(); let url: String = format!( "{}://{}:{}/metrics.json", args.typesense_protocol, args.typesense_host, args.typesense_port ); - let res = client + let res = match client .get(url) .header("X-TYPESENSE-API-KEY", format!("{}", args.typesense_api_key)) .send() .await - .unwrap(); + { + Ok(res) => res, + Err(e) => { + println!("Metrics endpoint: request failed (timeout/connection error): {:?}", e); + return Ok(stats_data); + } + }; match res.status() { reqwest::StatusCode::OK => { @@ -36,7 +47,7 @@ pub async fn get_typesense_metrics(args: Arc) -> Result { - panic!("Uh oh! Something unexpected happened."); + println!("Uh oh! Something unexpected happened."); } }; diff --git a/src/typesense/mod.rs b/src/typesense/mod.rs index b49b373..4eb279d 100644 --- a/src/typesense/mod.rs +++ b/src/typesense/mod.rs @@ -1,3 +1,5 @@ pub mod metrics; pub mod stats; +pub mod health; +pub mod debug; pub mod models; \ No newline at end of file diff --git a/src/typesense/models/mod.rs b/src/typesense/models/mod.rs index f7f46ee..9e3086b 100644 --- a/src/typesense/models/mod.rs +++ b/src/typesense/models/mod.rs @@ -1,2 +1,4 @@ pub mod typesense_metrics_model; -pub mod typesense_stats_model; \ No newline at end of file +pub mod typesense_stats_model; +pub mod typesense_health_model; +pub mod typesense_debug_model; \ No newline at end of file diff --git a/src/typesense/models/typesense_debug_model.rs b/src/typesense/models/typesense_debug_model.rs new file mode 100644 index 0000000..55466a5 --- /dev/null +++ b/src/typesense/models/typesense_debug_model.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TypesenseDebug { + pub version: String, + pub state: i32, +} + +impl Default for TypesenseDebug { + fn default() -> TypesenseDebug { + TypesenseDebug { + version: String::new(), + state: 0, + } + } +} + diff --git a/src/typesense/models/typesense_health_model.rs b/src/typesense/models/typesense_health_model.rs new file mode 100644 index 0000000..ee102a6 --- /dev/null +++ b/src/typesense/models/typesense_health_model.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct TypesenseHealth { + pub ok: bool, + #[serde(default)] + pub resource_error: Option, +} + +impl Default for TypesenseHealth { + fn default() -> TypesenseHealth { + TypesenseHealth { + ok: false, + resource_error: None, + } + } +} + diff --git a/src/typesense/stats.rs b/src/typesense/stats.rs index a7ab01b..b8098e4 100644 --- a/src/typesense/stats.rs +++ b/src/typesense/stats.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::Duration; use crate::{cli::CliArgs, typesense::models::typesense_stats_model::TypesenseStats}; use axum::Error; @@ -6,19 +7,29 @@ use axum::Error; pub async fn get_typesense_stats(args: Arc) -> Result { let mut stats_data: TypesenseStats = TypesenseStats::default(); - let client = reqwest::Client::new(); + let mut client_builder = reqwest::Client::builder(); + if args.typesense_timeout >= 0 { + client_builder = client_builder.timeout(Duration::from_secs(args.typesense_timeout as u64)); + } + let client = client_builder.build().unwrap(); let url = format!( "{}://{}:{}/stats.json", args.typesense_protocol, args.typesense_host, args.typesense_port ); - let res = client + let res = match client .get(url) .header("X-TYPESENSE-API-KEY", format!("{}", args.typesense_api_key)) .send() .await - .unwrap(); + { + Ok(res) => res, + Err(e) => { + println!("Stats endpoint: request failed (timeout/connection error): {:?}", e); + return Ok(stats_data); + } + }; match res.status() { reqwest::StatusCode::OK => { @@ -33,7 +44,7 @@ pub async fn get_typesense_stats(args: Arc) -> Result { - panic!("Uh oh! Something unexpected happened."); + println!("Uh oh! Something unexpected happened."); } };