Expose system health via RPC and REST (#1269)

* Implement health endpoint.

* Expose health API.
This commit is contained in:
Tomasz Drwięga
2018-12-17 11:19:24 +01:00
committed by Gav Wood
parent 090ca9ee7c
commit c1b08cd9b0
11 changed files with 262 additions and 69 deletions
+14
View File
@@ -19,9 +19,16 @@
use rpc;
use errors;
use system::helpers::Health;
error_chain! {
errors {
/// Node is not fully functional
NotHealthy(h: Health) {
description("node is not healthy"),
display("Node is not fully functional: {}", h)
}
/// Not implemented yet
Unimplemented {
description("not yet implemented"),
@@ -30,10 +37,17 @@ error_chain! {
}
}
const ERROR: i64 = 2000;
impl From<Error> for rpc::Error {
fn from(e: Error) -> Self {
match e {
Error(ErrorKind::Unimplemented, _) => errors::unimplemented(),
Error(ErrorKind::NotHealthy(h), _) => rpc::Error {
code: rpc::ErrorCode::ServerError(ERROR + 1),
message: "node is not healthy".into(),
data:serde_json::to_value(h).ok(),
},
e => errors::internal(e),
}
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2017-2018 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Substrate system API helpers.
use std::fmt;
use serde_derive::{Serialize};
use serde_json::{Value, map::Map};
/// Node properties
pub type Properties = Map<String, Value>;
/// Running node's static details.
#[derive(Clone, Debug)]
pub struct SystemInfo {
/// Implementation name.
pub impl_name: String,
/// Implementation version.
pub impl_version: String,
/// Chain name.
pub chain_name: String,
/// A custom set of properties defined in the chain spec.
pub properties: Properties,
}
/// Health struct returned by the RPC
#[derive(Debug, PartialEq, Serialize)]
pub struct Health {
/// Number of connected peers
pub peers: usize,
/// Is the node syncing
pub is_syncing: bool,
}
impl fmt::Display for Health {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{} peers ({})", self.peers, if self.is_syncing {
"syncing"
} else { "idle" })
}
}
+74 -1
View File
@@ -18,10 +18,16 @@
pub mod error;
mod helpers;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use network;
use runtime_primitives::traits;
use self::error::Result;
pub use self::helpers::{Properties, SystemInfo, Health};
build_rpc_trait! {
/// Substrate system RPC API
@@ -40,6 +46,73 @@ build_rpc_trait! {
/// Get a custom set of properties as a JSON object, defined in the chain spec.
#[rpc(name = "system_properties")]
fn system_properties(&self) -> Result<serde_json::map::Map<String, serde_json::Value>>;
fn system_properties(&self) -> Result<Properties>;
/// Return health status of the node.
///
/// Node is considered healthy if it is:
/// - connected to some peers (unless running in dev mode)
/// - not performing a major sync
#[rpc(name = "system_health")]
fn system_health(&self) -> Result<Health>;
}
}
/// System API implementation
pub struct System<B: traits::Block> {
info: SystemInfo,
sync: Arc<network::SyncProvider<B>>,
should_have_peers: bool,
}
impl<B: traits::Block> System<B> {
/// Creates new `System` given the `SystemInfo`.
pub fn new(
info: SystemInfo,
sync: Arc<network::SyncProvider<B>>,
should_have_peers: bool,
) -> Self {
System {
info,
should_have_peers,
sync,
}
}
}
impl<B: traits::Block> SystemApi for System<B> {
fn system_name(&self) -> Result<String> {
Ok(self.info.impl_name.clone())
}
fn system_version(&self) -> Result<String> {
Ok(self.info.impl_version.clone())
}
fn system_chain(&self) -> Result<String> {
Ok(self.info.chain_name.clone())
}
fn system_properties(&self) -> Result<Properties> {
Ok(self.info.properties.clone())
}
fn system_health(&self) -> Result<Health> {
let status = self.sync.status();
let is_syncing = status.sync.is_major_syncing();
let peers = status.num_peers;
let health = Health {
peers,
is_syncing,
};
let has_no_peers = peers == 0 && self.should_have_peers;
if has_no_peers || is_syncing {
Err(error::ErrorKind::NotHealthy(health))?
} else {
Ok(health)
}
}
}
+83 -17
View File
@@ -15,27 +15,46 @@
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
use super::*;
use super::error::*;
impl SystemApi for () {
fn system_name(&self) -> Result<String> {
Ok("testclient".into())
}
fn system_version(&self) -> Result<String> {
Ok("0.2.0".into())
}
fn system_chain(&self) -> Result<String> {
Ok("testchain".into())
}
fn system_properties(&self) -> Result<serde_json::map::Map<String, serde_json::Value>> {
Ok(serde_json::map::Map::new())
use network::{self, SyncState, SyncStatus, ProtocolStatus};
use test_client::runtime::Block;
#[derive(Default)]
struct Status {
pub peers: usize,
pub is_syncing: bool,
pub is_dev: bool,
}
impl network::SyncProvider<Block> for Status {
fn status(&self) -> ProtocolStatus<Block> {
ProtocolStatus {
sync: SyncStatus {
state: if self.is_syncing { SyncState::Downloading } else { SyncState::Idle },
best_seen_block: None,
},
num_peers: self.peers,
num_active_peers: 0,
}
}
}
fn api<T: Into<Option<Status>>>(sync: T) -> System<Block> {
let status = sync.into().unwrap_or_default();
let should_have_peers = !status.is_dev;
System::new(SystemInfo {
impl_name: "testclient".into(),
impl_version: "0.2.0".into(),
chain_name: "testchain".into(),
properties: Default::default(),
}, Arc::new(status), should_have_peers)
}
#[test]
fn system_name_works() {
assert_eq!(
SystemApi::system_name(&()).unwrap(),
api(None).system_name().unwrap(),
"testclient".to_owned()
);
}
@@ -43,7 +62,7 @@ fn system_name_works() {
#[test]
fn system_version_works() {
assert_eq!(
SystemApi::system_version(&()).unwrap(),
api(None).system_version().unwrap(),
"0.2.0".to_owned()
);
}
@@ -51,7 +70,7 @@ fn system_version_works() {
#[test]
fn system_chain_works() {
assert_eq!(
SystemApi::system_chain(&()).unwrap(),
api(None).system_chain().unwrap(),
"testchain".to_owned()
);
}
@@ -59,7 +78,54 @@ fn system_chain_works() {
#[test]
fn system_properties_works() {
assert_eq!(
SystemApi::system_properties(&()).unwrap(),
api(None).system_properties().unwrap(),
serde_json::map::Map::new()
);
}
#[test]
fn system_health() {
assert_matches!(
api(None).system_health().unwrap_err().kind(),
error::ErrorKind::NotHealthy(Health {
peers: 0,
is_syncing: false,
})
);
assert_matches!(
api(Status {
peers: 5,
is_syncing: true,
is_dev: true,
}).system_health().unwrap_err().kind(),
error::ErrorKind::NotHealthy(Health {
peers: 5,
is_syncing: true,
})
);
assert_eq!(
api(Status {
peers: 5,
is_syncing: false,
is_dev: false,
}).system_health().unwrap(),
Health {
peers: 5,
is_syncing: false,
}
);
assert_eq!(
api(Status {
peers: 0,
is_syncing: false,
is_dev: true,
}).system_health().unwrap(),
Health {
peers: 0,
is_syncing: false,
}
);
}