···
use dropshot::ConfigLogging;
use dropshot::ConfigLoggingLevel;
22
+
use dropshot::HttpResponse;
use dropshot::RequestContext;
use dropshot::ServerBuilder;
26
-
use http::{Response, StatusCode};
26
+
use dropshot::ServerContext;
28
+
header::{ORIGIN, USER_AGENT},
29
+
Response, StatusCode,
31
+
use metrics::{counter, describe_counter, describe_histogram, histogram, Unit};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
35
+
use std::future::Future;
37
+
use std::time::Instant;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
40
+
fn describe_metrics() {
42
+
"server_requests_total",
44
+
"total requests handled"
46
+
describe_histogram!(
47
+
"server_handler_latency",
49
+
"time to respond to a request in microseconds, excluding dropshot overhead"
53
+
async fn instrument_handler<T, H, R>(ctx: &RequestContext<T>, handler: H) -> Result<R, HttpError>
56
+
H: Future<Output = Result<R, HttpError>>,
59
+
let start = Instant::now();
60
+
let result = handler.await;
61
+
let latency = start.elapsed();
62
+
let status_code = match &result {
63
+
Ok(response) => response.status_code(),
64
+
Err(ref e) => e.status_code.as_status(),
67
+
let endpoint = ctx.endpoint.operation_id.clone();
68
+
let headers = ctx.request.headers();
69
+
let origin = headers
71
+
.and_then(|v| v.to_str().ok())
76
+
.and_then(|v| v.to_str().ok())
78
+
if ua.starts_with("Mozilla/5.0 ") {
86
+
counter!("server_requests_total",
87
+
"endpoint" => endpoint.clone(),
90
+
"status_code" => status_code,
93
+
histogram!("server_handler_latency", "endpoint" => endpoint).record(latency.as_micros() as f64);
pub spec: Arc<serde_json::Value>,
storage: Box<dyn StoreReader>,
···
66
-
async fn index(_ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
67
-
Ok(Response::builder()
68
-
.status(StatusCode::OK)
69
-
.header(http::header::CONTENT_TYPE, "text/html")
70
-
.body(INDEX_HTML.into())?)
130
+
async fn index(ctx: RequestContext<Context>) -> Result<Response<Body>, HttpError> {
131
+
instrument_handler(&ctx, async {
132
+
Ok(Response::builder()
133
+
.status(StatusCode::OK)
134
+
.header(http::header::CONTENT_TYPE, "text/html")
135
+
.body(INDEX_HTML.into())?)
/// Meta: get the openapi spec for this api
···
async fn get_openapi(ctx: RequestContext<Context>) -> OkCorsResponse<serde_json::Value> {
83
-
let spec = (*ctx.context().spec).clone();
150
+
instrument_handler(&ctx, async {
151
+
let spec = (*ctx.context().spec).clone();
152
+
OkCors(spec).into()
#[derive(Debug, Serialize, JsonSchema)]
···
|what| move |e| HttpError::for_internal_error(format!("failed to get {what}: {e:?}"));
103
-
let storage_info = storage
104
-
.get_storage_stats()
106
-
.map_err(failed_to_get("storage info"))?;
173
+
instrument_handler(&ctx, async {
174
+
let storage_info = storage
175
+
.get_storage_stats()
177
+
.map_err(failed_to_get("storage info"))?;
108
-
let consumer = storage
109
-
.get_consumer_info()
111
-
.map_err(failed_to_get("consumer info"))?;
179
+
let consumer = storage
180
+
.get_consumer_info()
182
+
.map_err(failed_to_get("consumer info"))?;
114
-
storage_name: storage.name(),
115
-
storage: storage_info,
185
+
storage_name: storage.name(),
186
+
storage: storage_info,
// TODO: replace with normal (🙃) multi-qs value somehow
···
collection_query: Query<RecordsCollectionsQuery>,
) -> OkCorsResponse<Vec<ApiRecord>> {
let Context { storage, .. } = ctx.context();
171
-
let mut limit = 42;
172
-
let query = collection_query.into_inner();
173
-
let collections = if let Some(provided_collection) = query.collection {
174
-
to_multiple_nsids(&provided_collection)
175
-
.map_err(|reason| HttpError::for_bad_request(None, reason))?
178
-
let min_time_ago = SystemTime::now() - Duration::from_secs(86_400 * 3); // we want at least 3 days of data
179
-
let since: WeekTruncatedCursor = Cursor::at(min_time_ago).into();
180
-
let (collections, _) = storage
183
-
Default::default(),
184
-
Some(since.try_as().unwrap()),
244
+
instrument_handler(&ctx, async {
245
+
let mut limit = 42;
246
+
let query = collection_query.into_inner();
247
+
let collections = if let Some(provided_collection) = query.collection {
248
+
to_multiple_nsids(&provided_collection)
249
+
.map_err(|reason| HttpError::for_bad_request(None, reason))?
252
+
let min_time_ago = SystemTime::now() - Duration::from_secs(86_400 * 3); // we want at least 3 days of data
253
+
let since: WeekTruncatedCursor = Cursor::at(min_time_ago).into();
254
+
let (collections, _) = storage
257
+
Default::default(),
258
+
Some(since.try_as().unwrap()),
262
+
.map_err(|e| HttpError::for_internal_error(e.to_string()))?;
265
+
.map(|c| Nsid::new(c.nsid).unwrap())
269
+
let records = storage
270
+
.get_records_by_collections(collections, limit, true)
188
-
.map_err(|e| HttpError::for_internal_error(e.to_string()))?;
272
+
.map_err(|e| HttpError::for_internal_error(e.to_string()))?
191
-
.map(|c| Nsid::new(c.nsid).unwrap())
195
-
let records = storage
196
-
.get_records_by_collections(collections, limit, true)
198
-
.map_err(|e| HttpError::for_internal_error(e.to_string()))?
203
-
OkCors(records).into()
277
+
OkCors(records).into()
#[derive(Debug, Deserialize, JsonSchema)]
···
query: Query<CollectionsStatsQuery>,
) -> OkCorsResponse<HashMap<String, JustCount>> {
let Context { storage, .. } = ctx.context();
235
-
let q = query.into_inner();
236
-
let collections: HashSet<Nsid> = collections_query.try_into()?;
238
-
let since = q.since.map(dt_to_cursor).transpose()?.unwrap_or_else(|| {
239
-
let week_ago_secs = 7 * 86_400;
240
-
let week_ago = SystemTime::now() - Duration::from_secs(week_ago_secs);
241
-
Cursor::at(week_ago).into()
312
+
instrument_handler(&ctx, async {
313
+
let q = query.into_inner();
314
+
let collections: HashSet<Nsid> = collections_query.try_into()?;
244
-
let until = q.until.map(dt_to_cursor).transpose()?;
316
+
let since = q.since.map(dt_to_cursor).transpose()?.unwrap_or_else(|| {
317
+
let week_ago_secs = 7 * 86_400;
318
+
let week_ago = SystemTime::now() - Duration::from_secs(week_ago_secs);
319
+
Cursor::at(week_ago).into()
246
-
let mut seen_by_collection = HashMap::with_capacity(collections.len());
322
+
let until = q.until.map(dt_to_cursor).transpose()?;
248
-
for collection in &collections {
249
-
let counts = storage
250
-
.get_collection_counts(collection, since, until)
252
-
.map_err(|e| HttpError::for_internal_error(format!("boooo: {e:?}")))?;
324
+
let mut seen_by_collection = HashMap::with_capacity(collections.len());
326
+
for collection in &collections {
327
+
let counts = storage
328
+
.get_collection_counts(collection, since, until)
330
+
.map_err(|e| HttpError::for_internal_error(format!("boooo: {e:?}")))?;
254
-
seen_by_collection.insert(collection.to_string(), counts);
332
+
seen_by_collection.insert(collection.to_string(), counts);
257
-
OkCors(seen_by_collection).into()
335
+
OkCors(seen_by_collection).into()
#[derive(Debug, Serialize, JsonSchema)]
···
let Context { storage, .. } = ctx.context();
let q = query.into_inner();
340
-
if q.cursor.is_some() && q.order.is_some() {
341
-
let msg = "`cursor` is mutually exclusive with `order`. ordered results cannot be paged.";
342
-
return Err(HttpError::for_bad_request(None, msg.to_string()));
420
+
instrument_handler(&ctx, async {
421
+
if q.cursor.is_some() && q.order.is_some() {
423
+
"`cursor` is mutually exclusive with `order`. ordered results cannot be paged.";
424
+
return Err(HttpError::for_bad_request(None, msg.to_string()));
345
-
let order = if let Some(ref o) = q.order {
350
-
.and_then(|c| if c.is_empty() { None } else { Some(c) })
351
-
.map(|c| URL_SAFE_NO_PAD.decode(&c))
353
-
.map_err(|e| HttpError::for_bad_request(None, format!("invalid cursor: {e:?}")))?;
354
-
OrderCollectionsBy::Lexi { cursor }
427
+
let order = if let Some(ref o) = q.order {
432
+
.and_then(|c| if c.is_empty() { None } else { Some(c) })
433
+
.map(|c| URL_SAFE_NO_PAD.decode(&c))
435
+
.map_err(|e| HttpError::for_bad_request(None, format!("invalid cursor: {e:?}")))?;
436
+
OrderCollectionsBy::Lexi { cursor }
357
-
let limit = match (q.limit, q.order) {
358
-
(Some(limit), _) => limit,
359
-
(None, Some(_)) => 32,
360
-
(None, None) => 100,
439
+
let limit = match (q.limit, q.order) {
440
+
(Some(limit), _) => limit,
441
+
(None, Some(_)) => 32,
442
+
(None, None) => 100,
363
-
if !(1..=200).contains(&limit) {
364
-
let msg = format!("limit not in 1..=200: {}", limit);
365
-
return Err(HttpError::for_bad_request(None, msg));
445
+
if !(1..=200).contains(&limit) {
446
+
let msg = format!("limit not in 1..=200: {}", limit);
447
+
return Err(HttpError::for_bad_request(None, msg));
368
-
let since = q.since.map(dt_to_cursor).transpose()?;
369
-
let until = q.until.map(dt_to_cursor).transpose()?;
450
+
let since = q.since.map(dt_to_cursor).transpose()?;
451
+
let until = q.until.map(dt_to_cursor).transpose()?;
371
-
let (collections, next_cursor) = storage
372
-
.get_collections(limit, order, since, until)
374
-
.map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
453
+
let (collections, next_cursor) = storage
454
+
.get_collections(limit, order, since, until)
456
+
.map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
376
-
let next_cursor = next_cursor.map(|c| URL_SAFE_NO_PAD.encode(c));
458
+
let next_cursor = next_cursor.map(|c| URL_SAFE_NO_PAD.encode(c));
378
-
OkCors(CollectionsResponse {
380
-
cursor: next_cursor,
460
+
OkCors(CollectionsResponse {
462
+
cursor: next_cursor,
#[derive(Debug, Serialize, JsonSchema)]
···
let Context { storage, .. } = ctx.context();
let q = query.into_inner();
462
-
let prefix = NsidPrefix::new(&q.prefix).map_err(|e| {
463
-
HttpError::for_bad_request(
465
-
format!("{:?} was not a valid NSID prefix: {e:?}", q.prefix),
546
+
instrument_handler(&ctx, async {
547
+
let prefix = NsidPrefix::new(&q.prefix).map_err(|e| {
548
+
HttpError::for_bad_request(
550
+
format!("{:?} was not a valid NSID prefix: {e:?}", q.prefix),
469
-
if q.cursor.is_some() && q.order.is_some() {
470
-
let msg = "`cursor` is mutually exclusive with `order`. ordered results cannot be paged.";
471
-
return Err(HttpError::for_bad_request(None, msg.to_string()));
554
+
if q.cursor.is_some() && q.order.is_some() {
556
+
"`cursor` is mutually exclusive with `order`. ordered results cannot be paged.";
557
+
return Err(HttpError::for_bad_request(None, msg.to_string()));
474
-
let order = if let Some(ref o) = q.order {
479
-
.and_then(|c| if c.is_empty() { None } else { Some(c) })
480
-
.map(|c| URL_SAFE_NO_PAD.decode(&c))
482
-
.map_err(|e| HttpError::for_bad_request(None, format!("invalid cursor: {e:?}")))?;
483
-
OrderCollectionsBy::Lexi { cursor }
560
+
let order = if let Some(ref o) = q.order {
565
+
.and_then(|c| if c.is_empty() { None } else { Some(c) })
566
+
.map(|c| URL_SAFE_NO_PAD.decode(&c))
568
+
.map_err(|e| HttpError::for_bad_request(None, format!("invalid cursor: {e:?}")))?;
569
+
OrderCollectionsBy::Lexi { cursor }
486
-
let limit = match (q.limit, q.order) {
487
-
(Some(limit), _) => limit,
488
-
(None, Some(_)) => 32,
489
-
(None, None) => 100,
572
+
let limit = match (q.limit, q.order) {
573
+
(Some(limit), _) => limit,
574
+
(None, Some(_)) => 32,
575
+
(None, None) => 100,
492
-
if !(1..=200).contains(&limit) {
493
-
let msg = format!("limit not in 1..=200: {}", limit);
494
-
return Err(HttpError::for_bad_request(None, msg));
578
+
if !(1..=200).contains(&limit) {
579
+
let msg = format!("limit not in 1..=200: {}", limit);
580
+
return Err(HttpError::for_bad_request(None, msg));
497
-
let since = q.since.map(dt_to_cursor).transpose()?;
498
-
let until = q.until.map(dt_to_cursor).transpose()?;
583
+
let since = q.since.map(dt_to_cursor).transpose()?;
584
+
let until = q.until.map(dt_to_cursor).transpose()?;
500
-
let (total, children, next_cursor) = storage
501
-
.get_prefix(prefix, limit, order, since, until)
503
-
.map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
586
+
let (total, children, next_cursor) = storage
587
+
.get_prefix(prefix, limit, order, since, until)
589
+
.map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
505
-
let next_cursor = next_cursor.map(|c| URL_SAFE_NO_PAD.encode(c));
591
+
let next_cursor = next_cursor.map(|c| URL_SAFE_NO_PAD.encode(c));
507
-
OkCors(PrefixResponse {
510
-
cursor: next_cursor,
593
+
OkCors(PrefixResponse {
596
+
cursor: next_cursor,
#[derive(Debug, Deserialize, JsonSchema)]
···
let Context { storage, .. } = ctx.context();
let q = query.into_inner();
552
-
let since = q.since.map(dt_to_cursor).transpose()?.unwrap_or_else(|| {
553
-
let week_ago_secs = 7 * 86_400;
554
-
let week_ago = SystemTime::now() - Duration::from_secs(week_ago_secs);
555
-
Cursor::at(week_ago).into()
640
+
instrument_handler(&ctx, async {
641
+
let since = q.since.map(dt_to_cursor).transpose()?.unwrap_or_else(|| {
642
+
let week_ago_secs = 7 * 86_400;
643
+
let week_ago = SystemTime::now() - Duration::from_secs(week_ago_secs);
644
+
Cursor::at(week_ago).into()
558
-
let until = q.until.map(dt_to_cursor).transpose()?;
647
+
let until = q.until.map(dt_to_cursor).transpose()?;
560
-
let step = if let Some(secs) = q.step {
562
-
let msg = format!("step is too small: {}", secs);
563
-
Err(HttpError::for_bad_request(None, msg))?;
565
-
(secs / 3600) * 3600 // trucate to hour
649
+
let step = if let Some(secs) = q.step {
651
+
let msg = format!("step is too small: {}", secs);
652
+
Err(HttpError::for_bad_request(None, msg))?;
654
+
(secs / 3600) * 3600 // trucate to hour
570
-
let nsid = Nsid::new(q.collection).map_err(|e| {
571
-
HttpError::for_bad_request(None, format!("collection was not a valid NSID: {:?}", e))
659
+
let nsid = Nsid::new(q.collection).map_err(|e| {
660
+
HttpError::for_bad_request(None, format!("collection was not a valid NSID: {:?}", e))
574
-
let (range_cursors, series) = storage
575
-
.get_timeseries(vec![nsid], since, until, step)
577
-
.map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
663
+
let (range_cursors, series) = storage
664
+
.get_timeseries(vec![nsid], since, until, step)
666
+
.map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
579
-
let range = range_cursors
581
-
.map(|c| DateTime::<Utc>::from_timestamp_micros(c.to_raw_u64() as i64).unwrap())
668
+
let range = range_cursors
670
+
.map(|c| DateTime::<Utc>::from_timestamp_micros(c.to_raw_u64() as i64).unwrap())
584
-
let series = series
586
-
.map(|(k, v)| (k.to_string(), v.iter().map(Into::into).collect()))
673
+
let series = series
675
+
.map(|(k, v)| (k.to_string(), v.iter().map(Into::into).collect()))
589
-
OkCors(CollectionTimeseriesResponse { range, series }).into()
678
+
OkCors(CollectionTimeseriesResponse { range, series }).into()
#[derive(Debug, Deserialize, JsonSchema)]
···
) -> OkCorsResponse<SearchResponse> {
let Context { storage, .. } = ctx.context();
let q = query.into_inner();
614
-
// TODO: query validation
615
-
// TODO: also handle multi-space stuff (ufos-app tries to on client)
616
-
let terms: Vec<String> = q.q.split(' ').map(Into::into).collect();
617
-
let matches = storage
618
-
.search_collections(terms)
620
-
.map_err(|e| HttpError::for_internal_error(format!("oh ugh: {e:?}")))?;
621
-
OkCors(SearchResponse { matches }).into()
705
+
instrument_handler(&ctx, async {
706
+
// TODO: query validation
707
+
// TODO: also handle multi-space stuff (ufos-app tries to on client)
708
+
let terms: Vec<String> = q.q.split(' ').map(Into::into).collect();
709
+
let matches = storage
710
+
.search_collections(terms)
712
+
.map_err(|e| HttpError::for_internal_error(format!("oh ugh: {e:?}")))?;
713
+
OkCors(SearchResponse { matches }).into()
pub async fn serve(storage: impl StoreReader + 'static) -> Result<(), String> {
719
+
describe_metrics();
let log = ConfigLogging::StderrTerminal {
626
-
level: ConfigLoggingLevel::Info,
721
+
level: ConfigLoggingLevel::Warn,
628
-
.to_logger("hello-ufos")
723
+
.to_logger("server")
.map_err(|e| e.to_string())?;
let mut api = ApiDescription::new();