1use std::{borrow::Cow, fmt::Display, path::PathBuf};
27
28use derive_more::From;
29use futures::FutureExt;
30use serde::{Deserialize, Serialize};
31use snafu::Snafu;
32use tide_disco::{api::ApiError, method::ReadState, Api, RequestError, StatusCode};
33use vbs::version::StaticVersionType;
34
35use crate::api::load_api;
36
37pub(crate) mod data_source;
38
39pub use data_source::*;
40
41#[derive(Default)]
42pub struct Options {
43 pub api_path: Option<PathBuf>,
44
45 pub extensions: Vec<toml::Value>,
50}
51
52#[derive(Clone, Debug, From, Snafu, Deserialize, Serialize)]
53pub enum Error {
54 Request { source: RequestError },
55 Internal { reason: String },
56}
57
58impl Error {
59 pub fn status(&self) -> StatusCode {
60 match self {
61 Self::Request { .. } => StatusCode::BAD_REQUEST,
62 Self::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
63 }
64 }
65}
66
67fn internal<M: Display>(msg: M) -> Error {
68 Error::Internal {
69 reason: msg.to_string(),
70 }
71}
72
73pub fn define_api<State, Ver: StaticVersionType + 'static>(
74 options: &Options,
75 _: Ver,
76 api_ver: semver::Version,
77) -> Result<Api<State, Error, Ver>, ApiError>
78where
79 State: 'static + Send + Sync + ReadState,
80 <State as ReadState>::State: Send + Sync + StatusDataSource,
81{
82 let mut api = load_api::<State, Error, Ver>(
83 options.api_path.as_ref(),
84 include_str!("../api/status.toml"),
85 options.extensions.clone(),
86 )?;
87 api.with_version(api_ver)
88 .get("block_height", |_, state| {
89 async { state.block_height().await.map_err(internal) }.boxed()
90 })?
91 .get("success_rate", |_, state| {
92 async { state.success_rate().await.map_err(internal) }.boxed()
93 })?
94 .get("time_since_last_decide", |_, state| {
95 async {
96 state
97 .elapsed_time_since_last_decide()
98 .await
99 .map_err(internal)
100 }
101 .boxed()
102 })?
103 .metrics("metrics", |_, state| {
104 async { Ok(Cow::Borrowed(state.metrics())) }.boxed()
105 })?;
106 Ok(api)
107}
108
109#[cfg(test)]
110mod test {
111 use std::{str::FromStr, time::Duration};
112
113 use async_lock::RwLock;
114 use futures::FutureExt;
115 use portpicker::pick_unused_port;
116 use reqwest::redirect::Policy;
117 use surf_disco::Client;
118 use tempfile::TempDir;
119 use tide_disco::{App, Url};
120 use toml::toml;
121
122 use super::*;
123 use crate::{
124 data_source::ExtensibleDataSource,
125 task::BackgroundTask,
126 testing::{
127 consensus::{MockDataSource, MockNetwork},
128 mocks::{MockBase, MockVersions},
129 sleep,
130 },
131 ApiState, Error,
132 };
133
134 #[test_log::test(tokio::test(flavor = "multi_thread"))]
135 async fn test_api() {
136 let mut network = MockNetwork::<MockDataSource, MockVersions>::init().await;
138
139 let port = pick_unused_port().unwrap();
141 let mut app = App::<_, Error>::with_state(ApiState::from(network.data_source()));
142 app.register_module(
143 "status",
144 define_api(
145 &Default::default(),
146 MockBase::instance(),
147 "0.0.1".parse().unwrap(),
148 )
149 .unwrap(),
150 )
151 .unwrap();
152 network.spawn(
153 "server",
154 app.serve(format!("0.0.0.0:{port}"), MockBase::instance()),
155 );
156
157 let url = Url::from_str(&format!("http://localhost:{port}/status")).unwrap();
159 let client = Client::<Error, MockBase>::new(url.clone());
160 assert!(client.connect(Some(Duration::from_secs(60))).await);
161
162 assert_eq!(client.get::<u64>("block-height").send().await.unwrap(), 0);
164
165 let reqwest_client = reqwest::Client::builder()
168 .redirect(Policy::limited(5))
169 .build()
170 .unwrap();
171
172 let res = reqwest_client
174 .get(format!("{url}/metrics"))
175 .send()
176 .await
177 .unwrap();
178
179 assert_eq!(res.status(), StatusCode::OK);
181 let prometheus = res.text().await.unwrap();
182 let lines = prometheus.lines().collect::<Vec<_>>();
183 assert!(
184 lines.contains(&"consensus_current_view 0"),
185 "Missing consensus_current_view in metrics:\n{prometheus}"
186 );
187
188 network.start().await;
190
191 while client.get::<u64>("block-height").send().await.unwrap() <= 1 {
195 tracing::info!("waiting for block height to update");
196 sleep(Duration::from_secs(1)).await;
197 }
198 let success_rate = client.get::<f64>("success-rate").send().await.unwrap();
199 assert!(success_rate.is_finite(), "{success_rate}");
202 assert!(success_rate > 0.0, "{success_rate}");
204
205 network.shut_down().await;
206 }
207
208 #[test_log::test(tokio::test(flavor = "multi_thread"))]
209 async fn test_extensions() {
210 let dir = TempDir::with_prefix("test_status_extensions").unwrap();
211 let data_source = ExtensibleDataSource::new(
212 MockDataSource::create(dir.path(), Default::default())
213 .await
214 .unwrap(),
215 0,
216 );
217
218 let extensions = toml! {
219 [route.post_ext]
220 PATH = ["/ext/:val"]
221 METHOD = "POST"
222 ":val" = "Integer"
223
224 [route.get_ext]
225 PATH = ["/ext"]
226 METHOD = "GET"
227 };
228
229 let mut api = define_api::<RwLock<ExtensibleDataSource<MockDataSource, u64>>, MockBase>(
230 &Options {
231 extensions: vec![extensions.into()],
232 ..Default::default()
233 },
234 MockBase::instance(),
235 "0.0.1".parse().unwrap(),
236 )
237 .unwrap();
238 api.get("get_ext", |_, state| {
239 async move { Ok(*state.as_ref()) }.boxed()
240 })
241 .unwrap()
242 .post("post_ext", |req, state| {
243 async move {
244 *state.as_mut() = req.integer_param("val")?;
245 Ok(())
246 }
247 .boxed()
248 })
249 .unwrap();
250
251 let mut app = App::<_, Error>::with_state(RwLock::new(data_source));
252 app.register_module("status", api).unwrap();
253
254 let port = pick_unused_port().unwrap();
255 let _server = BackgroundTask::spawn(
256 "server",
257 app.serve(format!("0.0.0.0:{port}"), MockBase::instance()),
258 );
259
260 let client = Client::<Error, MockBase>::new(
261 format!("http://localhost:{port}/status").parse().unwrap(),
262 );
263 assert!(client.connect(Some(Duration::from_secs(60))).await);
264
265 assert_eq!(client.get::<u64>("ext").send().await.unwrap(), 0);
266 client.post::<()>("ext/42").send().await.unwrap();
267 assert_eq!(client.get::<u64>("ext").send().await.unwrap(), 42);
268
269 assert_eq!(client.get::<u64>("block-height").send().await.unwrap(), 0);
271 }
272}