feature(rpc): Extend `getinfo` (#9261)

* introduce new fields to `GetInfo`

* add address book to methods for getinfo connections field

* add the version field to getinfo

* track last node error or warning and display it in getinfo rpc method

* add the rest of the fields, minor cleanup

* fix the tests

* clippy
This commit is contained in:
Alfredo Garcia 2025-02-18 00:23:55 -03:00 committed by GitHub
parent b4211aa1cf
commit 5cf5178d50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 380 additions and 40 deletions

View File

@ -6174,6 +6174,7 @@ dependencies = [
"proptest",
"prost",
"rand 0.8.5",
"semver",
"serde",
"serde_json",
"thiserror 2.0.11",

View File

@ -62,6 +62,7 @@ jsonrpsee-types = { workspace = true }
jsonrpsee-proc-macros = { workspace = true }
hyper = { workspace = true }
http-body-util = { workspace = true }
semver = { workspace = true }
serde_json = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }

View File

@ -37,6 +37,7 @@ use zebra_chain::{
},
};
use zebra_consensus::ParameterCheckpoint;
use zebra_network::address_book_peers::AddressBookPeers;
use zebra_node_services::mempool;
use zebra_state::{
HashOrHeight, OutputIndex, OutputLocation, ReadRequest, ReadResponse, TransactionLocation,
@ -89,7 +90,7 @@ pub trait Rpc {
/// Some fields from the zcashd reference are missing from Zebra's [`GetInfo`]. It only contains the fields
/// [required for lightwalletd support.](https://github.com/zcash/lightwalletd/blob/v0.4.9/common/common.go#L91-L95)
#[method(name = "getinfo")]
fn get_info(&self) -> Result<GetInfo>;
async fn get_info(&self) -> Result<GetInfo>;
/// Returns blockchain state information, as a [`GetBlockChainInfo`] JSON struct.
///
@ -358,7 +359,7 @@ pub trait Rpc {
/// RPC method implementations.
#[derive(Clone)]
pub struct RpcImpl<Mempool, State, Tip>
pub struct RpcImpl<Mempool, State, Tip, AddressBook>
where
Mempool: Service<
mempool::Request,
@ -379,6 +380,7 @@ where
+ 'static,
State::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
// Configuration
//
@ -414,9 +416,19 @@ where
//
/// A sender component of a channel used to send transactions to the mempool queue.
queue_sender: broadcast::Sender<UnminedTx>,
/// Peer address book.
address_book: AddressBook,
/// The last warning or error event logged by the server.
last_event: LoggedLastEvent,
}
impl<Mempool, State, Tip> Debug for RpcImpl<Mempool, State, Tip>
/// A type alias for the last event logged by the server.
pub type LoggedLastEvent =
Arc<std::sync::Mutex<Option<(String, tracing::Level, chrono::DateTime<Utc>)>>>;
impl<Mempool, State, Tip, AddressBook> Debug for RpcImpl<Mempool, State, Tip, AddressBook>
where
Mempool: Service<
mempool::Request,
@ -437,6 +449,7 @@ where
+ 'static,
State::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Skip fields without Debug impls, and skip channels
@ -450,7 +463,7 @@ where
}
}
impl<Mempool, State, Tip> RpcImpl<Mempool, State, Tip>
impl<Mempool, State, Tip, AddressBook> RpcImpl<Mempool, State, Tip, AddressBook>
where
Mempool: Service<
mempool::Request,
@ -471,6 +484,7 @@ where
+ 'static,
State::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
/// Create a new instance of the RPC handler.
//
@ -486,6 +500,8 @@ where
mempool: Mempool,
state: State,
latest_chain_tip: Tip,
address_book: AddressBook,
last_event: LoggedLastEvent,
) -> (Self, JoinHandle<()>)
where
VersionString: ToString + Clone + Send + 'static,
@ -511,6 +527,8 @@ where
state: state.clone(),
latest_chain_tip: latest_chain_tip.clone(),
queue_sender,
address_book,
last_event,
};
// run the process queue
@ -525,7 +543,7 @@ where
}
#[async_trait]
impl<Mempool, State, Tip> RpcServer for RpcImpl<Mempool, State, Tip>
impl<Mempool, State, Tip, AddressBook> RpcServer for RpcImpl<Mempool, State, Tip, AddressBook>
where
Mempool: Service<
mempool::Request,
@ -546,11 +564,56 @@ where
+ 'static,
State::Future: Send,
Tip: ChainTip + Clone + Send + Sync + 'static,
AddressBook: AddressBookPeers + Clone + Send + Sync + 'static,
{
fn get_info(&self) -> Result<GetInfo> {
async fn get_info(&self) -> Result<GetInfo> {
let version = GetInfo::version(&self.build_version).ok_or(ErrorObject::owned(
server::error::LegacyCode::Misc.into(),
"invalid version string",
None::<()>,
))?;
// TODO: Change to use `currently_live_peers()` after #9214
let connections = self.address_book.recently_live_peers(Utc::now()).len();
let last_error_recorded = self.last_event.lock().expect("mutex poisoned").clone();
let (last_event, _last_event_level, last_event_time) = last_error_recorded.unwrap_or((
GetInfo::default().errors,
tracing::Level::INFO,
Utc::now(),
));
let tip_height = self
.latest_chain_tip
.best_tip_height()
.unwrap_or(Height::MIN);
let testnet = self.network.is_a_test_network();
// This field is behind the `ENABLE_WALLET` feature flag in zcashd:
// https://github.com/zcash/zcash/blob/v6.1.0/src/rpc/misc.cpp#L113
// However it is not documented as optional:
// https://github.com/zcash/zcash/blob/v6.1.0/src/rpc/misc.cpp#L70
// For compatibility, we keep the field in the response, but always return 0.
let pay_tx_fee = 0.0;
let relay_fee = zebra_chain::transaction::zip317::MIN_MEMPOOL_TX_FEE_RATE as f64
/ (zebra_chain::amount::COIN as f64);
let difficulty = chain_tip_difficulty(self.network.clone(), self.state.clone()).await?;
let response = GetInfo {
version,
build: self.build_version.clone(),
subversion: self.user_agent.clone(),
protocol_version: zebra_network::constants::CURRENT_NETWORK_PROTOCOL_VERSION.0,
blocks: tip_height.0,
connections,
proxy: None,
difficulty,
testnet,
pay_tx_fee,
relay_fee,
errors: last_event,
errors_timestamp: last_event_time.to_string(),
};
Ok(response)
@ -1542,33 +1605,161 @@ where
/// Response to a `getinfo` RPC request.
///
/// See the notes for the [`Rpc::get_info` method].
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct GetInfo {
/// The node version
version: u64,
/// The node version build number
build: String,
/// The server sub-version identifier, used as the network protocol user-agent
subversion: String,
/// The protocol version
#[serde(rename = "protocolversion")]
protocol_version: u32,
/// The current number of blocks processed in the server
blocks: u32,
/// The total (inbound and outbound) number of connections the node has
connections: usize,
/// The proxy (if any) used by the server. Currently always `None` in Zebra.
#[serde(skip_serializing_if = "Option::is_none")]
proxy: Option<String>,
/// The current network difficulty
difficulty: f64,
/// True if the server is running in testnet mode, false otherwise
testnet: bool,
/// The minimum transaction fee in ZEC/kB
#[serde(rename = "paytxfee")]
pay_tx_fee: f64,
/// The minimum relay fee for non-free transactions in ZEC/kB
#[serde(rename = "relayfee")]
relay_fee: f64,
/// The last error or warning message, or "no errors" if there are no errors
errors: String,
/// The time of the last error or warning message, or "no errors timestamp" if there are no errors
#[serde(rename = "errorstimestamp")]
errors_timestamp: String,
}
impl Default for GetInfo {
fn default() -> Self {
GetInfo {
version: 0,
build: "some build version".to_string(),
subversion: "some subversion".to_string(),
protocol_version: 0,
blocks: 0,
connections: 0,
proxy: None,
difficulty: 0.0,
testnet: false,
pay_tx_fee: 0.0,
relay_fee: 0.0,
errors: "no errors".to_string(),
errors_timestamp: "no errors timestamp".to_string(),
}
}
}
impl GetInfo {
/// Constructs [`GetInfo`] from its constituent parts.
pub fn from_parts(build: String, subversion: String) -> Self {
Self { build, subversion }
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
version: u64,
build: String,
subversion: String,
protocol_version: u32,
blocks: u32,
connections: usize,
proxy: Option<String>,
difficulty: f64,
testnet: bool,
pay_tx_fee: f64,
relay_fee: f64,
errors: String,
errors_timestamp: String,
) -> Self {
Self {
version,
build,
subversion,
protocol_version,
blocks,
connections,
proxy,
difficulty,
testnet,
pay_tx_fee,
relay_fee,
errors,
errors_timestamp,
}
}
/// Returns the contents of ['GetInfo'].
pub fn into_parts(self) -> (String, String) {
(self.build, self.subversion)
pub fn into_parts(
self,
) -> (
u64,
String,
String,
u32,
u32,
usize,
Option<String>,
f64,
bool,
f64,
f64,
String,
String,
) {
(
self.version,
self.build,
self.subversion,
self.protocol_version,
self.blocks,
self.connections,
self.proxy,
self.difficulty,
self.testnet,
self.pay_tx_fee,
self.relay_fee,
self.errors,
self.errors_timestamp,
)
}
/// Create the node version number.
pub fn version(build_string: &str) -> Option<u64> {
let semver_version = semver::Version::parse(build_string.strip_prefix('v')?).ok()?;
let build_number = semver_version
.build
.as_str()
.split('.')
.next()
.and_then(|num_str| num_str.parse::<u64>().ok())
.unwrap_or_default();
// https://github.com/zcash/zcash/blob/v6.1.0/src/clientversion.h#L55-L59
let version_number = 1_000_000 * semver_version.major
+ 10_000 * semver_version.minor
+ 100 * semver_version.patch
+ build_number;
Some(version_number)
}
}

View File

@ -24,6 +24,7 @@ use zebra_chain::{
};
use zebra_consensus::ParameterCheckpoint;
use zebra_network::address_book_peers::MockAddressBookPeers;
use zebra_node_services::mempool;
use zebra_state::{BoxError, GetBlockTemplateChainInfo};
@ -968,6 +969,7 @@ fn mock_services<Tip>(
zebra_state::ReadRequest,
>,
Tip,
MockAddressBookPeers,
>,
tokio::task::JoinHandle<()>,
)
@ -978,7 +980,7 @@ where
let state = MockService::build().for_prop_tests();
let (rpc, mempool_tx_queue) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
network,
false,
@ -986,6 +988,8 @@ where
mempool.clone(),
Buffer::new(state.clone(), 1),
chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
(mempool, state, rpc, mempool_tx_queue)

View File

@ -25,6 +25,7 @@ use zebra_chain::{
serialization::ZcashDeserializeInto,
subtree::NoteCommitmentSubtreeData,
};
use zebra_network::address_book_peers::MockAddressBookPeers;
use zebra_node_services::BoxError;
use zebra_state::{ReadRequest, ReadResponse, MAX_ON_DISK_HEIGHT};
use zebra_test::mock_service::MockService;
@ -116,6 +117,8 @@ async fn test_z_get_treestate() {
Buffer::new(MockService::build().for_unit_tests::<_, _, BoxError>(), 1),
state,
tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Request the treestate by a hash.
@ -198,7 +201,7 @@ async fn test_rpc_response_data_for_network(network: &Network) {
// Init RPC
let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"/Zebra:RPC test/",
network.clone(),
false,
@ -206,6 +209,8 @@ async fn test_rpc_response_data_for_network(network: &Network) {
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// We only want a snapshot of the `getblocksubsidy` and `getblockchaininfo` methods for the non-default Testnet (with an NU6 activation height).
@ -220,7 +225,10 @@ async fn test_rpc_response_data_for_network(network: &Network) {
}
// `getinfo`
let get_info = rpc.get_info().expect("We should have a GetInfo struct");
let get_info = rpc
.get_info()
.await
.expect("We should have a GetInfo struct");
snapshot_rpc_getinfo(get_info, &settings);
// `getblockchaininfo`
@ -522,7 +530,7 @@ async fn test_mocked_rpc_response_data_for_network(network: &Network) {
let mempool = MockService::build().for_unit_tests();
let (rpc, _) = RpcImpl::new(
"RPC test",
"0.0.1",
"/Zebra:RPC test/",
network.clone(),
false,
@ -530,6 +538,8 @@ async fn test_mocked_rpc_response_data_for_network(network: &Network) {
mempool,
state.clone(),
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Test the response format from `z_getsubtreesbyindex` for Sapling.
@ -599,6 +609,9 @@ fn snapshot_rpc_getinfo(info: GetInfo, settings: &insta::Settings) {
// replace with:
"[SubVersion]"
}),
".errorstimestamp" => dynamic_redaction(|_value, _path| {
"[LastErrorTimestamp]"
}),
})
});
}

View File

@ -1,9 +1,18 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 161
expression: info
---
{
"build": "vRPC test",
"subversion": "[SubVersion]"
"version": 100,
"build": "v0.0.1",
"subversion": "[SubVersion]",
"protocolversion": 170120,
"blocks": 10,
"connections": 0,
"difficulty": 1.0,
"testnet": false,
"paytxfee": 0.0,
"relayfee": 0.000001,
"errors": "no errors",
"errorstimestamp": "[LastErrorTimestamp]"
}

View File

@ -1,9 +1,18 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 161
expression: info
---
{
"build": "vRPC test",
"subversion": "[SubVersion]"
"version": 100,
"build": "v0.0.1",
"subversion": "[SubVersion]",
"protocolversion": 170120,
"blocks": 10,
"connections": 0,
"difficulty": 1.0,
"testnet": true,
"paytxfee": 0.0,
"relayfee": 0.000001,
"errors": "no errors",
"errorstimestamp": "[LastErrorTimestamp]"
}

View File

@ -14,9 +14,10 @@ use zebra_chain::{
serialization::{ZcashDeserializeInto, ZcashSerialize},
transaction::UnminedTxId,
};
use zebra_network::address_book_peers::MockAddressBookPeers;
use zebra_node_services::BoxError;
use zebra_state::{LatestChainTip, ReadStateService};
use zebra_state::{GetBlockTemplateChainInfo, LatestChainTip, ReadStateService};
use zebra_test::mock_service::MockService;
use super::super::*;
@ -29,7 +30,7 @@ async fn rpc_getinfo() {
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"/Zebra:RPC test/",
Mainnet,
false,
@ -37,13 +38,36 @@ async fn rpc_getinfo() {
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
let get_info = rpc.get_info().expect("We should have a GetInfo struct");
let getinfo_future = tokio::spawn(async move { rpc.get_info().await });
// Make the mock service respond with
let response_handler = state
.expect_request(zebra_state::ReadRequest::ChainInfo)
.await;
response_handler.respond(zebra_state::ReadResponse::ChainInfo(
GetBlockTemplateChainInfo {
tip_hash: Mainnet.genesis_hash(),
tip_height: Height::MIN,
history_tree: Default::default(),
expected_difficulty: Default::default(),
cur_time: zebra_chain::serialization::DateTime32::now(),
min_time: zebra_chain::serialization::DateTime32::now(),
max_time: zebra_chain::serialization::DateTime32::now(),
},
));
let get_info = getinfo_future
.await
.expect("getinfo future should not panic")
.expect("getinfo future should not return an error");
// make sure there is a `build` field in the response,
// and that is equal to the provided string, with an added 'v' version prefix.
assert_eq!(get_info.build, "vRPC test");
assert_eq!(get_info.build, "v0.0.1");
// make sure there is a `subversion` field,
// and that is equal to the Zebra user agent.
@ -120,7 +144,7 @@ async fn rpc_getblock() {
// Init RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -128,6 +152,8 @@ async fn rpc_getblock() {
Buffer::new(mempool.clone(), 1),
read_state.clone(),
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Make height calls with verbosity=0 and check response
@ -461,7 +487,7 @@ async fn rpc_getblock_parse_error() {
// Init RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -469,6 +495,8 @@ async fn rpc_getblock_parse_error() {
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Make sure we get an error if Zebra can't parse the block height.
@ -504,7 +532,7 @@ async fn rpc_getblock_missing_error() {
// Init RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -512,6 +540,8 @@ async fn rpc_getblock_missing_error() {
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Make sure Zebra returns the correct error code `-8` for missing blocks
@ -566,7 +596,7 @@ async fn rpc_getblockheader() {
// Init RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -574,6 +604,8 @@ async fn rpc_getblockheader() {
Buffer::new(mempool.clone(), 1),
read_state.clone(),
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Make height calls with verbose=false and check response
@ -677,7 +709,7 @@ async fn rpc_getbestblockhash() {
// Init RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -685,6 +717,8 @@ async fn rpc_getbestblockhash() {
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Get the tip hash using RPC method `get_best_block_hash`
@ -723,7 +757,7 @@ async fn rpc_getrawtransaction() {
// Init RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -731,6 +765,8 @@ async fn rpc_getrawtransaction() {
Buffer::new(mempool.clone(), 1),
read_state.clone(),
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// Test case where transaction is in mempool.
@ -899,7 +935,7 @@ async fn rpc_getaddresstxids_invalid_arguments() {
zebra_state::populated_state(blocks.clone(), &Mainnet).await;
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -907,6 +943,8 @@ async fn rpc_getaddresstxids_invalid_arguments() {
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// call the method with an invalid address string
@ -1048,7 +1086,7 @@ async fn rpc_getaddresstxids_response_with(
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
network.clone(),
false,
@ -1056,6 +1094,8 @@ async fn rpc_getaddresstxids_response_with(
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip.clone(),
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// call the method with valid arguments
@ -1100,7 +1140,7 @@ async fn rpc_getaddressutxos_invalid_arguments() {
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
let rpc = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -1108,6 +1148,8 @@ async fn rpc_getaddressutxos_invalid_arguments() {
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// call the method with an invalid address string
@ -1145,7 +1187,7 @@ async fn rpc_getaddressutxos_response() {
zebra_state::populated_state(blocks.clone(), &Mainnet).await;
let rpc = RpcImpl::new(
"RPC test",
"0.0.1",
"RPC test",
Mainnet,
false,
@ -1153,6 +1195,8 @@ async fn rpc_getaddressutxos_response() {
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip,
MockAddressBookPeers::new(vec![]),
crate::methods::LoggedLastEvent::new(None.into()),
);
// call the method with a valid address

View File

@ -24,7 +24,7 @@ use zebra_node_services::mempool;
use crate::{
config::Config,
methods::{RpcImpl, RpcServer as _},
methods::{LoggedLastEvent, RpcImpl, RpcServer as _},
server::{
http_request_compatibility::HttpRequestMiddlewareLayer,
rpc_call_compatibility::FixRpcResponseMiddleware,
@ -122,6 +122,7 @@ impl RpcServer {
network: Network,
#[cfg_attr(not(feature = "getblocktemplate-rpcs"), allow(unused_variables))]
mined_block_sender: Option<watch::Sender<(block::Hash, block::Height)>>,
last_event: LoggedLastEvent,
) -> Result<(ServerTask, JoinHandle<()>), tower::BoxError>
where
VersionString: ToString + Clone + Send + 'static,
@ -171,7 +172,7 @@ impl RpcServer {
latest_chain_tip.clone(),
block_verifier_router,
sync_status,
address_book,
address_book.clone(),
mined_block_sender,
);
@ -188,6 +189,8 @@ impl RpcServer {
mempool,
state,
latest_chain_tip,
address_book,
last_event,
);
let http_middleware_layer = if config.enable_cookie_auth {

View File

@ -57,6 +57,7 @@ async fn rpc_server_spawn() {
NoChainTip,
Mainnet,
None,
crate::methods::LoggedLastEvent::new(None.into()),
);
info!("spawned RPC server, checking services...");
@ -117,6 +118,7 @@ async fn rpc_spawn_unallocated_port(do_shutdown: bool) {
NoChainTip,
Mainnet,
None,
crate::methods::LoggedLastEvent::new(None.into()),
)
.await
.expect("");
@ -173,6 +175,7 @@ async fn rpc_server_spawn_port_conflict() {
NoChainTip,
Mainnet,
None,
crate::methods::LoggedLastEvent::new(None.into()),
)
.await;
@ -193,6 +196,7 @@ async fn rpc_server_spawn_port_conflict() {
NoChainTip,
Mainnet,
None,
crate::methods::LoggedLastEvent::new(None.into()),
)
.await;

View File

@ -36,6 +36,11 @@ fn fatal_error(app_name: String, err: &dyn std::error::Error) -> ! {
/// Application state
pub static APPLICATION: AppCell<ZebradApp> = AppCell::new();
lazy_static::lazy_static! {
/// The last log event that occurred in the application.
pub static ref LAST_LOG_EVENT: Arc<std::sync::Mutex<Option<(String, tracing::Level, chrono::DateTime<chrono::Utc>)>>> = Arc::new(std::sync::Mutex::new(None));
}
/// Returns the `zebrad` version for this build, in SemVer 2.0 format.
///
/// Includes `git describe` build metatata if available:

View File

@ -90,7 +90,7 @@ use zebra_rpc::server::RpcServer;
use zebra_rpc::methods::get_block_template_rpcs::types::submit_block::SubmitBlockChannel;
use crate::{
application::{build_version, user_agent},
application::{build_version, user_agent, LAST_LOG_EVENT},
components::{
inbound::{self, InboundSetupData, MAX_INBOUND_RESPONSE_TIME},
mempool::{self, Mempool},
@ -273,6 +273,7 @@ impl StartCmd {
Some(submit_block_channel.sender()),
#[cfg(not(feature = "getblocktemplate-rpcs"))]
None,
LAST_LOG_EVENT.clone(),
);
rpc_task_handle.await.unwrap()
} else {

View File

@ -3,9 +3,12 @@
use std::{
fs::{self, File},
io::Write,
sync::{Arc, Mutex},
};
use abscissa_core::{Component, FrameworkError, Shutdown};
use tracing::{field::Visit, Level};
use tracing_appender::non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard};
use tracing_error::ErrorLayer;
use tracing_subscriber::{
@ -13,7 +16,7 @@ use tracing_subscriber::{
layer::SubscriberExt,
reload::Handle,
util::SubscriberInitExt,
EnvFilter,
EnvFilter, Layer,
};
use zebra_chain::parameters::Network;
@ -184,7 +187,13 @@ impl Tracing {
#[cfg(not(feature = "filter-reload"))]
let filter_handle = None;
let subscriber = logger.finish().with(ErrorLayer::default());
let warn_error_layer = LastWarnErrorLayer {
last_event: crate::application::LAST_LOG_EVENT.clone(),
};
let subscriber = logger
.finish()
.with(warn_error_layer)
.with(ErrorLayer::default());
(subscriber, filter_handle)
};
@ -426,3 +435,46 @@ impl Drop for Tracing {
howudoin::disable();
}
}
// Visitor to extract only the "message" field from a log event.
struct MessageVisitor {
message: Option<String>,
}
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = Some(format!("{:?}", value));
}
}
}
// Layer to store the last WARN or ERROR log event.
#[derive(Debug, Clone)]
struct LastWarnErrorLayer {
last_event: Arc<Mutex<Option<(String, Level, chrono::DateTime<chrono::Utc>)>>>,
}
impl<S> Layer<S> for LastWarnErrorLayer
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let level = *event.metadata().level();
let timestamp = chrono::Utc::now();
if level == Level::WARN || level == Level::ERROR {
let mut visitor = MessageVisitor { message: None };
event.record(&mut visitor);
if let Some(message) = visitor.message {
let mut last_event = self.last_event.lock().expect("lock poisoned");
*last_event = Some((message, level, timestamp));
}
}
}
}

View File

@ -1600,6 +1600,9 @@ async fn rpc_endpoint(parallel_cpu_threads: bool) -> Result<()> {
// Create an http client
let client = RpcRequestClient::new(rpc_address);
// Run `zebrad` for a few seconds before testing the endpoint
std::thread::sleep(LAUNCH_DELAY);
// Make the call to the `getinfo` RPC method
let res = client.call("getinfo", "[]".to_string()).await?;