1#![deny(unsafe_op_in_unsafe_fn)]
19
20pub mod attrs;
21pub mod block;
22pub mod bloom;
23pub mod error;
24pub mod frame;
25pub mod identity;
26pub mod json;
27pub mod logs;
28pub mod metrics;
29pub mod query;
30pub mod schema;
31pub mod series;
32pub mod signal;
33pub mod traces;
34pub mod wal;
35pub mod zone;
36
37pub use error::{Error, Result};
38pub use signal::{Sealed, SignalBuilder};
39
40static DEGRADED_SYNCS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
42
43pub fn degraded_syncs() -> u64 {
50 DEGRADED_SYNCS.load(std::sync::atomic::Ordering::Relaxed)
51}
52
53pub fn sync_all(file: &std::fs::File) -> std::io::Result<()> {
68 sync_with(file, std::fs::File::sync_all)
69}
70
71pub fn sync_data(file: &std::fs::File) -> std::io::Result<()> {
73 sync_with(file, std::fs::File::sync_data)
74}
75
76fn sync_with(
80 file: &std::fs::File,
81 strong: impl Fn(&std::fs::File) -> std::io::Result<()>,
82) -> std::io::Result<()> {
83 match strong(file) {
84 Ok(()) => Ok(()),
85 Err(e) => degrade(file, e),
86 }
87}
88
89#[cfg(target_vendor = "apple")]
90fn degrade(file: &std::fs::File, e: std::io::Error) -> std::io::Result<()> {
91 use std::os::fd::AsRawFd;
92 if !matches!(e.raw_os_error(), Some(libc::ENOTSUP | libc::EINVAL)) {
93 return Err(e);
94 }
95 if unsafe { libc::fsync(file.as_raw_fd()) } != 0 {
97 return Err(std::io::Error::last_os_error());
98 }
99 DEGRADED_SYNCS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
100 Ok(())
101}
102
103#[cfg(not(target_vendor = "apple"))]
106fn degrade(_file: &std::fs::File, e: std::io::Error) -> std::io::Result<()> {
107 Err(e)
108}
109
110#[cfg(test)]
111mod sync_tests {
112 use super::*;
113 use std::io::Error;
114
115 #[test]
121 fn the_barrier_runs_and_only_enotsup_degrades() {
122 let path = std::env::temp_dir().join(format!("mira-sync-{}", std::process::id()));
123 let file = std::fs::File::create(&path).expect("create");
124
125 sync_all(&file).expect("sync_all");
127 sync_data(&file).expect("sync_data");
128 assert_eq!(degraded_syncs(), 0, "a real volume degrades nothing");
129
130 let e = sync_with(&file, |_| Err(Error::from_raw_os_error(libc::EIO)))
132 .expect_err("EIO must not be swallowed");
133 assert_eq!(e.raw_os_error(), Some(libc::EIO));
134
135 let e = sync_with(&file, |_| Err(Error::other("synthetic")))
137 .expect_err("no errno, no fallback");
138 assert!(e.raw_os_error().is_none());
139 assert_eq!(degraded_syncs(), 0, "neither of those is a missing barrier");
140
141 for (i, errno) in [libc::ENOTSUP, libc::EINVAL].into_iter().enumerate() {
147 let r = sync_with(&file, |_| Err(Error::from_raw_os_error(errno)));
148 if cfg!(target_vendor = "apple") {
149 r.unwrap_or_else(|e| panic!("errno {errno} should have degraded, got {e}"));
150 assert_eq!(degraded_syncs(), i as u64 + 1);
151 } else {
152 assert_eq!(
153 r.expect_err("no fallback off Apple").raw_os_error(),
154 Some(errno)
155 );
156 assert_eq!(degraded_syncs(), 0);
157 }
158 }
159
160 let _ = std::fs::remove_file(&path);
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use arrow_array::{Array, StringArray, UInt32Array, UInt64Array};
168 use mira_proto::collector::logs::v1::ExportLogsServiceRequest;
169 use mira_proto::common::v1::{AnyValue, InstrumentationScope, KeyValue, any_value::Value};
170 use mira_proto::logs::v1::{LogRecord, ResourceLogs, ScopeLogs};
171 use mira_proto::resource::v1::Resource;
172
173 fn kv(k: &str, v: &str) -> KeyValue {
174 KeyValue {
175 key: k.into(),
176 value: Some(AnyValue {
177 value: Some(Value::StringValue(v.into())),
178 }),
179 }
180 }
181
182 fn request(service: &str, n: usize, base_ts: u64) -> ExportLogsServiceRequest {
183 request_from(service, &[], n, base_ts)
184 }
185
186 fn request_from(
189 service: &str,
190 extra: &[KeyValue],
191 n: usize,
192 base_ts: u64,
193 ) -> ExportLogsServiceRequest {
194 let mut attributes = vec![
195 kv("service.name", service),
196 kv("service.instance.id", "7f3a"),
197 ];
198 attributes.extend_from_slice(extra);
199 ExportLogsServiceRequest {
200 resource_logs: vec![ResourceLogs {
201 resource: Some(Resource {
202 attributes,
203 ..Default::default()
204 }),
205 scope_logs: vec![ScopeLogs {
206 scope: Some(InstrumentationScope {
207 name: "test".into(),
208 ..Default::default()
209 }),
210 log_records: (0..n)
211 .map(|i| LogRecord {
212 time_unix_nano: base_ts + i as u64,
213 severity_number: 9,
214 severity_text: "INFO".into(),
215 body: Some(AnyValue {
216 value: Some(Value::StringValue(format!("line {i}"))),
217 }),
218 attributes: vec![kv("http.method", "GET")],
219 trace_id: vec![7u8; 16].into(),
220 span_id: vec![3u8; 8].into(),
221 ..Default::default()
222 })
223 .collect(),
224 ..Default::default()
225 }],
226 ..Default::default()
227 }],
228 }
229 }
230
231 #[test]
235 fn roundtrip_is_zero_copy_and_prunable() {
236 let root = std::env::temp_dir().join(format!("mira-test-{}", std::process::id()));
237 let _ = std::fs::remove_dir_all(&root);
238
239 let mut b = logs::LogsBuilder::new();
240 assert_eq!(
242 b.append_request(&request("checkout", 500, 1_000)).unwrap(),
243 500
244 );
245 assert_eq!(
246 b.append_request(&request("checkout", 500, 2_000)).unwrap(),
247 500
248 );
249 assert_eq!(
252 b.append_request(&request_from(
253 "checkout",
254 &[kv("k8s.node.name", "node-4")],
255 100,
256 3_000
257 ))
258 .unwrap(),
259 100
260 );
261 assert_eq!(
262 b.append_request(&request("payments", 100, 3_100)).unwrap(),
263 100
264 );
265
266 let sealed = b.finish().unwrap();
267 assert_eq!(sealed.num_rows, 1200);
268 assert_eq!(sealed.min_ts, 1_000);
269 assert_eq!(sealed.max_ts, 3_199);
270 assert_eq!(
272 sealed.tables.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
273 schema::LOGS_BLOCK_TABLES
274 );
275 let rows = |t: &str| sealed.table(t).unwrap().num_rows();
276 assert_eq!(rows("resources"), 3);
278 assert_eq!(rows("resource_attrs"), 7);
279 assert_eq!(rows("scope_attrs"), 1);
280 assert_eq!(rows("log_attrs"), 1200);
281
282 let keys = sealed
285 .table("resources")
286 .unwrap()
287 .column_by_name("key")
288 .unwrap()
289 .as_any()
290 .downcast_ref::<UInt64Array>()
291 .unwrap();
292 assert_eq!(
293 keys.value(0),
294 keys.value(1),
295 "attribute drift must not fork the entity"
296 );
297 assert_ne!(keys.value(0), keys.value(2));
298
299 let node = block::node_id("replica-a");
300 let published = block::publish(&root, "logs", node, 1, 0, &sealed).unwrap();
301 assert_eq!(published.node, node);
302 assert_ne!(node, block::node_id("replica-b"));
304
305 let catalog = block::scan(&root, "logs").unwrap();
307 assert_eq!(catalog, vec![published.clone()]);
308 assert!(catalog[0].overlaps(0, 1_500));
309 assert!(!catalog[0].overlaps(10_000, 20_000));
310
311 let logs = block::open_table(&published.dir.join("logs.arrow")).unwrap();
314 assert_eq!(logs.batches.len(), 1);
315 let rb = &logs.batches[0];
316 assert_eq!(rb.num_rows(), 1200);
317
318 let (inside, total) = logs.zero_copy_ratio();
322 assert_eq!(inside, total, "{inside}/{total} buffers zero-copy");
323 assert!(total >= 13, "expected one buffer per column at least");
324
325 let ids = rb
326 .column_by_name("id")
327 .unwrap()
328 .as_any()
329 .downcast_ref::<UInt32Array>()
330 .unwrap();
331 assert_eq!(ids.value(0), 0);
332 assert_eq!(ids.value(1199), 1199, "ids must be dense and block-local");
333
334 let body = rb
335 .column_by_name("body")
336 .unwrap()
337 .as_any()
338 .downcast_ref::<StringArray>()
339 .unwrap();
340 assert_eq!(body.value(0), "line 0");
341
342 let attrs = block::open_table(&published.dir.join("log_attrs.arrow")).unwrap();
344 let parents = attrs.batches[0]
345 .column_by_name("parent_id")
346 .unwrap()
347 .as_any()
348 .downcast_ref::<UInt32Array>()
349 .unwrap();
350 assert_eq!(parents.value(1199), 1199);
351
352 assert_eq!(block::expire(&root, "logs", 10_000).unwrap(), 1);
354 assert!(block::scan(&root, "logs").unwrap().is_empty());
355
356 let _ = std::fs::remove_dir_all(&root);
357 }
358
359 #[test]
364 fn seal_decision_sees_bodies_and_dictionary_pressure() {
365 let mut small = logs::LogsBuilder::new();
366 small
367 .append_request(&request("checkout", 100, 1_000))
368 .unwrap();
369
370 let prompt = "x".repeat(32 << 10);
371 let mut req = request("checkout", 100, 1_000);
372 for sl in &mut req.resource_logs[0].scope_logs {
373 for r in &mut sl.log_records {
374 r.body = Some(AnyValue {
375 value: Some(Value::StringValue(prompt.clone())),
376 });
377 }
378 }
379 let mut big = logs::LogsBuilder::new();
380 big.append_request(&req).unwrap();
381 assert!(small.approx_bytes() < 64 << 10, "{}", small.approx_bytes());
384 assert!(big.approx_bytes() > 3 << 20, "{}", big.approx_bytes());
385
386 let mut wide = request("checkout", 1, 1_000);
389 wide.resource_logs[0].scope_logs[0].log_records[0].attributes = (0..=schema::DICT_CAP)
390 .map(|i| kv(&format!("k{i}"), "v"))
391 .collect();
392 assert!(!small.has_headroom_for(&wide));
393 assert!(small.has_headroom_for(&request("checkout", 10, 1)));
394 }
395
396 #[test]
401 fn span_events_and_links_are_child_tables_with_their_own_ids() {
402 use mira_proto::collector::trace::v1::ExportTraceServiceRequest;
403 use mira_proto::trace::v1::span::{Event, Link};
404 use mira_proto::trace::v1::{ResourceSpans, ScopeSpans, Span, Status, status::StatusCode};
405
406 let span = |i: u64, events: usize, links: usize| Span {
410 trace_id: vec![1u8; 16].into(),
411 span_id: vec![i as u8; 8].into(),
412 name: "GET /checkout".into(),
413 kind: 2,
414 start_time_unix_nano: 10_000 + i,
415 end_time_unix_nano: 10_000 + i + 500,
416 attributes: vec![kv("http.method", "GET")],
417 status: Some(Status {
418 code: StatusCode::Error as i32,
419 message: "boom".into(),
420 }),
421 events: (0..events)
422 .map(|e| Event {
423 time_unix_nano: 10_100 + e as u64,
424 name: "exception".into(),
425 attributes: vec![kv("exception.type", "IOError")],
426 ..Default::default()
427 })
428 .collect(),
429 links: (0..links)
432 .map(|_| Link {
433 trace_id: vec![9u8; 16].into(),
434 span_id: vec![8u8; 8].into(),
435 ..Default::default()
436 })
437 .collect(),
438 ..Default::default()
439 };
440
441 let mut b = traces::TracesBuilder::new();
442 let req = ExportTraceServiceRequest {
443 resource_spans: vec![ResourceSpans {
444 resource: Some(Resource {
445 attributes: vec![
446 kv("service.name", "checkout"),
447 kv("service.instance.id", "7f3a"),
448 ],
449 ..Default::default()
450 }),
451 scope_spans: vec![ScopeSpans {
452 scope: Some(InstrumentationScope {
453 name: "test".into(),
454 ..Default::default()
455 }),
456 spans: vec![span(0, 0, 0), span(1, 2, 1)],
457 ..Default::default()
458 }],
459 ..Default::default()
460 }],
461 };
462 assert!(b.has_headroom_for(&req));
463 assert_eq!(b.append_request(&req).unwrap(), 2);
464
465 let sealed = b.finish().unwrap();
466 assert_eq!(sealed.num_rows, 2);
467 assert_eq!(
468 sealed.tables.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
469 schema::TRACES_BLOCK_TABLES
470 );
471 assert_eq!(sealed.min_ts, 10_000);
474 assert_eq!(sealed.max_ts, 10_501);
475
476 let rows = |t: &str| sealed.table(t).unwrap().num_rows();
477 assert_eq!(rows("span_events"), 2);
478 assert_eq!(rows("span_links"), 1);
479 assert_eq!(rows("span_event_attrs"), 2);
480 assert_eq!(rows("span_link_attrs"), 0);
481 assert_eq!(rows("span_attrs"), 2);
482
483 let u32col = |t: &str, c: &str| {
484 sealed
485 .table(t)
486 .unwrap()
487 .column_by_name(c)
488 .unwrap()
489 .as_any()
490 .downcast_ref::<UInt32Array>()
491 .unwrap()
492 .clone()
493 };
494 assert_eq!(u32col("span_events", "parent_id").values(), &[1, 1]);
498 assert_eq!(u32col("span_events", "id").values(), &[0, 1]);
499 assert_eq!(u32col("span_event_attrs", "parent_id").values(), &[0, 1]);
500 assert_eq!(u32col("span_links", "parent_id").values(), &[1]);
501
502 let root = std::env::temp_dir().join(format!("mira-tr-{}", std::process::id()));
506 let _ = std::fs::remove_dir_all(&root);
507 let published =
508 block::publish(&root, "traces", block::node_id("a"), 1, 0, &sealed).unwrap();
509 assert!(!published.dir.join("span_link_attrs.arrow").exists());
510 assert!(
511 block::open_table_opt(&published.dir.join("span_link_attrs.arrow"))
512 .unwrap()
513 .is_none()
514 );
515 let ev = block::open_table_opt(&published.dir.join("span_events.arrow"))
516 .unwrap()
517 .unwrap();
518 let (inside, total) = ev.zero_copy_ratio();
519 assert_eq!(inside, total, "{inside}/{total} buffers zero-copy");
520 let _ = std::fs::remove_dir_all(&root);
521
522 let spans = sealed.table("spans").unwrap();
523 let durations = spans
524 .column_by_name("duration_nano")
525 .unwrap()
526 .as_any()
527 .downcast_ref::<UInt64Array>()
528 .unwrap();
529 assert_eq!(durations.values(), &[500, 500]);
530
531 let lt = sealed.table("span_links").unwrap();
534 let lt = lt
535 .column_by_name("trace_id")
536 .unwrap()
537 .as_any()
538 .downcast_ref::<arrow_array::FixedSizeBinaryArray>()
539 .unwrap();
540 assert_eq!(lt.value(0), &[9u8; 16]);
541 }
542
543 #[test]
547 fn malformed_span_times_do_not_widen_the_block_range() {
548 use mira_proto::collector::trace::v1::ExportTraceServiceRequest;
549 use mira_proto::trace::v1::{ResourceSpans, ScopeSpans, Span};
550
551 let mut b = traces::TracesBuilder::new();
552 b.append_request(&ExportTraceServiceRequest {
553 resource_spans: vec![ResourceSpans {
554 scope_spans: vec![ScopeSpans {
555 spans: vec![
556 Span {
557 name: "no-clock".into(),
558 ..Default::default()
559 },
560 Span {
561 name: "backwards".into(),
562 start_time_unix_nano: 5_000,
563 end_time_unix_nano: 4_000,
564 ..Default::default()
565 },
566 Span {
567 name: "fine".into(),
568 start_time_unix_nano: 6_000,
569 end_time_unix_nano: 6_100,
570 ..Default::default()
571 },
572 ],
573 ..Default::default()
574 }],
575 ..Default::default()
576 }],
577 })
578 .unwrap();
579
580 let sealed = b.finish().unwrap();
581 assert_eq!(
582 sealed.num_rows, 3,
583 "malformed spans are stored, not dropped"
584 );
585 assert_eq!(sealed.min_ts, 5_000);
586 assert_eq!(sealed.max_ts, 6_100);
587 let d = sealed.table("spans").unwrap();
589 let d = d
590 .column_by_name("duration_nano")
591 .unwrap()
592 .as_any()
593 .downcast_ref::<UInt64Array>()
594 .unwrap();
595 assert_eq!(d.values(), &[0, 0, 100]);
596 }
597
598 #[test]
603 fn every_metric_type_lands_in_its_own_table_from_one_id_space() {
604 use arrow_array::{Float64Array, ListArray, UInt8Array};
605 use mira_proto::collector::metrics::v1::ExportMetricsServiceRequest;
606 use mira_proto::metrics::v1::metric::Data;
607 use mira_proto::metrics::v1::number_data_point::Value as NumValue;
608 use mira_proto::metrics::v1::summary_data_point::ValueAtQuantile;
609 use mira_proto::metrics::v1::{
610 AggregationTemporality, Exemplar, ExponentialHistogram, ExponentialHistogramDataPoint,
611 Gauge, Histogram, HistogramDataPoint, Metric, NumberDataPoint, ResourceMetrics,
612 ScopeMetrics, Sum, Summary, SummaryDataPoint,
613 exponential_histogram_data_point::Buckets,
614 };
615
616 let number = |t: u64, v: i64| NumberDataPoint {
617 attributes: vec![kv("route", "/checkout")],
618 time_unix_nano: t,
619 start_time_unix_nano: 1, value: Some(NumValue::AsInt(v)),
621 ..Default::default()
622 };
623 let hist = |t: u64| HistogramDataPoint {
626 time_unix_nano: t,
627 count: 3,
628 sum: Some(1.5),
629 bucket_counts: vec![1, 1, 1],
630 explicit_bounds: vec![0.1, 0.5],
631 exemplars: vec![Exemplar {
632 time_unix_nano: t,
633 trace_id: vec![4u8; 16].into(),
634 span_id: vec![5u8; 8].into(),
635 ..Default::default()
636 }],
637 ..Default::default()
638 };
639
640 let metrics = vec![
641 Metric {
642 name: "http.server.request.count".into(),
643 unit: "{request}".into(),
644 data: Some(Data::Sum(Sum {
645 data_points: vec![number(1_000, 7), number(2_000, 9)],
646 aggregation_temporality: AggregationTemporality::Cumulative as i32,
647 is_monotonic: true,
648 })),
649 metadata: vec![kv("owner", "checkout-team")],
650 ..Default::default()
651 },
652 Metric {
653 name: "process.memory".into(),
654 data: Some(Data::Gauge(Gauge {
655 data_points: vec![number(1_500, 42)],
656 })),
657 ..Default::default()
658 },
659 Metric {
660 name: "http.server.duration".into(),
661 data: Some(Data::Histogram(Histogram {
662 data_points: vec![hist(1_100), hist(2_100)],
663 aggregation_temporality: AggregationTemporality::Delta as i32,
664 })),
665 ..Default::default()
666 },
667 Metric {
668 name: "rpc.duration".into(),
669 data: Some(Data::ExponentialHistogram(ExponentialHistogram {
670 data_points: vec![ExponentialHistogramDataPoint {
671 time_unix_nano: 1_200,
672 count: 4,
673 scale: 3,
674 zero_count: 1,
675 positive: Some(Buckets {
676 offset: 2,
677 bucket_counts: vec![1, 2],
678 }),
679 ..Default::default()
680 }],
681 aggregation_temporality: AggregationTemporality::Delta as i32,
682 })),
683 ..Default::default()
684 },
685 Metric {
686 name: "legacy.latency".into(),
687 data: Some(Data::Summary(Summary {
688 data_points: vec![SummaryDataPoint {
689 time_unix_nano: 3_000,
690 count: 10,
691 sum: 5.0,
692 quantile_values: vec![
693 ValueAtQuantile {
694 quantile: 0.5,
695 value: 0.4,
696 },
697 ValueAtQuantile {
698 quantile: 0.99,
699 value: 0.9,
700 },
701 ],
702 ..Default::default()
703 }],
704 })),
705 ..Default::default()
706 },
707 ];
708
709 let mut b = metrics::MetricsBuilder::new();
710 let req = ExportMetricsServiceRequest {
711 resource_metrics: vec![ResourceMetrics {
712 resource: Some(Resource {
713 attributes: vec![kv("service.name", "checkout")],
714 ..Default::default()
715 }),
716 scope_metrics: vec![ScopeMetrics {
717 metrics,
718 ..Default::default()
719 }],
720 ..Default::default()
721 }],
722 };
723 assert!(b.has_headroom_for(&req));
724 assert_eq!(b.append_request(&req).unwrap(), 7);
726
727 let sealed = b.finish().unwrap();
728 assert_eq!(sealed.num_rows, 7);
729 assert_eq!(
730 sealed.tables.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
731 schema::METRICS_BLOCK_TABLES
732 );
733 let rows = |t: &str| sealed.table(t).unwrap().num_rows();
734 assert_eq!(rows("metrics"), 5);
735 assert_eq!(rows("number_dp"), 3);
736 assert_eq!(rows("hist_dp"), 2);
737 assert_eq!(rows("exp_hist_dp"), 1);
738 assert_eq!(rows("summary_dp"), 1);
739 assert_eq!(rows("exemplars"), 2);
740 assert_eq!(rows("metric_attrs"), 1);
741 assert_eq!(rows("hist_bounds"), 1);
744
745 assert_eq!(sealed.min_ts, 1_000);
748 assert_eq!(sealed.max_ts, 3_000);
749
750 let u32col = |t: &str, c: &str| {
751 sealed
752 .table(t)
753 .unwrap()
754 .column_by_name(c)
755 .unwrap()
756 .as_any()
757 .downcast_ref::<UInt32Array>()
758 .unwrap()
759 .clone()
760 };
761 assert_eq!(u32col("number_dp", "id").values(), &[0, 1, 2]);
766 assert_eq!(u32col("hist_dp", "id").values(), &[3, 4]);
767 assert_eq!(u32col("exp_hist_dp", "id").values(), &[5]);
768 assert_eq!(u32col("summary_dp", "id").values(), &[6]);
769 assert_eq!(u32col("exemplars", "parent_id").values(), &[3, 4]);
770 assert_eq!(u32col("dp_attrs", "parent_id").values(), &[0, 1, 2]);
773 assert_eq!(u32col("hist_dp", "bounds_id").values(), &[0, 0]);
775
776 let kinds = sealed.table("metrics").unwrap();
779 let kind = kinds
780 .column_by_name("kind")
781 .unwrap()
782 .as_any()
783 .downcast_ref::<UInt8Array>()
784 .unwrap();
785 assert_eq!(kind.values(), &[2, 1, 3, 4, 5]);
786
787 let sd = sealed.table("summary_dp").unwrap();
789 let q = sd
790 .column_by_name("quantile")
791 .unwrap()
792 .as_any()
793 .downcast_ref::<ListArray>()
794 .unwrap()
795 .value(0);
796 let q = q.as_any().downcast_ref::<Float64Array>().unwrap();
797 assert_eq!(q.values(), &[0.5, 0.99]);
798
799 let root = std::env::temp_dir().join(format!("mira-me-{}", std::process::id()));
803 let _ = std::fs::remove_dir_all(&root);
804 let published =
805 block::publish(&root, "metrics", block::node_id("a"), 1, 0, &sealed).unwrap();
806 for t in ["number_dp", "hist_dp", "hist_bounds", "summary_dp"] {
807 let m = block::open_table(&published.dir.join(format!("{t}.arrow"))).unwrap();
808 let (inside, total) = m.zero_copy_ratio();
809 assert_eq!(inside, total, "{t}: {inside}/{total} buffers zero-copy");
810 }
811 assert!(!published.dir.join("exemplar_attrs.arrow").exists());
814 let _ = std::fs::remove_dir_all(&root);
815 }
816
817 #[test]
822 fn search_filters_across_attribute_levels_and_prunes_by_time() {
823 use query::{Op, Search, Signal, Target, Term, Value as QV};
824
825 let root = std::env::temp_dir().join(format!("mira-q-{}", std::process::id()));
826 let _ = std::fs::remove_dir_all(&root);
827
828 for (seq, (service, base)) in [("checkout", 1_000u64), ("payments", 5_000)]
830 .into_iter()
831 .enumerate()
832 {
833 let mut b = logs::LogsBuilder::new();
834 b.append_request(&request(service, 10, base)).unwrap();
835 let sealed = b.finish().unwrap();
836 block::publish(&root, "logs", block::node_id("a"), seq as u64, 0, &sealed).unwrap();
837 }
838
839 let q = |terms: Vec<Term>, from: i64, to: i64, limit: usize| Search {
840 signal: Signal::Logs,
841 from,
842 to,
843 terms,
844 limit,
845 after: None,
846 };
847 let attr = |k: &str, v: &str| Term {
848 target: Target::Attr(k.into()),
849 op: Op::Eq,
850 value: QV::Str(v.into()),
851 };
852 let field = |c: &str, op: Op, v: QV| Term {
853 target: Target::Field(c.into()),
854 op,
855 value: v,
856 };
857
858 let r = query::search(
861 &root,
862 &q(vec![attr("service.name", "payments")], 0, i64::MAX, 100),
863 )
864 .unwrap();
865 assert_eq!(r.stats.rows_matched, 10);
866 assert!(r.json.contains("\"service.name\":\"payments\""));
867 assert!(!r.json.contains("checkout"));
868 assert_eq!(r.stats.blocks_scanned, 1);
872
873 let r = query::search(
875 &root,
876 &q(vec![attr("http.method", "GET")], 0, i64::MAX, 100),
877 )
878 .unwrap();
879 assert_eq!(r.stats.rows_matched, 20);
880
881 let r = query::search(&root, &q(vec![attr("nope", "x")], 0, i64::MAX, 100)).unwrap();
883 assert_eq!(r.stats.rows_matched, 0);
884 assert_eq!(r.json, "[]");
885
886 let r = query::search(&root, &q(vec![], 5_000, 9_000, 100)).unwrap();
889 assert_eq!(r.stats.blocks_total, 2);
890 assert_eq!(
891 r.stats.blocks_scanned, 1,
892 "the 1_000-range block must prune"
893 );
894 assert_eq!(r.stats.rows_matched, 10);
895
896 let r = query::search(
899 &root,
900 &q(
901 vec![field("severity_text", Op::Contains, QV::Str("NF".into()))],
902 0,
903 i64::MAX,
904 100,
905 ),
906 )
907 .unwrap();
908 assert_eq!(r.stats.rows_matched, 20);
909
910 let r = query::search(
913 &root,
914 &q(
915 vec![field("severity_number", Op::Gte, QV::Str("9".into()))],
916 0,
917 i64::MAX,
918 100,
919 ),
920 )
921 .unwrap();
922 assert_eq!(r.stats.rows_matched, 20);
923
924 let r = query::search(
926 &root,
927 &q(
928 vec![field("trace_id", Op::Eq, QV::Str("07".repeat(16)))],
929 0,
930 i64::MAX,
931 100,
932 ),
933 )
934 .unwrap();
935 assert_eq!(r.stats.rows_matched, 20);
936 let r = query::search(
938 &root,
939 &q(
940 vec![field("trace_id", Op::Eq, QV::Str("07".into()))],
941 0,
942 i64::MAX,
943 100,
944 ),
945 )
946 .unwrap();
947 assert_eq!(r.stats.rows_matched, 0);
948
949 let r = query::search(
951 &root,
952 &q(
953 vec![attr("service.name", "checkout"), attr("http.method", "GET")],
954 0,
955 i64::MAX,
956 100,
957 ),
958 )
959 .unwrap();
960 assert_eq!(r.stats.rows_matched, 10);
961
962 let r = query::search(&root, &q(vec![], 0, i64::MAX, 5)).unwrap();
965 assert_eq!(r.stats.blocks_scanned, 1);
966 assert_eq!(r.json.matches("\"body\"").count(), 5);
967 assert!(r.json.starts_with("[{\"time_unix_nano\":\"5009\","));
970 assert!(!r.json.contains("resource_id"));
972 assert!(!r.json.contains("\"id\""));
973 assert!(r.json.contains("\"otel.scope.name\":\"test\""));
976
977 let _ = std::fs::remove_dir_all(&root);
978 }
979
980 #[test]
989 fn paging_returns_every_row_once_even_across_ties() {
990 use query::{Cursor, Search, Signal};
991
992 let root = std::env::temp_dir().join(format!("mira-page-{}", std::process::id()));
993 let _ = std::fs::remove_dir_all(&root);
994
995 for seq in 0..4u64 {
999 let base = if seq < 2 { 5_000 } else { 1_000 };
1000 let mut req = request("svc", 25, base);
1001 for (i, r) in req.resource_logs[0].scope_logs[0]
1007 .log_records
1008 .iter_mut()
1009 .enumerate()
1010 {
1011 r.time_unix_nano = base + i as u64 / 5;
1012 }
1013 let mut b = logs::LogsBuilder::new();
1014 b.append_request(&req).unwrap();
1015 let sealed = b.finish().unwrap();
1016 block::publish(&root, "logs", block::node_id("a"), seq, 0, &sealed).unwrap();
1017 }
1018
1019 let page = |limit: usize, after| {
1020 query::search(
1021 &root,
1022 &Search {
1023 signal: Signal::Logs,
1024 from: 0,
1025 to: i64::MAX,
1026 terms: vec![],
1027 limit,
1028 after,
1029 },
1030 )
1031 .unwrap()
1032 };
1033
1034 let all = page(1_000, None);
1035 assert_eq!(all.json.matches("\"body\"").count(), 100);
1036 assert!(all.next.is_none(), "a short page is the last page");
1037
1038 let mut rows = Vec::new();
1041 let mut after = None;
1042 let mut deepest = 0;
1043 for _ in 0..100 {
1044 let r = page(7, after);
1045 rows.push(r.json[1..r.json.len() - 1].to_owned());
1046 deepest = r.stats.blocks_scanned;
1047 match r.next {
1048 Some(c) => after = Some(c),
1049 None => break,
1050 }
1051 }
1052 assert_eq!(rows.len(), 15);
1053 assert_eq!(
1054 format!("[{}]", rows.join(",")),
1055 all.json,
1056 "paged reads must reassemble the one-shot answer byte for byte"
1057 );
1058
1059 assert_eq!(all.stats.blocks_scanned, 4);
1063 assert_eq!(deepest, 2);
1064
1065 let end = Cursor {
1068 ts: 0,
1069 node: 0,
1070 seq: 0,
1071 row: 0,
1072 };
1073 assert_eq!(page(7, Some(end)).json, "[]");
1074
1075 let c = page(7, None).next.unwrap();
1078 assert_eq!(c.to_string().parse::<Cursor>(), Ok(c));
1079 for bad in ["", "1.2.3", "1.2.3.4.5", "1.-2.3.4", "a.2.3.4", "1.2.3.x"] {
1080 assert!(bad.parse::<Cursor>().is_err(), "{bad:?} parsed");
1081 }
1082
1083 let _ = std::fs::remove_dir_all(&root);
1084 }
1085
1086 #[test]
1090 fn a_trace_lookup_opens_only_the_block_holding_the_trace() {
1091 use mira_proto::collector::trace::v1::ExportTraceServiceRequest;
1092 use mira_proto::trace::v1::{ResourceSpans, ScopeSpans, Span};
1093 use query::{Op, Search, Signal, Target, Term, Value as QV};
1094
1095 let root = std::env::temp_dir().join(format!("mira-bloom-{}", std::process::id()));
1096 let _ = std::fs::remove_dir_all(&root);
1097
1098 let tid = |n: u64| {
1101 let mut b = [0u8; 16];
1102 b[..8].copy_from_slice(&identity::hash64(&n.to_le_bytes()).to_le_bytes());
1103 b[8..].copy_from_slice(&identity::hash64(&(!n).to_le_bytes()).to_le_bytes());
1104 b
1105 };
1106
1107 const BLOCKS: u64 = 8;
1108 for seq in 0..BLOCKS {
1109 let mut b = traces::TracesBuilder::new();
1110 let spans = (0..4)
1113 .map(|i| Span {
1114 trace_id: tid(seq * 4 + i).to_vec().into(),
1115 span_id: vec![i as u8 + 1; 8].into(),
1116 name: "GET /checkout".into(),
1117 start_time_unix_nano: 1_000 + i,
1118 end_time_unix_nano: 1_100 + i,
1119 ..Default::default()
1120 })
1121 .collect();
1122 b.append_request(&ExportTraceServiceRequest {
1123 resource_spans: vec![ResourceSpans {
1124 resource: Some(Resource::default()),
1125 scope_spans: vec![ScopeSpans {
1126 spans,
1127 ..Default::default()
1128 }],
1129 ..Default::default()
1130 }],
1131 })
1132 .unwrap();
1133 let sealed = b.finish().unwrap();
1134 assert!(
1135 sealed.sidecars.iter().any(|(n, _)| *n == bloom::TRACE_IDX),
1136 "traces publish a trace filter"
1137 );
1138 block::publish(&root, "traces", block::node_id("a"), seq, 0, &sealed).unwrap();
1139 }
1140
1141 for seq in 0..BLOCKS {
1145 let mut req = request("checkout", 4, 1_000);
1146 for (i, r) in req.resource_logs[0].scope_logs[0]
1147 .log_records
1148 .iter_mut()
1149 .enumerate()
1150 {
1151 r.trace_id = tid(seq * 4 + i as u64).to_vec().into();
1152 }
1153 let mut b = logs::LogsBuilder::new();
1154 b.append_request(&req).unwrap();
1155 let sealed = b.finish().unwrap();
1156 block::publish(&root, "logs", block::node_id("a"), seq, 0, &sealed).unwrap();
1157 }
1158
1159 let lookup = |signal, id: String| Search {
1160 signal,
1161 from: 0,
1162 to: i64::MAX,
1163 terms: vec![Term {
1164 target: Target::Field("trace_id".into()),
1165 op: Op::Eq,
1166 value: QV::Str(id),
1167 }],
1168 limit: 100,
1169 after: None,
1170 };
1171
1172 let hex = |b: [u8; 16]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
1173 for signal in [Signal::Traces, Signal::Logs] {
1174 let r = query::search(&root, &lookup(signal, hex(tid(17)))).unwrap();
1175 assert_eq!(r.stats.blocks_total, BLOCKS as usize);
1176 assert_eq!(r.stats.blocks_scanned, 1, "{signal:?}: skip the rest");
1177 assert_eq!(r.stats.rows_matched, 1);
1178
1179 let r = query::search(&root, &lookup(signal, hex(tid(9_999)))).unwrap();
1182 assert_eq!(r.stats.rows_matched, 0);
1183 assert!(r.stats.blocks_scanned <= 1, "{}", r.stats.blocks_scanned);
1184 }
1185
1186 let one = block::scan(&root, "traces").unwrap();
1188 for b in &one {
1189 std::fs::remove_file(b.dir.join(bloom::TRACE_IDX)).unwrap();
1190 }
1191 let r = query::search(&root, &lookup(Signal::Traces, hex(tid(17)))).unwrap();
1192 assert_eq!(r.stats.blocks_scanned, BLOCKS as usize);
1193 assert_eq!(r.stats.rows_matched, 1);
1194
1195 let _ = std::fs::remove_dir_all(&root);
1196 }
1197
1198 #[test]
1217 fn a_scan_wide_enough_to_fan_out_answers_exactly_as_a_narrow_one() {
1218 use query::{Search, Signal};
1219
1220 let root = std::env::temp_dir().join(format!("mira-fanout-{}", std::process::id()));
1221 let _ = std::fs::remove_dir_all(&root);
1222
1223 const BLOCKS: u64 = 24;
1227 for seq in 0..BLOCKS {
1228 let base = 1_000_000 + (seq * 7 % BLOCKS) * 1_000;
1229 let mut req = request("svc", 10, base);
1230 for (i, r) in req.resource_logs[0].scope_logs[0]
1231 .log_records
1232 .iter_mut()
1233 .enumerate()
1234 {
1235 r.time_unix_nano = base + i as u64;
1236 }
1237 let mut b = logs::LogsBuilder::new();
1238 b.append_request(&req).unwrap();
1239 let sealed = b.finish().unwrap();
1240 block::publish(&root, "logs", block::node_id("a"), seq, 0, &sealed).unwrap();
1241 }
1242
1243 let page = |limit: usize, after| {
1244 query::search(
1245 &root,
1246 &Search {
1247 signal: Signal::Logs,
1248 from: 0,
1249 to: i64::MAX,
1250 terms: vec![],
1251 limit,
1252 after,
1253 },
1254 )
1255 .unwrap()
1256 };
1257
1258 let held = query::Helpers::claim(usize::MAX);
1261 let serial = page(1_000, None);
1262 drop(held);
1263
1264 let all = page(1_000, None);
1266 assert_eq!(all.stats.blocks_scanned, BLOCKS as usize);
1267 assert_eq!(all.json.matches("\"body\"").count(), 240);
1268 assert_eq!(all.json, serial.json, "fanning out changed the answer");
1269
1270 let mut rows = Vec::new();
1271 let mut after = None;
1272 for _ in 0..100 {
1273 let r = page(7, after);
1274 rows.push(r.json[1..r.json.len() - 1].to_owned());
1275 match r.next {
1276 Some(c) => after = Some(c),
1277 None => break,
1278 }
1279 }
1280 assert_eq!(
1281 format!("[{}]", rows.join(",")),
1282 all.json,
1283 "a fanned-out scan must page like a serial one"
1284 );
1285
1286 let _ = std::fs::remove_dir_all(&root);
1287 }
1288
1289 #[test]
1306 fn a_frame_walk_reconstructs_a_call_chain_from_one_log_line() {
1307 use mira_proto::collector::trace::v1::ExportTraceServiceRequest;
1308 use mira_proto::trace::v1::{ResourceSpans, ScopeSpans, Span, Status};
1309 use query::{Op, Search, Signal, Target, Term, Value as QV};
1310
1311 let root = std::env::temp_dir().join(format!("mira-frame-{}", std::process::id()));
1312 let _ = std::fs::remove_dir_all(&root);
1313
1314 const T0: u64 = 1_000_000_000;
1317 const LOG_TS: u64 = T0 + 5_000_000_000;
1318 let tid = |n: u8| [n; 16];
1319 let sid = |svc: u8, n: u8| [svc, n, 0, 0, 0, 0, 0, 0];
1320
1321 let svc = |name: &str, spans: Vec<Span>| ResourceSpans {
1323 resource: Some(Resource {
1324 attributes: vec![kv("service.name", name), kv("service.instance.id", "7f3a")],
1329 ..Default::default()
1330 }),
1331 scope_spans: vec![ScopeSpans {
1332 spans,
1333 ..Default::default()
1334 }],
1335 ..Default::default()
1336 };
1337 let span = |t: u8, id: [u8; 8], parent: Option<[u8; 8]>, dur: u64, bad: bool| Span {
1338 trace_id: tid(t).to_vec().into(),
1339 span_id: id.to_vec().into(),
1340 parent_span_id: parent.map(|p| p.to_vec()).unwrap_or_default().into(),
1341 name: "GET /checkout".into(),
1342 start_time_unix_nano: T0 + u64::from(t),
1343 end_time_unix_nano: T0 + u64::from(t) + dur,
1344 status: bad.then(|| Status {
1345 code: 2,
1346 ..Default::default()
1347 }),
1348 ..Default::default()
1349 };
1350 let mut b = traces::TracesBuilder::new();
1351 b.append_request(&ExportTraceServiceRequest {
1352 resource_spans: vec![
1353 svc(
1354 "gateway",
1355 (1..=3)
1356 .map(|t| span(t, sid(1, t), None, 900, false))
1357 .collect(),
1358 ),
1359 svc(
1360 "api",
1361 (1..=3)
1362 .map(|t| span(t, sid(2, t), Some(sid(1, t)), 500, false))
1363 .collect(),
1364 ),
1365 svc(
1366 "db",
1367 (1..=3)
1368 .map(|t| span(t, sid(3, t), Some(sid(2, t)), 100, t == 3))
1369 .collect(),
1370 ),
1371 ],
1372 })
1373 .unwrap();
1374 let sealed = b.finish().unwrap();
1375 block::publish(&root, "traces", block::node_id("a"), 0, 0, &sealed).unwrap();
1376
1377 let mut req = request("api", 3, LOG_TS);
1380 for (i, r) in req.resource_logs[0].scope_logs[0]
1381 .log_records
1382 .iter_mut()
1383 .enumerate()
1384 {
1385 r.trace_id = tid(i as u8 + 1).to_vec().into();
1386 r.body = Some(AnyValue {
1387 value: Some(Value::StringValue(if i == 2 {
1388 "boom".into()
1389 } else {
1390 "fine".into()
1391 })),
1392 });
1393 }
1394 let mut b = logs::LogsBuilder::new();
1395 b.append_request(&req).unwrap();
1396 let sealed = b.finish().unwrap();
1397 block::publish(&root, "logs", block::node_id("a"), 0, 0, &sealed).unwrap();
1398
1399 let q = Search {
1401 signal: Signal::Logs,
1402 from: LOG_TS as i64 - 1_000_000_000,
1403 to: LOG_TS as i64 + 1_000_000_000,
1404 terms: vec![Term {
1405 target: Target::Field("body".into()),
1406 op: Op::Eq,
1407 value: QV::Str("boom".into()),
1408 }],
1409 limit: 10,
1410 after: None,
1411 };
1412 let (f, st) = frame::anchor(&root, &q, &[]).unwrap();
1413 assert_eq!(f.traces, vec![tid(3)], "one row matched, so one trace");
1414 assert_eq!(f.entities.len(), 1, "the service that wrote the line, only");
1415 let api = f.entities[0];
1416 assert!(!f.truncated);
1417 assert_eq!(st.rows_matched, 1);
1418
1419 let (early, _) = frame::expand(&root, &f, &[frame::Expand::Peers], &[]).unwrap();
1422 assert_eq!(early.entities, f.entities, "peers before traces finds none");
1423
1424 let ops = [frame::Expand::Traces, frame::Expand::Peers];
1425 let (walked, st) = frame::expand(&root, &f, &ops, &[]).unwrap();
1426 assert_eq!(
1427 walked.from,
1428 T0 as i64 + 3,
1429 "the window reaches back to the first span of the trace"
1430 );
1431 assert_eq!(walked.to, q.to, "the traces end inside the window already");
1432 assert_eq!(walked.entities.len(), 3, "gateway, api and db");
1433 assert_eq!((st.rows_scanned, st.rows_matched), (18, 6));
1435
1436 let names = frame::names_of(&root, &walked, &[]).unwrap();
1437 assert_eq!(
1438 names.get(&api).map(String::as_str),
1439 Some("api"),
1440 "the key harvested from a log row is the key the spans carry"
1441 );
1442 let mut got: Vec<&str> = names.values().map(String::as_str).collect();
1443 got.sort_unstable();
1444 assert_eq!(got, ["api", "db", "gateway"]);
1445
1446 let m = frame::map(&root, 0, i64::MAX, 1_000_000, &[]).unwrap();
1448 let edge = |from: &str, to: &str| {
1449 let (fk, tk) = (key_of(&names, from), key_of(&names, to));
1450 let from = if from == "entry" {
1451 "\"entry\"".to_owned()
1452 } else {
1453 format!("\"{fk}\"")
1454 };
1455 format!("\"from\":{from},\"to\":\"{tk}\"")
1456 };
1457 for (a, b) in [("entry", "gateway"), ("gateway", "api"), ("api", "db")] {
1458 assert!(m.json.contains(&edge(a, b)), "{a} -> {b} in {}", m.json);
1459 }
1460 assert!(
1461 m.json.contains("\"unresolved\":0"),
1462 "every parent is in the same block: {}",
1463 m.json
1464 );
1465 assert_eq!(
1466 m.json.matches("\"errors\":1").count(),
1467 2,
1468 "the failing leaf shows on its node and on its one in-edge: {}",
1469 m.json
1470 );
1471
1472 let e = frame::entities(&root, 0, i64::MAX, &[]).unwrap();
1474 assert_eq!(e.stats.rows_matched, 3);
1475 assert!(
1476 e.json.find("gateway") > e.json.find("db"),
1477 "sorted by name, for a human: {}",
1478 e.json
1479 );
1480
1481 let _ = std::fs::remove_dir_all(&root);
1482 }
1483
1484 fn key_of(names: &std::collections::HashMap<u64, String>, name: &str) -> u64 {
1486 names
1487 .iter()
1488 .find(|(_, v)| *v == name)
1489 .map_or(0, |(k, _)| *k)
1490 }
1491
1492 #[test]
1500 fn the_attribute_filter_skips_blocks_without_losing_coercions() {
1501 use query::{Op, Search, Signal, Target, Term, Value as QV};
1502
1503 let root = std::env::temp_dir().join(format!("mira-attrs-{}", std::process::id()));
1504 let _ = std::fs::remove_dir_all(&root);
1505
1506 let attr = |k: &str, v: Value| KeyValue {
1507 key: k.into(),
1508 value: Some(AnyValue { value: Some(v) }),
1509 };
1510
1511 const BLOCKS: u64 = 8;
1512 for seq in 0..BLOCKS {
1513 let mut req = request("checkout", 2, 1_000);
1516 let mut attrs = vec![attr(
1517 "k8s.pod.name",
1518 Value::StringValue(format!("api-{seq}")),
1519 )];
1520 if seq == 3 {
1522 attrs.push(attr("http.status_code", Value::IntValue(200)));
1523 attrs.push(attr("retry", Value::BoolValue(true)));
1524 attrs.push(attr("ratio", Value::DoubleValue(1.5)));
1525 attrs.push(attr("status.text", Value::StringValue("404".into())));
1528 }
1529 for r in &mut req.resource_logs[0].scope_logs[0].log_records {
1530 r.attributes = attrs.clone();
1531 }
1532 let mut b = logs::LogsBuilder::new();
1533 b.append_request(&req).unwrap();
1534 let sealed = b.finish().unwrap();
1535 let names: Vec<_> = sealed.sidecars.iter().map(|(n, _)| *n).collect();
1536 assert_eq!(
1537 names,
1538 [bloom::ATTR_IDX, zone::ZONE_IDX, bloom::TRACE_IDX],
1539 "logs sidecars"
1540 );
1541 block::publish(&root, "logs", block::node_id("a"), seq, 0, &sealed).unwrap();
1542 }
1543
1544 let find_op = |key: &str, op: Op, value: QV| {
1545 query::search(
1546 &root,
1547 &Search {
1548 signal: Signal::Logs,
1549 from: 0,
1550 to: i64::MAX,
1551 terms: vec![Term {
1552 target: Target::Attr(key.into()),
1553 op,
1554 value,
1555 }],
1556 limit: 100,
1557 after: None,
1558 },
1559 )
1560 .unwrap()
1561 };
1562 let find = |key: &str, value: QV| find_op(key, Op::Eq, value);
1563 let s = |v: &str| QV::Str(v.into());
1564
1565 let r = find("k8s.pod.name", s("api-5"));
1568 assert_eq!(r.stats.blocks_total, BLOCKS as usize);
1569 assert_eq!(r.stats.blocks_scanned, 1, "the filter must skip the rest");
1570 assert_eq!(r.stats.rows_matched, 2);
1571
1572 let r = find("k8s.pod.name", s("api-99"));
1577 assert_eq!(r.stats.rows_matched, 0);
1578 assert!(r.stats.blocks_scanned <= 1, "{}", r.stats.blocks_scanned);
1579
1580 for (key, value) in [
1582 ("http.status_code", QV::Int(200)),
1583 ("http.status_code", s("200")),
1584 ("http.status_code", QV::Double(200.0)),
1586 ("retry", QV::Bool(true)),
1587 ("retry", s("true")),
1588 ("ratio", QV::Double(1.5)),
1591 ("ratio", s("1.5")),
1592 ] {
1593 let r = find(key, value.clone());
1594 assert_eq!(r.stats.rows_matched, 2, "{key} = {value:?}");
1595 }
1596
1597 let r = find("status.text", QV::Int(404));
1603 assert_eq!(r.stats.rows_matched, 2);
1604 assert_eq!(r.stats.blocks_scanned, 1, "and it is still pruned");
1605
1606 let r = find_op("status.text", Op::Lt, QV::Int(500));
1616 assert_eq!(r.stats.rows_matched, 2);
1617 assert_eq!(r.stats.blocks_scanned, 1, "the zone map must skip the rest");
1618 let r = find_op("status.text", Op::Gt, QV::Int(99));
1619 assert_eq!(r.stats.rows_matched, 2);
1620 assert_eq!(r.stats.blocks_scanned, 1);
1621 let r = find_op("http.status_code", Op::Gte, QV::Int(500));
1625 assert_eq!(r.stats.rows_matched, 0);
1626 assert_eq!(r.stats.blocks_scanned, 0);
1627 assert_eq!(
1629 find_op("status.text", Op::Gt, s("99")).stats.rows_matched,
1630 0
1631 );
1632 assert_eq!(
1634 find_op("status.text", Op::Contains, QV::Int(40))
1635 .stats
1636 .rows_matched,
1637 2
1638 );
1639
1640 let r = find("ratio", s("banana"));
1644 assert_eq!(r.stats.rows_matched, 0);
1645 assert!(r.stats.blocks_scanned <= 1, "{}", r.stats.blocks_scanned);
1646
1647 for b in block::scan(&root, "logs").unwrap() {
1651 std::fs::remove_file(b.dir.join(bloom::ATTR_IDX)).unwrap();
1652 std::fs::remove_file(b.dir.join(zone::ZONE_IDX)).unwrap();
1653 }
1654 let r = find("k8s.pod.name", s("api-5"));
1655 assert_eq!(r.stats.blocks_scanned, BLOCKS as usize);
1656 assert_eq!(r.stats.rows_matched, 2);
1657 let r = find_op("status.text", Op::Lt, QV::Int(500));
1658 assert_eq!(r.stats.blocks_scanned, BLOCKS as usize);
1659 assert_eq!(r.stats.rows_matched, 2);
1660
1661 let _ = std::fs::remove_dir_all(&root);
1662 }
1663
1664 #[test]
1673 fn a_large_export_the_headroom_hint_rejects_still_fits_one_block() {
1674 let mut req = request("checkout", 20_000, 1_000);
1677 for r in &mut req.resource_logs[0].scope_logs[0].log_records {
1678 r.attributes = vec![
1679 kv("http.method", "GET"),
1680 kv("http.route", "/checkout"),
1681 kv("net.peer.name", "db"),
1682 kv("http.scheme", "https"),
1683 ];
1684 }
1685
1686 let mut b = logs::LogsBuilder::new();
1687 assert!(
1688 !b.has_headroom_for(&req),
1689 "the hint is expected to be conservative here; if it stopped being \
1690 so, this test is no longer testing anything"
1691 );
1692 assert_eq!(b.append_request(&req).unwrap(), 20_000);
1694 let sealed = b.finish().unwrap();
1695 assert_eq!(sealed.num_rows, 20_000);
1696 assert_eq!(sealed.table("log_attrs").unwrap().num_rows(), 80_000);
1697 }
1698
1699 #[test]
1702 fn compaction_shrinks_aged_blocks_without_changing_what_they_answer() {
1703 use query::{Op, Search, Signal, Target, Term, Value as QV};
1704
1705 let root = std::env::temp_dir().join(format!("mira-cold-{}", std::process::id()));
1706 let _ = std::fs::remove_dir_all(&root);
1707
1708 let now = 4 * block::COLD_AFTER_NS;
1710 let old = (now - 90 * 60 * 1_000_000_000) as u64;
1711 let new = (now - 60 * 1_000_000_000) as u64;
1712 let mut dirs = Vec::new();
1713 for (seq, base) in [old, new].into_iter().enumerate() {
1714 let mut b = logs::LogsBuilder::new();
1715 b.append_request(&request("checkout", 500, base)).unwrap();
1716 let sealed = b.finish().unwrap();
1717 let node = block::node_id("a");
1718 dirs.push(
1719 block::publish(&root, "logs", node, seq as u64, 0, &sealed)
1720 .unwrap()
1721 .dir,
1722 );
1723 }
1724 let size = |dir: &std::path::Path| {
1725 std::fs::read_dir(dir)
1726 .unwrap()
1727 .filter_map(|e| e.ok())
1728 .filter(|e| e.path().extension().is_some_and(|x| x == "arrow"))
1729 .map(|e| e.metadata().unwrap().len())
1730 .sum::<u64>()
1731 };
1732 let before = size(&dirs[0]);
1733
1734 let partial = dirs[0].join("log_attrs.arrow");
1738 let staged = dirs[0].join("log_attrs.staged");
1739 let batch = block::open_table(&partial).unwrap().batches[0].clone();
1740 block::write_table_zstd(&staged, &batch).unwrap();
1745 drop(batch);
1746 std::fs::rename(&staged, &partial).unwrap();
1747 assert!(block::open_table(&dirs[0].join("logs.arrow")).is_ok());
1748
1749 let n = block::compact(
1750 &root,
1751 "logs",
1752 block::node_id("a"),
1753 now - block::COLD_AFTER_NS,
1754 )
1755 .unwrap();
1756 assert_eq!(n, 1, "only the aged block is cold");
1757 assert!(dirs[0].join("cold").exists());
1758 assert!(!dirs[1].join("cold").exists(), "the fresh block is hot");
1759 let after = size(&dirs[0]);
1760 assert!(after * 2 < before, "{before} -> {after} is not a tier");
1761
1762 let stamp = std::fs::metadata(dirs[0].join("logs.arrow"))
1764 .unwrap()
1765 .modified()
1766 .unwrap();
1767 assert_eq!(
1768 block::compact(
1769 &root,
1770 "logs",
1771 block::node_id("a"),
1772 now - block::COLD_AFTER_NS
1773 )
1774 .unwrap(),
1775 0
1776 );
1777 assert_eq!(
1778 std::fs::metadata(dirs[0].join("logs.arrow"))
1779 .unwrap()
1780 .modified()
1781 .unwrap(),
1782 stamp
1783 );
1784
1785 let cold = block::open_table(&dirs[0].join("logs.arrow")).unwrap();
1788 let (inside, total) = cold.zero_copy_ratio();
1789 assert_eq!(cold.batches[0].num_rows(), 500);
1790 assert!(
1793 inside < total,
1794 "{inside}/{total} — a decompressed buffer is a copy"
1795 );
1796 let hot = block::open_table(&dirs[1].join("logs.arrow")).unwrap();
1797 let (inside, total) = hot.zero_copy_ratio();
1798 assert_eq!(inside, total, "the hot tier stays zero-copy");
1799
1800 let r = query::search(
1803 &root,
1804 &Search {
1805 signal: Signal::Logs,
1806 from: 0,
1807 to: i64::MAX,
1808 terms: vec![Term {
1809 target: Target::Attr("service.name".into()),
1810 op: Op::Eq,
1811 value: QV::Str("checkout".into()),
1812 }],
1813 limit: 2_000,
1814 after: None,
1815 },
1816 )
1817 .unwrap();
1818 assert_eq!(r.stats.rows_matched, 1_000);
1819 assert_eq!(r.stats.blocks_scanned, 2);
1820
1821 let _ = std::fs::remove_dir_all(&root);
1822 }
1823
1824 #[test]
1829 fn the_filesystem_guard_passes_a_local_directory() {
1830 let dir = std::env::temp_dir().join(format!("mira-fs-{}", std::process::id()));
1831 std::fs::create_dir_all(&dir).unwrap();
1832 block::check_filesystem(&dir).unwrap();
1833 block::check_filesystem(&dir.join("nope")).unwrap();
1835 let _ = std::fs::remove_dir_all(&dir);
1836 }
1837
1838 #[test]
1842 fn the_write_guard_refuses_a_read_only_directory() {
1843 use std::os::unix::fs::PermissionsExt;
1844
1845 let dir = std::env::temp_dir().join(format!("mira-w-{}", std::process::id()));
1846 let _ = std::fs::remove_dir_all(&dir);
1847 std::fs::create_dir_all(&dir).unwrap();
1848 block::check_writable(&dir).unwrap();
1849 assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0);
1851
1852 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o555)).unwrap();
1853 let blocked = std::fs::write(dir.join("canary"), []).is_err();
1857 let verdict = block::check_writable(&dir);
1858 assert_eq!(
1859 verdict.is_err(),
1860 blocked,
1861 "the guard must agree with the filesystem: {verdict:?}"
1862 );
1863 assert!(
1864 !blocked || matches!(verdict, Err(Error::NotWritable { .. })),
1865 "{verdict:?}"
1866 );
1867 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
1868
1869 assert!(block::check_writable(&dir.join("nope")).is_err());
1872 let _ = std::fs::remove_dir_all(&dir);
1873 }
1874
1875 #[test]
1876 fn corrupt_body_is_caught_not_returned_as_data() {
1877 let dir = std::env::temp_dir().join(format!("mira-crc-{}", std::process::id()));
1878 std::fs::create_dir_all(&dir).unwrap();
1879 let path = dir.join("logs.arrow");
1880
1881 let mut b = logs::LogsBuilder::new();
1882 b.append_request(&request("checkout", 64, 1_000)).unwrap();
1883 let sealed = b.finish().unwrap();
1884 block::write_table(&path, sealed.table("logs").unwrap()).unwrap();
1885 assert!(block::open_table(&path).is_ok());
1886
1887 let mut bytes = std::fs::read(&path).unwrap();
1890 let mid = bytes.len() / 2;
1891 bytes[mid] ^= 0x01;
1892 std::fs::write(&path, &bytes).unwrap();
1893
1894 assert!(matches!(
1895 block::open_table(&path),
1896 Err(Error::BadChecksum { .. })
1897 ));
1898
1899 for junk in [&b"ARROW"[..], &[0u8; 64][..]] {
1903 std::fs::write(&path, junk).unwrap();
1904 assert!(
1905 matches!(block::open_table(&path), Err(Error::BadMagic { .. })),
1906 "{junk:?} decoded as a block"
1907 );
1908 }
1909
1910 let batch = sealed.table("logs").unwrap();
1914 let mut w = arrow_ipc::writer::FileWriter::try_new(
1915 std::fs::File::create(&path).unwrap(),
1916 &batch.schema(),
1917 )
1918 .unwrap();
1919 w.write(batch).unwrap();
1920 w.finish().unwrap();
1921 let e = block::open_table(&path)
1925 .err()
1926 .expect("a file with no checksum in it was read as a block");
1927 assert!(matches!(e, Error::MissingMetadata { .. }), "{e}");
1928
1929 let _ = std::fs::remove_dir_all(&dir);
1930 }
1931
1932 #[test]
1939 fn the_catalog_ignores_everything_it_did_not_write() {
1940 let root = std::env::temp_dir().join(format!("mira-junk-{}", std::process::id()));
1941 let _ = std::fs::remove_dir_all(&root);
1942 let node = block::node_id("a");
1943
1944 let seal = |base: u64| {
1945 let mut b = logs::LogsBuilder::new();
1946 b.append_request(&request("checkout", 4, base)).unwrap();
1947 b.finish().unwrap()
1948 };
1949 let published = block::publish(&root, "logs", node, 0, 0, &seal(1_000)).unwrap();
1950 let partition = published.dir.parent().unwrap().to_path_buf();
1951 let name = published.dir.file_name().unwrap().to_str().unwrap();
1952
1953 std::fs::write(root.join("logs").join(".DS_Store"), b"junk").unwrap();
1957 for junk in ["not-a-block", &format!("{name}-1")] {
1958 std::fs::create_dir_all(partition.join(junk)).unwrap();
1959 }
1960 assert_eq!(block::scan(&root, "logs").unwrap(), vec![published.clone()]);
1961
1962 let stale = root
1968 .join(".tmp")
1969 .join(format!("logs-{node:08x}-{:012}-x", 1));
1970 let other_node = root.join(".tmp").join("logs-deadbeef-000000000001-x");
1971 let other_signal = root.join(".tmp").join(format!("traces-{node:08x}-1-x"));
1972 for d in [&stale, &other_node, &other_signal] {
1973 std::fs::create_dir_all(d).unwrap();
1974 std::fs::write(d.join("logs.arrow"), b"leftovers").unwrap();
1975 }
1976 assert_eq!(block::sweep_staging(&root, "logs", node).unwrap(), 1);
1977 assert!(!stale.exists());
1978 assert!(other_node.is_dir() && other_signal.is_dir());
1979 assert_eq!(
1981 block::sweep_staging(&root.join("nope"), "logs", node).unwrap(),
1982 0
1983 );
1984
1985 let second = block::publish(&root, "logs", node, 1, 0, &seal(2_000)).unwrap();
1986 assert!(block::open_table(&second.dir.join("logs.arrow")).is_ok());
1987 assert_eq!(block::scan(&root, "logs").unwrap().len(), 2);
1988
1989 assert_eq!(block::expire(&root, "logs", i64::MAX).unwrap(), 2);
1991 assert!(block::scan(&root, "logs").unwrap().is_empty());
1992 assert!(partition.join("not-a-block").is_dir());
1993
1994 let _ = std::fs::remove_dir_all(&root);
1995 }
1996
1997 #[test]
2002 fn every_codec_round_trips_the_same_rows() {
2003 let dir = std::env::temp_dir().join(format!("mira-codec-{}", std::process::id()));
2004 let _ = std::fs::remove_dir_all(&dir);
2005 std::fs::create_dir_all(&dir).unwrap();
2006
2007 let mut b = logs::LogsBuilder::new();
2008 b.append_request(&request("checkout", 500, 1_000)).unwrap();
2009 let sealed = b.finish().unwrap();
2010 let batch = sealed.table("logs").unwrap();
2011
2012 type Writer = fn(&std::path::Path, &arrow_array::RecordBatch) -> Result<()>;
2013 let write: [(&str, Writer); 3] = [
2014 ("plain", block::write_table),
2015 ("zstd", block::write_table_zstd),
2016 ("lz4", block::write_table_lz4),
2017 ];
2018 let mut sizes = Vec::new();
2019 for (name, f) in write {
2020 let path = dir.join(format!("{name}.arrow"));
2021 f(&path, batch).unwrap();
2022 let read = block::open_table(&path).unwrap();
2023 assert_eq!(read.batches, vec![batch.clone()], "{name}");
2024 sizes.push((name, std::fs::metadata(&path).unwrap().len()));
2025 }
2026 let plain = sizes[0].1;
2027 for (name, size) in &sizes[1..] {
2028 assert!(*size < plain, "{name} is {size} against {plain} plain");
2029 }
2030
2031 let _ = std::fs::remove_dir_all(&dir);
2032 }
2033}