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
10 changes: 8 additions & 2 deletions zebrad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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", "hyper-util/tokio", "serde/derive", "chrono/serde", "serde_json"]

progress-bar = [
"howudoin",
Expand Down Expand Up @@ -234,6 +235,9 @@ 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,
Expand Down Expand Up @@ -266,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"] }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider restoring serde_json under [dev-dependencies] (while keeping it in [dependencies] with optional = true), because cargo test without --features=health-endpoint currently fails with unresolved imports in tests that use serde_json directly, e.g.:

error[E0432]: unresolved import `serde_json`
   --> zebrad/tests/acceptance.rs:158:5
    |
158 | use serde_json::Value;
    |     ^^^^^^^^^^ help: a similar path exists: `jsonrpc_core::serde_json`

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

V

tempfile = "3.13.0"

hyper = { version = "1.5.0", features = ["http1", "http2", "server"]}
Expand All @@ -286,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"] }
Expand Down
4 changes: 4 additions & 0 deletions zebrad/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ 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,
};
Expand Down Expand Up @@ -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)?;
Expand Down
2 changes: 2 additions & 0 deletions zebrad/src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! 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)]
pub mod mempool;
Expand Down
110 changes: 110 additions & 0 deletions zebrad/src/components/health.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! A simple HTTP health endpoint for Zebra.
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)]
pub struct HealthEndpoint {}
impl HealthEndpoint {
/// Create the component.
pub fn new(config: &Config) -> Result<Self, FrameworkError> {
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);
}
});
info!("Opened health endpoint at {}", addr);
}
Ok(Self {})
}
async fn run_server(addr: SocketAddr) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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))
.await
{
error!("Failed to serve connection: {}", err);
}
});
}
}
async fn handle_request(req: Request<Incoming>) -> Result<Response<String>, Infallible> {
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")),
}
}
}
/// 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)]
pub struct Config {
/// The address to bind the health endpoint to
pub endpoint_addr: Option<SocketAddr>,
}
impl Default for Config {
fn default() -> Self {
Self {
endpoint_addr: Some(SocketAddr::from(([127, 0, 0, 1], 8080))),
}
}
}
4 changes: 4 additions & 0 deletions zebrad/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down