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
3 changes: 3 additions & 0 deletions zebra-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ getblocktemplate-rpcs = [
"zebra-chain/getblocktemplate-rpcs",
]

# Health RPC support
gethealthinfo-rpc = []

# Experimental internal miner support
internal-miner = []

Expand Down
68 changes: 68 additions & 0 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,23 @@ mod tests;
#[rpc(server)]
/// RPC method signatures.
pub trait Rpc {
// TODO: Consider mentioning this new method in the zcashd RPC docs and add a reference to it here.
/// 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).
// NOTE: We can’t put a cfg with a feature flag on this method because #[rpc] ignores it.
// So we have to provide a dummy struct and a dummy impl below to support builds without the feature.
#[rpc(name = "gethealthinfo")]
fn get_health_info(&self) -> Result<GetHealthInfo>;

#[rpc(name = "getinfo")]
/// Returns software information from the RPC server, as a [`GetInfo`] JSON struct.
///
Expand Down Expand Up @@ -506,6 +523,17 @@ where
State::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
{
#[cfg(feature = "gethealthinfo-rpc")]
fn get_health_info(&self) -> Result<GetHealthInfo> {
Ok(GetHealthInfo::snapshot())
}

// Dummy impl: return MethodNotFound if the feature is disabled.
#[cfg(not(feature = "gethealthinfo-rpc"))]
fn get_health_info(&self) -> Result<GetHealthInfo> {
Err(jsonrpc_core::Error::method_not_found())
}

fn get_info(&self) -> Result<GetInfo> {
let response = GetInfo {
build: self.build_version.clone(),
Expand Down Expand Up @@ -1396,6 +1424,46 @@ 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].
#[cfg(feature = "gethealthinfo-rpc")]
#[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,
}

// Dummy type so the trait always compiles when the feature is off.
#[cfg(not(feature = "gethealthinfo-rpc"))]
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct GetHealthInfo;

#[cfg(feature = "gethealthinfo-rpc")]
impl GetHealthInfo {
/// Creates a snapshot of the node's health and build metadata.
#[inline]
pub fn snapshot() -> Self {
Self {
status: "healthy".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
git_tag: option_env!("GIT_TAG").unwrap_or("unknown").to_string(),
git_commit: option_env!("GIT_COMMIT_FULL")
.unwrap_or("unknown")
.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
}

/// Response to a `getinfo` RPC request.
///
/// See the notes for the [`Rpc::get_info` method].
Expand Down
19 changes: 19 additions & 0 deletions zebra-rpc/src/methods/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ async fn test_rpc_response_data_for_network(network: &Network) {
return;
}

// `gethealthinfo`
#[cfg(feature = "gethealthinfo-rpc")]
{
let get_health_info = rpc
.get_health_info()
.expect("We should have a GetHealthInfo struct");
snapshot_rpc_gethealthinfo(get_health_info, &settings);
}

// `getinfo`
let get_info = rpc.get_info().expect("We should have a GetInfo struct");
snapshot_rpc_getinfo(get_info, &settings);
Expand Down Expand Up @@ -538,6 +547,16 @@ async fn test_mocked_rpc_response_data_for_network(network: &Network) {
});
}

/// Snapshot `gethealthinfo` response, using `cargo insta` and JSON serialization.
#[cfg(feature = "gethealthinfo-rpc")]
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);
});
}

/// Snapshot `getinfo` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getinfo(info: GetInfo, settings: &insta::Settings) {
settings.bind(|| {
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: 551
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: 551
expression: status_only
---
{
"status": "healthy"
}
9 changes: 7 additions & 2 deletions zebra-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,16 @@ impl RpcServer {

// Use a different tokio executor from the rest of Zebra,
// so that large RPCs and any task handling bugs don't impact Zebra.
let server_instance = ServerBuilder::new(io)
let builder = ServerBuilder::new(io)
.threads(parallel_cpu_threads)
// TODO: disable this security check if we see errors from lightwalletd
//.allowed_hosts(DomainsValidation::Disabled)
.request_middleware(middleware)
.request_middleware(middleware);

#[cfg(feature = "gethealthinfo-rpc")]
let builder = builder.health_api(("/health", "gethealthinfo"));

let server_instance = builder
.start_http(&listen_addr)
.expect("Unable to start RPC server");

Expand Down
7 changes: 6 additions & 1 deletion zebrad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ features = [

[features]
# In release builds, don't compile debug logging code, to improve performance.
default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs"]
default = ["release_max_level_info", "progress-bar", "getblocktemplate-rpcs", "gethealthinfo-rpc"]

# Default features for official ZF binary release builds
default-release-binaries = ["default", "sentry"]
Expand All @@ -71,6 +71,11 @@ getblocktemplate-rpcs = [
"zebra-chain/getblocktemplate-rpcs",
]

# Health RPC support
gethealthinfo-rpc = [
"zebra-rpc/gethealthinfo-rpc"
]

# Experimental internal miner support
internal-miner = [
"thread-priority",
Expand Down
28 changes: 28 additions & 0 deletions zebrad/tests/acceptance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,18 @@ 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 if feature is enabled.
#[cfg(feature = "gethealthinfo-rpc")]
{
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 +1663,22 @@ async fn rpc_endpoint_client_content_type() -> Result<()> {
// Create an http client
let client = RpcRequestClient::new(rpc_address);

#[cfg(feature = "gethealthinfo-rpc")]
{
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