mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-28 08:39:59 -07:00
global: snapshot
This commit is contained in:
103
crates/brk_server/src/api/metrics/bulk.rs
Normal file
103
crates/brk_server/src/api/metrics/bulk.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_query::{MetricSelection, Output};
|
||||
use brk_types::Format;
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{
|
||||
CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<MetricSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(uri, headers, query, state).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let mut response =
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
|
||||
response.headers_mut().insert_cors();
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn req_to_response_res(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelection>,
|
||||
AppState { query, cache, .. }: AppState,
|
||||
) -> brk_error::Result<Response> {
|
||||
let format = params.format();
|
||||
let height = query.sync(|q| q.height());
|
||||
let to = params.to();
|
||||
|
||||
let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || {
|
||||
height.into()
|
||||
});
|
||||
|
||||
if cache_params.matches_etag(&headers) {
|
||||
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
|
||||
response.headers_mut().insert_cors();
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
let cache_key = format!(
|
||||
"{}{}{}",
|
||||
uri.path(),
|
||||
uri.query().unwrap_or(""),
|
||||
cache_params.etag_str()
|
||||
);
|
||||
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
|
||||
|
||||
let mut response = if let GuardResult::Value(v) = guard_res {
|
||||
Response::new(Body::from(v))
|
||||
} else {
|
||||
match query
|
||||
.run(move |q| q.search_and_format_bulk_checked(params, MAX_WEIGHT))
|
||||
.await?
|
||||
{
|
||||
Output::CSV(s) => {
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
let _ = g.insert(s.clone().into());
|
||||
}
|
||||
s.into_response()
|
||||
}
|
||||
Output::Json(v) => {
|
||||
let json = v.to_vec();
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
let _ = g.insert(json.clone().into());
|
||||
}
|
||||
json.into_response()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
if let Some(etag) = &cache_params.etag {
|
||||
headers.insert_etag(etag);
|
||||
}
|
||||
headers.insert_cache_control(&cache_params.cache_control);
|
||||
|
||||
match format {
|
||||
Format::CSV => {
|
||||
headers.insert_content_disposition_attachment();
|
||||
headers.insert_content_type_text_csv()
|
||||
}
|
||||
Format::JSON => headers.insert_content_type_application_json(),
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Handler for single metric data endpoint.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
@@ -10,13 +12,12 @@ use brk_query::{MetricSelection, Output};
|
||||
use brk_types::Format;
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{CacheStrategy, cache::CacheParams, extended::HeaderMapExtended};
|
||||
use crate::{
|
||||
CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
/// Maximum allowed request weight in bytes (650KB)
|
||||
const MAX_WEIGHT: usize = 65 * 10_000;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
@@ -44,7 +45,9 @@ async fn req_to_response_res(
|
||||
let height = query.sync(|q| q.height());
|
||||
let to = params.to();
|
||||
|
||||
let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || height.into());
|
||||
let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || {
|
||||
height.into()
|
||||
});
|
||||
|
||||
if cache_params.matches_etag(&headers) {
|
||||
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
|
||||
@@ -52,7 +55,12 @@ async fn req_to_response_res(
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
let cache_key = format!("{}{}{}", uri.path(), uri.query().unwrap_or(""), cache_params.etag_str());
|
||||
let cache_key = format!(
|
||||
"single-{}{}{}",
|
||||
uri.path(),
|
||||
uri.query().unwrap_or(""),
|
||||
cache_params.etag_str()
|
||||
);
|
||||
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
|
||||
|
||||
let mut response = if let GuardResult::Value(v) = guard_res {
|
||||
|
||||
105
crates/brk_server/src/api/metrics/legacy.rs
Normal file
105
crates/brk_server/src/api/metrics/legacy.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Deprecated handler for legacy endpoints - returns raw data without MetricData wrapper.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use brk_query::{MetricSelection, OutputLegacy};
|
||||
use brk_types::Format;
|
||||
use quick_cache::sync::GuardResult;
|
||||
|
||||
use crate::{
|
||||
CacheStrategy, api::metrics::MAX_WEIGHT, cache::CacheParams, extended::HeaderMapExtended,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub async fn handler(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
query: Query<MetricSelection>,
|
||||
State(state): State<AppState>,
|
||||
) -> Response {
|
||||
match req_to_response_res(uri, headers, query, state).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let mut response =
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response();
|
||||
response.headers_mut().insert_cors();
|
||||
response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn req_to_response_res(
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Query(params): Query<MetricSelection>,
|
||||
AppState { query, cache, .. }: AppState,
|
||||
) -> brk_error::Result<Response> {
|
||||
let format = params.format();
|
||||
let height = query.sync(|q| q.height());
|
||||
let to = params.to();
|
||||
|
||||
let cache_params = CacheParams::resolve(&CacheStrategy::height_with(format!("{to:?}")), || {
|
||||
height.into()
|
||||
});
|
||||
|
||||
if cache_params.matches_etag(&headers) {
|
||||
let mut response = (StatusCode::NOT_MODIFIED, "").into_response();
|
||||
response.headers_mut().insert_cors();
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
let cache_key = format!(
|
||||
"legacy-{}{}{}",
|
||||
uri.path(),
|
||||
uri.query().unwrap_or(""),
|
||||
cache_params.etag_str()
|
||||
);
|
||||
let guard_res = cache.get_value_or_guard(&cache_key, Some(Duration::from_millis(50)));
|
||||
|
||||
let mut response = if let GuardResult::Value(v) = guard_res {
|
||||
Response::new(Body::from(v))
|
||||
} else {
|
||||
match query
|
||||
.run(move |q| q.search_and_format_legacy_checked(params, MAX_WEIGHT))
|
||||
.await?
|
||||
{
|
||||
OutputLegacy::CSV(s) => {
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
let _ = g.insert(s.clone().into());
|
||||
}
|
||||
s.into_response()
|
||||
}
|
||||
OutputLegacy::Json(v) => {
|
||||
let json = v.to_vec();
|
||||
if let GuardResult::Guard(g) = guard_res {
|
||||
let _ = g.insert(json.clone().into());
|
||||
}
|
||||
json.into_response()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let headers = response.headers_mut();
|
||||
headers.insert_cors();
|
||||
if let Some(etag) = &cache_params.etag {
|
||||
headers.insert_etag(etag);
|
||||
}
|
||||
headers.insert_cache_control(&cache_params.cache_control);
|
||||
|
||||
match format {
|
||||
Format::CSV => {
|
||||
headers.insert_content_disposition_attachment();
|
||||
headers.insert_content_type_text_csv()
|
||||
}
|
||||
Format::JSON => headers.insert_content_type_application_json(),
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
@@ -5,15 +5,24 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_query::{DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination};
|
||||
use brk_query::{
|
||||
DataRangeFormat, MetricSelection, MetricSelectionLegacy, PaginatedMetrics, Pagination,
|
||||
};
|
||||
use brk_traversable::TreeNode;
|
||||
use brk_types::{Index, IndexInfo, Limit, Metric, MetricCount, Metrics};
|
||||
use brk_types::{
|
||||
Index, IndexInfo, Limit, Metric, MetricCount, MetricData, MetricWithIndex, Metrics,
|
||||
};
|
||||
|
||||
use crate::{CacheStrategy, extended::TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod bulk;
|
||||
mod data;
|
||||
mod legacy;
|
||||
|
||||
/// Maximum allowed request weight in bytes (650KB)
|
||||
const MAX_WEIGHT: usize = 65 * 10_000;
|
||||
|
||||
pub trait ApiMetricsRoutes {
|
||||
fn add_metrics_routes(self) -> Self;
|
||||
@@ -48,7 +57,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
headers: HeaderMap,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.get_indexes().to_vec())).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.indexes().to_vec())).await
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
@@ -68,7 +77,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
State(state): State<AppState>,
|
||||
Query(pagination): Query<Pagination>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.get_metrics(pagination))).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, move |q| Ok(q.metrics(pagination))).await
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
@@ -82,7 +91,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
"/api/metrics/catalog",
|
||||
get_with(
|
||||
async |headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.get_metrics_catalog().clone())).await
|
||||
state.cached_json(&headers, CacheStrategy::Static, |q| Ok(q.metrics_catalog().clone())).await
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
@@ -125,13 +134,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
if let Some(indexes) = q.metric_to_indexes(metric.clone()) {
|
||||
return Ok(indexes.clone())
|
||||
}
|
||||
Err(brk_error::Error::String(
|
||||
if let Some(first) = q.match_metric(&metric, Limit::MIN).first() {
|
||||
format!("Could not find '{metric}', did you mean '{first}' ?")
|
||||
} else {
|
||||
format!("Could not find '{metric}'.")
|
||||
}
|
||||
))
|
||||
Err(q.metric_not_found_error(&metric))
|
||||
}).await
|
||||
},
|
||||
|op| op
|
||||
@@ -146,25 +149,48 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
// WIP
|
||||
.route("/api/metrics/bulk", get(data::handler))
|
||||
.route(
|
||||
.api_route(
|
||||
"/api/metric/{metric}/{index}",
|
||||
get(
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
state: State<AppState>,
|
||||
Path((metric, index)): Path<(Metric, Index)>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::handler(
|
||||
uri,
|
||||
headers,
|
||||
Query(MetricSelection::from((index, metric, range))),
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
.summary("Get metric data")
|
||||
.description(
|
||||
"Fetch data for a specific metric at the given index. \
|
||||
Use query parameters to filter by date range and format (json/csv)."
|
||||
)
|
||||
.ok_response::<MetricData>()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/bulk",
|
||||
get_with(
|
||||
bulk::handler,
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
.summary("Bulk metric data")
|
||||
.description(
|
||||
"Fetch multiple metrics in a single request. Supports filtering by index and date range. \
|
||||
Returns an array of MetricData objects."
|
||||
)
|
||||
.ok_response::<Vec<MetricData>>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
// !!!
|
||||
@@ -178,7 +204,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
Query(params): Query<MetricSelectionLegacy>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
data::handler(uri, headers, Query(params.into()), state).await
|
||||
legacy::handler(uri, headers, Query(params.into()), state).await
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -208,7 +234,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
Metrics::from(split.collect::<Vec<_>>().join(separator)),
|
||||
range,
|
||||
));
|
||||
data::handler(uri, headers, Query(params), state).await
|
||||
legacy::handler(uri, headers, Query(params), state).await
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user