From 23334a37e9852cd40a77e77190a0ea38e22cbfa5 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 25 Aug 2025 08:42:56 +0200 Subject: [PATCH 01/18] Add health endpoint --- health-test.toml | 87 ++++++++++++++ zebrad/Cargo.toml | 5 +- zebrad/src/application.rs | 4 + zebrad/src/components.rs | 1 + zebrad/src/components/health.rs | 120 +++++++++++++++++++ zebrad/src/config.rs | 4 + zebrad/tests/common/configs/health-test.toml | 12 ++ 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 health-test.toml create mode 100644 zebrad/src/components/health.rs create mode 100644 zebrad/tests/common/configs/health-test.toml diff --git a/health-test.toml b/health-test.toml new file mode 100644 index 00000000000..3cff1bb3b16 --- /dev/null +++ b/health-test.toml @@ -0,0 +1,87 @@ +# Default configuration for zebrad. +# +# This file can be used as a skeleton for custom configs. +# +# Unspecified fields use default values. Optional fields are Some(field) if the +# field is present and None if it is absent. +# +# This file is generated as an example using zebrad's current defaults. +# You should set only the config options you want to keep, and delete the rest. +# Only a subset of fields are present in the skeleton, since optional values +# whose default is None are omitted. +# +# The config format (including a complete list of sections and fields) is +# documented here: +# https://docs.rs/zebrad/latest/zebrad/config/struct.ZebradConfig.html +# +# zebrad attempts to load configs in the following order: +# +# 1. The -c flag on the command line, e.g., `zebrad -c myconfig.toml start`; +# 2. The file `zebrad.toml` in the users's preference directory (platform-dependent); +# 3. The default config. +# +# The user's preference directory and the default path to the `zebrad` config are platform dependent, +# based on `dirs::preference_dir`, see https://docs.rs/dirs/latest/dirs/fn.preference_dir.html : +# +# | Platform | Value | Example | +# | -------- | ------------------------------------- | ---------------------------------------------- | +# | Linux | `$XDG_CONFIG_HOME` or `$HOME/.config` | `/home/alice/.config/zebrad.toml` | +# | macOS | `$HOME/Library/Preferences` | `/Users/Alice/Library/Preferences/zebrad.toml` | +# | Windows | `{FOLDERID_RoamingAppData}` | `C:\Users\Alice\AppData\Local\zebrad.toml` | + +[consensus] +checkpoint_sync = true + +[health] +endpoint_addr = "127.0.0.1:8080" + +[mempool] +eviction_memory_time = "1h" +tx_cost_limit = 80000000 + +[metrics] + +[mining] +debug_like_zcashd = true + +[network] +cache_dir = true +crawl_new_peer_interval = "1m 1s" +initial_mainnet_peers = [ + "dnsseed.z.cash:8233", + "dnsseed.str4d.xyz:8233", + "mainnet.seeder.zfnd.org:8233", + "mainnet.is.yolo.money:8233", +] +initial_testnet_peers = [ + "dnsseed.testnet.z.cash:18233", + "testnet.seeder.zfnd.org:18233", + "testnet.is.yolo.money:18233", +] +listen_addr = "0.0.0.0:8233" +max_connections_per_ip = 1 +network = "Mainnet" +peerset_initial_target_size = 25 + +[rpc] +cookie_dir = "/Users/arsenikalma/Library/Caches/zebra" +debug_force_finished_sync = false +enable_cookie_auth = true +parallel_cpu_threads = 0 + +[state] +cache_dir = "/Users/arsenikalma/Library/Caches/zebra" +delete_old_database = true +ephemeral = false + +[sync] +checkpoint_verify_concurrency_limit = 1000 +download_concurrency_limit = 50 +full_verify_concurrency_limit = 20 +parallel_cpu_threads = 0 + +[tracing] +buffer_limit = 128000 +force_use_color = false +use_color = true +use_journald = false diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 72a8afd7dc0..218d877939d 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -89,6 +89,7 @@ elasticsearch = [ sentry = ["dep:sentry"] journald = ["tracing-journald"] filter-reload = ["hyper", "http-body-util", "hyper-util", "bytes"] +health-endpoint = ["hyper", "hyper-util", "serde/derive", "chrono/serde", "serde_json"] progress-bar = [ "howudoin", @@ -231,6 +232,9 @@ http-body-util = { version = "0.1.2", optional = true } hyper-util = { version = "0.1.9", optional = true } bytes = { version = "1.8.0", optional = true } +# prod feature health-endpoint +serde_json = { version = "1.0.132", features = ["preserve_order"], optional = true } + # prod feature prometheus metrics-exporter-prometheus = { version = "0.16.0", default-features = false, features = ["http-listener"], optional = true } @@ -267,7 +271,6 @@ regex = "1.11.0" insta = { version = "1.40.0", features = ["json"] } # zebra-rpc needs the preserve_order feature, it also makes test results more stable -serde_json = { version = "1.0.132", features = ["preserve_order"] } tempfile = "3.13.0" hyper = { version = "1.5.0", features = ["http1", "http2", "server"]} diff --git a/zebrad/src/application.rs b/zebrad/src/application.rs index 5794e7dc556..ad6fffcd0df 100644 --- a/zebrad/src/application.rs +++ b/zebrad/src/application.rs @@ -232,6 +232,8 @@ impl Application for ZebradApp { use crate::components::{ metrics::MetricsEndpoint, tokio::TokioComponent, tracing::TracingEndpoint, }; + #[cfg(feature = "health-endpoint")] + use crate::components::health::HealthEndpoint; let mut components = self.framework_components(command)?; @@ -482,6 +484,8 @@ impl Application for ZebradApp { components.push(Box::new(TokioComponent::new()?)); components.push(Box::new(TracingEndpoint::new(cfg_ref)?)); components.push(Box::new(MetricsEndpoint::new(&metrics_config)?)); + #[cfg(feature = "health-endpoint")] + components.push(Box::new(HealthEndpoint::new(&config.health)?)); } self.state.components_mut().register(components)?; diff --git a/zebrad/src/components.rs b/zebrad/src/components.rs index 43b051f1209..864c669f678 100644 --- a/zebrad/src/components.rs +++ b/zebrad/src/components.rs @@ -5,6 +5,7 @@ //! component and dependency injection models are designed to work together, but //! don't fit the async context well. +pub mod health; pub mod inbound; #[allow(missing_docs)] pub mod mempool; diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs new file mode 100644 index 00000000000..4adc17acae1 --- /dev/null +++ b/zebrad/src/components/health.rs @@ -0,0 +1,120 @@ +//! A simple HTTP health endpoint for Zebra. + +use std::net::SocketAddr; + +use abscissa_core::{Component, FrameworkError}; +use serde::{Deserialize, Serialize}; +use tokio::spawn; +use hyper::{Request, Response, StatusCode}; +use hyper::body::Incoming; +use hyper::service::service_fn; +use std::convert::Infallible; +use tracing::{info, error}; +use crate::config::ZebradConfig; + +/// Abscissa component which runs a health endpoint. +#[derive(Debug, Component)] +#[cfg(feature = "health-endpoint")] +pub struct HealthEndpoint {} + +#[cfg(feature = "health-endpoint")] +impl HealthEndpoint { + /// Create the component. + pub fn new(config: &Config) -> Result { + if let Some(addr) = config.endpoint_addr { + info!("Trying to open health endpoint at {}...", addr); + + // Start the health endpoint server in a separate thread to avoid Tokio runtime issues + std::thread::spawn(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + if let Err(e) = Self::run_server(addr).await { + error!("Health endpoint server failed: {}", e); + } + }); + }); + + info!("Opened health endpoint at {}", addr); + } + + Ok(Self {}) + } + + async fn run_server(addr: SocketAddr) -> Result<(), Box> { + let listener = tokio::net::TcpListener::bind(addr).await?; + + loop { + let (stream, _) = listener.accept().await?; + let io = hyper_util::rt::TokioIo::new(stream); + + spawn(async move { + if let Err(err) = hyper::server::conn::http1::Builder::new() + .serve_connection(io, service_fn(Self::handle_request)) + .await + { + error!("Health endpoint connection error: {}", err); + } + }); + } + } + + async fn handle_request(req: Request) -> Result, Infallible> { + // Check if the request is for the health endpoint + if req.uri().path() != "/health" { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Type", "application/json") + .body("{\"error\": \"Not Found\"}".to_string()) + .unwrap()); + } + + let health_info = HealthInfo { + 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(), + }; + + let response_body = serde_json::to_string_pretty(&health_info) + .unwrap_or_else(|_| "{\"error\": \"Failed to serialize health info\"}".to_string()); + + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(response_body) + .unwrap()) + } +} + + + +/// Health information response. +#[derive(Debug, Serialize)] +struct HealthInfo { + status: String, + version: String, + git_tag: String, + git_commit: String, + timestamp: String, +} + +/// Health endpoint configuration section. +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(deny_unknown_fields, default)] +#[cfg(feature = "health-endpoint")] +pub struct Config { + /// The address used for the health endpoint. + /// + /// The endpoint is disabled if this is set to `None`. + pub endpoint_addr: Option, +} + +#[cfg(feature = "health-endpoint")] +impl Default for Config { + fn default() -> Self { + Self { + endpoint_addr: Some("127.0.0.1:8080".parse().unwrap()), + } + } +} diff --git a/zebrad/src/config.rs b/zebrad/src/config.rs index a6174599ef8..21c7e20b2ae 100644 --- a/zebrad/src/config.rs +++ b/zebrad/src/config.rs @@ -51,6 +51,10 @@ pub struct ZebradConfig { /// RPC configuration pub rpc: zebra_rpc::config::Config, + #[cfg(feature = "health-endpoint")] + /// Health endpoint configuration + pub health: crate::components::health::Config, + #[serde(skip_serializing_if = "zebra_rpc::config::mining::Config::skip_getblocktemplate")] /// Mining configuration pub mining: zebra_rpc::config::mining::Config, diff --git a/zebrad/tests/common/configs/health-test.toml b/zebrad/tests/common/configs/health-test.toml new file mode 100644 index 00000000000..8f823e4818a --- /dev/null +++ b/zebrad/tests/common/configs/health-test.toml @@ -0,0 +1,12 @@ +[health] +endpoint_addr = "127.0.0.1:8080" + +[network] +network = "Testnet" + +[metrics] +endpoint_addr = "127.0.0.1:9999" + +[tracing] +endpoint_addr = "127.0.0.1:3000" + From 77d6dc4ff2c8a52da95427c115ab26bcc700e31c Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 25 Aug 2025 08:58:59 +0200 Subject: [PATCH 02/18] Add health endpoint clean --- zebrad/Cargo.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 218d877939d..ffdd8fa92fd 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -89,7 +89,7 @@ elasticsearch = [ sentry = ["dep:sentry"] journald = ["tracing-journald"] filter-reload = ["hyper", "http-body-util", "hyper-util", "bytes"] -health-endpoint = ["hyper", "hyper-util", "serde/derive", "chrono/serde", "serde_json"] +health-endpoint = ["hyper", "hyper-util", "serde/derive", "chrono/serde"] progress-bar = [ "howudoin", @@ -232,9 +232,6 @@ http-body-util = { version = "0.1.2", optional = true } hyper-util = { version = "0.1.9", optional = true } bytes = { version = "1.8.0", optional = true } -# prod feature health-endpoint -serde_json = { version = "1.0.132", features = ["preserve_order"], optional = true } - # prod feature prometheus metrics-exporter-prometheus = { version = "0.16.0", default-features = false, features = ["http-listener"], optional = true } @@ -271,6 +268,7 @@ regex = "1.11.0" insta = { version = "1.40.0", features = ["json"] } # zebra-rpc needs the preserve_order feature, it also makes test results more stable +serde_json = { version = "1.0.132", features = ["preserve_order"] } tempfile = "3.13.0" hyper = { version = "1.5.0", features = ["http1", "http2", "server"]} From 7efbbc6b06a6d05345c9917ec973f1b39ba973d8 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 25 Aug 2025 10:20:06 +0200 Subject: [PATCH 03/18] Add health endpoint cleaning --- health-test.toml | 87 -------------------- zebrad/Cargo.toml | 9 +- zebrad/src/components/health.rs | 23 ++---- zebrad/tests/common/configs/health-test.toml | 12 --- 4 files changed, 14 insertions(+), 117 deletions(-) delete mode 100644 health-test.toml delete mode 100644 zebrad/tests/common/configs/health-test.toml diff --git a/health-test.toml b/health-test.toml deleted file mode 100644 index 3cff1bb3b16..00000000000 --- a/health-test.toml +++ /dev/null @@ -1,87 +0,0 @@ -# Default configuration for zebrad. -# -# This file can be used as a skeleton for custom configs. -# -# Unspecified fields use default values. Optional fields are Some(field) if the -# field is present and None if it is absent. -# -# This file is generated as an example using zebrad's current defaults. -# You should set only the config options you want to keep, and delete the rest. -# Only a subset of fields are present in the skeleton, since optional values -# whose default is None are omitted. -# -# The config format (including a complete list of sections and fields) is -# documented here: -# https://docs.rs/zebrad/latest/zebrad/config/struct.ZebradConfig.html -# -# zebrad attempts to load configs in the following order: -# -# 1. The -c flag on the command line, e.g., `zebrad -c myconfig.toml start`; -# 2. The file `zebrad.toml` in the users's preference directory (platform-dependent); -# 3. The default config. -# -# The user's preference directory and the default path to the `zebrad` config are platform dependent, -# based on `dirs::preference_dir`, see https://docs.rs/dirs/latest/dirs/fn.preference_dir.html : -# -# | Platform | Value | Example | -# | -------- | ------------------------------------- | ---------------------------------------------- | -# | Linux | `$XDG_CONFIG_HOME` or `$HOME/.config` | `/home/alice/.config/zebrad.toml` | -# | macOS | `$HOME/Library/Preferences` | `/Users/Alice/Library/Preferences/zebrad.toml` | -# | Windows | `{FOLDERID_RoamingAppData}` | `C:\Users\Alice\AppData\Local\zebrad.toml` | - -[consensus] -checkpoint_sync = true - -[health] -endpoint_addr = "127.0.0.1:8080" - -[mempool] -eviction_memory_time = "1h" -tx_cost_limit = 80000000 - -[metrics] - -[mining] -debug_like_zcashd = true - -[network] -cache_dir = true -crawl_new_peer_interval = "1m 1s" -initial_mainnet_peers = [ - "dnsseed.z.cash:8233", - "dnsseed.str4d.xyz:8233", - "mainnet.seeder.zfnd.org:8233", - "mainnet.is.yolo.money:8233", -] -initial_testnet_peers = [ - "dnsseed.testnet.z.cash:18233", - "testnet.seeder.zfnd.org:18233", - "testnet.is.yolo.money:18233", -] -listen_addr = "0.0.0.0:8233" -max_connections_per_ip = 1 -network = "Mainnet" -peerset_initial_target_size = 25 - -[rpc] -cookie_dir = "/Users/arsenikalma/Library/Caches/zebra" -debug_force_finished_sync = false -enable_cookie_auth = true -parallel_cpu_threads = 0 - -[state] -cache_dir = "/Users/arsenikalma/Library/Caches/zebra" -delete_old_database = true -ephemeral = false - -[sync] -checkpoint_verify_concurrency_limit = 1000 -download_concurrency_limit = 50 -full_verify_concurrency_limit = 20 -parallel_cpu_threads = 0 - -[tracing] -buffer_limit = 128000 -force_use_color = false -use_color = true -use_journald = false diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index ffdd8fa92fd..4b5ba95a03b 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -89,7 +89,7 @@ elasticsearch = [ sentry = ["dep:sentry"] journald = ["tracing-journald"] filter-reload = ["hyper", "http-body-util", "hyper-util", "bytes"] -health-endpoint = ["hyper", "hyper-util", "serde/derive", "chrono/serde"] +health-endpoint = ["hyper", "hyper-util", "serde/derive", "chrono/serde", "serde_json"] progress-bar = [ "howudoin", @@ -235,9 +235,12 @@ bytes = { version = "1.8.0", optional = true } # prod feature prometheus metrics-exporter-prometheus = { version = "0.16.0", default-features = false, features = ["http-listener"], optional = true } +# zebra-rpc needs the preserve_order feature, it also makes test results more stable +serde_json = { version = "1.0.132", features = ["preserve_order"], optional = true } + # prod feature release_max_level_info # -# zebrad uses tracing for logging, +# zebra uses tracing for logging, # we only use `log` to set and print the static log levels in transitive dependencies log = "0.4.22" @@ -267,8 +270,6 @@ once_cell = "1.20.2" regex = "1.11.0" insta = { version = "1.40.0", features = ["json"] } -# zebra-rpc needs the preserve_order feature, it also makes test results more stable -serde_json = { version = "1.0.132", features = ["preserve_order"] } tempfile = "3.13.0" hyper = { version = "1.5.0", features = ["http1", "http2", "server"]} diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 4adc17acae1..12b2b4ce29e 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -1,16 +1,15 @@ //! A simple HTTP health endpoint for Zebra. use std::net::SocketAddr; +use std::convert::Infallible; use abscissa_core::{Component, FrameworkError}; -use serde::{Deserialize, Serialize}; -use tokio::spawn; -use hyper::{Request, Response, StatusCode}; use hyper::body::Incoming; +use hyper::server::conn::http1; use hyper::service::service_fn; -use std::convert::Infallible; -use tracing::{info, error}; -use crate::config::ZebradConfig; +use hyper::{Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; +use tracing::{error, info}; /// Abscissa component which runs a health endpoint. #[derive(Debug, Component)] @@ -47,12 +46,12 @@ impl HealthEndpoint { let (stream, _) = listener.accept().await?; let io = hyper_util::rt::TokioIo::new(stream); - spawn(async move { - if let Err(err) = hyper::server::conn::http1::Builder::new() + tokio::spawn(async move { + if let Err(err) = http1::Builder::new() .serve_connection(io, service_fn(Self::handle_request)) .await { - error!("Health endpoint connection error: {}", err); + error!("Failed to serve connection: {}", err); } }); } @@ -101,12 +100,8 @@ struct HealthInfo { /// Health endpoint configuration section. #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] -#[serde(deny_unknown_fields, default)] -#[cfg(feature = "health-endpoint")] pub struct Config { - /// The address used for the health endpoint. - /// - /// The endpoint is disabled if this is set to `None`. + /// The address to bind the health endpoint to pub endpoint_addr: Option, } diff --git a/zebrad/tests/common/configs/health-test.toml b/zebrad/tests/common/configs/health-test.toml deleted file mode 100644 index 8f823e4818a..00000000000 --- a/zebrad/tests/common/configs/health-test.toml +++ /dev/null @@ -1,12 +0,0 @@ -[health] -endpoint_addr = "127.0.0.1:8080" - -[network] -network = "Testnet" - -[metrics] -endpoint_addr = "127.0.0.1:9999" - -[tracing] -endpoint_addr = "127.0.0.1:3000" - From 0d805d23ae50460041b1166b35c8fc4f2db0a5ff Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 25 Aug 2025 12:04:41 +0200 Subject: [PATCH 04/18] Fix typo --- zebrad/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 4b5ba95a03b..5e286563519 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -240,7 +240,7 @@ serde_json = { version = "1.0.132", features = ["preserve_order"], optional = tr # prod feature release_max_level_info # -# zebra uses tracing for logging, +# zebrad uses tracing for logging, # we only use `log` to set and print the static log levels in transitive dependencies log = "0.4.22" From 3475252384f237db864b0fb933ce65289b18b27f Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:11:51 +0200 Subject: [PATCH 05/18] Update zebrad/Cargo.toml Co-authored-by: Dmitry Demin --- zebrad/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 5e286563519..17476cfa406 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -89,7 +89,7 @@ elasticsearch = [ sentry = ["dep:sentry"] journald = ["tracing-journald"] filter-reload = ["hyper", "http-body-util", "hyper-util", "bytes"] -health-endpoint = ["hyper", "hyper-util", "serde/derive", "chrono/serde", "serde_json"] +health-endpoint = ["hyper", "hyper-util", "hyper-util/tokio", "serde/derive", "chrono/serde", "serde_json"] progress-bar = [ "howudoin", From ebbfc4914165cd876eb5afe7f35ff0de6c3860a5 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:13:17 +0200 Subject: [PATCH 06/18] Update zebrad/src/components.rs Co-authored-by: Dmitry Demin --- zebrad/src/components.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zebrad/src/components.rs b/zebrad/src/components.rs index 864c669f678..d6c4a10340a 100644 --- a/zebrad/src/components.rs +++ b/zebrad/src/components.rs @@ -5,6 +5,7 @@ //! component and dependency injection models are designed to work together, but //! don't fit the async context well. +#[cfg(feature = "health-endpoint")] pub mod health; pub mod inbound; #[allow(missing_docs)] From e24527e38cf131a8474a491e4e3da0f1a2fff716 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:13:57 +0200 Subject: [PATCH 07/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 12b2b4ce29e..c1866fe3e11 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -13,7 +13,6 @@ use tracing::{error, info}; /// Abscissa component which runs a health endpoint. #[derive(Debug, Component)] -#[cfg(feature = "health-endpoint")] pub struct HealthEndpoint {} #[cfg(feature = "health-endpoint")] From a87ffefe4b5d919be0b86edf3f896a77e11b1bbb Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:14:05 +0200 Subject: [PATCH 08/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index c1866fe3e11..830701b149e 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -15,7 +15,6 @@ use tracing::{error, info}; #[derive(Debug, Component)] pub struct HealthEndpoint {} -#[cfg(feature = "health-endpoint")] impl HealthEndpoint { /// Create the component. pub fn new(config: &Config) -> Result { From a64ab67813faabe086a49fb1824ac7efcf779bd6 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:14:18 +0200 Subject: [PATCH 09/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 830701b149e..bf3515e1959 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -23,7 +23,7 @@ impl HealthEndpoint { // Start the health endpoint server in a separate thread to avoid Tokio runtime issues std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().unwrap(); + let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be available"); rt.block_on(async { if let Err(e) = Self::run_server(addr).await { error!("Health endpoint server failed: {}", e); From cf909a710c7497f3f18d03770aafa6cf160c80bb Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:14:38 +0200 Subject: [PATCH 10/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index bf3515e1959..4b1b319b357 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -62,7 +62,7 @@ impl HealthEndpoint { .status(StatusCode::NOT_FOUND) .header("Content-Type", "application/json") .body("{\"error\": \"Not Found\"}".to_string()) - .unwrap()); + .expect("response should build successfully")); } let health_info = HealthInfo { From 1a84606ef8ca65297cff734f9b27124da3cfd6f7 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:14:48 +0200 Subject: [PATCH 11/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 4b1b319b357..21efad64142 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -80,7 +80,7 @@ impl HealthEndpoint { .status(StatusCode::OK) .header("Content-Type", "application/json") .body(response_body) - .unwrap()) + .expect("response with should build successfully")) } } From ecb49c861c7d3178471f763ccc5041f9ab49b794 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:15:23 +0200 Subject: [PATCH 12/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 21efad64142..d2fe92352a0 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -107,7 +107,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - endpoint_addr: Some("127.0.0.1:8080".parse().unwrap()), + endpoint_addr: Some(SocketAddr::from(([127, 0, 0, 1], 8080))), } } } From 8758fe108a7a8a14204cf24efb811264d180b4a9 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:15:40 +0200 Subject: [PATCH 13/18] Update zebrad/src/components/health.rs Co-authored-by: Dmitry Demin --- zebrad/src/components/health.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index d2fe92352a0..01bf1455322 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -103,7 +103,6 @@ pub struct Config { pub endpoint_addr: Option, } -#[cfg(feature = "health-endpoint")] impl Default for Config { fn default() -> Self { Self { From 25671951b2616368c63c67146d0818458b3c2bbf Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:18:28 +0200 Subject: [PATCH 14/18] Update Cargo.toml --- zebrad/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 17476cfa406..153abf5d618 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -288,6 +288,10 @@ proptest-derive = "0.5.0" # enable span traces and track caller in tests color-eyre = { version = "0.6.3" } +# Make serde_json available for tests regardless of feature flags +# (while keeping it optional in [dependencies] for production builds) +serde_json = { version = "1.0.132", features = ["preserve_order"] } + zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.41", features = ["proptest-impl"] } zebra-consensus = { path = "../zebra-consensus", version = "1.0.0-beta.41", features = ["proptest-impl"] } zebra-network = { path = "../zebra-network", version = "1.0.0-beta.41", features = ["proptest-impl"] } From 28e73fa430894f28e6ed428b0e4349cff8b06c5a Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 10:21:49 +0200 Subject: [PATCH 15/18] Update health.rs --- zebrad/src/components/health.rs | 72 +++++++++++++++------------------ 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 01bf1455322..4103ca88dd5 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -1,26 +1,21 @@ //! A simple HTTP health endpoint for Zebra. - use std::net::SocketAddr; use std::convert::Infallible; - use abscissa_core::{Component, FrameworkError}; use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::{Request, Response, StatusCode}; +use hyper::{Method, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; - /// Abscissa component which runs a health endpoint. #[derive(Debug, Component)] pub struct HealthEndpoint {} - impl HealthEndpoint { /// Create the component. pub fn new(config: &Config) -> Result { if let Some(addr) = config.endpoint_addr { info!("Trying to open health endpoint at {}...", addr); - // Start the health endpoint server in a separate thread to avoid Tokio runtime issues std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be available"); @@ -30,13 +25,10 @@ impl HealthEndpoint { } }); }); - info!("Opened health endpoint at {}", addr); } - Ok(Self {}) } - async fn run_server(addr: SocketAddr) -> Result<(), Box> { let listener = tokio::net::TcpListener::bind(addr).await?; @@ -54,38 +46,42 @@ impl HealthEndpoint { }); } } - async fn handle_request(req: Request) -> Result, Infallible> { - // Check if the request is for the health endpoint - if req.uri().path() != "/health" { - return Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .header("Content-Type", "application/json") - .body("{\"error\": \"Not Found\"}".to_string()) - .expect("response should build successfully")); + match (req.method(), req.uri().path()) { + (&Method::GET, "/health") => { + let health_info = HealthInfo { + 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(), + }; + let response_body = serde_json::to_string_pretty(&health_info) + .unwrap_or_else(|_| "{\"error\": \"Failed to serialize health info\"}".to_string()); + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(response_body) + .expect("response should build successfully")) + } + (_, "/health") => { + Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header("Allow", "GET") + .header("Content-Type", "application/json") + .body("{\"error\": \"Method Not Allowed\"}".to_string()) + .expect("response should build successfully")) + } + _ => { + Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Type", "application/json") + .body("{\"error\": \"Not Found\"}".to_string()) + .expect("response should build successfully")) + } } - - let health_info = HealthInfo { - 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(), - }; - - let response_body = serde_json::to_string_pretty(&health_info) - .unwrap_or_else(|_| "{\"error\": \"Failed to serialize health info\"}".to_string()); - - Ok(Response::builder() - .status(StatusCode::OK) - .header("Content-Type", "application/json") - .body(response_body) - .expect("response with should build successfully")) } } - - - /// Health information response. #[derive(Debug, Serialize)] struct HealthInfo { @@ -95,14 +91,12 @@ struct HealthInfo { git_commit: String, timestamp: String, } - /// Health endpoint configuration section. #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] pub struct Config { /// The address to bind the health endpoint to pub endpoint_addr: Option, } - impl Default for Config { fn default() -> Self { Self { From f3e490f466c493ca107c092ff35ac12eeab1a9fe Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 11:44:45 +0200 Subject: [PATCH 16/18] Fix format --- zebrad/src/application.rs | 4 +-- zebrad/src/components/health.rs | 46 ++++++++++++++++----------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/zebrad/src/application.rs b/zebrad/src/application.rs index ad6fffcd0df..ce36f912203 100644 --- a/zebrad/src/application.rs +++ b/zebrad/src/application.rs @@ -229,11 +229,11 @@ impl Application for ZebradApp { #[allow(clippy::print_stderr)] #[allow(clippy::unwrap_in_result)] fn register_components(&mut self, command: &Self::Cmd) -> Result<(), FrameworkError> { + #[cfg(feature = "health-endpoint")] + use crate::components::health::HealthEndpoint; use crate::components::{ metrics::MetricsEndpoint, tokio::TokioComponent, tracing::TracingEndpoint, }; - #[cfg(feature = "health-endpoint")] - use crate::components::health::HealthEndpoint; let mut components = self.framework_components(command)?; diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 4103ca88dd5..8750b97b790 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -1,12 +1,12 @@ //! A simple HTTP health endpoint for Zebra. -use std::net::SocketAddr; -use std::convert::Infallible; use abscissa_core::{Component, FrameworkError}; use hyper::body::Incoming; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Method, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; +use std::convert::Infallible; +use std::net::SocketAddr; use tracing::{error, info}; /// Abscissa component which runs a health endpoint. #[derive(Debug, Component)] @@ -18,7 +18,7 @@ impl HealthEndpoint { info!("Trying to open health endpoint at {}...", addr); // Start the health endpoint server in a separate thread to avoid Tokio runtime issues std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be available"); + let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be available"); rt.block_on(async { if let Err(e) = Self::run_server(addr).await { error!("Health endpoint server failed: {}", e); @@ -31,11 +31,11 @@ impl HealthEndpoint { } async fn run_server(addr: SocketAddr) -> Result<(), Box> { let listener = tokio::net::TcpListener::bind(addr).await?; - + loop { let (stream, _) = listener.accept().await?; let io = hyper_util::rt::TokioIo::new(stream); - + tokio::spawn(async move { if let Err(err) = http1::Builder::new() .serve_connection(io, service_fn(Self::handle_request)) @@ -53,32 +53,32 @@ impl HealthEndpoint { 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(), + git_commit: option_env!("GIT_COMMIT_FULL") + .unwrap_or("unknown") + .to_string(), timestamp: chrono::Utc::now().to_rfc3339(), }; - let response_body = serde_json::to_string_pretty(&health_info) - .unwrap_or_else(|_| "{\"error\": \"Failed to serialize health info\"}".to_string()); + let response_body = + serde_json::to_string_pretty(&health_info).unwrap_or_else(|_| { + "{\"error\": \"Failed to serialize health info\"}".to_string() + }); Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(response_body) .expect("response should build successfully")) } - (_, "/health") => { - Ok(Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .header("Allow", "GET") - .header("Content-Type", "application/json") - .body("{\"error\": \"Method Not Allowed\"}".to_string()) - .expect("response should build successfully")) - } - _ => { - Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .header("Content-Type", "application/json") - .body("{\"error\": \"Not Found\"}".to_string()) - .expect("response should build successfully")) - } + (_, "/health") => Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header("Allow", "GET") + .header("Content-Type", "application/json") + .body("{\"error\": \"Method Not Allowed\"}".to_string()) + .expect("response should build successfully")), + _ => Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .header("Content-Type", "application/json") + .body("{\"error\": \"Not Found\"}".to_string()) + .expect("response should build successfully")), } } } From 37574253d656455e9c7ba2bcc0a7bcabc904acce Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 13:36:04 +0200 Subject: [PATCH 17/18] Fix clippy error --- zebrad/src/components/health.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 8750b97b790..36cdb2ced8f 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -18,12 +18,18 @@ impl HealthEndpoint { info!("Trying to open health endpoint at {}...", addr); // Start the health endpoint server in a separate thread to avoid Tokio runtime issues std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Tokio runtime should be available"); - rt.block_on(async { - if let Err(e) = Self::run_server(addr).await { - error!("Health endpoint server failed: {}", e); + match tokio::runtime::Runtime::new() { + Ok(rt) => { + rt.block_on(async { + if let Err(e) = Self::run_server(addr).await { + error!("Health endpoint server failed: {}", e); + } + }); } - }); + Err(e) => { + error!("Failed to create Tokio runtime for health endpoint: {}", e); + } + } }); info!("Opened health endpoint at {}", addr); } From ad83b102a2a72a6c065486e2314c046d2fc0eb16 Mon Sep 17 00:00:00 2001 From: Arseni Kalma Date: Mon, 1 Sep 2025 14:31:25 +0200 Subject: [PATCH 18/18] Fix fmt --- zebrad/src/components/health.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/zebrad/src/components/health.rs b/zebrad/src/components/health.rs index 36cdb2ced8f..e297e35a643 100644 --- a/zebrad/src/components/health.rs +++ b/zebrad/src/components/health.rs @@ -17,18 +17,16 @@ impl HealthEndpoint { if let Some(addr) = config.endpoint_addr { info!("Trying to open health endpoint at {}...", addr); // Start the health endpoint server in a separate thread to avoid Tokio runtime issues - std::thread::spawn(move || { - match tokio::runtime::Runtime::new() { - Ok(rt) => { - rt.block_on(async { - if let Err(e) = Self::run_server(addr).await { - error!("Health endpoint server failed: {}", e); - } - }); - } - Err(e) => { - error!("Failed to create Tokio runtime for health endpoint: {}", e); - } + std::thread::spawn(move || match tokio::runtime::Runtime::new() { + Ok(rt) => { + rt.block_on(async { + if let Err(e) = Self::run_server(addr).await { + error!("Health endpoint server failed: {}", e); + } + }); + } + Err(e) => { + error!("Failed to create Tokio runtime for health endpoint: {}", e); } }); info!("Opened health endpoint at {}", addr);