diff --git a/Cargo.lock b/Cargo.lock index 0aed14970b5..53ee2debe53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6649,7 +6649,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "3.0.0" +version = "3.1.0" dependencies = [ "ammonia", "anyhow", diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 104e4c98630..7e63db3672f 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "3.0.0" +version = "3.1.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -28,7 +28,7 @@ moka = { workspace = true, features = ["future"] } # Nym API: revert after Cheddar is out nym-contracts-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-mixnet-contract-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } -nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } +nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = ["openapi"]} nym-node-status-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } nym-http-api-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" } diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs index 5bc06be2b2f..bd4300275b7 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod metrics; pub(crate) mod mixnodes; pub(crate) mod nym_nodes; pub(crate) mod services; +pub(crate) mod status; pub(crate) mod summary; pub(crate) mod testruns; @@ -39,7 +40,8 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()) - .nest("/metrics", metrics::routes()), + .nest("/metrics", metrics::routes()) + .nest("/status", status::routes()), ) .nest( "/explorer/v3", diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/status.rs b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs new file mode 100644 index 00000000000..306a7f9c2b0 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/status.rs @@ -0,0 +1,46 @@ +use axum::{extract::State, Json, Router}; +use nym_validator_client::models::BinaryBuildInformationOwned; +use tracing::instrument; + +use crate::http::{ + error::HttpResult, + state::{AppState, HealthInfo}, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/build_information", axum::routing::get(build_information)) + .route("/health", axum::routing::get(health)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/build_information", + context_path = "/v2/status", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn build_information( + State(state): State, +) -> HttpResult> { + let build_info = state.build_information().to_owned(); + + Ok(Json(build_info)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/health", + context_path = "/v2/status", + responses( + (status = 200, body = HealthInfo) + ) +)] +#[instrument(level = tracing::Level::INFO, skip_all)] +async fn health(State(state): State) -> HttpResult> { + Ok(Json(state.health())) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 0a5ef4fca96..8d197f9a077 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -1,15 +1,20 @@ use cosmwasm_std::Decimal; use itertools::Itertools; use moka::{future::Cache, Entry}; +use nym_bin_common::bin_info_owned; use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::ed25519::PublicKey; use nym_mixnet_contract_common::NodeId; use nym_validator_client::nym_api::SkimmedNode; use semver::Version; +use serde::Serialize; use std::{collections::HashMap, sync::Arc, time::Duration}; +use time::UtcDateTime; use tokio::sync::RwLock; use tracing::{error, instrument, warn}; +use utoipa::ToSchema; +use super::models::SessionStats; use crate::{ db::{queries, DbPool}, http::models::{ @@ -18,7 +23,7 @@ use crate::{ monitor::{DelegationsCache, NodeGeoCache}, }; -use super::models::SessionStats; +pub(crate) use nym_validator_client::models::BinaryBuildInformationOwned; #[derive(Debug, Clone)] pub(crate) struct AppState { @@ -28,6 +33,7 @@ pub(crate) struct AppState { agent_max_count: i64, node_geocache: NodeGeoCache, node_delegations: Arc>, + bin_info: BinaryInfo, } impl AppState { @@ -46,6 +52,7 @@ impl AppState { agent_max_count, node_geocache, node_delegations, + bin_info: BinaryInfo::new(), } } @@ -78,6 +85,15 @@ impl AppState { .await .delegations_owned(node_id) } + + pub(crate) fn health(&self) -> HealthInfo { + let uptime = (UtcDateTime::now() - self.bin_info.startup_time).whole_seconds(); + HealthInfo { uptime } + } + + pub(crate) fn build_information(&self) -> &BinaryBuildInformationOwned { + &self.bin_info.build_info + } } static GATEWAYS_LIST_KEY: &str = "gateways"; @@ -631,3 +647,23 @@ async fn aggregate_node_info_from_db( Ok(parsed_nym_nodes) } + +#[derive(Debug, Clone)] +pub(crate) struct BinaryInfo { + startup_time: UtcDateTime, + build_info: BinaryBuildInformationOwned, +} + +impl BinaryInfo { + fn new() -> Self { + Self { + startup_time: UtcDateTime::now(), + build_info: bin_info_owned!(), + } + } +} + +#[derive(Serialize, ToSchema)] +pub(crate) struct HealthInfo { + uptime: i64, +}