Skip to content
Closed
Show file tree
Hide file tree
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
55 changes: 55 additions & 0 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,20 @@ pub trait Rpc {
address_strings: AddressStrings,
) -> BoxFuture<Result<Vec<GetAddressUtxos>>>;

/// Returns node health and build metadata, as a [`GetHealthInfo`] JSON struct.
///
/// zcashd reference: none
/// method: post
/// tags: control
///
/// # Notes
///
/// - This method provides a simple liveness/readiness signal and basic build info.
/// - When the HTTP health endpoint is enabled via `ServerBuilder::health_api`,
/// it is also available as `GET /health` (no parameters).
#[rpc(name = "gethealthinfo")]
fn get_health_info(&self) -> Result<GetHealthInfo>;

/// Stop the running zebrad process.
///
/// # Notes
Expand Down Expand Up @@ -1358,6 +1372,10 @@ where
.boxed()
}

fn get_health_info(&self) -> Result<GetHealthInfo> {
Ok(GetHealthInfo::new())
}

fn stop(&self) -> Result<String> {
#[cfg(not(target_os = "windows"))]
if self.network.is_regtest() {
Expand Down Expand Up @@ -1396,6 +1414,43 @@ where
.ok_or_server_error("No blocks in state")
}

/// Response to a `gethealthinfo` RPC request.
///
/// See the notes for the [`Rpc::get_health_info` method].
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct GetHealthInfo {
/// Static health status string.
status: String,
/// Zebra package version
version: String,
/// Build Git tag (if available).
git_tag: String,
/// Full Git commit hash (if available).
git_commit: String,
/// Server timestamp in RFC 3339 format.
timestamp: String,
}

impl Default for GetHealthInfo {
fn default() -> Self {
Self::new()
}
}

impl GetHealthInfo {
/// Creates a new health info instance with current node status and build metadata.
#[inline]
pub fn new() -> Self {
Self {
status: "healthy".into(),
version: env!("CARGO_PKG_VERSION").into(),
git_tag: option_env!("GIT_TAG").unwrap_or("unknown").into(),
git_commit: option_env!("GIT_COMMIT_FULL").unwrap_or("unknown").into(),
timestamp: Utc::now().to_rfc3339(),
}
}
}

/// Response to a `getinfo` RPC request.
///
/// See the notes for the [`Rpc::get_info` method].
Expand Down
15 changes: 15 additions & 0 deletions zebra-rpc/src/methods/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ async fn test_rpc_response_data_for_network(network: &Network) {
.await
.expect("We should have a vector of strings");
snapshot_rpc_getaddressutxos(get_address_utxos, &settings);

// `gethealthinfo`
let get_health_info = rpc
.get_health_info()
.expect("We should have a GetHealthInfo struct");
snapshot_rpc_gethealthinfo(get_health_info, &settings);
}

async fn test_mocked_rpc_response_data_for_network(network: &Network) {
Expand Down Expand Up @@ -669,6 +675,15 @@ fn snapshot_rpc_getaddressutxos(utxos: Vec<GetAddressUtxos>, settings: &insta::S
settings.bind(|| insta::assert_json_snapshot!("get_address_utxos", utxos));
}

/// Snapshot `gethealthinfo` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_gethealthinfo(info: GetHealthInfo, settings: &insta::Settings) {
// Snapshot only the `status` field since other fields vary per build/run.
let status_only = serde_json::json!({ "status": info.status });
settings.bind(|| {
insta::assert_json_snapshot!("get_health_info_status", status_only);
});
}

/// Utility function to convert a `Network` to a lowercase string.
fn network_string(network: &Network) -> String {
let mut net_suffix = network.to_string();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 556
expression: status_only
---
{
"status": "healthy"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 556
expression: status_only
---
{
"status": "healthy"
}
1 change: 1 addition & 0 deletions zebra-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ impl RpcServer {
// TODO: disable this security check if we see errors from lightwalletd
//.allowed_hosts(DomainsValidation::Disabled)
.request_middleware(middleware)
.health_api(("/health", "gethealthinfo"))
.start_http(&listen_addr)
.expect("Unable to start RPC server");

Expand Down
1 change: 1 addition & 0 deletions zebrad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ getblocktemplate-rpcs = [
"zebra-chain/getblocktemplate-rpcs",
]


# Experimental internal miner support
internal-miner = [
"thread-priority",
Expand Down
23 changes: 23 additions & 0 deletions zebrad/tests/acceptance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,15 @@ async fn rpc_endpoint(parallel_cpu_threads: bool) -> Result<()> {
// Create an http client
let client = RpcRequestClient::new(rpc_address);

// Make the call to the `gethealthinfo` RPC method.
let res = client.call("gethealthinfo", "[]".to_string()).await?;
assert!(res.status().is_success());

let body = res.bytes().await?;
let parsed: Value = serde_json::from_slice(&body)?;
let status = parsed["result"]["status"].as_str().unwrap();
assert_eq!(status, "healthy");

// Make the call to the `getinfo` RPC method
let res = client.call("getinfo", "[]".to_string()).await?;

Expand Down Expand Up @@ -1651,6 +1660,20 @@ async fn rpc_endpoint_client_content_type() -> Result<()> {
// Create an http client
let client = RpcRequestClient::new(rpc_address);

// Just test with plain content type, similar to getinfo.
let res = client
.call_with_content_type(
"gethealthinfo",
"[]".to_string(),
"application/json".to_string(),
)
.await?;
assert!(res.status().is_success());

let body = res.bytes().await?;
let parsed: Value = serde_json::from_slice(&body)?;
assert_eq!(parsed["result"]["status"], "healthy");

// Call to `getinfo` RPC method with a no content type.
let res = client
.call_with_no_content_type("getinfo", "[]".to_string())
Expand Down