1use std::collections::HashMap;
41use std::sync::Arc;
42
43use arrow_array::builder::{
44 BooleanBuilder, FixedSizeBinaryBuilder, Float64Builder, Int32Builder, Int64Builder,
45 ListBuilder, StringBuilder, TimestampNanosecondBuilder, UInt8Builder, UInt16Builder,
46 UInt32Builder, UInt64Builder,
47};
48use arrow_array::{ArrayRef, RecordBatch};
49
50use mira_proto::collector::metrics::v1::ExportMetricsServiceRequest;
51use mira_proto::metrics::v1::exponential_histogram_data_point::Buckets;
52use mira_proto::metrics::v1::metric::Data;
53use mira_proto::metrics::v1::{
54 Exemplar, ExponentialHistogramDataPoint, HistogramDataPoint, Metric, NumberDataPoint,
55 SummaryDataPoint, exemplar, number_data_point,
56};
57
58use crate::attrs::{AttrsBuilder, DictColumn, ResourceScope, resource_kv, scope_kv};
59use crate::error::Result;
60use crate::logs::{append_fixed, nanos};
61use crate::schema::{
62 EXEMPLARS, EXP_HIST_DP, HIST_BOUNDS, HIST_DP, METRICS, MetricKind, NUMBER_DP, SUMMARY_DP,
63};
64use crate::signal::{Sealed, Sidecars, SignalBuilder};
65
66struct DpHead {
69 id: UInt32Builder,
70 metric_id: UInt32Builder,
71 start: TimestampNanosecondBuilder,
72 time: TimestampNanosecondBuilder,
73 flags: UInt32Builder,
74}
75
76impl DpHead {
77 fn new() -> Self {
78 Self {
79 id: UInt32Builder::new(),
80 metric_id: UInt32Builder::new(),
81 start: TimestampNanosecondBuilder::new(),
82 time: TimestampNanosecondBuilder::new(),
83 flags: UInt32Builder::new(),
84 }
85 }
86
87 fn append(&mut self, id: u32, metric_id: u32, start: u64, time: u64, flags: u32) {
88 self.id.append_value(id);
89 self.metric_id.append_value(metric_id);
90 let start = nanos(start);
93 if start != 0 {
94 self.start.append_value(start);
95 } else {
96 self.start.append_null();
97 }
98 self.time.append_value(nanos(time));
99 self.flags.append_value(flags);
100 }
101
102 fn finish(&self) -> Vec<ArrayRef> {
103 vec![
104 Arc::new(self.id.finish_cloned()),
105 Arc::new(self.metric_id.finish_cloned()),
106 Arc::new(self.start.finish_cloned()),
107 Arc::new(self.time.finish_cloned()),
108 Arc::new(self.flags.finish_cloned()),
109 ]
110 }
111}
112
113struct Stats {
115 count: UInt64Builder,
116 sum: Float64Builder,
117 min: Float64Builder,
118 max: Float64Builder,
119}
120
121impl Stats {
122 fn new() -> Self {
123 Self {
124 count: UInt64Builder::new(),
125 sum: Float64Builder::new(),
126 min: Float64Builder::new(),
127 max: Float64Builder::new(),
128 }
129 }
130
131 fn append(&mut self, count: u64, sum: Option<f64>, min: Option<f64>, max: Option<f64>) {
132 self.count.append_value(count);
133 self.sum.append_option(sum);
134 self.min.append_option(min);
135 self.max.append_option(max);
136 }
137
138 fn finish(&self) -> Vec<ArrayRef> {
139 vec![
140 Arc::new(self.count.finish_cloned()),
141 Arc::new(self.sum.finish_cloned()),
142 Arc::new(self.min.finish_cloned()),
143 Arc::new(self.max.finish_cloned()),
144 ]
145 }
146}
147
148pub struct MetricsBuilder {
149 m_id: UInt32Builder,
151 m_name: DictColumn,
152 m_description: StringBuilder,
153 m_unit: DictColumn,
154 m_kind: UInt8Builder,
155 m_temporality: UInt8Builder,
156 m_monotonic: BooleanBuilder,
157 m_resource_id: UInt16Builder,
158 m_scope_id: UInt16Builder,
159 metric_attrs: AttrsBuilder,
160 next_metric_id: u32,
161
162 num: DpHead,
163 num_int: Int64Builder,
164 num_double: Float64Builder,
165
166 hist: DpHead,
167 hist_stats: Stats,
168 hist_counts: ListBuilder<UInt64Builder>,
169 hist_bounds_id: UInt32Builder,
170
171 bounds_index: HashMap<Vec<u64>, u32>,
177 bounds_id: UInt32Builder,
178 bounds_values: ListBuilder<Float64Builder>,
179 next_bounds_id: u32,
180
181 exp: DpHead,
182 exp_stats: Stats,
183 exp_scale: Int32Builder,
184 exp_zero_count: UInt64Builder,
185 exp_zero_threshold: Float64Builder,
186 exp_pos_offset: Int32Builder,
187 exp_pos_counts: ListBuilder<UInt64Builder>,
188 exp_neg_offset: Int32Builder,
189 exp_neg_counts: ListBuilder<UInt64Builder>,
190
191 summ: DpHead,
192 summ_count: UInt64Builder,
193 summ_sum: Float64Builder,
194 summ_quantile: ListBuilder<Float64Builder>,
195 summ_value: ListBuilder<Float64Builder>,
196
197 dp_attrs: AttrsBuilder,
198 next_dp_id: u32,
200
201 ex_id: UInt32Builder,
202 ex_parent: UInt32Builder,
203 ex_time: TimestampNanosecondBuilder,
204 ex_int: Int64Builder,
205 ex_double: Float64Builder,
206 ex_trace_id: FixedSizeBinaryBuilder,
207 ex_span_id: FixedSizeBinaryBuilder,
208 exemplar_attrs: AttrsBuilder,
209 next_exemplar_id: u32,
210
211 rs: ResourceScope,
212 list_values: usize,
215 min_ts: i64,
216 max_ts: i64,
217}
218
219impl Default for MetricsBuilder {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225impl MetricsBuilder {
226 pub fn new() -> Self {
227 Self {
228 m_id: UInt32Builder::new(),
229 m_name: DictColumn::new("metrics.name"),
230 m_description: StringBuilder::new(),
231 m_unit: DictColumn::new("metrics.unit"),
232 m_kind: UInt8Builder::new(),
233 m_temporality: UInt8Builder::new(),
234 m_monotonic: BooleanBuilder::new(),
235 m_resource_id: UInt16Builder::new(),
236 m_scope_id: UInt16Builder::new(),
237 metric_attrs: AttrsBuilder::new("metric_attrs.key"),
238 next_metric_id: 0,
239
240 num: DpHead::new(),
241 num_int: Int64Builder::new(),
242 num_double: Float64Builder::new(),
243
244 hist: DpHead::new(),
245 hist_stats: Stats::new(),
246 hist_counts: ListBuilder::new(UInt64Builder::new()),
247 hist_bounds_id: UInt32Builder::new(),
248
249 bounds_index: HashMap::new(),
250 bounds_id: UInt32Builder::new(),
251 bounds_values: ListBuilder::new(Float64Builder::new()),
252 next_bounds_id: 0,
253
254 exp: DpHead::new(),
255 exp_stats: Stats::new(),
256 exp_scale: Int32Builder::new(),
257 exp_zero_count: UInt64Builder::new(),
258 exp_zero_threshold: Float64Builder::new(),
259 exp_pos_offset: Int32Builder::new(),
260 exp_pos_counts: ListBuilder::new(UInt64Builder::new()),
261 exp_neg_offset: Int32Builder::new(),
262 exp_neg_counts: ListBuilder::new(UInt64Builder::new()),
263
264 summ: DpHead::new(),
265 summ_count: UInt64Builder::new(),
266 summ_sum: Float64Builder::new(),
267 summ_quantile: ListBuilder::new(Float64Builder::new()),
268 summ_value: ListBuilder::new(Float64Builder::new()),
269
270 dp_attrs: AttrsBuilder::new("dp_attrs.key"),
271 next_dp_id: 0,
272
273 ex_id: UInt32Builder::new(),
274 ex_parent: UInt32Builder::new(),
275 ex_time: TimestampNanosecondBuilder::new(),
276 ex_int: Int64Builder::new(),
277 ex_double: Float64Builder::new(),
278 ex_trace_id: FixedSizeBinaryBuilder::new(16),
279 ex_span_id: FixedSizeBinaryBuilder::new(8),
280 exemplar_attrs: AttrsBuilder::new("exemplar_attrs.key"),
281 next_exemplar_id: 0,
282
283 rs: ResourceScope::new(),
284 list_values: 0,
285 min_ts: i64::MAX,
286 max_ts: i64::MIN,
287 }
288 }
289
290 pub fn num_rows(&self) -> usize {
293 self.next_dp_id as usize
294 }
295
296 pub fn is_empty(&self) -> bool {
297 self.next_metric_id == 0
298 }
299
300 pub fn approx_bytes(&self) -> usize {
301 self.next_dp_id as usize * 48
302 + self.next_metric_id as usize * 32
303 + self.next_exemplar_id as usize * 56
304 + self.list_values * 8
305 + (self.dp_attrs.len()
306 + self.metric_attrs.len()
307 + self.exemplar_attrs.len()
308 + self.rs.len())
309 * 48
310 + self.m_description.values_slice().len()
311 + self.dp_attrs.heap_bytes()
312 + self.metric_attrs.heap_bytes()
313 + self.exemplar_attrs.heap_bytes()
314 + self.rs.heap_bytes()
315 }
316
317 pub fn has_headroom_for(&self, req: &ExportMetricsServiceRequest) -> bool {
318 let (mut resources, mut scopes) = (0usize, 0usize);
319 let (mut res_kv, mut sc_kv) = (0usize, 0usize);
320 let (mut names, mut meta_kv, mut dp_kv, mut ex_kv) = (0usize, 0usize, 0usize, 0usize);
321 for rm in &req.resource_metrics {
322 resources += 1;
323 res_kv += resource_kv(rm.resource.as_ref());
324 for sm in &rm.scope_metrics {
325 scopes += 1;
326 sc_kv += scope_kv(sm.scope.as_ref());
327 names += sm.metrics.len();
328 for m in &sm.metrics {
329 meta_kv += m.metadata.len();
330 for_each_point(m, |attrs, exemplars| {
331 dp_kv += attrs;
332 ex_kv += exemplars;
333 });
334 }
335 }
336 }
337 self.rs.has_headroom(resources, scopes, res_kv, sc_kv)
338 && self.metric_attrs.has_headroom(meta_kv)
339 && self.dp_attrs.has_headroom(dp_kv)
340 && self.exemplar_attrs.has_headroom(ex_kv)
341 && self.m_name.has_headroom(names)
344 && self.m_unit.has_headroom(names)
345 }
346
347 pub fn append_request(&mut self, req: &ExportMetricsServiceRequest) -> Result<usize> {
348 let mut added = 0;
349 for rm in &req.resource_metrics {
350 let rid = self.rs.resource(rm.resource.as_ref())?;
351 for sm in &rm.scope_metrics {
352 let sid = self.rs.scope(sm.scope.as_ref())?;
353 for m in &sm.metrics {
354 added += self.append_metric(m, rid, sid)?;
355 }
356 }
357 }
358 Ok(added)
359 }
360
361 fn append_metric(&mut self, m: &Metric, rid: u16, sid: u16) -> Result<usize> {
362 self.m_name.append(&m.name)?;
365 self.m_unit.append(&m.unit)?;
366
367 let mid = self.next_metric_id;
368 self.next_metric_id += 1;
369
370 let (kind, temporality, monotonic) = match &m.data {
374 None => (MetricKind::Unset, 0, false),
375 Some(Data::Gauge(_)) => (MetricKind::Gauge, 0, false),
376 Some(Data::Sum(s)) => (
377 MetricKind::Sum,
378 clamp_u8(s.aggregation_temporality, 2),
379 s.is_monotonic,
380 ),
381 Some(Data::Histogram(h)) => (
382 MetricKind::Histogram,
383 clamp_u8(h.aggregation_temporality, 2),
384 false,
385 ),
386 Some(Data::ExponentialHistogram(h)) => (
387 MetricKind::ExponentialHistogram,
388 clamp_u8(h.aggregation_temporality, 2),
389 false,
390 ),
391 Some(Data::Summary(_)) => (MetricKind::Summary, 0, false),
392 };
393
394 self.m_id.append_value(mid);
395 if m.description.is_empty() {
396 self.m_description.append_null();
397 } else {
398 self.m_description.append_value(&m.description);
399 }
400 self.m_kind.append_value(kind as u8);
401 self.m_temporality.append_value(temporality);
402 self.m_monotonic.append_value(monotonic);
403 self.m_resource_id.append_value(rid);
404 self.m_scope_id.append_value(sid);
405 self.metric_attrs.append_all(mid, &m.metadata)?;
406
407 let mut added = 0;
408 match &m.data {
409 None => {}
410 Some(Data::Gauge(g)) => {
411 for p in &g.data_points {
412 self.append_number(p, mid)?;
413 added += 1;
414 }
415 }
416 Some(Data::Sum(s)) => {
417 for p in &s.data_points {
418 self.append_number(p, mid)?;
419 added += 1;
420 }
421 }
422 Some(Data::Histogram(h)) => {
423 for p in &h.data_points {
424 self.append_hist(p, mid)?;
425 added += 1;
426 }
427 }
428 Some(Data::ExponentialHistogram(h)) => {
429 for p in &h.data_points {
430 self.append_exp_hist(p, mid)?;
431 added += 1;
432 }
433 }
434 Some(Data::Summary(s)) => {
435 for p in &s.data_points {
436 self.append_summary(p, mid)?;
437 added += 1;
438 }
439 }
440 }
441 Ok(added)
442 }
443
444 fn next_point(&mut self, time: u64) -> u32 {
449 let id = self.next_dp_id;
450 self.next_dp_id += 1;
451 let time = nanos(time);
455 if time != 0 {
456 self.min_ts = self.min_ts.min(time);
457 self.max_ts = self.max_ts.max(time);
458 }
459 id
460 }
461
462 fn append_number(&mut self, p: &NumberDataPoint, mid: u32) -> Result<()> {
463 let id = self.next_point(p.time_unix_nano);
464 self.num
465 .append(id, mid, p.start_time_unix_nano, p.time_unix_nano, p.flags);
466 match p.value {
467 Some(number_data_point::Value::AsInt(i)) => {
471 self.num_int.append_value(i);
472 self.num_double.append_null();
473 }
474 Some(number_data_point::Value::AsDouble(d)) => {
475 self.num_int.append_null();
476 self.num_double.append_value(d);
477 }
478 None => {
479 self.num_int.append_null();
480 self.num_double.append_null();
481 }
482 }
483 self.dp_attrs.append_all(id, &p.attributes)?;
484 self.append_exemplars(id, &p.exemplars)
485 }
486
487 fn append_hist(&mut self, p: &HistogramDataPoint, mid: u32) -> Result<()> {
488 let id = self.next_point(p.time_unix_nano);
489 self.hist
490 .append(id, mid, p.start_time_unix_nano, p.time_unix_nano, p.flags);
491 self.hist_stats.append(p.count, p.sum, p.min, p.max);
492 self.hist_counts
493 .append_value(p.bucket_counts.iter().copied().map(Some));
494 self.list_values += p.bucket_counts.len();
495
496 if p.explicit_bounds.is_empty() {
497 self.hist_bounds_id.append_null();
498 } else {
499 let key: Vec<u64> = p.explicit_bounds.iter().map(|b| b.to_bits()).collect();
500 let bid = match self.bounds_index.get(&key) {
501 Some(&b) => b,
502 None => {
503 let b = self.next_bounds_id;
504 self.next_bounds_id += 1;
505 self.bounds_id.append_value(b);
506 self.bounds_values
507 .append_value(p.explicit_bounds.iter().copied().map(Some));
508 self.list_values += p.explicit_bounds.len();
509 self.bounds_index.insert(key, b);
510 b
511 }
512 };
513 self.hist_bounds_id.append_value(bid);
514 }
515
516 self.dp_attrs.append_all(id, &p.attributes)?;
517 self.append_exemplars(id, &p.exemplars)
518 }
519
520 fn append_exp_hist(&mut self, p: &ExponentialHistogramDataPoint, mid: u32) -> Result<()> {
521 let id = self.next_point(p.time_unix_nano);
522 self.exp
523 .append(id, mid, p.start_time_unix_nano, p.time_unix_nano, p.flags);
524 self.exp_stats.append(p.count, p.sum, p.min, p.max);
525 self.exp_scale.append_value(p.scale);
526 self.exp_zero_count.append_value(p.zero_count);
527 self.exp_zero_threshold.append_value(p.zero_threshold);
528 let buckets = |b: &Option<Buckets>,
529 off: &mut Int32Builder,
530 counts: &mut ListBuilder<UInt64Builder>,
531 total: &mut usize| {
532 match b {
533 Some(b) => {
534 off.append_value(b.offset);
535 counts.append_value(b.bucket_counts.iter().copied().map(Some));
536 *total += b.bucket_counts.len();
537 }
538 None => {
539 off.append_value(0);
540 counts.append_null();
541 }
542 }
543 };
544 buckets(
545 &p.positive,
546 &mut self.exp_pos_offset,
547 &mut self.exp_pos_counts,
548 &mut self.list_values,
549 );
550 buckets(
551 &p.negative,
552 &mut self.exp_neg_offset,
553 &mut self.exp_neg_counts,
554 &mut self.list_values,
555 );
556 self.dp_attrs.append_all(id, &p.attributes)?;
557 self.append_exemplars(id, &p.exemplars)
558 }
559
560 fn append_summary(&mut self, p: &SummaryDataPoint, mid: u32) -> Result<()> {
561 let id = self.next_point(p.time_unix_nano);
562 self.summ
563 .append(id, mid, p.start_time_unix_nano, p.time_unix_nano, p.flags);
564 self.summ_count.append_value(p.count);
565 self.summ_sum.append_value(p.sum);
566 self.summ_quantile
567 .append_value(p.quantile_values.iter().map(|q| Some(q.quantile)));
568 self.summ_value
569 .append_value(p.quantile_values.iter().map(|q| Some(q.value)));
570 self.list_values += p.quantile_values.len() * 2;
571 self.dp_attrs.append_all(id, &p.attributes)
573 }
574
575 fn append_exemplars(&mut self, dp_id: u32, exemplars: &[Exemplar]) -> Result<()> {
576 for e in exemplars {
577 let eid = self.next_exemplar_id;
578 self.next_exemplar_id += 1;
579 self.ex_id.append_value(eid);
580 self.ex_parent.append_value(dp_id);
581 self.ex_time.append_value(nanos(e.time_unix_nano));
582 match e.value {
583 Some(exemplar::Value::AsInt(i)) => {
584 self.ex_int.append_value(i);
585 self.ex_double.append_null();
586 }
587 Some(exemplar::Value::AsDouble(d)) => {
588 self.ex_int.append_null();
589 self.ex_double.append_value(d);
590 }
591 None => {
592 self.ex_int.append_null();
593 self.ex_double.append_null();
594 }
595 }
596 append_fixed(&mut self.ex_trace_id, &e.trace_id, 16)?;
597 append_fixed(&mut self.ex_span_id, &e.span_id, 8)?;
598 self.exemplar_attrs
599 .append_all(eid, &e.filtered_attributes)?;
600 }
601 Ok(())
602 }
603
604 pub fn finish(&mut self) -> Result<Sealed> {
607 let out = self.seal(Sidecars::Build);
608 *self = Self::new();
609 out
610 }
611
612 fn seal(&self, sidecars: Sidecars) -> Result<Sealed> {
613 let cols: Vec<ArrayRef> = vec![
614 Arc::new(self.m_id.finish_cloned()),
615 self.m_name.finish(),
616 Arc::new(self.m_description.finish_cloned()),
617 self.m_unit.finish(),
618 Arc::new(self.m_kind.finish_cloned()),
619 Arc::new(self.m_temporality.finish_cloned()),
620 Arc::new(self.m_monotonic.finish_cloned()),
621 Arc::new(self.m_resource_id.finish_cloned()),
622 Arc::new(self.m_scope_id.finish_cloned()),
623 ];
624 let metrics = RecordBatch::try_new(METRICS.clone(), cols)?;
625
626 let mut number = self.num.finish();
627 number.push(Arc::new(self.num_int.finish_cloned()));
628 number.push(Arc::new(self.num_double.finish_cloned()));
629
630 let mut hist = self.hist.finish();
631 hist.extend(self.hist_stats.finish());
632 hist.push(Arc::new(self.hist_counts.finish_cloned()));
633 hist.push(Arc::new(self.hist_bounds_id.finish_cloned()));
634
635 let cols: Vec<ArrayRef> = vec![
636 Arc::new(self.bounds_id.finish_cloned()),
637 Arc::new(self.bounds_values.finish_cloned()),
638 ];
639 let bounds = RecordBatch::try_new(HIST_BOUNDS.clone(), cols)?;
640
641 let mut exp = self.exp.finish();
642 exp.extend(self.exp_stats.finish());
643 exp.push(Arc::new(self.exp_scale.finish_cloned()));
644 exp.push(Arc::new(self.exp_zero_count.finish_cloned()));
645 exp.push(Arc::new(self.exp_zero_threshold.finish_cloned()));
646 exp.push(Arc::new(self.exp_pos_offset.finish_cloned()));
647 exp.push(Arc::new(self.exp_pos_counts.finish_cloned()));
648 exp.push(Arc::new(self.exp_neg_offset.finish_cloned()));
649 exp.push(Arc::new(self.exp_neg_counts.finish_cloned()));
650
651 let mut summ = self.summ.finish();
652 summ.push(Arc::new(self.summ_count.finish_cloned()));
653 summ.push(Arc::new(self.summ_sum.finish_cloned()));
654 summ.push(Arc::new(self.summ_quantile.finish_cloned()));
655 summ.push(Arc::new(self.summ_value.finish_cloned()));
656
657 let cols: Vec<ArrayRef> = vec![
658 Arc::new(self.ex_id.finish_cloned()),
659 Arc::new(self.ex_parent.finish_cloned()),
660 Arc::new(self.ex_time.finish_cloned()),
661 Arc::new(self.ex_int.finish_cloned()),
662 Arc::new(self.ex_double.finish_cloned()),
663 Arc::new(self.ex_trace_id.finish_cloned()),
664 Arc::new(self.ex_span_id.finish_cloned()),
665 ];
666 let exemplars = RecordBatch::try_new(EXEMPLARS.clone(), cols)?;
667
668 let mut tables = vec![
670 ("metrics", metrics),
671 ("metric_attrs", self.metric_attrs.finish()?),
672 (
673 "number_dp",
674 RecordBatch::try_new(NUMBER_DP.clone(), number)?,
675 ),
676 ("hist_dp", RecordBatch::try_new(HIST_DP.clone(), hist)?),
677 ("hist_bounds", bounds),
678 (
679 "exp_hist_dp",
680 RecordBatch::try_new(EXP_HIST_DP.clone(), exp)?,
681 ),
682 (
683 "summary_dp",
684 RecordBatch::try_new(SUMMARY_DP.clone(), summ)?,
685 ),
686 ("dp_attrs", self.dp_attrs.finish()?),
687 ("exemplars", exemplars),
688 ("exemplar_attrs", self.exemplar_attrs.finish()?),
689 ];
690 tables.extend(self.rs.finish()?);
691 Ok(Sealed::with(
692 sidecars,
693 self.next_dp_id as usize,
694 tables,
695 self.min_ts,
696 self.max_ts,
697 ))
698 }
699}
700
701impl SignalBuilder for MetricsBuilder {
702 type Request = ExportMetricsServiceRequest;
703 const SIGNAL: &'static str = "metrics";
704
705 fn has_headroom_for(&self, req: &Self::Request) -> bool {
706 MetricsBuilder::has_headroom_for(self, req)
707 }
708 fn append_request(&mut self, req: &Self::Request) -> Result<usize> {
709 MetricsBuilder::append_request(self, req)
710 }
711 fn approx_bytes(&self) -> usize {
712 MetricsBuilder::approx_bytes(self)
713 }
714 fn is_empty(&self) -> bool {
715 MetricsBuilder::is_empty(self)
716 }
717 fn finish(&mut self) -> Result<Sealed> {
718 MetricsBuilder::finish(self)
719 }
720 fn snapshot(&self) -> Result<Sealed> {
721 self.seal(Sidecars::Skip)
722 }
723}
724
725fn for_each_point(m: &Metric, mut f: impl FnMut(usize, usize)) {
729 let ex = |e: &[Exemplar]| e.iter().map(|x| x.filtered_attributes.len()).sum::<usize>();
730 match &m.data {
731 None => {}
732 Some(Data::Gauge(g)) => {
733 for p in &g.data_points {
734 f(p.attributes.len(), ex(&p.exemplars));
735 }
736 }
737 Some(Data::Sum(s)) => {
738 for p in &s.data_points {
739 f(p.attributes.len(), ex(&p.exemplars));
740 }
741 }
742 Some(Data::Histogram(h)) => {
743 for p in &h.data_points {
744 f(p.attributes.len(), ex(&p.exemplars));
745 }
746 }
747 Some(Data::ExponentialHistogram(h)) => {
748 for p in &h.data_points {
749 f(p.attributes.len(), ex(&p.exemplars));
750 }
751 }
752 Some(Data::Summary(s)) => {
753 for p in &s.data_points {
754 f(p.attributes.len(), 0);
755 }
756 }
757 }
758}
759
760fn clamp_u8(v: i32, max: u8) -> u8 {
763 u8::try_from(v).unwrap_or(0).min(max)
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769 use arrow_array::cast::AsArray;
770 use arrow_array::types::{Float64Type, Int64Type, UInt8Type, UInt32Type, UInt64Type};
771 use arrow_array::{Array, RecordBatch, TimestampNanosecondArray};
772 use mira_proto::metrics::v1::summary_data_point::ValueAtQuantile;
773 use mira_proto::metrics::v1::{
774 ExponentialHistogram, Gauge, Histogram, ResourceMetrics, ScopeMetrics, Sum, Summary,
775 };
776
777 fn request(metrics: Vec<Metric>) -> ExportMetricsServiceRequest {
780 ExportMetricsServiceRequest {
781 resource_metrics: vec![ResourceMetrics {
782 scope_metrics: vec![ScopeMetrics {
783 metrics,
784 ..Default::default()
785 }],
786 ..Default::default()
787 }],
788 }
789 }
790
791 #[test]
797 fn a_metric_with_no_data_is_a_descriptor_and_no_points() {
798 let req = ExportMetricsServiceRequest {
799 resource_metrics: vec![ResourceMetrics {
800 scope_metrics: vec![ScopeMetrics {
801 metrics: vec![
802 Metric {
803 name: "queue.depth".into(),
804 description: "items awaiting a worker".into(),
805 data: None,
806 ..Default::default()
807 },
808 Metric {
809 name: "http.server.duration".into(),
810 data: Some(Data::Gauge(Gauge {
811 data_points: vec![NumberDataPoint {
812 time_unix_nano: 1_000,
813 exemplars: vec![Exemplar {
814 time_unix_nano: 1_000,
815 value: Some(exemplar::Value::AsInt(7)),
816 ..Default::default()
817 }],
818 ..Default::default()
819 }],
820 })),
821 ..Default::default()
822 },
823 ],
824 ..Default::default()
825 }],
826 ..Default::default()
827 }],
828 };
829
830 let mut b = MetricsBuilder::new();
831 assert!(SignalBuilder::has_headroom_for(&b, &req));
834 assert_eq!(b.append_request(&req).unwrap(), 1, "one point, two metrics");
835 assert_eq!(b.num_rows(), 1, "points, not descriptors");
836
837 let sealed = b.finish().unwrap();
838 let metrics = sealed.table("metrics").unwrap();
839 assert_eq!(metrics.num_rows(), 2);
840 let kind = metrics.column_by_name("kind").unwrap();
841 assert_eq!(
842 kind.as_primitive::<UInt8Type>().values(),
843 &[MetricKind::Unset as u8, MetricKind::Gauge as u8]
844 );
845 let desc = metrics.column_by_name("description").unwrap();
846 assert_eq!(desc.as_string::<i32>().value(0), "items awaiting a worker");
847 assert!(desc.is_null(1), "an empty description is absent, not \"\"");
848
849 let ex = sealed.table("exemplars").unwrap();
852 let int = ex.column_by_name("int").unwrap();
853 assert_eq!(int.as_primitive::<Int64Type>().value(0), 7);
854 assert!(ex.column_by_name("double").unwrap().is_null(0));
855 }
856
857 #[test]
861 fn point_times_past_i64_do_not_wrap_the_block_range() {
862 let point = |start: u64, time: u64| NumberDataPoint {
863 start_time_unix_nano: start,
864 time_unix_nano: time,
865 exemplars: vec![Exemplar {
866 time_unix_nano: u64::MAX,
867 ..Default::default()
868 }],
869 ..Default::default()
870 };
871
872 let mut b = MetricsBuilder::new();
873 b.append_request(&ExportMetricsServiceRequest {
874 resource_metrics: vec![ResourceMetrics {
875 scope_metrics: vec![ScopeMetrics {
876 metrics: vec![Metric {
877 name: "process.memory".into(),
878 data: Some(Data::Gauge(Gauge {
879 data_points: vec![point(u64::MAX, u64::MAX), point(u64::MAX, 2_000)],
880 })),
881 ..Default::default()
882 }],
883 ..Default::default()
884 }],
885 ..Default::default()
886 }],
887 })
888 .unwrap();
889
890 let sealed = b.finish().unwrap();
891 assert_eq!(
892 sealed.num_rows, 2,
893 "malformed points are stored, not dropped"
894 );
895 assert_eq!((sealed.min_ts, sealed.max_ts), (2_000, 2_000));
896
897 let ts = |t: &str, c: &str| {
898 sealed
899 .table(t)
900 .unwrap()
901 .column_by_name(c)
902 .unwrap()
903 .as_any()
904 .downcast_ref::<TimestampNanosecondArray>()
905 .unwrap()
906 .clone()
907 };
908 assert!(ts("number_dp", "start_time_unix_nano").is_null(0));
910 assert_eq!(ts("number_dp", "time_unix_nano").values(), &[0, 2_000]);
911 assert_eq!(ts("exemplars", "time_unix_nano").values(), &[0, 0]);
912 }
913
914 #[test]
927 fn every_otlp_metric_shape_lands_in_the_table_written_for_it() {
928 let num = |t: u64, v: Option<number_data_point::Value>| NumberDataPoint {
929 time_unix_nano: t,
930 value: v,
931 ..Default::default()
932 };
933 let bounds = vec![1.0, 2.0];
934 let hist = |sum: Option<f64>, explicit_bounds: Vec<f64>| HistogramDataPoint {
935 time_unix_nano: 3_000,
936 count: 6,
937 sum,
938 bucket_counts: vec![1, 2, 3],
939 explicit_bounds,
940 ..Default::default()
941 };
942 let req = request(vec![
943 Metric {
947 name: "declared.only".into(),
948 description: "registered by an exporter that never fired".into(),
949 data: None,
950 ..Default::default()
951 },
952 Metric {
953 name: "gauge".into(),
954 data: Some(Data::Gauge(Gauge {
955 data_points: vec![
956 num(1_000, Some(number_data_point::Value::AsInt(7))),
957 num(1_001, Some(number_data_point::Value::AsDouble(0.5))),
958 num(1_002, None),
961 ],
962 })),
963 ..Default::default()
964 },
965 Metric {
966 name: "counter".into(),
967 unit: "1".into(),
968 data: Some(Data::Sum(Sum {
969 is_monotonic: true,
970 aggregation_temporality: -3,
974 data_points: vec![NumberDataPoint {
975 time_unix_nano: 2_000,
976 value: Some(number_data_point::Value::AsInt(11)),
977 exemplars: vec![
978 Exemplar {
979 time_unix_nano: 2_000,
980 value: Some(exemplar::Value::AsInt(11)),
981 trace_id: vec![1u8; 16].into(),
982 span_id: vec![2u8; 8].into(),
983 ..Default::default()
984 },
985 Exemplar {
986 time_unix_nano: 2_001,
987 value: Some(exemplar::Value::AsDouble(1.5)),
988 ..Default::default()
989 },
990 Exemplar {
991 time_unix_nano: 2_002,
992 value: None,
993 ..Default::default()
994 },
995 ],
996 ..Default::default()
997 }],
998 })),
999 ..Default::default()
1000 },
1001 Metric {
1002 name: "hist".into(),
1003 data: Some(Data::Histogram(Histogram {
1004 aggregation_temporality: 99,
1005 data_points: vec![
1006 hist(Some(4.5), bounds.clone()),
1007 hist(None, bounds),
1011 hist(Some(1.0), Vec::new()),
1015 ],
1016 })),
1017 ..Default::default()
1018 },
1019 Metric {
1020 name: "exp".into(),
1021 data: Some(Data::ExponentialHistogram(ExponentialHistogram {
1022 aggregation_temporality: 1,
1023 data_points: vec![ExponentialHistogramDataPoint {
1024 time_unix_nano: 4_000,
1025 count: 3,
1026 scale: -2,
1027 zero_count: 1,
1028 zero_threshold: 0.25,
1029 positive: Some(Buckets {
1030 offset: 5,
1031 bucket_counts: vec![1, 1],
1032 }),
1033 negative: None,
1036 ..Default::default()
1037 }],
1038 })),
1039 ..Default::default()
1040 },
1041 Metric {
1042 name: "summ".into(),
1043 data: Some(Data::Summary(Summary {
1044 data_points: vec![SummaryDataPoint {
1045 time_unix_nano: 5_000,
1046 count: 2,
1047 sum: 3.0,
1048 quantile_values: vec![
1049 ValueAtQuantile {
1050 quantile: 0.5,
1051 value: 1.0,
1052 },
1053 ValueAtQuantile {
1054 quantile: 0.99,
1055 value: 2.0,
1056 },
1057 ],
1058 ..Default::default()
1059 }],
1060 })),
1061 ..Default::default()
1062 },
1063 ]);
1064
1065 let mut b = MetricsBuilder::new();
1066 assert!(b.is_empty(), "a fresh builder holds no descriptors");
1067 assert!(SignalBuilder::has_headroom_for(&b, &req));
1072 assert_eq!(SignalBuilder::append_request(&mut b, &req).unwrap(), 9);
1073 assert_eq!(b.num_rows(), 9);
1076 assert!(!b.is_empty());
1077 assert!(b.approx_bytes() > 0);
1078
1079 let sealed = SignalBuilder::finish(&mut b).unwrap();
1080 assert_eq!(sealed.num_rows, 9);
1081 let table = |n: &str| sealed.table(n).expect(n).clone();
1082 let rows = |n: &str| table(n).num_rows();
1083 assert_eq!(
1084 [
1085 rows("number_dp"),
1086 rows("hist_dp"),
1087 rows("exp_hist_dp"),
1088 rows("summary_dp")
1089 ],
1090 [4, 3, 1, 1],
1091 "each point type in the table written for its shape"
1092 );
1093
1094 let mut ids: Vec<u32> = Vec::new();
1098 for t in ["number_dp", "hist_dp", "exp_hist_dp", "summary_dp"] {
1099 let b = table(t);
1100 let col = b.column_by_name("id").unwrap();
1101 ids.extend(col.as_primitive::<UInt32Type>().values().iter().copied());
1102 }
1103 ids.sort_unstable();
1104 assert_eq!(ids, (0..9).collect::<Vec<u32>>());
1105
1106 let m = table("metrics");
1107 let u8s = |b: &RecordBatch, c: &str| {
1108 b.column_by_name(c)
1109 .unwrap()
1110 .as_primitive::<UInt8Type>()
1111 .values()
1112 .to_vec()
1113 };
1114 assert_eq!(
1115 u8s(&m, "kind"),
1116 [
1117 MetricKind::Unset as u8,
1118 MetricKind::Gauge as u8,
1119 MetricKind::Sum as u8,
1120 MetricKind::Histogram as u8,
1121 MetricKind::ExponentialHistogram as u8,
1122 MetricKind::Summary as u8,
1123 ]
1124 );
1125 assert_eq!(u8s(&m, "temporality"), [0, 0, 0, 2, 1, 0]);
1129 let mono = m.column_by_name("is_monotonic").unwrap().as_boolean();
1130 assert_eq!(
1131 (0..6).map(|i| mono.value(i)).collect::<Vec<bool>>(),
1132 [false, false, true, false, false, false],
1133 "only a Sum can be monotonic"
1134 );
1135 let desc = m.column_by_name("description").unwrap().as_string::<i32>();
1136 assert_eq!(desc.value(0), "registered by an exporter that never fired");
1137 assert!(
1138 (1..6).all(|i| desc.is_null(i)),
1139 "an empty description is absent, not an empty string"
1140 );
1141
1142 let n = table("number_dp");
1147 let ints = n.column_by_name("int").unwrap().as_primitive::<Int64Type>();
1148 let dbls = n
1149 .column_by_name("double")
1150 .unwrap()
1151 .as_primitive::<Float64Type>();
1152 assert_eq!(
1153 (0..4)
1154 .map(|i| (ints.is_null(i), dbls.is_null(i)))
1155 .collect::<Vec<_>>(),
1156 [(false, true), (true, false), (true, true), (false, true)]
1157 );
1158
1159 let h = table("hist_dp");
1162 assert_eq!(table("hist_bounds").num_rows(), 1);
1163 let bid = h
1164 .column_by_name("bounds_id")
1165 .unwrap()
1166 .as_primitive::<UInt32Type>();
1167 assert_eq!((bid.value(0), bid.value(1)), (0, 0));
1168 assert!(bid.is_null(2));
1169 let hsum = h
1170 .column_by_name("sum")
1171 .unwrap()
1172 .as_primitive::<Float64Type>();
1173 assert!(hsum.is_null(1), "a histogram may report count and no sum");
1174
1175 let e = table("exp_hist_dp");
1179 assert!(e.column_by_name("positive_counts").unwrap().is_valid(0));
1180 assert!(e.column_by_name("negative_counts").unwrap().is_null(0));
1181 assert_eq!(
1182 e.column_by_name("zero_count")
1183 .unwrap()
1184 .as_primitive::<UInt64Type>()
1185 .value(0),
1186 1
1187 );
1188
1189 let ex = table("exemplars");
1191 assert_eq!(ex.num_rows(), 3);
1192 let exi = ex
1193 .column_by_name("int")
1194 .unwrap()
1195 .as_primitive::<Int64Type>();
1196 let exd = ex
1197 .column_by_name("double")
1198 .unwrap()
1199 .as_primitive::<Float64Type>();
1200 assert_eq!(
1201 (0..3)
1202 .map(|i| (exi.is_null(i), exd.is_null(i)))
1203 .collect::<Vec<_>>(),
1204 [(false, true), (true, false), (true, true)]
1205 );
1206 assert_eq!(exi.value(0), 11);
1207 let tid = ex.column_by_name("trace_id").unwrap();
1210 assert!(tid.is_valid(0) && tid.is_null(1));
1211
1212 let s = table("summary_dp");
1214 assert_eq!(
1215 s.column_by_name("quantile")
1216 .unwrap()
1217 .as_list::<i32>()
1218 .value(0)
1219 .len(),
1220 2
1221 );
1222 }
1223
1224 #[test]
1235 fn a_torn_append_fails_the_seal_instead_of_publishing_a_ragged_block() {
1236 let mut b = MetricsBuilder::new();
1237 for i in 0..crate::schema::DICT_CAP {
1242 b.m_unit.append(&format!("u{i}")).unwrap();
1243 }
1244 assert!(!b.m_unit.has_headroom(1), "the unit dictionary is full");
1245
1246 let one = |unit: &str| Metric {
1247 name: "requests".into(),
1248 unit: unit.into(),
1249 data: Some(Data::Gauge(Gauge {
1250 data_points: vec![NumberDataPoint {
1251 time_unix_nano: 1_000,
1252 value: Some(number_data_point::Value::AsInt(1)),
1253 ..Default::default()
1254 }],
1255 })),
1256 ..Default::default()
1257 };
1258 let err = b.append_metric(&one("brand-new"), 0, 0).unwrap_err();
1259 assert!(
1260 matches!(err, crate::error::Error::DictionaryFull("metrics.unit")),
1261 "{err}"
1262 );
1263 let Err(err) = b.finish() else {
1265 panic!("a ragged descriptor table sealed");
1266 };
1267 assert!(matches!(err, crate::error::Error::Arrow(_)), "{err}");
1268
1269 assert!(b.is_empty());
1271 b.append_request(&request(vec![one("ms")])).unwrap();
1272 let sealed = b.finish().unwrap();
1273 assert_eq!(sealed.num_rows, 1);
1274 assert_eq!(sealed.table("metrics").unwrap().num_rows(), 1);
1275 }
1276}