1use std::path::PathBuf;
22use std::sync::{Arc, LazyLock};
23
24use axum::Router;
25use axum::extract::State;
26use axum::http::{StatusCode, header};
27use axum::response::{IntoResponse, Response};
28use axum::routing::post;
29use tokio::sync::Semaphore;
30use yaml_rust2::parser::{Event, EventReceiver, Parser};
31use yaml_rust2::{Yaml, YamlLoader};
32
33use mira_core::frame::{self, Expand};
34use mira_core::query::{self, Op, Search, Signal, Target, Term, Value};
35use mira_core::series::{self, SeriesQuery};
36
37use crate::pipeline;
38
39#[derive(Clone, Default)]
40pub struct Api {
41 pub data_dir: Arc<PathBuf>,
42 pub open: pipeline::OpenSlots,
48 pub alerts: Arc<crate::alert::Engine>,
55}
56
57impl Api {
58 pub(crate) async fn open(&self, signal: &str) -> Vec<Arc<mira_core::signal::Open>> {
67 let Some(i) = pipeline::SIGNALS.iter().position(|s| *s == signal) else {
68 return Vec::new();
69 };
70 self.open[i].fresh().await
71 }
72
73 pub(crate) async fn open_all(&self) -> Vec<Vec<Arc<mira_core::signal::Open>>> {
79 let mut out = Vec::with_capacity(pipeline::SIGNALS.len());
80 for s in pipeline::SIGNALS {
81 out.push(self.open(s).await);
82 }
83 out
84 }
85}
86
87pub fn router(api: Api) -> Router {
88 Router::new()
89 .route("/api/v1/query", post(query_handler))
90 .route("/api/v1/metrics/query", post(series_handler))
91 .route("/api/v1/metrics/names", post(names_handler))
92 .route("/api/v1/correlate", post(correlate_handler))
93 .route("/api/v1/map", post(map_handler))
94 .route("/api/v1/entities", post(entities_handler))
95 .with_state(api)
96}
97
98const MAX_LIMIT: usize = 10_000;
104
105async fn query_handler(State(api): State<Api>, body: String) -> Response {
112 let q = match parse_search(&body, now_nanos()) {
113 Ok(q) => q,
114 Err(e) => return bad_request(&e),
115 };
116 let dir = api.data_dir.clone();
117 let open = api.open(q.signal.dir()).await;
118 run("rows", move || query::search_open(&dir, &q, &open)).await
119}
120
121async fn series_handler(State(api): State<Api>, body: String) -> Response {
124 let q = match parse_series(&body, now_nanos()) {
125 Ok(q) => q,
126 Err(e) => return bad_request(&e),
127 };
128 let dir = api.data_dir.clone();
129 let open = api.open("metrics").await;
130 run("series", move || series::series_open(&dir, &q, &open)).await
131}
132
133async fn names_handler(State(api): State<Api>, body: String) -> Response {
139 let now = now_nanos();
140 let doc = match window(if body.trim().is_empty() { "{}" } else { &body }, now) {
143 Ok(w) => w,
144 Err(e) => return bad_request(&e),
145 };
146 let dir = api.data_dir.clone();
147 let open = api.open("metrics").await;
148 run("names", move || {
149 series::names_open(&dir, doc.0, doc.1, &open)
150 })
151 .await
152}
153
154async fn correlate_handler(State(api): State<Api>, body: String) -> Response {
171 let now = now_nanos();
172 let (q, ops) = match parse_correlate(&body, now) {
173 Ok(v) => v,
174 Err(e) => return bad_request(&e),
175 };
176 let dir = api.data_dir.clone();
177 let anchored = api.open(q.signal.dir()).await;
180 let all = api.open_all().await;
181 run("frame", move || correlate(&dir, &q, &ops, &anchored, &all)).await
182}
183
184pub(crate) fn correlate(
187 dir: &std::path::Path,
188 q: &Search,
189 ops: &[Expand],
190 anchored: &[Arc<mira_core::signal::Open>],
191 all: &[Vec<Arc<mira_core::signal::Open>>],
192) -> mira_core::error::Result<query::Results> {
193 let (f, a) = frame::anchor(dir, q, anchored)?;
194 let traces = all.get(1).map_or(&[][..], Vec::as_slice);
197 let (f, w) = frame::expand(dir, &f, ops, traces)?;
198 let names = frame::names_of(dir, &f, all)?;
199 let mut j = mira_core::json::Json::new();
200 f.write_json(&mut j, &names);
201 Ok(query::Results {
202 json: j.into_string(),
203 stats: query::Stats {
204 blocks_total: a.blocks_total + w.blocks_total,
205 blocks_scanned: a.blocks_scanned + w.blocks_scanned,
206 rows_scanned: a.rows_scanned + w.rows_scanned,
207 rows_matched: a.rows_matched + w.rows_matched,
208 ..Default::default()
209 },
210 next: None,
211 })
212}
213
214async fn map_handler(State(api): State<Api>, body: String) -> Response {
224 let now = now_nanos();
225 let doc = match parse(if body.trim().is_empty() { "{}" } else { &body }) {
226 Ok(d) => d,
227 Err(e) => return bad_request(&e),
228 };
229 let (from, to, max_spans) = match map_doc(&doc, now) {
230 Ok(v) => v,
231 Err(e) => return bad_request(&e),
232 };
233 let dir = api.data_dir.clone();
234 let open = api.open("traces").await;
235 run("map", move || frame::map(&dir, from, to, max_spans, &open)).await
236}
237
238async fn entities_handler(State(api): State<Api>, body: String) -> Response {
244 let now = now_nanos();
245 let (from, to) = match window(if body.trim().is_empty() { "{}" } else { &body }, now) {
246 Ok(w) => w,
247 Err(e) => return bad_request(&e),
248 };
249 let dir = api.data_dir.clone();
250 let open = api.open_all().await;
251 run("entities", move || frame::entities(&dir, from, to, &open)).await
252}
253
254static SCANS: LazyLock<Semaphore> =
267 LazyLock::new(|| Semaphore::new(std::thread::available_parallelism().map_or(4, |n| n.get())));
268
269pub(crate) async fn scan<T: Send + 'static>(
275 f: impl FnOnce() -> T + Send + 'static,
276) -> Result<T, tokio::task::JoinError> {
277 let _permit = SCANS.acquire().await.ok();
281 tokio::task::spawn_blocking(f).await
282}
283
284async fn run(
285 field: &'static str,
286 f: impl FnOnce() -> mira_core::error::Result<query::Results> + Send + 'static,
287) -> Response {
288 let t = std::time::Instant::now();
289 match scan(f).await {
290 Ok(Ok(r)) => json_ok(envelope(field, &r, t.elapsed())),
291 Ok(Err(e)) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
292 Err(e) => (
293 StatusCode::INTERNAL_SERVER_ERROR,
294 format!("query task panicked: {e}"),
295 )
296 .into_response(),
297 }
298}
299
300pub fn envelope(field: &str, r: &query::Results, elapsed: std::time::Duration) -> String {
310 format!(
311 "{{\"{field}\":{},\"stats\":{{\"blocks_total\":{},\"blocks_scanned\":{},\
312 \"rows_scanned\":{},\"rows_matched\":{},\"elapsed_us\":{}}}{}}}",
313 r.json,
314 r.stats.blocks_total,
315 r.stats.blocks_scanned,
316 r.stats.rows_scanned,
317 r.stats.rows_matched,
318 elapsed.as_micros(),
319 r.next
322 .map(|c| format!(",\"next\":\"{c}\""))
323 .unwrap_or_default()
324 )
325}
326
327fn json_ok(body: String) -> Response {
328 (
329 StatusCode::OK,
330 [(header::CONTENT_TYPE, "application/json")],
331 body,
332 )
333 .into_response()
334}
335
336fn bad_request(msg: &str) -> Response {
341 let mut j = mira_core::json::Json::new();
342 j.obj(|j| {
343 j.key("error");
344 j.str(msg);
345 });
346 (
347 StatusCode::BAD_REQUEST,
348 [(header::CONTENT_TYPE, "application/json")],
349 j.into_string(),
350 )
351 .into_response()
352}
353
354pub fn now_nanos() -> i64 {
355 std::time::SystemTime::now()
356 .duration_since(std::time::UNIX_EPOCH)
357 .unwrap_or_default()
358 .as_nanos() as i64
359}
360
361pub fn parse_search(text: &str, now: i64) -> Result<Search, String> {
391 search_doc(&parse(text)?, now)
392}
393
394pub fn search_doc(doc: &Yaml, now: i64) -> Result<Search, String> {
397 known(doc, &["signal", "from", "to", "where", "limit", "after"])?;
398 let signal = match doc["signal"].as_str() {
399 Some(s) => Signal::parse(s).ok_or(format!("unknown signal {s:?}"))?,
400 None => Signal::Logs,
401 };
402 let (from, to) = bounds(doc, now)?;
403 let limit = positive(doc, "limit", 100, MAX_LIMIT)?;
404 let after = match &doc["after"] {
405 Yaml::BadValue | Yaml::Null => None,
406 y => Some(
410 y.as_str()
411 .ok_or("after: quote the cursor; it is a string")?
412 .parse()?,
413 ),
414 };
415 Ok(Search {
416 signal,
417 from,
418 to,
419 terms: terms(doc)?,
420 limit,
421 after,
422 })
423}
424
425pub fn parse_series(text: &str, now: i64) -> Result<SeriesQuery, String> {
439 series_doc(&parse(text)?, now)
440}
441
442pub fn series_doc(doc: &Yaml, now: i64) -> Result<SeriesQuery, String> {
443 known(
444 doc,
445 &["name", "from", "to", "where", "max_series", "max_points"],
446 )?;
447 let (from, to) = bounds(doc, now)?;
448 Ok(SeriesQuery {
449 name: doc["name"].as_str().map(str::to_owned),
450 from,
451 to,
452 terms: terms(doc)?,
453 max_series: positive(doc, "max_series", 200, 2_000)?,
454 max_points: positive(doc, "max_points", 5_000, 100_000)?,
455 })
456}
457
458pub fn parse_correlate(text: &str, now: i64) -> Result<(Search, Vec<Expand>), String> {
461 correlate_doc(&parse(text)?, now)
462}
463
464pub fn correlate_doc(doc: &Yaml, now: i64) -> Result<(Search, Vec<Expand>), String> {
466 known(
467 doc,
468 &["signal", "from", "to", "where", "limit", "after", "expand"],
469 )?;
470 Ok((correlate_search(doc, now)?, expands(&doc["expand"])?))
471}
472
473pub fn map_doc(doc: &Yaml, now: i64) -> Result<(i64, i64, usize), String> {
475 known(doc, &["from", "to", "max_spans"])?;
476 let (from, to) = bounds(doc, now)?;
477 Ok((from, to, positive(doc, "max_spans", 1_000_000, 20_000_000)?))
478}
479
480fn correlate_search(doc: &Yaml, now: i64) -> Result<Search, String> {
487 let mut map = doc.as_hash().cloned().unwrap_or_default();
488 map.remove(&Yaml::String("expand".into()));
489 search_doc(&Yaml::Hash(map), now)
490}
491
492fn expands(y: &Yaml) -> Result<Vec<Expand>, String> {
497 let items = match y {
498 Yaml::BadValue | Yaml::Null => return Ok(Vec::new()),
499 Yaml::Array(a) => a,
500 _ => return Err("`expand` must be a list of steps".into()),
501 };
502 items
503 .iter()
504 .map(|s| {
505 let s = s.as_str().ok_or("each `expand` step must be a string")?;
506 match s.split_once(':') {
507 Some(("around", d)) => {
508 Ok(Expand::Around(crate::config::duration(d)?.as_nanos() as i64))
509 }
510 None if s == "traces" => Ok(Expand::Traces),
511 None if s == "peers" => Ok(Expand::Peers),
512 _ => Err(format!(
513 "unknown expand step {s:?}; expected traces, peers, or around:<duration>"
514 )),
515 }
516 })
517 .collect()
518}
519
520pub fn window(text: &str, now: i64) -> Result<(i64, i64), String> {
522 window_doc(&parse(text)?, now)
523}
524
525pub fn window_doc(doc: &Yaml, now: i64) -> Result<(i64, i64), String> {
527 known(doc, &["from", "to"])?;
528 bounds(doc, now)
529}
530
531pub fn known(doc: &Yaml, keys: &[&str]) -> Result<(), String> {
540 if doc.is_badvalue() || doc.is_null() {
543 return Ok(());
544 }
545 let map = doc.as_hash().ok_or("a query must be a mapping")?;
546 for k in map.keys() {
547 let k = k.as_str().ok_or("query keys must be strings")?;
548 if !keys.contains(&k) {
549 return Err(format!(
550 "unknown query key {k:?}; expected one of {}",
551 keys.join(" ")
552 ));
553 }
554 }
555 Ok(())
556}
557
558pub fn parse(text: &str) -> Result<Yaml, String> {
560 if has_alias(text) {
561 return Err(
562 "KYAML has no anchors or aliases: write the value out instead of \
563 referring to an anchor with `*`"
564 .into(),
565 );
566 }
567 let docs = match YamlLoader::load_from_str(text) {
568 Ok(docs) => docs,
569 Err(e) => YamlLoader::load_from_str(&fold_surrogates(text))
577 .map_err(|_| format!("not valid KYAML: {e}"))?,
578 };
579 docs.into_iter().next().ok_or("empty query".into())
580}
581
582fn has_alias(text: &str) -> bool {
604 if !text.contains('*') {
605 return false;
606 }
607 #[derive(Default)]
608 struct Spy(bool);
609 impl EventReceiver for Spy {
610 fn on_event(&mut self, ev: Event) {
611 self.0 |= matches!(ev, Event::Alias(_));
612 }
613 }
614 let mut spy = Spy::default();
615 let _ = Parser::new_from_str(text).load(&mut spy, true);
618 spy.0
619}
620
621fn fold_surrogates(text: &str) -> String {
626 fn hex4(s: &str) -> Option<u32> {
627 u32::from_str_radix(s.strip_prefix("\\u")?.get(..4)?, 16).ok()
628 }
629 let mut out = String::with_capacity(text.len());
630 let mut rest = text;
631 while let Some(i) = rest.find("\\u") {
632 out.push_str(&rest[..i]);
633 let folded = match (hex4(&rest[i..]), rest.get(i + 6..).and_then(hex4)) {
634 (Some(h @ 0xD800..=0xDBFF), Some(l @ 0xDC00..=0xDFFF)) => {
635 char::from_u32(0x10000 + ((h - 0xD800) << 10) + (l - 0xDC00))
636 }
637 _ => None,
638 };
639 match folded {
640 Some(c) => {
641 out.push(c);
642 rest = &rest[i + 12..];
643 }
644 None => {
645 out.push_str("\\u");
646 rest = &rest[i + 2..];
647 }
648 }
649 }
650 out.push_str(rest);
651 out
652}
653
654pub fn bounds(doc: &Yaml, now: i64) -> Result<(i64, i64), String> {
658 let from = time_field(&doc["from"], now, now - 3_600_000_000_000)?;
659 let to = time_field(&doc["to"], now, now)?;
660 if from > to {
661 return Err(format!("from ({from}) is after to ({to})"));
662 }
663 Ok((from, to))
664}
665
666fn terms(doc: &Yaml) -> Result<Vec<Term>, String> {
667 match &doc["where"] {
668 Yaml::BadValue | Yaml::Null => Ok(Vec::new()),
669 Yaml::Array(a) => a.iter().map(parse_term).collect(),
670 _ => Err("`where` must be a list of terms".into()),
671 }
672}
673
674fn positive(doc: &Yaml, key: &str, default: usize, max: usize) -> Result<usize, String> {
677 match &doc[key] {
678 Yaml::BadValue | Yaml::Null => Ok(default),
679 Yaml::Integer(n) if *n > 0 => Ok((*n as usize).min(max)),
680 other => Err(format!("{key} must be a positive integer, got {other:?}")),
681 }
682}
683
684fn parse_term(y: &Yaml) -> Result<Term, String> {
685 let map = y.as_hash().ok_or("each `where` term must be a mapping")?;
686 let mut target = None;
687 let mut opval = None;
688 for (k, v) in map {
689 let k = k.as_str().ok_or("term keys must be strings")?;
690 match k {
691 "attr" => {
692 target = Some(Target::Attr(
693 v.as_str().ok_or("`attr` must be a string")?.to_owned(),
694 ));
695 }
696 "field" => {
697 target = Some(Target::Field(
698 v.as_str().ok_or("`field` must be a string")?.to_owned(),
699 ));
700 }
701 other => {
702 let op = Op::parse(other).ok_or(format!(
703 "unknown term key {other:?}; expected attr, field, or one of \
704 eq ne lt lte gt gte contains"
705 ))?;
706 opval = Some((op, scalar(v)?));
707 }
708 }
709 }
710 let target = target.ok_or("a term needs `attr` or `field`")?;
711 let (op, value) = opval.ok_or("a term needs an operator, such as `eq`")?;
712 Ok(Term { target, op, value })
713}
714
715fn scalar(y: &Yaml) -> Result<Value, String> {
723 Ok(match y {
724 Yaml::String(s) => Value::Str(s.clone()),
725 Yaml::Integer(i) => Value::Int(*i),
726 Yaml::Boolean(b) => Value::Bool(*b),
727 Yaml::Real(r) => Value::Double(r.parse().map_err(|_| format!("{r:?} is not a number"))?),
728 other => return Err(format!("{other:?} is not a comparable value")),
729 })
730}
731
732fn time_field(y: &Yaml, now: i64, default: i64) -> Result<i64, String> {
739 match y {
740 Yaml::BadValue | Yaml::Null => Ok(default),
741 Yaml::Integer(n) => Ok(*n),
742 Yaml::String(s) if s == "now" => Ok(now),
743 Yaml::String(s) => {
744 let (sign, rest) = match s.strip_prefix('-') {
745 Some(r) => (-1i64, r),
746 None => (1, s.strip_prefix('+').unwrap_or(s)),
747 };
748 let d = crate::config::duration(rest)?;
749 Ok(now + sign * (d.as_nanos() as i64))
750 }
751 other => Err(format!("{other:?} is not a time")),
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 #[test]
763 fn json_from_a_browser_parses_as_kyaml() {
764 let now = 1_000_000_000_000_000_000;
765 let q = parse_search(
766 r#"{"signal":"traces","from":"-15m","to":"now","limit":50,
767 "where":[{"attr":"service.name","eq":"checkout"},
768 {"field":"duration_nano","gte":500000000},
769 {"field":"name","contains":"GET"}]}"#,
770 now,
771 )
772 .unwrap();
773 assert_eq!(q.signal, Signal::Traces);
774 assert_eq!(q.to, now);
775 assert_eq!(q.from, now - 900_000_000_000);
776 assert_eq!(q.limit, 50);
777 assert_eq!(q.terms.len(), 3);
778 assert!(matches!(&q.terms[0].target, Target::Attr(k) if k == "service.name"));
779 assert_eq!(q.terms[1].op, Op::Gte);
780 assert_eq!(q.terms[1].value, Value::Int(500_000_000));
781 assert_eq!(q.terms[2].op, Op::Contains);
782 }
783
784 #[test]
789 fn json_surrogate_escapes_are_folded_into_the_character() {
790 let q = parse_search(
791 r#"{"where":[{"field":"body","contains":"caf\u00e9 \ud83d\ude00"}]}"#,
792 0,
793 )
794 .unwrap();
795 assert_eq!(q.terms[0].value, Value::Str("café 😀".into()));
796 let err =
800 parse_search(r#"{"where":[{"field":"body","contains":"\ud83d"}]}"#, 0).unwrap_err();
801 assert!(err.contains("not valid KYAML"), "{err}");
802 }
803
804 #[test]
808 fn kyaml_with_comments_and_trailing_commas_parses_the_same() {
809 let q = parse_search(
810 r#"{
811 "signal": "logs",
812 # only the errors
813 "where": [
814 { "field": "severity_number", "gte": 17 },
815 ],
816 }"#,
817 0,
818 )
819 .unwrap();
820 assert_eq!(q.signal, Signal::Logs);
821 assert_eq!(q.terms.len(), 1);
822 assert_eq!(q.limit, 100);
823 assert_eq!(q.from, -3_600_000_000_000);
826 }
827
828 #[test]
829 fn malformed_queries_say_what_is_wrong() {
830 let err = |s: &str| parse_search(s, 0).unwrap_err();
831 assert!(err(r#"{"signal":"jaeger"}"#).contains("unknown signal"));
832 assert!(err(r#"{"where":[{"attr":"a","like":"b"}]}"#).contains("unknown term key"));
833 assert!(err(r#"{"where":[{"eq":"b"}]}"#).contains("needs `attr` or `field`"));
834 assert!(err(r#"{"where":[{"attr":"a"}]}"#).contains("needs an operator"));
835 assert!(err(r#"{"from":"now","to":"-1h"}"#).contains("is after"));
836 assert!(err(r#"{"limit":0}"#).contains("positive integer"));
837 assert!(err(r#"{"signal":"logs","filters":[]}"#).contains("unknown query key"));
841 assert!(err(r#"{"query":{"signal":"logs"}}"#).contains("unknown query key"));
842 assert!(err("[1,2,3]").contains("must be a mapping"));
844 let step = parse_series(r#"{"name":"m","step":"1m"}"#, 0).unwrap_err();
845 assert!(step.contains("step"), "{step}");
846 let w = window(r#"{"from":0,"limit":5}"#, 0).unwrap_err();
847 assert!(w.contains("limit"), "{w}");
848 assert!(err(r#"{"where":[{"attr":"a","eq":[1,2]}]}"#).contains("not a comparable value"));
853 assert!(err(r#"{"where":[{"attr":"a","eq":{}}]}"#).contains("not a comparable value"));
854 assert!(err(r#"{"from":[1]}"#).contains("is not a time"));
855 assert!(err(r#"{"to":{"at":1}}"#).contains("is not a time"));
856 }
857
858 #[test]
864 fn every_document_the_clients_send_is_accepted() {
865 for d in [
866 r#"{"signal":"logs","from":"-1h","to":"now","where":[],"limit":200}"#,
867 r#"{"signal":"traces","from":0,"to":"now","limit":2000,
868 "where":[{"field":"trace_id","eq":"ab"}]}"#,
869 r#"{"signal":"logs","limit":100,"after":"1757241600000000000.2718281828.7.41"}"#,
870 ] {
871 assert!(parse_search(d, 0).is_ok(), "{d}");
872 }
873 for d in [
874 r#"{"name":"m","from":"-1h","to":"now","where":[]}"#,
875 r#"{"name":"m","from":"-1h","to":"now","max_series":64,"max_points":400,"where":[]}"#,
876 ] {
877 assert!(parse_series(d, 0).is_ok(), "{d}");
878 }
879 for d in [
880 r#"{"signal":"logs","from":"-1h","to":"now","where":[],"limit":200,
881 "expand":["traces","peers"]}"#,
882 r#"{"expand":[]}"#,
883 "{}",
884 ] {
885 assert!(parse_correlate(d, 0).is_ok(), "{d}");
886 }
887 assert!(window(r#"{"from":"-1h","to":"now"}"#, 0).is_ok());
888 assert!(window("{}", 0).is_ok());
889 assert!(search_doc(&Yaml::BadValue, 0).is_ok());
891 assert!(correlate_doc(&Yaml::BadValue, 0).is_ok());
892 assert!(map_doc(&Yaml::BadValue, 0).is_ok());
893 }
894
895 #[test]
899 fn an_expansion_walk_parses_in_order_and_says_what_it_does_not_know() {
900 let (q, ops) = parse_correlate(
901 r#"{"signal":"traces","expand":["around:2s","traces","peers"]}"#,
902 0,
903 )
904 .unwrap();
905 assert_eq!(q.signal.dir(), "traces");
906 assert_eq!(
907 ops,
908 [Expand::Around(2_000_000_000), Expand::Traces, Expand::Peers]
909 );
910 for (d, want) in [
911 (r#"{"expand":"traces"}"#, "list of steps"),
912 (r#"{"expand":[7]}"#, "must be a string"),
913 (r#"{"expand":["sideways"]}"#, "sideways"),
914 (r#"{"expand":["around:soon"]}"#, "soon"),
915 (r#"{"expand":["peers:1"]}"#, "peers:1"),
916 (r#"{"expanded":[]}"#, "unknown query key"),
917 ] {
918 assert!(parse_correlate(d, 0).unwrap_err().contains(want), "{d}");
919 }
920 }
921
922 #[test]
930 fn an_alias_bomb_is_refused_before_it_is_expanded() {
931 let bomb = r#"{"a":&a "lol",
932 "b":&b [*a,*a,*a,*a,*a,*a,*a,*a,*a],
933 "c":&c [*b,*b,*b,*b,*b,*b,*b,*b,*b],
934 "d":&d [*c,*c,*c,*c,*c,*c,*c,*c,*c],
935 "e":&e [*d,*d,*d,*d,*d,*d,*d,*d,*d],
936 "f":&f [*e,*e,*e,*e,*e,*e,*e,*e,*e],
937 "g":[*f,*f,*f,*f,*f,*f,*f,*f,*f]}"#;
938 let err = parse(bomb).unwrap_err();
939 assert!(err.contains("alias"), "{err}");
942 assert!(err.contains("KYAML"), "{err}");
943 assert!(parse_search(bomb, 0).is_err());
945 assert!(parse_series(bomb, 0).is_err());
946 assert!(window(bomb, 0).is_err());
947 assert!(parse(r#"{"signal":&s "logs"}"#).is_ok());
950
951 let three = r#"{"a":&a "lol","b":&b [*a,*a,*a,*a,*a,*a,*a,*a,*a],
955 "c":&c [*b,*b,*b,*b,*b,*b,*b,*b,*b],"d":[*c,*c,*c,*c,*c,*c,*c,*c,*c]}"#;
956 let expanded = YamlLoader::load_from_str(three).unwrap().remove(0);
957 assert_eq!(expanded["d"].as_vec().unwrap().len(), 9);
958 assert_eq!(expanded["d"][8][8].as_vec().unwrap().len(), 9);
959 assert_eq!(expanded["d"][8][8][8].as_str(), Some("lol"));
960 }
961
962 #[test]
967 fn a_star_inside_a_string_is_not_an_alias() {
968 let q = parse_search(r#"{"where":[{"field":"body","contains":"rate *"}]}"#, 0).unwrap();
969 assert_eq!(q.terms[0].value, Value::Str("rate *".into()));
970 }
971
972 #[test]
973 fn limit_is_capped_rather_than_refused() {
974 let q = parse_search(r#"{"limit":9999999}"#, 0).unwrap();
975 assert_eq!(q.limit, MAX_LIMIT);
976 }
977
978 async fn answer(res: Response) -> (StatusCode, String, String) {
980 let status = res.status();
981 let ct = res
982 .headers()
983 .get(header::CONTENT_TYPE)
984 .and_then(|v| v.to_str().ok())
985 .unwrap_or_default()
986 .to_owned();
987 let body = axum::body::to_bytes(res.into_body(), usize::MAX)
988 .await
989 .unwrap();
990 (status, ct, String::from_utf8(body.to_vec()).unwrap())
991 }
992
993 #[tokio::test]
1003 async fn every_endpoint_refuses_a_document_it_cannot_honour_with_a_json_400() {
1004 let api = Api::default();
1005 let st = || State(api.clone());
1006 for (res, want) in [
1007 (
1008 query_handler(st(), r#"{"signal":"jaeger"}"#.into()).await,
1009 "unknown signal",
1010 ),
1011 (
1012 series_handler(st(), r#"{"max_series":0}"#.into()).await,
1013 "max_series must be a positive integer",
1014 ),
1015 (
1016 names_handler(st(), r#"{"from":"yesterday"}"#.into()).await,
1017 "yesterday",
1018 ),
1019 (
1020 correlate_handler(st(), r#"{"expand":"traces"}"#.into()).await,
1021 "`expand` must be a list of steps",
1022 ),
1023 (
1026 map_handler(st(), "{not: [kyaml".into()).await,
1027 "not valid KYAML",
1028 ),
1029 (
1030 map_handler(st(), r#"{"max_spans":0}"#.into()).await,
1031 "max_spans must be a positive integer",
1032 ),
1033 (
1034 entities_handler(st(), r#"{"limit":5}"#.into()).await,
1035 "unknown query key \\\"limit\\\"",
1036 ),
1037 ] {
1038 let (status, ct, body) = answer(res).await;
1039 assert_eq!(status, StatusCode::BAD_REQUEST, "{want}: {body}");
1040 assert_eq!(ct, "application/json", "{want}");
1041 assert!(body.starts_with(r#"{"error":""#), "{want}: {body}");
1042 assert!(body.contains(want), "{body}");
1043 }
1044
1045 let dir = std::env::temp_dir().join(format!("mira-api-empty-{}", std::process::id()));
1049 std::fs::create_dir_all(&dir).unwrap();
1050 let api = Api {
1051 data_dir: Arc::new(dir.clone()),
1052 ..Default::default()
1053 };
1054 let st = || State(api.clone());
1055 for res in [
1056 names_handler(st(), String::new()).await,
1057 map_handler(st(), " ".into()).await,
1058 entities_handler(st(), String::new()).await,
1059 ] {
1060 let (status, ct, body) = answer(res).await;
1061 assert_eq!(status, StatusCode::OK, "{body}");
1062 assert_eq!(ct, "application/json");
1063 assert!(body.contains("\"stats\":"), "{body}");
1064 }
1065 let _ = std::fs::remove_dir_all(&dir);
1066 }
1067
1068 #[tokio::test]
1076 async fn a_failed_or_panicking_scan_answers_500_with_the_reason() {
1077 let dir = std::env::temp_dir().join(format!("mira-api-broken-{}", std::process::id()));
1078 let _ = std::fs::remove_dir_all(&dir);
1079 std::fs::create_dir_all(&dir).unwrap();
1080 std::fs::write(dir.join("logs"), b"not a directory").unwrap();
1081 let api = Api {
1082 data_dir: Arc::new(dir.clone()),
1083 ..Default::default()
1084 };
1085 let (status, _, body) = answer(query_handler(State(api), "{}".into()).await).await;
1086 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR, "{body}");
1087 assert!(!body.is_empty(), "a 500 with no reason is not a report");
1088 assert!(!body.contains("panicked"), "{body}");
1090
1091 let (status, _, body) = answer(run("rows", || panic!("a page fault, say")).await).await;
1094 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
1095 assert!(body.contains("query task panicked"), "{body}");
1096 let _ = std::fs::remove_dir_all(&dir);
1097 }
1098
1099 #[tokio::test]
1112 async fn an_unknown_signal_has_no_open_block_rather_than_an_index_out_of_range() {
1113 let dir = std::env::temp_dir().join(format!("mira-api-slots-{}", std::process::id()));
1114 let _ = std::fs::remove_dir_all(&dir);
1115 std::fs::create_dir_all(&dir).unwrap();
1116 let pcfg = Arc::new(pipeline::Config {
1117 data_dir: dir.clone(),
1118 node: 0x51,
1119 wal: Some(Arc::new(mira_core::wal::Wal::open(&dir, 0x51).unwrap())),
1125 max_block_age: std::time::Duration::from_secs(3_600),
1128 ..Default::default()
1129 });
1130 let (ingest, logs_slot, flusher) = pipeline::spawn::<mira_core::logs::LogsBuilder>(&pcfg);
1131 assert!(
1132 ingest
1133 .submit(crate::e2e::logs_export("checkout", 1_000, 3))
1134 .await
1135 .is_ok()
1136 );
1137 let api = Api {
1138 data_dir: Arc::new(dir.clone()),
1139 open: [logs_slot, Default::default(), Default::default()],
1140 ..Default::default()
1141 };
1142
1143 let logs = api.open("logs").await;
1144 assert_eq!(logs.len(), 1, "the served slot answers with its open block");
1145 assert_eq!(logs[0].sealed.num_rows, 3);
1146 for absent in ["profiles", "", "traces", "metrics"] {
1148 assert!(api.open(absent).await.is_empty(), "{absent}");
1149 }
1150
1151 let all = api.open_all().await;
1155 assert_eq!(all.len(), pipeline::SIGNALS.len());
1156 let i = pipeline::SIGNALS.iter().position(|s| *s == "logs").unwrap();
1157 for (j, blocks) in all.iter().enumerate() {
1158 assert_eq!(blocks.len(), usize::from(j == i), "slot {j}");
1159 }
1160
1161 drop(ingest);
1162 flusher.await.unwrap();
1163 let _ = std::fs::remove_dir_all(&dir);
1164 }
1165}