1use ahash::HashSet;
16use api::greptime_proto::io::prometheus::write::v2::histogram::{
17 Count as PromCount, ResetHint, ZeroCount as PromZeroCount,
18};
19use api::greptime_proto::io::prometheus::write::v2::{BucketSpan, Histogram as PromHistogram};
20use api::v1::value::ValueData;
21use api::v1::{ColumnSchema, RowInsertRequests, SemanticType, Value};
22use common_grpc::precision::Precision;
23use common_query::native_histogram::{
24 encode_native_histogram, native_histogram_column_schema, native_histogram_value_type,
25};
26use common_query::prelude::{
27 GREPTIME_TEMPORALITY_DELTA, OTLP_AGGREGATION_TEMPORALITY_LABEL, greptime_timestamp,
28 greptime_value,
29};
30use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS;
31use lazy_static::lazy_static;
32use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
33use otel_arrow_rust::proto::opentelemetry::common::v1::{AnyValue, KeyValue, any_value};
34use otel_arrow_rust::proto::opentelemetry::metrics::v1::{metric, number_data_point, *};
35use session::protocol_ctx::{MetricType, OtlpMetricCtx};
36use snafu::OptionExt;
37use table::requests::{
38 METADATA_QUALITY_DECLARED, METRIC_TEMPORALITY_CUMULATIVE, METRIC_TEMPORALITY_DELTA,
39 SEMANTIC_METRIC_METADATA_QUALITY, SEMANTIC_METRIC_ORIGINAL_NAME, SEMANTIC_METRIC_TEMPORALITY,
40 SEMANTIC_METRIC_TYPE, SEMANTIC_METRIC_UNIT,
41};
42
43use crate::error::{self, Result};
44use crate::metrics::OTLP_EXPONENTIAL_HISTOGRAM_REJECTED_DATA_POINTS;
45use crate::otlp::trace::{KEY_SERVICE_INSTANCE_ID, KEY_SERVICE_NAME, KEY_SERVICE_NAMESPACE};
46use crate::query_handler::MetricsIngestOutcome;
47use crate::row_writer::{self, MultiTableData, TableData};
48pub use crate::semantic::SemanticIndex;
49use crate::semantic::{
50 METRIC_TYPE_COUNTER, METRIC_TYPE_GAUGE, METRIC_TYPE_HISTOGRAM, METRIC_TYPE_SUMMARY,
51 METRIC_TYPE_UPDOWN_COUNTER,
52};
53
54mod exemplars;
55mod prepare;
56mod resource_info;
57
58pub use prepare::{PreparedMetrics, prepare_metrics};
59mod translator;
60
61pub use resource_info::OTEL_RESOURCE_INFO_TABLE_NAME;
62use resource_info::ResourceInfoData;
63pub(crate) use translator::ucum_to_openmetrics_unit;
64use translator::{translate_label_name, translate_metric_name};
65
66const APPROXIMATE_COLUMN_COUNT: usize = 8;
68
69const COUNT_TABLE_SUFFIX: &str = "_count";
70const SUM_TABLE_SUFFIX: &str = "_sum";
71const BUCKET_TABLE_SUFFIX: &str = "_bucket";
72
73const JOB_KEY: &str = "job";
74const INSTANCE_KEY: &str = "instance";
75
76const DEFAULT_PROMOTE_ATTRS: [&str; 19] = [
78 "service.instance.id",
79 "service.name",
80 "service.namespace",
81 "service.version",
82 "cloud.availability_zone",
83 "cloud.region",
84 "container.name",
85 "deployment.environment",
86 "deployment.environment.name",
87 "k8s.cluster.name",
88 "k8s.container.name",
89 "k8s.cronjob.name",
90 "k8s.daemonset.name",
91 "k8s.deployment.name",
92 "k8s.job.name",
93 "k8s.namespace.name",
94 "k8s.pod.name",
95 "k8s.replicaset.name",
96 "k8s.statefulset.name",
97];
98
99lazy_static! {
100 static ref DEFAULT_PROMOTE_ATTRS_SET: HashSet<String> =
101 HashSet::from_iter(DEFAULT_PROMOTE_ATTRS.iter().map(|s| s.to_string()));
102}
103
104const OTEL_SCOPE_NAME: &str = "name";
105const OTEL_SCOPE_VERSION: &str = "version";
106const OTEL_SCOPE_SCHEMA_URL: &str = "schema_url";
107const MIN_EXPONENTIAL_HISTOGRAM_SCALE: i32 = -4;
108const MAX_EXPONENTIAL_HISTOGRAM_SCALE: i32 = 8;
109const MAX_REJECTION_MESSAGE_BYTES: usize = 512;
110
111#[derive(Debug)]
113pub struct MetricsConversion {
114 pub exemplars: Vec<crate::exemplar::Exemplar>,
116 pub requests: RowInsertRequests,
117 pub rows: usize,
119 pub semantic_index: SemanticIndex,
122 pub resource_info: Option<RowInsertRequests>,
125 pub outcome: MetricsIngestOutcome,
126}
127
128struct MetricMetadata {
129 name: String,
130 original_name: String,
131 native_schema: Option<ColumnSchema>,
132 semantics: SemanticIndex,
133}
134
135impl MetricMetadata {
136 fn new(metric: &Metric, ctx: &OtlpMetricCtx) -> Result<Self> {
137 let name = translate_metric_name(metric, &ctx.metric_type, ctx.metric_translation_strategy);
138 let emitted = emitted_semantic_tables(&ctx.metric_type, &name);
139 let mut semantics = SemanticIndex::default();
140 record_metric_semantics(&mut semantics, metric, &name, &emitted);
141 let native_schema = matches!(metric.data, Some(metric::Data::ExponentialHistogram(_)))
142 .then(native_histogram_column_schema)
143 .transpose()
144 .map_err(|error| {
145 error::InternalSnafu {
146 err_msg: error.to_string(),
147 }
148 .build()
149 })?;
150 Ok(Self {
151 name,
152 original_name: metric.name.clone(),
153 native_schema,
154 semantics,
155 })
156 }
157}
158
159pub fn to_grpc_insert_requests(
167 request: ExportMetricsServiceRequest,
168 metric_ctx: &mut OtlpMetricCtx,
169) -> Result<MetricsConversion> {
170 let prepared = prepare_metrics(request, metric_ctx)?;
171 let rejections = vec![None; prepared.num_points()];
172 prepared.finish(rejections, &std::collections::HashMap::new())
173}
174
175fn validate_sample_kind(table_name: &str, schema: &[ColumnSchema]) -> Result<()> {
176 let field_count = schema
177 .iter()
178 .filter(|column| column.semantic_type == SemanticType::Field as i32)
179 .count();
180 let has_native_histogram = schema.iter().any(|column| {
181 column.semantic_type == SemanticType::Field as i32
182 && api::helper::is_column_type_value_eq(
183 column.datatype,
184 column.datatype_extension.clone(),
185 native_histogram_value_type(),
186 )
187 });
188 if has_native_histogram && field_count != 1 {
189 return Err(error::InvalidParameterSnafu {
190 reason: format!(
191 "OTLP metric `{}` cannot mix native histogram and float sample fields",
192 table_name
193 ),
194 }
195 .build());
196 }
197 Ok(())
198}
199
200fn emitted_semantic_tables(metric_type: &MetricType, base: &str) -> Vec<(String, &'static str)> {
204 match metric_type {
205 MetricType::Gauge => vec![(base.to_string(), METRIC_TYPE_GAUGE)],
206 MetricType::MonotonicSum => vec![(base.to_string(), METRIC_TYPE_COUNTER)],
207 MetricType::NonMonotonicSum => vec![(base.to_string(), METRIC_TYPE_UPDOWN_COUNTER)],
208 MetricType::Histogram => vec![
209 (
210 format!("{base}{BUCKET_TABLE_SUFFIX}"),
211 METRIC_TYPE_HISTOGRAM,
212 ),
213 (format!("{base}{SUM_TABLE_SUFFIX}"), METRIC_TYPE_COUNTER),
214 (format!("{base}{COUNT_TABLE_SUFFIX}"), METRIC_TYPE_COUNTER),
215 ],
216 MetricType::Summary => vec![
217 (base.to_string(), METRIC_TYPE_SUMMARY),
218 (format!("{base}{COUNT_TABLE_SUFFIX}"), METRIC_TYPE_COUNTER),
219 (format!("{base}{SUM_TABLE_SUFFIX}"), METRIC_TYPE_COUNTER),
220 ],
221 MetricType::ExponentialHistogram => {
222 vec![(base.to_string(), METRIC_TYPE_HISTOGRAM)]
223 }
224 MetricType::Init => vec![],
225 }
226}
227
228fn temporality_value(data: &metric::Data) -> Option<&'static str> {
231 let raw = match data {
232 metric::Data::Sum(sum) => sum.aggregation_temporality,
233 metric::Data::Histogram(hist) => hist.aggregation_temporality,
234 metric::Data::ExponentialHistogram(hist) => hist.aggregation_temporality,
235 _ => return None,
236 };
237 match AggregationTemporality::try_from(raw) {
238 Ok(AggregationTemporality::Delta) => Some(METRIC_TEMPORALITY_DELTA),
239 Ok(AggregationTemporality::Cumulative) => Some(METRIC_TEMPORALITY_CUMULATIVE),
240 _ => None,
241 }
242}
243
244fn record_metric_semantics(
247 index: &mut SemanticIndex,
248 metric: &Metric,
249 name: &str,
250 emitted: &[(String, &str)],
251) {
252 if emitted.is_empty() {
253 return;
254 }
255
256 let temporality = metric.data.as_ref().and_then(temporality_value);
257 let unit = metric.unit.trim();
258 let original_name = (name != metric.name.as_str()).then_some(metric.name.as_str());
260
261 for (table, metric_type) in emitted {
262 index.record_scalar(table, SEMANTIC_METRIC_TYPE, metric_type);
263 index.record_scalar(
264 table,
265 SEMANTIC_METRIC_METADATA_QUALITY,
266 METADATA_QUALITY_DECLARED,
267 );
268 if let Some(temporality) = temporality {
269 index.record_scalar(table, SEMANTIC_METRIC_TEMPORALITY, temporality);
270 }
271 if !unit.is_empty() {
272 index.record_scalar(table, SEMANTIC_METRIC_UNIT, unit);
273 }
274 if let Some(original_name) = original_name {
275 index.record_scalar(table, SEMANTIC_METRIC_ORIGINAL_NAME, original_name);
276 }
277 }
278}
279
280fn from_metric_type(data: &metric::Data) -> MetricType {
281 match data {
282 metric::Data::Gauge(_) => MetricType::Gauge,
283 metric::Data::Sum(s) => {
284 if s.is_monotonic {
285 MetricType::MonotonicSum
286 } else {
287 MetricType::NonMonotonicSum
288 }
289 }
290 metric::Data::Histogram(_) => MetricType::Histogram,
291 metric::Data::ExponentialHistogram(_) => MetricType::ExponentialHistogram,
292 metric::Data::Summary(_) => MetricType::Summary,
293 }
294}
295
296fn scalar_value_string(value: Option<&AnyValue>) -> Option<String> {
298 match value.and_then(|v| v.value.as_ref())? {
299 any_value::Value::StringValue(s) => Some(s.clone()),
300 any_value::Value::IntValue(v) => Some(v.to_string()),
301 any_value::Value::DoubleValue(v) => Some(v.to_string()),
302 any_value::Value::BoolValue(v) => Some(v.to_string()),
303 _ => None,
304 }
305}
306
307pub(crate) struct ServiceIdentity {
313 pub job: Option<String>,
314 pub instance: Option<String>,
315}
316
317pub(crate) fn service_identity(attrs: &[KeyValue]) -> ServiceIdentity {
318 let mut name = None;
319 let mut namespace = None;
320 let mut instance = None;
321 for kv in attrs {
322 match kv.key.as_str() {
323 KEY_SERVICE_NAME => name = scalar_value_string(kv.value.as_ref()),
324 KEY_SERVICE_NAMESPACE => namespace = scalar_value_string(kv.value.as_ref()),
325 KEY_SERVICE_INSTANCE_ID => instance = scalar_value_string(kv.value.as_ref()),
326 _ => {}
327 }
328 }
329 let job = name.map(|name| match namespace {
330 Some(ns) if !ns.is_empty() => format!("{ns}/{name}"),
331 _ => name,
332 });
333 ServiceIdentity { job, instance }
334}
335
336fn string_key_value(key: &str, value: String) -> KeyValue {
337 KeyValue {
338 key: key.to_string(),
339 value: Some(AnyValue {
340 value: Some(any_value::Value::StringValue(value)),
341 }),
342 }
343}
344
345fn process_resource_attrs(attrs: &mut Vec<KeyValue>, metric_ctx: &OtlpMetricCtx) {
346 let ServiceIdentity { job, instance } = service_identity(attrs);
348
349 if metric_ctx.promote_all_resource_attrs {
351 attrs.retain(|kv| !metric_ctx.resource_attrs.contains(&kv.key));
352 } else {
353 attrs.retain(|kv| {
354 metric_ctx.resource_attrs.contains(&kv.key)
355 || DEFAULT_PROMOTE_ATTRS_SET.contains(&kv.key)
356 });
357 }
358
359 if let Some(job) = job {
360 attrs.push(string_key_value(JOB_KEY, job));
361 }
362 if let Some(instance) = instance {
363 attrs.push(string_key_value(INSTANCE_KEY, instance));
364 }
365}
366
367fn process_scope_attrs(scope: &ScopeMetrics, metric_ctx: &OtlpMetricCtx) -> Option<Vec<KeyValue>> {
368 if !metric_ctx.promote_scope_attrs {
369 return None;
370 }
371
372 scope.scope.as_ref().map(|s| {
374 let mut attrs = s.attributes.clone();
375 attrs.push(KeyValue {
376 key: OTEL_SCOPE_NAME.to_string(),
377 value: Some(AnyValue {
378 value: Some(any_value::Value::StringValue(s.name.clone())),
379 }),
380 });
381 attrs.push(KeyValue {
382 key: OTEL_SCOPE_VERSION.to_string(),
383 value: Some(AnyValue {
384 value: Some(any_value::Value::StringValue(s.version.clone())),
385 }),
386 });
387 attrs.push(KeyValue {
388 key: OTEL_SCOPE_SCHEMA_URL.to_string(),
389 value: Some(AnyValue {
390 value: Some(any_value::Value::StringValue(scope.schema_url.clone())),
391 }),
392 });
393 attrs
394 })
395}
396
397#[allow(clippy::too_many_arguments)]
398fn encode_metrics(
399 table_writer: &mut MultiTableData,
400 metric: &Metric,
401 metadata: &MetricMetadata,
402 resource_attrs: Option<&Vec<KeyValue>>,
403 scope_attrs: Option<&Vec<KeyValue>>,
404 metric_ctx: &OtlpMetricCtx,
405 outcome: &mut MetricsIngestOutcome,
406 exemplars: &mut Vec<exemplars::RowExemplars>,
407) -> Result<()> {
408 let name = &metadata.name;
409
410 if let Some(data) = &metric.data {
411 match data {
412 metric::Data::Gauge(gauge) => {
413 encode_gauge(
414 table_writer,
415 name,
416 gauge,
417 resource_attrs,
418 scope_attrs,
419 metric_ctx,
420 exemplars,
421 )?;
422 add_accepted_data_points(outcome, gauge.data_points.len())?;
423 }
424 metric::Data::Sum(sum) => {
425 encode_sum(
426 table_writer,
427 name,
428 sum,
429 resource_attrs,
430 scope_attrs,
431 metric_ctx,
432 exemplars,
433 )?;
434 add_accepted_data_points(outcome, sum.data_points.len())?;
435 }
436 metric::Data::Summary(summary) => {
437 encode_summary(
438 table_writer,
439 name,
440 summary,
441 resource_attrs,
442 scope_attrs,
443 metric_ctx,
444 )?;
445 add_accepted_data_points(outcome, summary.data_points.len())?;
446 }
447 metric::Data::Histogram(hist) => encode_histogram(
448 table_writer,
449 name,
450 hist,
451 resource_attrs,
452 scope_attrs,
453 metric_ctx,
454 outcome,
455 exemplars,
456 )?,
457 metric::Data::ExponentialHistogram(hist) => encode_exponential_histogram(
458 table_writer,
459 metadata,
460 hist,
461 resource_attrs,
462 scope_attrs,
463 metric_ctx,
464 outcome,
465 exemplars,
466 )?,
467 }
468 }
469
470 Ok(())
471}
472
473fn add_accepted_data_points(outcome: &mut MetricsIngestOutcome, count: usize) -> Result<()> {
474 let count = i64::try_from(count).map_err(|_| {
475 error::InvalidParameterSnafu {
476 reason: "OTLP metrics data-point count exceeds i64",
477 }
478 .build()
479 })?;
480 outcome.accepted_data_points =
481 outcome
482 .accepted_data_points
483 .checked_add(count)
484 .ok_or_else(|| {
485 error::InvalidParameterSnafu {
486 reason: "OTLP accepted data-point count overflows i64",
487 }
488 .build()
489 })?;
490 Ok(())
491}
492
493fn reject_data_points(
494 outcome: &mut MetricsIngestOutcome,
495 count: usize,
496 reason: impl FnOnce() -> String,
497) -> Result<()> {
498 if count == 0 {
499 return Ok(());
500 }
501 let count = i64::try_from(count).map_err(|_| {
502 error::InvalidParameterSnafu {
503 reason: "OTLP rejected data-point count exceeds i64",
504 }
505 .build()
506 })?;
507 outcome.rejected_data_points =
508 outcome
509 .rejected_data_points
510 .checked_add(count)
511 .ok_or_else(|| {
512 error::InvalidParameterSnafu {
513 reason: "OTLP rejected data-point count overflows i64",
514 }
515 .build()
516 })?;
517 append_rejection_message(&mut outcome.error_message, reason);
518 Ok(())
519}
520
521fn append_rejection_message(message: &mut Option<String>, reason: impl FnOnce() -> String) {
522 let message = message.get_or_insert_with(String::new);
523 let separator = if message.is_empty() { "" } else { "; " };
524 let Some(available) = MAX_REJECTION_MESSAGE_BYTES.checked_sub(message.len()) else {
525 return;
526 };
527 if available <= separator.len() {
528 return;
529 }
530 let reason = reason();
531 message.push_str(separator);
532
533 let available = MAX_REJECTION_MESSAGE_BYTES - message.len();
534 if reason.len() <= available {
535 message.push_str(&reason);
536 return;
537 }
538
539 const ELLIPSIS: &str = "...";
540 let mut end = available.saturating_sub(ELLIPSIS.len());
541 while !reason.is_char_boundary(end) {
542 end -= 1;
543 }
544 message.push_str(&reason[..end]);
545 if available >= ELLIPSIS.len() {
546 message.push_str(ELLIPSIS);
547 }
548}
549
550#[allow(clippy::too_many_arguments)]
551fn encode_exponential_histogram(
552 table_writer: &mut MultiTableData,
553 metadata: &MetricMetadata,
554 histogram: &ExponentialHistogram,
555 resource_attrs: Option<&Vec<KeyValue>>,
556 scope_attrs: Option<&Vec<KeyValue>>,
557 metric_ctx: &OtlpMetricCtx,
558 outcome: &mut MetricsIngestOutcome,
559 exemplars: &mut Vec<exemplars::RowExemplars>,
560) -> Result<()> {
561 let name = &metadata.name;
562 let temporality = match exponential_histogram_gate(histogram) {
563 Ok(temporality) => temporality,
564 Err(rejection) => {
565 reject_data_points(outcome, histogram.data_points.len(), || {
566 rejection.message(name)
567 })?;
568 OTLP_EXPONENTIAL_HISTOGRAM_REJECTED_DATA_POINTS
569 .with_label_values(&[rejection.reason_label()])
570 .inc_by(histogram.data_points.len() as u64);
571 return Ok(());
572 }
573 };
574
575 let column_schema = metadata
576 .native_schema
577 .as_ref()
578 .context(error::InternalSnafu {
579 err_msg: "missing native histogram schema",
580 })?;
581 for (index, data_point) in histogram.data_points.iter().enumerate() {
582 let (value, timestamp_nanos) = match exponential_histogram_value(data_point, temporality) {
583 Ok(value) => value,
584 Err(reason) => {
585 reject_data_points(outcome, 1, || {
586 format!("metric `{name}` data point {index}: {reason}")
587 })?;
588 OTLP_EXPONENTIAL_HISTOGRAM_REJECTED_DATA_POINTS
589 .with_label_values(&["invalid_data_point"])
590 .inc();
591 continue;
592 }
593 };
594
595 let table = table_writer.get_or_default_table_data(
596 name,
597 APPROXIMATE_COLUMN_COUNT,
598 histogram.data_points.len(),
599 );
600 let mut row = table.alloc_one_row();
601 write_tags_and_timestamp(
602 table,
603 &mut row,
604 resource_attrs,
605 scope_attrs,
606 Some(data_point.attributes.as_ref()),
607 timestamp_nanos,
608 metric_ctx,
609 )?;
610 write_temporality_tag(
611 table,
612 &mut row,
613 temporality == AggregationTemporality::Delta,
614 )?;
615 row_writer::write_by_schema(
616 table,
617 std::iter::once((column_schema.clone(), Some(value))),
618 &mut row,
619 )?;
620 exemplars::append(
621 exemplars,
622 &data_point.exemplars,
623 name,
624 table.num_rows(),
625 metric_ctx,
626 );
627 table.add_row(row);
628 add_accepted_data_points(outcome, 1)?;
629 }
630
631 Ok(())
632}
633
634#[derive(Debug)]
635pub(crate) enum ExponentialHistogramRejection {
636 UnspecifiedTemporality,
637}
638
639impl ExponentialHistogramRejection {
640 fn reason_label(&self) -> &'static str {
641 match self {
642 Self::UnspecifiedTemporality => "unspecified_temporality",
643 }
644 }
645
646 fn message(&self, name: &str) -> String {
647 match self {
648 Self::UnspecifiedTemporality => format!(
649 "metric `{name}` has unspecified OTLP exponential histogram temporality; delta or cumulative temporality is required"
650 ),
651 }
652 }
653}
654
655pub(crate) fn exponential_histogram_gate(
657 histogram: &ExponentialHistogram,
658) -> std::result::Result<AggregationTemporality, ExponentialHistogramRejection> {
659 match AggregationTemporality::try_from(histogram.aggregation_temporality) {
660 Ok(temporality @ (AggregationTemporality::Cumulative | AggregationTemporality::Delta)) => {
661 Ok(temporality)
662 }
663 _ => Err(ExponentialHistogramRejection::UnspecifiedTemporality),
664 }
665}
666
667pub(crate) fn exponential_histogram_value(
668 data_point: &ExponentialHistogramDataPoint,
669 temporality: AggregationTemporality,
670) -> std::result::Result<(ValueData, i64), String> {
671 if data_point.start_time_unix_nano > data_point.time_unix_nano {
672 return Err(format!(
673 "start_time_unix_nano {} exceeds time_unix_nano {}",
674 data_point.start_time_unix_nano, data_point.time_unix_nano
675 ));
676 }
677
678 let timestamp_nanos = i64::try_from(data_point.time_unix_nano)
679 .map_err(|_| format!("time_unix_nano {} overflows i64", data_point.time_unix_nano))?;
680 let timestamp = timestamp_millis(data_point.time_unix_nano, "time_unix_nano")?;
681 let start_timestamp =
682 timestamp_millis(data_point.start_time_unix_nano, "start_time_unix_nano")?;
683
684 let reset_hint = if temporality == AggregationTemporality::Delta {
685 ResetHint::Gauge
686 } else {
687 ResetHint::Unspecified
688 } as i32;
689 let no_recorded_value = data_point.flags & DataPointFlags::NoRecordedValueMask as u32 != 0;
690 let histogram = if no_recorded_value {
691 PromHistogram {
692 sum: f64::from_bits(PROMETHEUS_STALE_NAN_BITS),
693 schema: 0,
694 zero_threshold: 0.0,
695 reset_hint,
696 timestamp,
697 start_timestamp,
698 count: Some(PromCount::CountInt(0)),
699 zero_count: Some(PromZeroCount::ZeroCountInt(0)),
700 ..Default::default()
701 }
702 } else {
703 if data_point.scale < MIN_EXPONENTIAL_HISTOGRAM_SCALE {
704 return Err(format!(
705 "scale {} is unsupported; minimum supported scale is {}",
706 data_point.scale, MIN_EXPONENTIAL_HISTOGRAM_SCALE
707 ));
708 }
709 if !data_point.zero_threshold.is_finite() || data_point.zero_threshold < 0.0 {
710 return Err(format!(
711 "zero_threshold {} must be finite and non-negative",
712 data_point.zero_threshold
713 ));
714 }
715
716 let downscale_shift = if data_point.scale > MAX_EXPONENTIAL_HISTOGRAM_SCALE {
717 let shift = data_point
718 .scale
719 .checked_sub(MAX_EXPONENTIAL_HISTOGRAM_SCALE)
720 .ok_or_else(|| "downscale shift overflows i32".to_string())?;
721 u32::try_from(shift).map_err(|_| "downscale shift exceeds u32".to_string())?
722 } else {
723 0
724 };
725 let (positive_spans, positive_deltas, positive_count) =
726 convert_bucket_range("positive", data_point.positive.as_ref(), downscale_shift)?;
727 let (negative_spans, negative_deltas, negative_count) =
728 convert_bucket_range("negative", data_point.negative.as_ref(), downscale_shift)?;
729 let bucket_count = data_point
730 .zero_count
731 .checked_add(positive_count)
732 .and_then(|count| count.checked_add(negative_count))
733 .ok_or_else(|| "bucket observation total overflows u64".to_string())?;
734 if bucket_count != data_point.count {
735 return Err(format!(
736 "buckets contain {bucket_count} observations, declared count is {}",
737 data_point.count
738 ));
739 }
740
741 i64::try_from(data_point.count)
742 .map_err(|_| format!("count {} overflows i64", data_point.count))?;
743 i64::try_from(data_point.zero_count)
744 .map_err(|_| format!("zero_count {} overflows i64", data_point.zero_count))?;
745
746 let sum = match data_point.sum {
747 Some(sum) if sum.is_nan() => f64::NAN,
748 Some(sum) => sum,
749 None => f64::NAN,
750 };
751 PromHistogram {
752 sum,
753 schema: data_point.scale.min(MAX_EXPONENTIAL_HISTOGRAM_SCALE),
754 zero_threshold: data_point.zero_threshold,
755 negative_spans,
756 negative_deltas,
757 positive_spans,
758 positive_deltas,
759 reset_hint,
760 timestamp,
761 start_timestamp,
762 count: Some(PromCount::CountInt(data_point.count)),
763 zero_count: Some(PromZeroCount::ZeroCountInt(data_point.zero_count)),
764 ..Default::default()
765 }
766 };
767
768 encode_native_histogram(&histogram)
769 .map(|value| (value, timestamp_nanos))
770 .map_err(|error| format!("OTLP exponential histogram cannot be encoded: {error}"))
771}
772
773fn timestamp_millis(timestamp_nanos: u64, name: &str) -> std::result::Result<i64, String> {
774 i64::try_from(timestamp_nanos / 1_000_000)
775 .map_err(|_| format!("{name} {timestamp_nanos} milliseconds overflow i64"))
776}
777
778fn convert_bucket_range(
779 name: &str,
780 buckets: Option<&exponential_histogram_data_point::Buckets>,
781 downscale_shift: u32,
782) -> std::result::Result<(Vec<BucketSpan>, Vec<i64>, u64), String> {
783 let Some(buckets) = buckets else {
784 return Ok((Vec::new(), Vec::new(), 0));
785 };
786 if buckets.bucket_counts.is_empty() {
787 return Ok((Vec::new(), Vec::new(), 0));
788 }
789
790 let mut merged = Vec::<(i32, u64)>::with_capacity(buckets.bucket_counts.len());
791 let mut total = 0u64;
792 for (position, count) in buckets.bucket_counts.iter().copied().enumerate() {
793 let position =
794 i32::try_from(position).map_err(|_| format!("{name} bucket length exceeds i32"))?;
795 let source_index = buckets.offset.checked_add(position).ok_or_else(|| {
796 format!(
797 "{name} bucket index overflows i32 at offset {} and position {position}",
798 buckets.offset
799 )
800 })?;
801 let target_index = downscale_bucket_index(source_index, downscale_shift)?
802 .checked_add(1)
803 .ok_or_else(|| format!("{name} shifted bucket index overflows i32"))?;
804 if let Some((last_index, last_count)) = merged.last_mut() {
805 if *last_index == target_index {
806 *last_count = last_count
807 .checked_add(count)
808 .ok_or_else(|| format!("{name} merged bucket count overflows u64"))?;
809 total = total
810 .checked_add(count)
811 .ok_or_else(|| format!("{name} bucket count total overflows u64"))?;
812 continue;
813 }
814 let next_index = last_index
815 .checked_add(1)
816 .ok_or_else(|| format!("{name} target bucket index overflows i32"))?;
817 if target_index != next_index {
818 return Err(format!(
819 "{name} bucket indexes are not contiguous after downscaling"
820 ));
821 }
822 }
823 total = total
824 .checked_add(count)
825 .ok_or_else(|| format!("{name} bucket count total overflows u64"))?;
826 merged.push((target_index, count));
827 }
828
829 let mut spans = Vec::<BucketSpan>::new();
830 let mut deltas = Vec::with_capacity(merged.len());
831 let mut previous_index = None::<i32>;
832 let mut previous = 0i64;
833 for (index, count) in merged {
834 if count == 0 {
835 continue;
836 }
837 match (spans.last_mut(), previous_index) {
838 (Some(span), Some(prev_index)) if prev_index.checked_add(1) == Some(index) => {
839 span.length = span
840 .length
841 .checked_add(1)
842 .ok_or_else(|| format!("{name} bucket span length exceeds u32"))?;
843 }
844 (_, prev_index) => {
845 let offset = match prev_index {
846 Some(prev_index) => index
847 .checked_sub(prev_index)
848 .and_then(|gap| gap.checked_sub(1))
849 .ok_or_else(|| format!("{name} bucket span offset overflows i32"))?,
850 None => index,
851 };
852 spans.push(BucketSpan { offset, length: 1 });
853 }
854 }
855 let count = i64::try_from(count)
856 .map_err(|_| format!("{name} bucket count {count} overflows i64"))?;
857 let delta = count
858 .checked_sub(previous)
859 .ok_or_else(|| format!("{name} bucket delta overflows i64"))?;
860 deltas.push(delta);
861 previous_index = Some(index);
862 previous = count;
864 }
865
866 Ok((spans, deltas, total))
867}
868
869fn downscale_bucket_index(index: i32, downscale_shift: u32) -> std::result::Result<i32, String> {
870 if downscale_shift == 0 {
871 return Ok(index);
872 }
873 if downscale_shift >= i32::BITS {
874 return Ok(if index < 0 { -1 } else { 0 });
875 }
876
877 let divisor = 1i64
878 .checked_shl(downscale_shift)
879 .ok_or_else(|| format!("downscale shift {downscale_shift} is invalid"))?;
880 i32::try_from(i64::from(index).div_euclid(divisor))
881 .map_err(|_| format!("downscaled bucket index {index} overflows i32"))
882}
883
884#[derive(Debug, Clone, Copy, PartialEq, Eq)]
885enum AttributeType {
886 Resource,
887 Scope,
888 DataPoint,
889}
890
891fn write_attributes(
892 writer: &mut TableData,
893 row: &mut Vec<Value>,
894 attrs: Option<&Vec<KeyValue>>,
895 attribute_type: AttributeType,
896 metric_ctx: &OtlpMetricCtx,
897) -> Result<()> {
898 let Some(attrs) = attrs else {
899 return Ok(());
900 };
901
902 let mut tags = Vec::with_capacity(attrs.len());
903 for attr in attrs {
904 let Some(value) = scalar_value_string(attr.value.as_ref()) else {
906 continue;
907 };
908 let key = match attribute_type {
909 AttributeType::Resource | AttributeType::DataPoint => {
910 translate_label_name(&attr.key, metric_ctx.metric_translation_strategy)
911 }
912 AttributeType::Scope => {
913 format!(
914 "otel_scope_{}",
915 translate_label_name(&attr.key, metric_ctx.metric_translation_strategy)
916 )
917 }
918 };
919 if key == OTLP_AGGREGATION_TEMPORALITY_LABEL || key == crate::prom_store::METRIC_NAME_LABEL
923 {
924 return Err(error::InvalidOtlpMetricInputSnafu {
925 reason: format!(
926 "OTLP attribute `{}` resolves to reserved label `{}`",
927 attr.key, key
928 ),
929 }
930 .build());
931 }
932 tags.push((key, value));
933 }
934 row_writer::write_tags(writer, tags.into_iter(), row)?;
935
936 Ok(())
937}
938
939fn write_timestamp(
940 table: &mut TableData,
941 row: &mut Vec<Value>,
942 time_nano: i64,
943 metric_ctx: &OtlpMetricCtx,
944) -> Result<()> {
945 if metric_ctx.with_metric_engine {
952 row_writer::write_ts_to_nanos(
953 table,
954 greptime_timestamp(),
955 Some(time_nano),
956 Precision::Nanosecond,
957 row,
958 )
959 } else {
960 row_writer::write_ts_to_millis(
961 table,
962 greptime_timestamp(),
963 Some(time_nano / 1000000),
964 Precision::Millisecond,
965 row,
966 )
967 }
968}
969
970fn write_data_point_value(
971 table: &mut TableData,
972 row: &mut Vec<Value>,
973 field: &str,
974 value: &Option<number_data_point::Value>,
975) -> Result<()> {
976 match value {
977 Some(number_data_point::Value::AsInt(val)) => {
978 row_writer::write_f64(table, field, *val as f64, row)?;
980 }
981 Some(number_data_point::Value::AsDouble(val)) => {
982 row_writer::write_f64(table, field, *val, row)?;
983 }
984 _ => {}
985 }
986 Ok(())
987}
988
989fn write_temporality_tag(
990 table: &mut TableData,
991 row: &mut Vec<Value>,
992 is_delta: bool,
993) -> Result<()> {
994 if is_delta {
995 row_writer::write_tag(
996 table,
997 OTLP_AGGREGATION_TEMPORALITY_LABEL,
998 GREPTIME_TEMPORALITY_DELTA,
999 row,
1000 )?;
1001 }
1002 Ok(())
1003}
1004
1005fn has_no_recorded_value(flags: u32) -> bool {
1006 flags & DataPointFlags::NoRecordedValueMask as u32 != 0
1007}
1008
1009fn write_tags_and_timestamp(
1010 table: &mut TableData,
1011 row: &mut Vec<Value>,
1012 resource_attrs: Option<&Vec<KeyValue>>,
1013 scope_attrs: Option<&Vec<KeyValue>>,
1014 data_point_attrs: Option<&Vec<KeyValue>>,
1015 timestamp_nanos: i64,
1016 metric_ctx: &OtlpMetricCtx,
1017) -> Result<()> {
1018 write_attributes(
1020 table,
1021 row,
1022 resource_attrs,
1023 AttributeType::Resource,
1024 metric_ctx,
1025 )?;
1026 write_attributes(table, row, scope_attrs, AttributeType::Scope, metric_ctx)?;
1027 write_attributes(
1028 table,
1029 row,
1030 data_point_attrs,
1031 AttributeType::DataPoint,
1032 metric_ctx,
1033 )?;
1034
1035 write_timestamp(table, row, timestamp_nanos, metric_ctx)?;
1036
1037 Ok(())
1038}
1039
1040fn encode_gauge(
1045 table_writer: &mut MultiTableData,
1046 name: &str,
1047 gauge: &Gauge,
1048 resource_attrs: Option<&Vec<KeyValue>>,
1049 scope_attrs: Option<&Vec<KeyValue>>,
1050 metric_ctx: &OtlpMetricCtx,
1051 exemplars: &mut Vec<exemplars::RowExemplars>,
1052) -> Result<()> {
1053 let table = table_writer.get_or_default_table_data(
1054 name,
1055 APPROXIMATE_COLUMN_COUNT,
1056 gauge.data_points.len(),
1057 );
1058
1059 for data_point in &gauge.data_points {
1060 let mut row = table.alloc_one_row();
1061 write_tags_and_timestamp(
1062 table,
1063 &mut row,
1064 resource_attrs,
1065 scope_attrs,
1066 Some(data_point.attributes.as_ref()),
1067 data_point.time_unix_nano as i64,
1068 metric_ctx,
1069 )?;
1070
1071 write_data_point_value(table, &mut row, greptime_value(), &data_point.value)?;
1072 exemplars::append(
1073 exemplars,
1074 &data_point.exemplars,
1075 name,
1076 table.num_rows(),
1077 metric_ctx,
1078 );
1079 table.add_row(row);
1080 }
1081
1082 Ok(())
1083}
1084
1085fn encode_sum(
1088 table_writer: &mut MultiTableData,
1089 name: &str,
1090 sum: &Sum,
1091 resource_attrs: Option<&Vec<KeyValue>>,
1092 scope_attrs: Option<&Vec<KeyValue>>,
1093 metric_ctx: &OtlpMetricCtx,
1094 exemplars: &mut Vec<exemplars::RowExemplars>,
1095) -> Result<()> {
1096 let is_delta = matches!(
1097 AggregationTemporality::try_from(sum.aggregation_temporality),
1098 Ok(AggregationTemporality::Delta)
1099 );
1100 let table = table_writer.get_or_default_table_data(
1101 name,
1102 APPROXIMATE_COLUMN_COUNT,
1103 sum.data_points.len(),
1104 );
1105
1106 for data_point in &sum.data_points {
1107 let mut row = table.alloc_one_row();
1108 write_tags_and_timestamp(
1109 table,
1110 &mut row,
1111 resource_attrs,
1112 scope_attrs,
1113 Some(data_point.attributes.as_ref()),
1114 data_point.time_unix_nano as i64,
1115 metric_ctx,
1116 )?;
1117 write_temporality_tag(table, &mut row, is_delta)?;
1118 if has_no_recorded_value(data_point.flags) {
1119 row_writer::write_f64(
1120 table,
1121 greptime_value(),
1122 f64::from_bits(PROMETHEUS_STALE_NAN_BITS),
1123 &mut row,
1124 )?;
1125 } else {
1126 write_data_point_value(table, &mut row, greptime_value(), &data_point.value)?;
1127 }
1128 exemplars::append(
1129 exemplars,
1130 &data_point.exemplars,
1131 name,
1132 table.num_rows(),
1133 metric_ctx,
1134 );
1135 table.add_row(row);
1136 }
1137
1138 Ok(())
1139}
1140
1141const HISTOGRAM_LE_COLUMN: &str = "le";
1142
1143#[allow(clippy::too_many_arguments)]
1157fn encode_histogram(
1158 table_writer: &mut MultiTableData,
1159 name: &str,
1160 hist: &Histogram,
1161 resource_attrs: Option<&Vec<KeyValue>>,
1162 scope_attrs: Option<&Vec<KeyValue>>,
1163 metric_ctx: &OtlpMetricCtx,
1164 outcome: &mut MetricsIngestOutcome,
1165 exemplars: &mut Vec<exemplars::RowExemplars>,
1166) -> Result<()> {
1167 let normalized_name = name;
1168
1169 let bucket_table_name = format!("{}{}", normalized_name, BUCKET_TABLE_SUFFIX);
1170 let sum_table_name = format!("{}{}", normalized_name, SUM_TABLE_SUFFIX);
1171 let count_table_name = format!("{}{}", normalized_name, COUNT_TABLE_SUFFIX);
1172
1173 let is_delta = matches!(
1174 AggregationTemporality::try_from(hist.aggregation_temporality),
1175 Ok(AggregationTemporality::Delta)
1176 );
1177 let stale_value = f64::from_bits(PROMETHEUS_STALE_NAN_BITS);
1178 for (index, data_point) in hist.data_points.iter().enumerate() {
1179 if let Some(reason) = histogram_data_point_rejection(data_point, is_delta) {
1180 reject_data_points(outcome, 1, || {
1181 format!("metric `{name}` data point {index}: {reason}")
1182 })?;
1183 continue;
1184 }
1185
1186 let bucket_table =
1187 table_writer.get_or_default_table_data(&bucket_table_name, APPROXIMATE_COLUMN_COUNT, 0);
1188 let no_recorded_value = has_no_recorded_value(data_point.flags);
1189 if no_recorded_value {
1190 bucket_table.reserve_rows(data_point.explicit_bounds.len());
1191 bucket_table.reserve_rows(1);
1192 } else {
1193 bucket_table.reserve_rows(data_point.bucket_counts.len().max(1));
1194 }
1195 let bucket_values = if no_recorded_value {
1196 data_point
1197 .explicit_bounds
1198 .iter()
1199 .copied()
1200 .chain(std::iter::once(f64::INFINITY))
1201 .map(|bound| (bound, stale_value))
1202 .collect::<Vec<_>>()
1203 } else if data_point.bucket_counts.is_empty() && data_point.explicit_bounds.is_empty() {
1204 vec![(f64::INFINITY, data_point.count as f64)]
1206 } else {
1207 let mut accumulated_count = 0u64;
1208 let mut values = Vec::with_capacity(data_point.bucket_counts.len());
1209 for (idx, count) in data_point.bucket_counts.iter().enumerate() {
1210 accumulated_count = accumulated_count.checked_add(*count).ok_or_else(|| {
1211 error::InvalidParameterSnafu {
1212 reason: format!(
1213 "metric `{name}` data point {index}: bucket prefix overflows u64"
1214 ),
1215 }
1216 .build()
1217 })?;
1218 let bound =
1219 data_point.explicit_bounds.get(idx).copied().or_else(|| {
1220 (idx == data_point.explicit_bounds.len()).then_some(f64::INFINITY)
1221 });
1222 if let Some(bound) = bound {
1223 values.push((bound, accumulated_count as f64));
1224 }
1225 }
1226 values
1227 };
1228 let mut by_bucket = vec![Vec::new(); bucket_values.len()];
1229 for exemplar in &data_point.exemplars {
1230 let value = match exemplars::value(exemplar) {
1231 Ok(value) => value,
1232 Err(_) => {
1233 crate::metrics::EXEMPLARS_DROPPED_TOTAL
1234 .with_label_values(&["otlp"])
1235 .inc();
1236 continue;
1237 }
1238 };
1239 let Some(bucket) = bucket_values.iter().position(|(bound, _)| {
1240 value <= *bound || (value.is_nan() && *bound == f64::INFINITY)
1241 }) else {
1242 crate::metrics::EXEMPLARS_DROPPED_TOTAL
1243 .with_label_values(&["otlp"])
1244 .inc();
1245 continue;
1246 };
1247 by_bucket[bucket].push(exemplar);
1248 }
1249 for ((bound, value), bucket_exemplars) in bucket_values.into_iter().zip(by_bucket) {
1250 let mut bucket_row = bucket_table.alloc_one_row();
1251 write_tags_and_timestamp(
1252 bucket_table,
1253 &mut bucket_row,
1254 resource_attrs,
1255 scope_attrs,
1256 Some(data_point.attributes.as_ref()),
1257 data_point.time_unix_nano as i64,
1258 metric_ctx,
1259 )?;
1260 write_temporality_tag(bucket_table, &mut bucket_row, is_delta)?;
1261 row_writer::write_tag(bucket_table, HISTOGRAM_LE_COLUMN, bound, &mut bucket_row)?;
1263 row_writer::write_f64(bucket_table, greptime_value(), value, &mut bucket_row)?;
1264
1265 exemplars::append(
1266 exemplars,
1267 bucket_exemplars,
1268 &bucket_table_name,
1269 bucket_table.num_rows(),
1270 metric_ctx,
1271 );
1272 bucket_table.add_row(bucket_row);
1273 }
1274
1275 if let Some(sum) = data_point.sum {
1276 let sum_table = table_writer.get_or_default_table_data(
1277 &sum_table_name,
1278 APPROXIMATE_COLUMN_COUNT,
1279 hist.data_points.len(),
1280 );
1281 let mut sum_row = sum_table.alloc_one_row();
1282 write_tags_and_timestamp(
1283 sum_table,
1284 &mut sum_row,
1285 resource_attrs,
1286 scope_attrs,
1287 Some(data_point.attributes.as_ref()),
1288 data_point.time_unix_nano as i64,
1289 metric_ctx,
1290 )?;
1291 write_temporality_tag(sum_table, &mut sum_row, is_delta)?;
1292 row_writer::write_f64(
1293 sum_table,
1294 greptime_value(),
1295 if no_recorded_value { stale_value } else { sum },
1296 &mut sum_row,
1297 )?;
1298 sum_table.add_row(sum_row);
1299 }
1300
1301 let count_table = table_writer.get_or_default_table_data(
1302 &count_table_name,
1303 APPROXIMATE_COLUMN_COUNT,
1304 hist.data_points.len(),
1305 );
1306 let mut count_row = count_table.alloc_one_row();
1307 write_tags_and_timestamp(
1308 count_table,
1309 &mut count_row,
1310 resource_attrs,
1311 scope_attrs,
1312 Some(data_point.attributes.as_ref()),
1313 data_point.time_unix_nano as i64,
1314 metric_ctx,
1315 )?;
1316 write_temporality_tag(count_table, &mut count_row, is_delta)?;
1317 row_writer::write_f64(
1318 count_table,
1319 greptime_value(),
1320 if no_recorded_value {
1321 stale_value
1322 } else {
1323 data_point.count as f64
1324 },
1325 &mut count_row,
1326 )?;
1327 count_table.add_row(count_row);
1328 add_accepted_data_points(outcome, 1)?;
1329 }
1330
1331 Ok(())
1332}
1333
1334pub(crate) fn histogram_data_point_rejection(
1335 data_point: &HistogramDataPoint,
1336 is_delta: bool,
1337) -> Option<String> {
1338 if has_no_recorded_value(data_point.flags) {
1339 return None;
1340 }
1341
1342 if is_delta {
1343 let valid_empty_layout =
1344 data_point.bucket_counts.is_empty() && data_point.explicit_bounds.is_empty();
1345 let expected_buckets = data_point.explicit_bounds.len().checked_add(1);
1346 if !valid_empty_layout && expected_buckets != Some(data_point.bucket_counts.len()) {
1347 return Some(format!(
1348 "bucket_counts length {} must equal explicit_bounds length {} plus one",
1349 data_point.bucket_counts.len(),
1350 data_point.explicit_bounds.len()
1351 ));
1352 }
1353 if data_point
1354 .explicit_bounds
1355 .iter()
1356 .any(|bound| !bound.is_finite())
1357 || data_point
1358 .explicit_bounds
1359 .windows(2)
1360 .any(|bounds| bounds[0] >= bounds[1])
1361 {
1362 return Some("explicit_bounds must be finite and strictly increasing".to_string());
1363 }
1364 if data_point.count == 0 && data_point.sum.is_some_and(|sum| sum != 0.0) {
1365 return Some("sum must be absent or zero when count is zero".to_string());
1366 }
1367 }
1368
1369 let bucket_total = data_point
1370 .bucket_counts
1371 .iter()
1372 .try_fold(0u64, |total, count| total.checked_add(*count));
1373 let Some(bucket_total) = bucket_total else {
1374 return Some("bucket prefix overflows u64".to_string());
1375 };
1376 if is_delta && !data_point.bucket_counts.is_empty() && bucket_total != data_point.count {
1377 return Some(format!(
1378 "buckets contain {bucket_total} observations, declared count is {}",
1379 data_point.count
1380 ));
1381 }
1382
1383 None
1384}
1385
1386fn encode_summary(
1387 table_writer: &mut MultiTableData,
1388 name: &str,
1389 summary: &Summary,
1390 resource_attrs: Option<&Vec<KeyValue>>,
1391 scope_attrs: Option<&Vec<KeyValue>>,
1392 metric_ctx: &OtlpMetricCtx,
1393) -> Result<()> {
1394 let metric_name = name;
1399 let count_name = format!("{}{}", metric_name, COUNT_TABLE_SUFFIX);
1400 let sum_name = format!("{}{}", metric_name, SUM_TABLE_SUFFIX);
1401
1402 for data_point in &summary.data_points {
1403 {
1404 let quantile_table = table_writer.get_or_default_table_data(
1405 metric_name,
1406 APPROXIMATE_COLUMN_COUNT,
1407 summary.data_points.len(),
1408 );
1409
1410 for quantile in &data_point.quantile_values {
1411 let mut row = quantile_table.alloc_one_row();
1412 write_tags_and_timestamp(
1413 quantile_table,
1414 &mut row,
1415 resource_attrs,
1416 scope_attrs,
1417 Some(data_point.attributes.as_ref()),
1418 data_point.time_unix_nano as i64,
1419 metric_ctx,
1420 )?;
1421 row_writer::write_tag(quantile_table, "quantile", quantile.quantile, &mut row)?;
1422 row_writer::write_f64(quantile_table, greptime_value(), quantile.value, &mut row)?;
1423 quantile_table.add_row(row);
1424 }
1425 }
1426 {
1427 let count_table = table_writer.get_or_default_table_data(
1428 &count_name,
1429 APPROXIMATE_COLUMN_COUNT,
1430 summary.data_points.len(),
1431 );
1432 let mut row = count_table.alloc_one_row();
1433 write_tags_and_timestamp(
1434 count_table,
1435 &mut row,
1436 resource_attrs,
1437 scope_attrs,
1438 Some(data_point.attributes.as_ref()),
1439 data_point.time_unix_nano as i64,
1440 metric_ctx,
1441 )?;
1442
1443 row_writer::write_f64(
1444 count_table,
1445 greptime_value(),
1446 data_point.count as f64,
1447 &mut row,
1448 )?;
1449
1450 count_table.add_row(row);
1451 }
1452 {
1453 let sum_table = table_writer.get_or_default_table_data(
1454 &sum_name,
1455 APPROXIMATE_COLUMN_COUNT,
1456 summary.data_points.len(),
1457 );
1458
1459 let mut row = sum_table.alloc_one_row();
1460 write_tags_and_timestamp(
1461 sum_table,
1462 &mut row,
1463 resource_attrs,
1464 scope_attrs,
1465 Some(data_point.attributes.as_ref()),
1466 data_point.time_unix_nano as i64,
1467 metric_ctx,
1468 )?;
1469
1470 row_writer::write_f64(sum_table, greptime_value(), data_point.sum, &mut row)?;
1471
1472 sum_table.add_row(row);
1473 }
1474 }
1475
1476 Ok(())
1477}
1478
1479#[cfg(test)]
1480mod tests {
1481 use api::v1::ColumnDataType;
1482 use common_query::prelude::set_default_prefix;
1483 use otel_arrow_rust::proto::opentelemetry::common::v1::any_value::Value as Val;
1484 use otel_arrow_rust::proto::opentelemetry::common::v1::{AnyValue, InstrumentationScope};
1485 use otel_arrow_rust::proto::opentelemetry::metrics::v1::number_data_point::Value;
1486 use otel_arrow_rust::proto::opentelemetry::metrics::v1::summary_data_point::ValueAtQuantile;
1487 use otel_arrow_rust::proto::opentelemetry::metrics::v1::{
1488 AggregationTemporality, HistogramDataPoint, NumberDataPoint, SummaryDataPoint,
1489 };
1490 use otel_arrow_rust::proto::opentelemetry::resource::v1::Resource;
1491 use session::protocol_ctx::OtlpMetricTranslationStrategy;
1492
1493 use super::*;
1494
1495 mod delta;
1496
1497 fn keyvalue(key: &str, value: &str) -> KeyValue {
1498 KeyValue {
1499 key: key.into(),
1500 value: Some(AnyValue {
1501 value: Some(Val::StringValue(value.into())),
1502 }),
1503 }
1504 }
1505
1506 fn bool_keyvalue(key: &str, value: bool) -> KeyValue {
1507 KeyValue {
1508 key: key.into(),
1509 value: Some(AnyValue {
1510 value: Some(Val::BoolValue(value)),
1511 }),
1512 }
1513 }
1514
1515 fn descriptor_ctx() -> OtlpMetricCtx {
1516 OtlpMetricCtx {
1517 resource_info: true,
1518 ..Default::default()
1519 }
1520 }
1521
1522 fn attr_value(attrs: &[KeyValue], key: &str) -> Option<String> {
1523 attrs
1524 .iter()
1525 .find(|kv| kv.key == key)
1526 .and_then(|kv| scalar_value_string(kv.value.as_ref()))
1527 }
1528
1529 fn gauge_request(
1530 resource_attrs: Vec<KeyValue>,
1531 metric_name: &str,
1532 ) -> ExportMetricsServiceRequest {
1533 use otel_arrow_rust::proto::opentelemetry::resource::v1::Resource;
1534 ExportMetricsServiceRequest {
1535 resource_metrics: vec![ResourceMetrics {
1536 resource: Some(Resource {
1537 attributes: resource_attrs,
1538 ..Default::default()
1539 }),
1540 scope_metrics: vec![ScopeMetrics {
1541 metrics: vec![Metric {
1542 name: metric_name.to_string(),
1543 data: Some(metric::Data::Gauge(Gauge {
1544 data_points: vec![NumberDataPoint {
1545 time_unix_nano: 1_000_000,
1546 value: Some(Value::AsInt(1)),
1547 ..Default::default()
1548 }],
1549 })),
1550 ..Default::default()
1551 }],
1552 ..Default::default()
1553 }],
1554 ..Default::default()
1555 }],
1556 }
1557 }
1558
1559 fn column_names(request: &RowInsertRequests, table: &str) -> Vec<String> {
1560 request
1561 .inserts
1562 .iter()
1563 .find(|r| r.table_name == table)
1564 .unwrap_or_else(|| panic!("missing table {table}"))
1565 .rows
1566 .as_ref()
1567 .unwrap()
1568 .schema
1569 .iter()
1570 .map(|c| c.column_name.clone())
1571 .collect()
1572 }
1573
1574 #[test]
1575 fn test_conversion_synthesizes_resource_descriptor() {
1576 set_default_prefix(None).unwrap();
1577 let request = gauge_request(
1578 vec![keyvalue("service.name", "api"), keyvalue("host.id", "h-1")],
1579 "my_gauge",
1580 );
1581 let conversion = to_grpc_insert_requests(request, &mut descriptor_ctx()).unwrap();
1582
1583 let resource_info = conversion.resource_info.expect("descriptor synthesized");
1586 let descriptor_cols = column_names(&resource_info, OTEL_RESOURCE_INFO_TABLE_NAME);
1587 assert!(descriptor_cols.contains(&"host.id".to_string()));
1588 assert!(descriptor_cols.contains(&"service.name".to_string()));
1589 assert!(descriptor_cols.contains(&"job".to_string()));
1590 let metric_cols = column_names(&conversion.requests, "my_gauge");
1591 assert!(metric_cols.contains(&"service_name".to_string()));
1592 assert!(!metric_cols.contains(&"service.name".to_string()));
1593 assert!(!metric_cols.contains(&"host_id".to_string()));
1595
1596 let decoded = decode(&conversion.semantic_index);
1597 let t = &decoded[OTEL_RESOURCE_INFO_TABLE_NAME];
1598 assert_eq!(
1599 t.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
1600 Some("info")
1601 );
1602 assert_eq!(
1603 t.get(SEMANTIC_METRIC_METADATA_QUALITY).map(String::as_str),
1604 Some("declared")
1605 );
1606 }
1607
1608 #[test]
1609 fn test_descriptors_and_exemplars_exclude_points_rejected_by_destination_preflight() {
1610 let mut request = gauge_request(vec![keyvalue("service.name", "api")], "accepted");
1611 let Some(metric::Data::Gauge(gauge)) = request.resource_metrics[0].scope_metrics[0].metrics
1612 [0]
1613 .data
1614 .as_mut() else {
1615 unreachable!()
1616 };
1617 gauge.data_points[0].exemplars.push(Exemplar {
1618 time_unix_nano: 1,
1619 value: Some(exemplar::Value::AsDouble(0.5)),
1620 ..Default::default()
1621 });
1622 let mut rejected = request.resource_metrics[0].clone();
1623 let metric = &mut rejected.scope_metrics[0].metrics[0];
1624 metric.name = "rejected".into();
1625 let Some(metric::Data::Gauge(gauge)) = metric.data.as_mut() else {
1626 unreachable!()
1627 };
1628 gauge.data_points[0].time_unix_nano =
1629 2 * common_catalog::consts::SEMANTIC_GRAPH_WINDOW_NANOS as u64;
1630 request.resource_metrics.push(rejected);
1631 let mut prepared = prepare_metrics(
1632 request,
1633 &mut OtlpMetricCtx {
1634 with_metric_engine: true,
1635 ..descriptor_ctx()
1636 },
1637 )
1638 .unwrap();
1639 for request in &mut prepared.requests {
1640 let rows = request.rows.as_mut().unwrap();
1641 rows.schema.push(ColumnSchema {
1642 column_name: "defaulted_host".into(),
1643 datatype: api::v1::ColumnDataType::String as i32,
1644 semantic_type: SemanticType::Tag as i32,
1645 ..Default::default()
1646 });
1647 for row in &mut rows.rows {
1648 row.values.push(api::v1::Value {
1649 value_data: Some(ValueData::StringValue("unknown".into())),
1650 });
1651 }
1652 }
1653 let conversion = prepared
1654 .finish(
1655 vec![None, Some("incompatible schema".into())],
1656 &Default::default(),
1657 )
1658 .unwrap();
1659 assert_eq!(
1660 (
1661 conversion.outcome.accepted_data_points,
1662 conversion.outcome.rejected_data_points
1663 ),
1664 (1, 1)
1665 );
1666 assert_eq!(conversion.exemplars.len(), 1);
1667 assert_eq!(
1668 conversion.exemplars[0].series_labels["defaulted_host"],
1669 "unknown"
1670 );
1671 let (exemplars, _) = crate::exemplar::into_rows(conversion.exemplars).unwrap();
1672 let exemplar_rows = exemplars.inserts[0].rows.as_ref().unwrap();
1673 let metric = exemplar_rows
1674 .schema
1675 .iter()
1676 .position(|c| c.column_name == common_query::prelude::EXEMPLAR_METRIC_COLUMN)
1677 .unwrap();
1678 assert_eq!(
1679 exemplar_rows.rows[0].values[metric].value_data,
1680 Some(ValueData::StringValue("accepted".into()))
1681 );
1682 let descriptors = conversion.resource_info.unwrap();
1683 let rows = descriptors.inserts[0].rows.as_ref().unwrap();
1684 assert_eq!(rows.rows.len(), 1);
1685 let timestamp = rows
1686 .schema
1687 .iter()
1688 .position(|c| c.column_name == greptime_timestamp())
1689 .unwrap();
1690 assert_eq!(
1691 rows.rows[0].values[timestamp].value_data,
1692 Some(ValueData::TimestampMillisecondValue(1))
1693 );
1694 }
1695
1696 #[test]
1697 fn test_finish_rejects_mismatched_point_counts() {
1698 for (requests, rejections) in [(0, 1), (2, 1), (1, 0), (1, 2)] {
1699 let mut prepared = prepare_metrics(
1700 gauge_request(vec![], "gauge"),
1701 &mut OtlpMetricCtx::default(),
1702 )
1703 .unwrap();
1704 prepared.requests.resize(requests, Default::default());
1705 let err = prepared
1706 .finish(vec![None; rejections], &Default::default())
1707 .unwrap_err();
1708 assert!(matches!(err, error::Error::Internal { .. }));
1709 }
1710 }
1711
1712 #[test]
1713 fn test_conversion_describes_only_accepted_exponential_histogram_windows() {
1714 set_default_prefix(None).unwrap();
1715 let window = common_catalog::consts::SEMANTIC_GRAPH_WINDOW_NANOS as u64;
1716 let mut valid = exponential_point();
1717 valid.time_unix_nano = window + 1_000_000;
1718
1719 let mut invalid_count = valid.clone();
1720 invalid_count.time_unix_nano += 8_000_000;
1721 invalid_count.count += 1;
1722 let mut invalid_scale = valid.clone();
1723 invalid_scale.time_unix_nano += window;
1724 invalid_scale.scale = -5;
1725 let invalid_native = ExponentialHistogramDataPoint {
1727 time_unix_nano: 3 * window + 1_000_000,
1728 count: 1,
1729 positive: Some(exponential_buckets(2000, vec![1])),
1730 ..Default::default()
1731 };
1732 assert!(
1733 exponential_histogram_value(&invalid_native, AggregationTemporality::Cumulative)
1734 .unwrap_err()
1735 .contains("out of range")
1736 );
1737 let stale = ExponentialHistogramDataPoint {
1738 time_unix_nano: 4 * window + 1_000_000,
1739 count: u64::MAX,
1740 scale: i32::MIN,
1741 flags: DataPointFlags::NoRecordedValueMask as u32,
1742 ..Default::default()
1743 };
1744 let invalid_time = ExponentialHistogramDataPoint {
1745 time_unix_nano: 5 * window + 1_000_000,
1746 start_time_unix_nano: 5 * window + 2_000_000,
1747 ..stale.clone()
1748 };
1749 let rejected = vec![invalid_count, invalid_scale, invalid_native, invalid_time];
1750 let mut mixed = rejected.clone();
1751 mixed.extend([valid, stale]);
1752 for (points, expected) in [
1753 (rejected, vec![]),
1754 (
1755 mixed,
1756 vec![
1757 (window / 1_000_000 + 1) as i64,
1758 (4 * window / 1_000_000 + 1) as i64,
1759 ],
1760 ),
1761 ] {
1762 let mut request = metrics_request(vec![exponential_metric(
1763 "latency",
1764 points,
1765 AggregationTemporality::Cumulative,
1766 )]);
1767 request.resource_metrics[0].resource = Some(Resource {
1768 attributes: vec![keyvalue("service.name", "api")],
1769 ..Default::default()
1770 });
1771 let conversion = to_grpc_insert_requests(request, &mut descriptor_ctx()).unwrap();
1772 assert_eq!(conversion.outcome.rejected_data_points, 4);
1773 assert_eq!(
1774 conversion.outcome.accepted_data_points,
1775 expected.len() as i64
1776 );
1777 assert_eq!(conversion.rows, expected.len());
1778 assert_eq!(conversion.resource_info.is_some(), !expected.is_empty());
1779 let actual = conversion
1780 .resource_info
1781 .map(|descriptor| {
1782 let rows = descriptor.inserts[0].rows.as_ref().unwrap();
1783 let ts_index = rows
1784 .schema
1785 .iter()
1786 .position(|column| column.column_name == greptime_timestamp())
1787 .unwrap();
1788 rows.rows
1789 .iter()
1790 .map(|row| match row.values[ts_index].value_data {
1791 Some(ValueData::TimestampMillisecondValue(ts)) => ts,
1792 _ => panic!("expected descriptor timestamp"),
1793 })
1794 .collect::<Vec<_>>()
1795 })
1796 .unwrap_or_default();
1797 assert_eq!(actual, expected);
1798 }
1799 }
1800
1801 #[test]
1802 fn test_conversion_skips_descriptor_on_metric_name_collision() {
1803 set_default_prefix(None).unwrap();
1804 for (enabled, attributes, warns) in [
1805 (true, vec![keyvalue("service.name", "api")], true),
1806 (false, vec![keyvalue("service.name", "api")], false),
1807 (true, vec![keyvalue("unprojected", "value")], false),
1808 ] {
1809 let logs = tempfile::NamedTempFile::new().unwrap();
1810 let writer = logs.reopen().unwrap();
1811 let subscriber = common_telemetry::tracing_subscriber::fmt()
1812 .without_time()
1813 .with_ansi(false)
1814 .with_writer(move || writer.try_clone().unwrap())
1815 .finish();
1816 let conversion = tracing::subscriber::with_default(subscriber, || {
1817 let mut ctx = descriptor_ctx();
1818 ctx.resource_info = enabled;
1819 to_grpc_insert_requests(
1820 gauge_request(attributes, OTEL_RESOURCE_INFO_TABLE_NAME),
1821 &mut ctx,
1822 )
1823 .unwrap()
1824 });
1825 let diagnostics = std::fs::read_to_string(logs.path()).unwrap();
1826 assert_eq!(
1827 diagnostics.contains("Skipping OTLP resource descriptor"),
1828 warns
1829 );
1830 assert!(conversion.resource_info.is_none());
1831 assert!(
1832 conversion
1833 .requests
1834 .inserts
1835 .iter()
1836 .any(|r| r.table_name == OTEL_RESOURCE_INFO_TABLE_NAME)
1837 );
1838 }
1839 }
1840
1841 #[test]
1842 fn test_job_composition_follows_service_namespace() {
1843 let mut attrs = vec![
1844 keyvalue("service.name", "api"),
1845 keyvalue("service.namespace", "shop"),
1846 ];
1847 process_resource_attrs(&mut attrs, &OtlpMetricCtx::default());
1848 assert_eq!(attr_value(&attrs, "job").as_deref(), Some("shop/api"));
1849
1850 let mut attrs = vec![keyvalue("service.name", "api")];
1851 process_resource_attrs(&mut attrs, &OtlpMetricCtx::default());
1852 assert_eq!(attr_value(&attrs, "job").as_deref(), Some("api"));
1853
1854 let mut attrs = vec![
1855 keyvalue("service.name", "api"),
1856 keyvalue("service.namespace", ""),
1857 ];
1858 process_resource_attrs(&mut attrs, &OtlpMetricCtx::default());
1859 assert_eq!(attr_value(&attrs, "job").as_deref(), Some("api"));
1860
1861 let mut attrs = vec![
1863 keyvalue("service.namespace", "shop"),
1864 keyvalue("service.instance.id", "inst-1"),
1865 ];
1866 process_resource_attrs(&mut attrs, &OtlpMetricCtx::default());
1867 assert_eq!(attr_value(&attrs, "job"), None);
1868 assert_eq!(attr_value(&attrs, "instance").as_deref(), Some("inst-1"));
1869 }
1870
1871 #[test]
1872 fn test_reject_metric_name_attribute() {
1873 set_default_prefix(None).unwrap();
1874 for (strategy, key) in [
1875 (
1876 OtlpMetricTranslationStrategy::UnderscoreEscapingWithSuffixes,
1877 "__name__",
1878 ),
1879 (
1880 OtlpMetricTranslationStrategy::UnderscoreEscapingWithoutSuffixes,
1881 "__name__",
1882 ),
1883 (
1884 OtlpMetricTranslationStrategy::NoUtf8EscapingWithSuffixes,
1885 "__name__",
1886 ),
1887 (OtlpMetricTranslationStrategy::NoTranslation, "__name__"),
1888 (
1889 OtlpMetricTranslationStrategy::UnderscoreEscapingWithSuffixes,
1890 "..name..",
1891 ),
1892 ] {
1893 for source in [
1894 AttributeType::Resource,
1895 AttributeType::Scope,
1896 AttributeType::DataPoint,
1897 ] {
1898 let mut request = gauge_request(vec![], "source_metric");
1899 let resource = &mut request.resource_metrics[0];
1900 let scope = &mut resource.scope_metrics[0];
1901 let Some(metric::Data::Gauge(gauge)) = scope.metrics[0].data.as_mut() else {
1902 unreachable!();
1903 };
1904 gauge.data_points[0].exemplars.push(Exemplar {
1905 value: Some(exemplar::Value::AsDouble(0.25)),
1906 time_unix_nano: 1_000_000,
1907 ..Default::default()
1908 });
1909 let attributes = vec![keyvalue(key, "wrong_metric")];
1910 match source {
1911 AttributeType::Resource => {
1912 resource.resource.as_mut().unwrap().attributes = attributes;
1913 }
1914 AttributeType::Scope => {
1915 scope.scope = Some(InstrumentationScope {
1916 attributes,
1917 ..Default::default()
1918 });
1919 }
1920 AttributeType::DataPoint => gauge.data_points[0].attributes = attributes,
1921 }
1922 let mut ctx = OtlpMetricCtx {
1923 with_metric_engine: true,
1924 metric_translation_strategy: strategy,
1925 promote_all_resource_attrs: true,
1926 promote_scope_attrs: true,
1927 ..Default::default()
1928 };
1929 let result = to_grpc_insert_requests(request, &mut ctx);
1930 if source == AttributeType::Scope {
1931 let conversion = result.unwrap();
1932 assert_eq!(1, conversion.exemplars.len());
1933 assert!(
1934 column_names(&conversion.requests, "source_metric")
1935 .contains(&"otel_scope___name__".to_string())
1936 );
1937 } else {
1938 let conversion = result.unwrap();
1939 assert_eq!(conversion.outcome.accepted_data_points, 0);
1940 assert_eq!(conversion.outcome.rejected_data_points, 1);
1941 assert_eq!(conversion.exemplars.len(), 0);
1942 assert!(conversion.outcome.error_message.unwrap().contains(&format!(
1943 "OTLP attribute `{key}` resolves to reserved label `__name__`"
1944 )));
1945 }
1946 }
1947 }
1948 }
1949
1950 #[test]
1951 fn test_boolean_attributes_keep_series_distinct() {
1952 set_default_prefix(None).unwrap();
1953 let request = metrics_request(vec![Metric {
1954 name: "bool_repro".to_string(),
1955 data: Some(metric::Data::Sum(Sum {
1956 data_points: vec![
1957 NumberDataPoint {
1958 attributes: vec![bool_keyvalue("failed", false)],
1959 start_time_unix_nano: 1_000_000,
1960 time_unix_nano: 2_000_000,
1961 value: Some(Value::AsDouble(100.0)),
1962 ..Default::default()
1963 },
1964 NumberDataPoint {
1965 attributes: vec![bool_keyvalue("failed", true)],
1966 start_time_unix_nano: 1_000_000,
1967 time_unix_nano: 2_000_000,
1968 value: Some(Value::AsDouble(7.0)),
1969 ..Default::default()
1970 },
1971 ],
1972 aggregation_temporality: AggregationTemporality::Cumulative as i32,
1973 is_monotonic: true,
1974 })),
1975 ..Default::default()
1976 }]);
1977 let conversion = to_grpc_insert_requests(request, &mut OtlpMetricCtx::default()).unwrap();
1978
1979 let rows = conversion
1980 .requests
1981 .inserts
1982 .iter()
1983 .find(|insert| insert.table_name == "bool_repro_total")
1984 .expect("missing bool_repro_total table")
1985 .rows
1986 .as_ref()
1987 .unwrap();
1988 assert_eq!(2, rows.rows.len());
1989 let failed = rows
1990 .schema
1991 .iter()
1992 .position(|column| column.column_name == "failed")
1993 .expect("boolean attribute `failed` was dropped from the label set");
1994 let values = rows
1995 .rows
1996 .iter()
1997 .map(|row| row.values[failed].value_data.clone())
1998 .collect::<Vec<_>>();
1999 assert_eq!(
2000 vec![
2001 Some(ValueData::StringValue("false".to_string())),
2002 Some(ValueData::StringValue("true".to_string())),
2003 ],
2004 values
2005 );
2006 }
2007
2008 #[test]
2009 fn test_encode_gauge() {
2010 let mut tables = MultiTableData::default();
2011
2012 let data_points = vec![
2013 NumberDataPoint {
2014 attributes: vec![keyvalue("host", "testsevrer")],
2015 time_unix_nano: 100,
2016 value: Some(Value::AsInt(100)),
2017 ..Default::default()
2018 },
2019 NumberDataPoint {
2020 attributes: vec![keyvalue("host", "testserver")],
2021 time_unix_nano: 105,
2022 value: Some(Value::AsInt(105)),
2023 ..Default::default()
2024 },
2025 ];
2026 let gauge = Gauge { data_points };
2027 encode_gauge(
2028 &mut tables,
2029 "datamon",
2030 &gauge,
2031 Some(&vec![]),
2032 Some(&vec![keyvalue("scope", "otel")]),
2033 &OtlpMetricCtx::default(),
2034 &mut Vec::new(),
2035 )
2036 .unwrap();
2037
2038 let table = tables.get_or_default_table_data("datamon", 0, 0);
2039 assert_eq!(table.num_rows(), 2);
2040 assert_eq!(table.num_columns(), 4);
2041 assert_eq!(
2042 table
2043 .columns()
2044 .iter()
2045 .map(|c| &c.column_name)
2046 .collect::<Vec<&String>>(),
2047 vec![
2048 "otel_scope_scope",
2049 "host",
2050 greptime_timestamp(),
2051 greptime_value()
2052 ]
2053 );
2054 }
2055
2056 #[test]
2057 fn test_encode_sum() {
2058 let mut tables = MultiTableData::default();
2059
2060 let data_points = vec![
2061 NumberDataPoint {
2062 attributes: vec![keyvalue("host", "testserver")],
2063 time_unix_nano: 100,
2064 value: Some(Value::AsInt(100)),
2065 ..Default::default()
2066 },
2067 NumberDataPoint {
2068 attributes: vec![keyvalue("host", "testserver")],
2069 time_unix_nano: 105,
2070 value: Some(Value::AsInt(0)),
2071 ..Default::default()
2072 },
2073 ];
2074 let sum = Sum {
2075 data_points,
2076 ..Default::default()
2077 };
2078 encode_sum(
2079 &mut tables,
2080 "datamon",
2081 &sum,
2082 Some(&vec![]),
2083 Some(&vec![keyvalue("scope", "otel")]),
2084 &OtlpMetricCtx::default(),
2085 &mut Vec::new(),
2086 )
2087 .unwrap();
2088
2089 let table = tables.get_or_default_table_data("datamon", 0, 0);
2090 assert_eq!(table.num_rows(), 2);
2091 assert_eq!(table.num_columns(), 4);
2092 assert_eq!(
2093 table
2094 .columns()
2095 .iter()
2096 .map(|c| &c.column_name)
2097 .collect::<Vec<&String>>(),
2098 vec![
2099 "otel_scope_scope",
2100 "host",
2101 greptime_timestamp(),
2102 greptime_value()
2103 ]
2104 );
2105 }
2106
2107 #[test]
2108 fn test_encode_summary() {
2109 let mut tables = MultiTableData::default();
2110
2111 let data_points = vec![SummaryDataPoint {
2112 attributes: vec![keyvalue("host", "testserver")],
2113 time_unix_nano: 100,
2114 count: 25,
2115 sum: 5400.0,
2116 quantile_values: vec![
2117 ValueAtQuantile {
2118 quantile: 0.90,
2119 value: 1000.0,
2120 },
2121 ValueAtQuantile {
2122 quantile: 0.95,
2123 value: 3030.0,
2124 },
2125 ],
2126 ..Default::default()
2127 }];
2128 let summary = Summary { data_points };
2129 encode_summary(
2130 &mut tables,
2131 "datamon",
2132 &summary,
2133 Some(&vec![]),
2134 Some(&vec![keyvalue("scope", "otel")]),
2135 &OtlpMetricCtx::default(),
2136 )
2137 .unwrap();
2138
2139 let table = tables.get_or_default_table_data("datamon", 0, 0);
2140 assert_eq!(table.num_rows(), 2);
2141 assert_eq!(table.num_columns(), 5);
2142 assert_eq!(
2143 table
2144 .columns()
2145 .iter()
2146 .map(|c| &c.column_name)
2147 .collect::<Vec<&String>>(),
2148 vec![
2149 "otel_scope_scope",
2150 "host",
2151 greptime_timestamp(),
2152 "quantile",
2153 greptime_value()
2154 ]
2155 );
2156
2157 let table = tables.get_or_default_table_data("datamon_count", 0, 0);
2158 assert_eq!(table.num_rows(), 1);
2159 assert_eq!(table.num_columns(), 4);
2160 assert_eq!(
2161 table
2162 .columns()
2163 .iter()
2164 .map(|c| &c.column_name)
2165 .collect::<Vec<&String>>(),
2166 vec![
2167 "otel_scope_scope",
2168 "host",
2169 greptime_timestamp(),
2170 greptime_value()
2171 ]
2172 );
2173
2174 let table = tables.get_or_default_table_data("datamon_sum", 0, 0);
2175 assert_eq!(table.num_rows(), 1);
2176 assert_eq!(table.num_columns(), 4);
2177 assert_eq!(
2178 table
2179 .columns()
2180 .iter()
2181 .map(|c| &c.column_name)
2182 .collect::<Vec<&String>>(),
2183 vec![
2184 "otel_scope_scope",
2185 "host",
2186 greptime_timestamp(),
2187 greptime_value()
2188 ]
2189 );
2190 }
2191
2192 #[test]
2193 fn test_encode_histogram() {
2194 let mut tables = MultiTableData::default();
2195 let mut outcome = MetricsIngestOutcome::default();
2196
2197 let data_points = vec![HistogramDataPoint {
2198 attributes: vec![keyvalue("host", "testserver")],
2199 time_unix_nano: 100,
2200 start_time_unix_nano: 23,
2201 count: 25,
2202 sum: Some(100.),
2203 max: Some(200.),
2204 min: Some(0.03),
2205 bucket_counts: vec![2, 4, 6, 9, 4],
2206 explicit_bounds: vec![0.1, 1., 10., 100.],
2207 ..Default::default()
2208 }];
2209
2210 let histogram = Histogram {
2211 data_points,
2212 aggregation_temporality: AggregationTemporality::Delta.into(),
2213 };
2214 encode_histogram(
2215 &mut tables,
2216 "histo",
2217 &histogram,
2218 Some(&vec![]),
2219 Some(&vec![keyvalue("scope", "otel")]),
2220 &OtlpMetricCtx::default(),
2221 &mut outcome,
2222 &mut Vec::new(),
2223 )
2224 .unwrap();
2225
2226 assert_eq!(3, tables.num_tables());
2227 assert_eq!(1, outcome.accepted_data_points);
2228
2229 let bucket_table = tables.get_or_default_table_data("histo_bucket", 0, 0);
2231 assert_eq!(bucket_table.num_rows(), 5);
2232 assert_eq!(bucket_table.num_columns(), 6);
2233 assert_eq!(
2234 bucket_table
2235 .columns()
2236 .iter()
2237 .map(|c| &c.column_name)
2238 .collect::<Vec<&String>>(),
2239 vec![
2240 "otel_scope_scope",
2241 "host",
2242 greptime_timestamp(),
2243 OTLP_AGGREGATION_TEMPORALITY_LABEL,
2244 "le",
2245 greptime_value(),
2246 ]
2247 );
2248
2249 let sum_table = tables.get_or_default_table_data("histo_sum", 0, 0);
2250 assert_eq!(sum_table.num_rows(), 1);
2251 assert_eq!(sum_table.num_columns(), 5);
2252 assert_eq!(
2253 sum_table
2254 .columns()
2255 .iter()
2256 .map(|c| &c.column_name)
2257 .collect::<Vec<&String>>(),
2258 vec![
2259 "otel_scope_scope",
2260 "host",
2261 greptime_timestamp(),
2262 OTLP_AGGREGATION_TEMPORALITY_LABEL,
2263 greptime_value()
2264 ]
2265 );
2266
2267 let count_table = tables.get_or_default_table_data("histo_count", 0, 0);
2268 assert_eq!(count_table.num_rows(), 1);
2269 assert_eq!(count_table.num_columns(), 5);
2270 assert_eq!(
2271 count_table
2272 .columns()
2273 .iter()
2274 .map(|c| &c.column_name)
2275 .collect::<Vec<&String>>(),
2276 vec![
2277 "otel_scope_scope",
2278 "host",
2279 greptime_timestamp(),
2280 OTLP_AGGREGATION_TEMPORALITY_LABEL,
2281 greptime_value()
2282 ]
2283 );
2284 }
2285
2286 use std::collections::BTreeMap;
2287
2288 use table::requests::validate_semantic_option;
2289
2290 fn decode(index: &SemanticIndex) -> BTreeMap<String, BTreeMap<String, String>> {
2291 let nested: BTreeMap<String, BTreeMap<String, BTreeMap<String, String>>> =
2292 serde_json::from_str(&index.encode("public").expect("non-empty index")).unwrap();
2293 nested.into_values().next().unwrap()
2294 }
2295
2296 fn record(metric: &Metric, metric_type: MetricType, name: &str) -> SemanticIndex {
2297 let ctx = OtlpMetricCtx {
2298 metric_type,
2299 ..Default::default()
2300 };
2301 let mut index = SemanticIndex::default();
2302 record_metric_semantics(
2303 &mut index,
2304 metric,
2305 name,
2306 &emitted_semantic_tables(&ctx.metric_type, name),
2307 );
2308 index
2309 }
2310
2311 #[test]
2312 fn test_metric_type_constants_validate() {
2313 for value in [
2314 METRIC_TYPE_COUNTER,
2315 METRIC_TYPE_UPDOWN_COUNTER,
2316 METRIC_TYPE_GAUGE,
2317 METRIC_TYPE_HISTOGRAM,
2318 METRIC_TYPE_SUMMARY,
2319 ] {
2320 assert!(
2321 validate_semantic_option(SEMANTIC_METRIC_TYPE, value),
2322 "metric.type value `{value}` must be in the vocabulary domain"
2323 );
2324 }
2325 for value in ["delta", "cumulative"] {
2326 assert!(validate_semantic_option(SEMANTIC_METRIC_TEMPORALITY, value));
2327 }
2328 }
2329
2330 #[test]
2331 fn test_record_monotonic_sum() {
2332 let metric = Metric {
2333 name: "claude_code.cost.usage".to_string(),
2334 unit: "USD".to_string(),
2335 data: Some(metric::Data::Sum(Sum {
2336 aggregation_temporality: AggregationTemporality::Delta as i32,
2337 is_monotonic: true,
2338 ..Default::default()
2339 })),
2340 ..Default::default()
2341 };
2342 let index = record(
2343 &metric,
2344 MetricType::MonotonicSum,
2345 "claude_code_cost_usage_USD_total",
2346 );
2347 let decoded = decode(&index);
2348 let t = &decoded["claude_code_cost_usage_USD_total"];
2349
2350 assert_eq!(
2351 t.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
2352 Some("counter")
2353 );
2354 assert_eq!(
2355 t.get(SEMANTIC_METRIC_TEMPORALITY).map(String::as_str),
2356 Some("delta")
2357 );
2358 assert_eq!(t.get(SEMANTIC_METRIC_UNIT).map(String::as_str), Some("USD"));
2359 assert_eq!(
2360 t.get(SEMANTIC_METRIC_ORIGINAL_NAME).map(String::as_str),
2361 Some("claude_code.cost.usage")
2362 );
2363 assert_eq!(
2364 t.get(SEMANTIC_METRIC_METADATA_QUALITY).map(String::as_str),
2365 Some("declared")
2366 );
2367 }
2368
2369 #[test]
2370 fn test_record_non_monotonic_sum() {
2371 let metric = Metric {
2372 name: "queue_size".to_string(),
2373 data: Some(metric::Data::Sum(Sum {
2374 aggregation_temporality: AggregationTemporality::Cumulative as i32,
2375 is_monotonic: false,
2376 ..Default::default()
2377 })),
2378 ..Default::default()
2379 };
2380 let index = record(&metric, MetricType::NonMonotonicSum, "queue_size");
2381 let decoded = decode(&index);
2382 let t = &decoded["queue_size"];
2383 assert_eq!(
2384 t.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
2385 Some("updown_counter")
2386 );
2387 assert_eq!(
2388 t.get(SEMANTIC_METRIC_TEMPORALITY).map(String::as_str),
2389 Some("cumulative")
2390 );
2391 assert_eq!(t.get(SEMANTIC_METRIC_ORIGINAL_NAME), None);
2393 }
2394
2395 #[test]
2396 fn test_record_gauge_has_no_temporality() {
2397 let metric = Metric {
2398 name: "temperature".to_string(),
2399 data: Some(metric::Data::Gauge(Gauge::default())),
2400 ..Default::default()
2401 };
2402 let index = record(&metric, MetricType::Gauge, "temperature");
2403 let decoded = decode(&index);
2404 let t = &decoded["temperature"];
2405 assert_eq!(
2406 t.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
2407 Some("gauge")
2408 );
2409 assert_eq!(t.get(SEMANTIC_METRIC_TEMPORALITY), None);
2410 }
2411
2412 #[test]
2413 fn test_record_histogram_fans_out_with_distinct_types() {
2414 let metric = Metric {
2415 name: "request.duration".to_string(),
2416 unit: "s".to_string(),
2417 data: Some(metric::Data::Histogram(Histogram {
2418 aggregation_temporality: AggregationTemporality::Cumulative as i32,
2419 ..Default::default()
2420 })),
2421 ..Default::default()
2422 };
2423 let index = record(&metric, MetricType::Histogram, "request_duration");
2424 let decoded = decode(&index);
2425
2426 let bucket = &decoded["request_duration_bucket"];
2427 assert_eq!(
2428 bucket.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
2429 Some("histogram")
2430 );
2431 assert_eq!(
2432 bucket.get(SEMANTIC_METRIC_UNIT).map(String::as_str),
2433 Some("s")
2434 );
2435
2436 for companion in ["request_duration_sum", "request_duration_count"] {
2437 let t = &decoded[companion];
2438 assert_eq!(
2439 t.get(SEMANTIC_METRIC_TYPE).map(String::as_str),
2440 Some("counter")
2441 );
2442 assert_eq!(
2443 t.get(SEMANTIC_METRIC_TEMPORALITY).map(String::as_str),
2444 Some("cumulative")
2445 );
2446 }
2447 }
2448
2449 #[test]
2450 fn test_record_summary_fans_out() {
2451 let metric = Metric {
2452 name: "rpc.latency".to_string(),
2453 data: Some(metric::Data::Summary(Summary::default())),
2454 ..Default::default()
2455 };
2456 let index = record(&metric, MetricType::Summary, "rpc_latency");
2457 let decoded = decode(&index);
2458
2459 assert_eq!(
2460 decoded["rpc_latency"]
2461 .get(SEMANTIC_METRIC_TYPE)
2462 .map(String::as_str),
2463 Some("summary")
2464 );
2465 assert_eq!(
2467 decoded["rpc_latency"].get(SEMANTIC_METRIC_TEMPORALITY),
2468 None
2469 );
2470 for companion in ["rpc_latency_count", "rpc_latency_sum"] {
2471 assert_eq!(
2472 decoded[companion]
2473 .get(SEMANTIC_METRIC_TYPE)
2474 .map(String::as_str),
2475 Some("counter")
2476 );
2477 }
2478 }
2479
2480 fn exponential_buckets(
2481 offset: i32,
2482 bucket_counts: Vec<u64>,
2483 ) -> exponential_histogram_data_point::Buckets {
2484 exponential_histogram_data_point::Buckets {
2485 offset,
2486 bucket_counts,
2487 }
2488 }
2489
2490 fn exponential_point() -> ExponentialHistogramDataPoint {
2491 ExponentialHistogramDataPoint {
2492 start_time_unix_nano: 1_000_000,
2493 time_unix_nano: 2_000_000,
2494 count: 28,
2495 sum: None,
2496 scale: 9,
2497 zero_count: 7,
2498 positive: Some(exponential_buckets(-3, vec![1, 2, 3, 4, 5, 6])),
2499 zero_threshold: 0.0,
2500 ..Default::default()
2501 }
2502 }
2503
2504 fn native_field(value: &ValueData, name: &str) -> Option<ValueData> {
2505 let ValueData::StructValue(value) = value else {
2506 panic!("expected native histogram Struct value");
2507 };
2508 let index = common_query::native_histogram::NATIVE_HISTOGRAM_FIELD_NAMES
2509 .iter()
2510 .position(|field| *field == name)
2511 .unwrap();
2512 value.items[index].value_data.clone()
2513 }
2514
2515 fn i32_list(value: Option<ValueData>) -> Vec<i32> {
2516 let Some(ValueData::ListValue(value)) = value else {
2517 panic!("expected i32 list");
2518 };
2519 value
2520 .items
2521 .into_iter()
2522 .map(|item| match item.value_data {
2523 Some(ValueData::I32Value(value)) => value,
2524 _ => panic!("expected i32 value"),
2525 })
2526 .collect()
2527 }
2528
2529 fn i64_list(value: Option<ValueData>) -> Vec<i64> {
2530 let Some(ValueData::ListValue(value)) = value else {
2531 panic!("expected i64 list");
2532 };
2533 value
2534 .items
2535 .into_iter()
2536 .map(|item| match item.value_data {
2537 Some(ValueData::I64Value(value)) => value,
2538 _ => panic!("expected i64 value"),
2539 })
2540 .collect()
2541 }
2542
2543 #[test]
2544 fn test_downscale_bucket_index_uses_signed_floor_division() {
2545 assert_eq!(downscale_bucket_index(-3, 1).unwrap(), -2);
2546 assert_eq!(downscale_bucket_index(-2, 1).unwrap(), -1);
2547 assert_eq!(downscale_bucket_index(-1, 1).unwrap(), -1);
2548 assert_eq!(downscale_bucket_index(0, 1).unwrap(), 0);
2549 assert_eq!(downscale_bucket_index(1, 1).unwrap(), 0);
2550 assert_eq!(downscale_bucket_index(2, 1).unwrap(), 1);
2551 assert_eq!(downscale_bucket_index(i32::MIN, 32).unwrap(), -1);
2552 assert_eq!(downscale_bucket_index(i32::MAX, 32).unwrap(), 0);
2553 }
2554
2555 #[test]
2556 fn test_convert_bucket_range_downscales_before_prometheus_shift() {
2557 let buckets = exponential_buckets(-3, vec![1, 2, 3, 4, 5, 6]);
2558 let (spans, deltas, total) = convert_bucket_range("positive", Some(&buckets), 1).unwrap();
2559
2560 assert_eq!(
2561 spans,
2562 vec![BucketSpan {
2563 offset: -1,
2564 length: 4
2565 }]
2566 );
2567 assert_eq!(deltas, vec![1, 4, 4, -3]);
2568 assert_eq!(total, 21);
2569 }
2570
2571 #[test]
2572 fn test_convert_bucket_range_compacts_zero_runs() {
2573 let large = (1u64 << 53) + 1;
2574 for (offset, counts, shift, expected_spans, expected_deltas, expected_total) in [
2575 (
2576 -3,
2577 vec![0, 3, 0, 0, 7, 9, 0],
2578 0,
2579 vec![(-1, 1), (2, 2)],
2580 vec![3, 4, 2],
2581 19,
2582 ),
2583 (
2584 -4,
2585 vec![0, 0, 2, 3, 0, 0, 0, 0, 7, 0],
2586 1,
2587 vec![(0, 1), (2, 1)],
2588 vec![5, 2],
2589 12,
2590 ),
2591 (0, vec![0, 0, 0], 0, vec![], vec![], 0),
2592 (0, vec![], 0, vec![], vec![], 0),
2593 (i32::MIN, vec![0, 1], 0, vec![(i32::MIN + 2, 1)], vec![1], 1),
2594 (i32::MAX - 1, vec![1], 0, vec![(i32::MAX, 1)], vec![1], 1),
2595 (
2596 0,
2597 vec![large, 0, large + 2],
2598 0,
2599 vec![(1, 1), (1, 1)],
2600 vec![large as i64, 2],
2601 large * 2 + 2,
2602 ),
2603 ] {
2604 let buckets = exponential_buckets(offset, counts);
2605 let (spans, deltas, total) =
2606 convert_bucket_range("positive", Some(&buckets), shift).unwrap();
2607 assert_eq!(
2608 spans
2609 .iter()
2610 .map(|span| (span.offset, span.length))
2611 .collect::<Vec<_>>(),
2612 expected_spans,
2613 "{buckets:?}, shift={shift}"
2614 );
2615 assert_eq!(deltas, expected_deltas, "{buckets:?}, shift={shift}");
2616 assert_eq!(total, expected_total);
2617 }
2618
2619 let buckets = exponential_buckets(i32::MAX, vec![0]);
2621 assert!(
2622 convert_bucket_range("positive", Some(&buckets), 0)
2623 .unwrap_err()
2624 .contains("shifted bucket index overflows")
2625 );
2626 }
2627
2628 #[test]
2629 fn test_exponential_histogram_compaction_preserves_integer_counts() {
2630 use common_query::native_histogram::{
2631 COUNT_I64_FIELD, NEGATIVE_BUCKETS_I64_FIELD, NEGATIVE_SPAN_LENGTHS_FIELD,
2632 NEGATIVE_SPAN_OFFSETS_FIELD, POSITIVE_BUCKETS_I64_FIELD, POSITIVE_SPAN_LENGTHS_FIELD,
2633 POSITIVE_SPAN_OFFSETS_FIELD,
2634 };
2635
2636 let large = (1u64 << 53) + 1;
2637 let buckets = exponential_buckets(-2, vec![0, large, 0, 3, 0]);
2638 let point = ExponentialHistogramDataPoint {
2639 count: 2 * (large + 3),
2640 positive: Some(buckets.clone()),
2641 negative: Some(buckets),
2642 ..Default::default()
2643 };
2644 let (value, _) =
2645 exponential_histogram_value(&point, AggregationTemporality::Cumulative).unwrap();
2646 assert_eq!(
2647 native_field(&value, COUNT_I64_FIELD),
2648 Some(ValueData::I64Value(point.count as i64))
2649 );
2650 for (offsets, lengths, counts) in [
2651 (
2652 POSITIVE_SPAN_OFFSETS_FIELD,
2653 POSITIVE_SPAN_LENGTHS_FIELD,
2654 POSITIVE_BUCKETS_I64_FIELD,
2655 ),
2656 (
2657 NEGATIVE_SPAN_OFFSETS_FIELD,
2658 NEGATIVE_SPAN_LENGTHS_FIELD,
2659 NEGATIVE_BUCKETS_I64_FIELD,
2660 ),
2661 ] {
2662 assert_eq!(i32_list(native_field(&value, offsets)), vec![0, 1]);
2663 assert_eq!(i32_list(native_field(&value, lengths)), vec![1, 1]);
2664 assert_eq!(
2665 i64_list(native_field(&value, counts)),
2666 vec![large as i64, 3]
2667 );
2668 }
2669 }
2670
2671 #[test]
2672 fn test_exponential_histogram_value_uses_integer_family() {
2673 use common_query::native_histogram::{
2674 COUNT_F64_FIELD, COUNT_I64_FIELD, POSITIVE_BUCKETS_I64_FIELD,
2675 POSITIVE_SPAN_LENGTHS_FIELD, POSITIVE_SPAN_OFFSETS_FIELD, SCHEMA_FIELD, SUM_FIELD,
2676 ZERO_COUNT_I64_FIELD,
2677 };
2678
2679 let (value, timestamp_nanos) =
2680 exponential_histogram_value(&exponential_point(), AggregationTemporality::Cumulative)
2681 .unwrap();
2682
2683 assert_eq!(timestamp_nanos, 2_000_000);
2684 assert_eq!(
2685 native_field(&value, SCHEMA_FIELD),
2686 Some(ValueData::I32Value(8))
2687 );
2688 assert_eq!(
2689 native_field(&value, COUNT_I64_FIELD),
2690 Some(ValueData::I64Value(28))
2691 );
2692 assert_eq!(
2693 native_field(&value, ZERO_COUNT_I64_FIELD),
2694 Some(ValueData::I64Value(7))
2695 );
2696 assert_eq!(native_field(&value, COUNT_F64_FIELD), None);
2697 assert_eq!(
2698 i32_list(native_field(&value, POSITIVE_SPAN_OFFSETS_FIELD)),
2699 vec![-1]
2700 );
2701 assert_eq!(
2702 i32_list(native_field(&value, POSITIVE_SPAN_LENGTHS_FIELD)),
2703 vec![4]
2704 );
2705 assert_eq!(
2706 i64_list(native_field(&value, POSITIVE_BUCKETS_I64_FIELD)),
2707 vec![1, 5, 9, 6]
2708 );
2709 let Some(ValueData::F64Value(sum)) = native_field(&value, SUM_FIELD) else {
2710 panic!("expected histogram sum");
2711 };
2712 assert_eq!(sum.to_bits(), f64::NAN.to_bits());
2713 assert_ne!(sum.to_bits(), PROMETHEUS_STALE_NAN_BITS);
2714 }
2715
2716 #[test]
2717 fn test_no_recorded_value_ignores_other_value_fields() {
2718 use common_query::native_histogram::{
2719 COUNT_I64_FIELD, POSITIVE_BUCKETS_I64_FIELD, SCHEMA_FIELD, SUM_FIELD,
2720 ZERO_COUNT_I64_FIELD, ZERO_THRESHOLD_FIELD,
2721 };
2722
2723 let point = ExponentialHistogramDataPoint {
2724 start_time_unix_nano: 1_000_000,
2725 time_unix_nano: 2_000_000,
2726 count: u64::MAX,
2727 sum: Some(1.0),
2728 scale: i32::MIN,
2729 zero_count: u64::MAX,
2730 positive: Some(exponential_buckets(i32::MAX, vec![u64::MAX])),
2731 flags: DataPointFlags::NoRecordedValueMask as u32,
2732 zero_threshold: f64::NAN,
2733 ..Default::default()
2734 };
2735 let (value, _) =
2736 exponential_histogram_value(&point, AggregationTemporality::Cumulative).unwrap();
2737
2738 assert_eq!(
2739 native_field(&value, SCHEMA_FIELD),
2740 Some(ValueData::I32Value(0))
2741 );
2742 assert_eq!(
2743 native_field(&value, ZERO_THRESHOLD_FIELD),
2744 Some(ValueData::F64Value(0.0))
2745 );
2746 assert_eq!(
2747 native_field(&value, COUNT_I64_FIELD),
2748 Some(ValueData::I64Value(0))
2749 );
2750 assert_eq!(
2751 native_field(&value, ZERO_COUNT_I64_FIELD),
2752 Some(ValueData::I64Value(0))
2753 );
2754 assert!(i64_list(native_field(&value, POSITIVE_BUCKETS_I64_FIELD)).is_empty());
2755 let Some(ValueData::F64Value(sum)) = native_field(&value, SUM_FIELD) else {
2756 panic!("expected histogram sum");
2757 };
2758 assert_eq!(sum.to_bits(), PROMETHEUS_STALE_NAN_BITS);
2759 }
2760
2761 #[test]
2762 fn test_non_flag_nan_sum_is_normalized() {
2763 use common_query::native_histogram::SUM_FIELD;
2764
2765 let point = ExponentialHistogramDataPoint {
2766 sum: Some(f64::from_bits(PROMETHEUS_STALE_NAN_BITS)),
2767 ..Default::default()
2768 };
2769 let (value, _) =
2770 exponential_histogram_value(&point, AggregationTemporality::Cumulative).unwrap();
2771 let Some(ValueData::F64Value(sum)) = native_field(&value, SUM_FIELD) else {
2772 panic!("expected histogram sum");
2773 };
2774 assert_eq!(sum.to_bits(), f64::NAN.to_bits());
2775 assert_ne!(sum.to_bits(), PROMETHEUS_STALE_NAN_BITS);
2776 }
2777
2778 #[test]
2779 fn test_exponential_histogram_rejects_invalid_values() {
2780 let mut cases = Vec::new();
2781
2782 let mut point = exponential_point();
2783 point.scale = -5;
2784 cases.push((point, "scale -5 is unsupported"));
2785
2786 let mut point = exponential_point();
2787 point.zero_threshold = f64::INFINITY;
2788 cases.push((point, "must be finite and non-negative"));
2789
2790 let mut point = exponential_point();
2791 point.start_time_unix_nano = point.time_unix_nano + 1;
2792 cases.push((point, "start_time_unix_nano"));
2793
2794 let mut point = exponential_point();
2795 point.count = 27;
2796 cases.push((point, "declared count is 27"));
2797
2798 let point = ExponentialHistogramDataPoint {
2799 count: u64::MAX,
2800 zero_count: u64::MAX,
2801 ..Default::default()
2802 };
2803 cases.push((point, "count 18446744073709551615 overflows i64"));
2804
2805 let point = ExponentialHistogramDataPoint {
2806 count: 1,
2807 scale: 8,
2808 positive: Some(exponential_buckets(i32::MAX, vec![1])),
2809 ..Default::default()
2810 };
2811 cases.push((point, "shifted bucket index overflows i32"));
2812
2813 for (point, expected) in cases {
2814 let error = exponential_histogram_value(&point, AggregationTemporality::Cumulative)
2815 .unwrap_err();
2816 assert!(
2817 error.contains(expected),
2818 "expected {expected:?}, got {error}"
2819 );
2820 }
2821
2822 let buckets = exponential_buckets(0, vec![u64::MAX, 1]);
2823 let error = convert_bucket_range("positive", Some(&buckets), 1).unwrap_err();
2824 assert!(
2825 error.contains("merged bucket count overflows u64"),
2826 "{error}"
2827 );
2828
2829 let buckets = exponential_buckets(i32::MAX, vec![1, 1]);
2830 let error = convert_bucket_range("positive", Some(&buckets), 1).unwrap_err();
2831 assert!(error.contains("bucket index overflows i32"), "{error}");
2832 }
2833
2834 fn metrics_request(metrics: Vec<Metric>) -> ExportMetricsServiceRequest {
2835 ExportMetricsServiceRequest {
2836 resource_metrics: vec![ResourceMetrics {
2837 scope_metrics: vec![ScopeMetrics {
2838 metrics,
2839 ..Default::default()
2840 }],
2841 ..Default::default()
2842 }],
2843 }
2844 }
2845
2846 #[test]
2847 fn test_invalid_exemplars_preserve_metric_payload() {
2848 use otel_arrow_rust::proto::opentelemetry::metrics::v1::{Exemplar, exemplar};
2849
2850 let valid = Exemplar {
2851 time_unix_nano: 1,
2852 value: Some(exemplar::Value::AsDouble(0.5)),
2853 ..Default::default()
2854 };
2855 for invalid in [
2856 Exemplar::default(),
2857 Exemplar {
2858 trace_id: vec![1],
2859 ..valid.clone()
2860 },
2861 Exemplar {
2862 span_id: vec![1],
2863 ..valid.clone()
2864 },
2865 Exemplar {
2866 time_unix_nano: u64::MAX,
2867 ..valid.clone()
2868 },
2869 Exemplar {
2870 filtered_attributes: vec![keyvalue("http.url", &"x".repeat(128))],
2871 ..valid.clone()
2872 },
2873 Exemplar {
2874 filtered_attributes: vec![keyvalue("a", "1"), keyvalue("a", "2")],
2875 ..valid.clone()
2876 },
2877 ] {
2878 let exemplars = vec![invalid.clone(), valid.clone(), invalid, valid.clone()];
2879 let point = NumberDataPoint {
2880 time_unix_nano: 1,
2881 value: Some(Value::AsDouble(1.0)),
2882 exemplars: exemplars.clone(),
2883 ..Default::default()
2884 };
2885 let metrics = vec![
2886 Metric {
2887 name: "gauge".into(),
2888 data: Some(metric::Data::Gauge(Gauge {
2889 data_points: vec![point.clone()],
2890 })),
2891 ..Default::default()
2892 },
2893 Metric {
2894 name: "sum".into(),
2895 data: Some(metric::Data::Sum(Sum {
2896 data_points: vec![point],
2897 aggregation_temporality: AggregationTemporality::Cumulative as i32,
2898 ..Default::default()
2899 })),
2900 ..Default::default()
2901 },
2902 exponential_metric(
2903 "histogram",
2904 vec![ExponentialHistogramDataPoint {
2905 exemplars,
2906 ..exponential_point()
2907 }],
2908 AggregationTemporality::Cumulative,
2909 ),
2910 ];
2911 let conversion = to_grpc_insert_requests(
2912 metrics_request(metrics),
2913 &mut OtlpMetricCtx {
2914 with_metric_engine: true,
2915 ..Default::default()
2916 },
2917 )
2918 .unwrap();
2919 assert_eq!(conversion.rows, 3);
2920 assert_eq!(conversion.outcome.accepted_data_points, 3);
2921 assert_eq!(conversion.outcome.rejected_data_points, 0);
2922 assert_eq!(conversion.exemplars.len(), 6);
2923 }
2924 }
2925
2926 #[test]
2927 fn test_histogram_exemplars_follow_emitted_buckets() {
2928 use common_query::prelude::EXEMPLAR_METRIC_COLUMN;
2929 use otel_arrow_rust::proto::opentelemetry::metrics::v1::{Exemplar, exemplar};
2930 use prost::Message;
2931
2932 for mode in ["delta", "empty", "stale", "missing_infinity"] {
2933 let mut point = HistogramDataPoint {
2934 time_unix_nano: 100,
2935 count: 4,
2936 explicit_bounds: vec![1.0, 2.0],
2937 bucket_counts: vec![1, 1, 2],
2938 exemplars: [5.0, 1.0, 0.5, f64::NEG_INFINITY, f64::INFINITY, f64::NAN]
2939 .into_iter()
2940 .enumerate()
2941 .map(|(i, value)| Exemplar {
2942 time_unix_nano: i as u64 + 1,
2943 value: Some(exemplar::Value::AsDouble(value)),
2944 ..Default::default()
2945 })
2946 .collect(),
2947 ..Default::default()
2948 };
2949 point.exemplars.extend([
2950 Exemplar::default(),
2951 Exemplar {
2952 trace_id: vec![1],
2953 value: Some(exemplar::Value::AsDouble(1.0)),
2954 ..Default::default()
2955 },
2956 ]);
2957 match mode {
2958 "empty" => {
2959 point.explicit_bounds.clear();
2960 point.bucket_counts.clear();
2961 }
2962 "stale" => point.flags = DataPointFlags::NoRecordedValueMask as u32,
2963 "missing_infinity" => {
2964 point.explicit_bounds = vec![1.0];
2966 point.bucket_counts = vec![1];
2967 point.count = 1;
2968 }
2969 _ => {}
2970 }
2971 let mut histogram = Histogram {
2972 data_points: vec![point],
2973 aggregation_temporality: if mode == "missing_infinity" {
2974 AggregationTemporality::Cumulative
2975 } else {
2976 AggregationTemporality::Delta
2977 } as i32,
2978 };
2979 if mode != "missing_infinity" {
2980 histogram.data_points.push(HistogramDataPoint {
2981 explicit_bounds: vec![1.0],
2982 bucket_counts: vec![4],
2983 count: 4,
2984 exemplars: vec![Exemplar::default()],
2985 ..Default::default()
2986 });
2987 }
2988 let mut request = gauge_request(vec![], "gauge");
2989 request.resource_metrics[0].scope_metrics[0]
2990 .metrics
2991 .push(Metric {
2992 name: "histogram".into(),
2993 data: Some(metric::Data::Histogram(histogram.clone())),
2994 ..Default::default()
2995 });
2996 let ctx = OtlpMetricCtx {
2997 with_metric_engine: true,
2998 ..Default::default()
2999 };
3000 let mut conversion =
3001 to_grpc_insert_requests(request.clone(), &mut ctx.clone()).unwrap();
3002 assert_eq!(conversion.outcome.accepted_data_points, 2, "{mode}");
3003 assert_eq!(
3004 conversion.outcome.rejected_data_points,
3005 i64::from(mode != "missing_infinity"),
3006 "{mode}"
3007 );
3008
3009 for point in &mut histogram.data_points {
3011 point.exemplars.clear();
3012 }
3013 request.resource_metrics[0].scope_metrics[0].metrics[1].data =
3014 Some(metric::Data::Histogram(histogram));
3015 let mut baseline = to_grpc_insert_requests(request, &mut ctx.clone()).unwrap();
3016 conversion
3017 .requests
3018 .inserts
3019 .sort_by(|a, b| a.table_name.cmp(&b.table_name));
3020 baseline
3021 .requests
3022 .inserts
3023 .sort_by(|a, b| a.table_name.cmp(&b.table_name));
3024 assert_eq!(
3026 conversion.requests.encode_to_vec(),
3027 baseline.requests.encode_to_vec(),
3028 "{mode}"
3029 );
3030
3031 let expected = match mode {
3032 "empty" => vec![
3033 (1, "inf"),
3034 (2, "inf"),
3035 (3, "inf"),
3036 (4, "inf"),
3037 (5, "inf"),
3038 (6, "inf"),
3039 ],
3040 "missing_infinity" => vec![(2, "1"), (3, "1"), (4, "1")],
3041 _ => vec![
3042 (1, "inf"),
3043 (2, "1"),
3044 (3, "1"),
3045 (4, "1"),
3046 (5, "inf"),
3047 (6, "inf"),
3048 ],
3049 };
3050 assert_eq!(conversion.exemplars.len(), expected.len(), "{mode}");
3051 let (exemplars, _) = crate::exemplar::into_rows(conversion.exemplars).unwrap();
3052 let rows = exemplars.inserts[0].rows.as_ref().unwrap();
3053 let index = |name| {
3054 rows.schema
3055 .iter()
3056 .position(|c| c.column_name == name)
3057 .unwrap()
3058 };
3059 let mut actual = Vec::new();
3060 for row in &rows.rows {
3061 assert_eq!(
3062 row.values[index(EXEMPLAR_METRIC_COLUMN)].value_data,
3063 Some(ValueData::StringValue("histogram_bucket".into()))
3064 );
3065 let Some(ValueData::TimestampNanosecondValue(timestamp)) =
3066 row.values[index(greptime_timestamp())].value_data
3067 else {
3068 panic!("missing exemplar timestamp")
3069 };
3070 let Some(ValueData::StringValue(ref bound)) = row.values[index("le")].value_data
3071 else {
3072 panic!("missing bucket label")
3073 };
3074 actual.push((timestamp, bound.as_str()));
3075 if timestamp == 3 {
3076 assert_eq!(
3077 row.values[index(greptime_value())].value_data,
3078 Some(ValueData::F64Value(0.5))
3079 );
3080 }
3081 }
3082 actual.sort_unstable();
3083 assert_eq!(actual, expected, "{mode}");
3084 }
3085 }
3086
3087 fn exponential_metric(
3088 name: impl Into<String>,
3089 data_points: Vec<ExponentialHistogramDataPoint>,
3090 temporality: AggregationTemporality,
3091 ) -> Metric {
3092 Metric {
3093 name: name.into(),
3094 data: Some(metric::Data::ExponentialHistogram(ExponentialHistogram {
3095 data_points,
3096 aggregation_temporality: temporality as i32,
3097 })),
3098 ..Default::default()
3099 }
3100 }
3101
3102 fn histogram_metric(name: impl Into<String>) -> Metric {
3103 Metric {
3104 name: name.into(),
3105 data: Some(metric::Data::Histogram(Histogram {
3106 data_points: vec![HistogramDataPoint {
3107 start_time_unix_nano: 1_000_000,
3108 time_unix_nano: 2_000_000,
3109 count: 1,
3110 sum: Some(1.0),
3111 bucket_counts: vec![1],
3112 ..Default::default()
3113 }],
3114 aggregation_temporality: AggregationTemporality::Cumulative as i32,
3115 })),
3116 ..Default::default()
3117 }
3118 }
3119
3120 #[test]
3121 fn test_metric_engine_path_keeps_nanosecond_precision() {
3122 let time_unix_nano = 1_704_067_200_123_456_789u64;
3123 let request = metrics_request(vec![Metric {
3124 name: "my_gauge".to_string(),
3125 data: Some(metric::Data::Gauge(Gauge {
3126 data_points: vec![NumberDataPoint {
3127 time_unix_nano,
3128 value: Some(Value::AsDouble(1.0)),
3129 ..Default::default()
3130 }],
3131 })),
3132 ..Default::default()
3133 }]);
3134
3135 let mut metric_ctx = OtlpMetricCtx {
3139 with_metric_engine: true,
3140 ..Default::default()
3141 };
3142 let MetricsConversion { requests, .. } =
3143 to_grpc_insert_requests(request, &mut metric_ctx).unwrap();
3144
3145 let rows = requests.inserts[0].rows.as_ref().unwrap();
3146 let ts_index = rows
3147 .schema
3148 .iter()
3149 .position(|column| column.column_name == greptime_timestamp())
3150 .unwrap();
3151 assert_eq!(
3152 rows.schema[ts_index].datatype,
3153 ColumnDataType::TimestampNanosecond as i32
3154 );
3155 assert!(matches!(
3156 rows.rows[0].values[ts_index].value_data,
3157 Some(ValueData::TimestampNanosecondValue(
3158 1_704_067_200_123_456_789
3159 ))
3160 ));
3161
3162 let mut compat_ctx = OtlpMetricCtx::default();
3165 let request = metrics_request(vec![Metric {
3166 name: "my_gauge".to_string(),
3167 data: Some(metric::Data::Gauge(Gauge {
3168 data_points: vec![NumberDataPoint {
3169 time_unix_nano,
3170 value: Some(Value::AsDouble(1.0)),
3171 ..Default::default()
3172 }],
3173 })),
3174 ..Default::default()
3175 }]);
3176 let MetricsConversion { requests, .. } =
3177 to_grpc_insert_requests(request, &mut compat_ctx).unwrap();
3178
3179 let rows = requests.inserts[0].rows.as_ref().unwrap();
3180 let ts_index = rows
3181 .schema
3182 .iter()
3183 .position(|column| column.column_name == greptime_timestamp())
3184 .unwrap();
3185 assert_eq!(
3186 rows.schema[ts_index].datatype,
3187 ColumnDataType::TimestampMillisecond as i32
3188 );
3189 assert!(matches!(
3190 rows.rows[0].values[ts_index].value_data,
3191 Some(ValueData::TimestampMillisecondValue(1_704_067_200_123))
3192 ));
3193 }
3194
3195 #[test]
3196 fn test_exponential_histogram_rejection_metrics() {
3197 const ISOLATED_ENV: &str = "GREPTIME_TEST_OTLP_REJECTION_METRICS_ISOLATED";
3199 if std::env::var_os(ISOLATED_ENV).is_none() {
3200 let output = std::process::Command::new(std::env::current_exe().unwrap())
3201 .args([
3202 "--exact",
3203 "otlp::metrics::tests::test_exponential_histogram_rejection_metrics",
3204 ])
3205 .env(ISOLATED_ENV, "1")
3206 .output()
3207 .unwrap();
3208 let stdout = String::from_utf8_lossy(&output.stdout);
3209 assert!(
3210 output.status.success() && stdout.contains("1 passed"),
3211 "isolated rejection metrics test failed\nstdout:\n{stdout}\nstderr:\n{}",
3212 String::from_utf8_lossy(&output.stderr)
3213 );
3214 return;
3215 }
3216
3217 let mut invalid = exponential_point();
3218 invalid.scale = -5;
3219 for (temporality, points, reason, accepted, rejected) in [
3220 (
3221 AggregationTemporality::Delta,
3222 vec![exponential_point(); 2],
3223 "invalid_data_point",
3224 2,
3225 0,
3226 ),
3227 (
3228 AggregationTemporality::Unspecified,
3229 vec![exponential_point(); 2],
3230 "unspecified_temporality",
3231 0,
3232 2,
3233 ),
3234 (
3235 AggregationTemporality::Cumulative,
3236 vec![exponential_point(), invalid],
3237 "invalid_data_point",
3238 1,
3239 1,
3240 ),
3241 (
3242 AggregationTemporality::Cumulative,
3243 vec![exponential_point()],
3244 "invalid_data_point",
3245 1,
3246 0,
3247 ),
3248 ] {
3249 let counter =
3250 OTLP_EXPONENTIAL_HISTOGRAM_REJECTED_DATA_POINTS.with_label_values(&[reason]);
3251 let before = counter.get();
3252 let mut request =
3253 metrics_request(vec![exponential_metric("latency", points, temporality)]);
3254 request.resource_metrics[0].resource = Some(Resource {
3255 attributes: vec![keyvalue("service.name", "api")],
3256 ..Default::default()
3257 });
3258 let mut ctx = descriptor_ctx();
3259 let conversion = to_grpc_insert_requests(request, &mut ctx).unwrap();
3260 assert_eq!(conversion.outcome.accepted_data_points, accepted);
3261 assert_eq!(conversion.outcome.rejected_data_points, rejected);
3262 assert_eq!(counter.get() - before, rejected as u64, "{reason}");
3263 assert_eq!(conversion.resource_info.is_some(), accepted > 0);
3264
3265 let before = counter.get();
3266 let empty = metrics_request(vec![exponential_metric("empty", vec![], temporality)]);
3267 assert_eq!(
3268 to_grpc_insert_requests(empty, &mut ctx)
3269 .unwrap()
3270 .outcome
3271 .rejected_data_points,
3272 0
3273 );
3274 assert_eq!(counter.get(), before, "empty metric: {reason}");
3275 }
3276 }
3277
3278 #[test]
3279 fn test_exponential_histogram_default_context_and_partial_outcome() {
3280 let request = metrics_request(vec![
3281 Metric {
3282 name: "temperature".to_string(),
3283 data: Some(metric::Data::Gauge(Gauge {
3284 data_points: vec![NumberDataPoint::default()],
3285 })),
3286 ..Default::default()
3287 },
3288 exponential_metric(
3289 "latency",
3290 vec![exponential_point()],
3291 AggregationTemporality::Cumulative,
3292 ),
3293 exponential_metric(
3294 "delta_latency",
3295 vec![exponential_point()],
3296 AggregationTemporality::Delta,
3297 ),
3298 ]);
3299 let MetricsConversion {
3300 requests,
3301 semantic_index,
3302 outcome,
3303 ..
3304 } = to_grpc_insert_requests(request, &mut OtlpMetricCtx::default()).unwrap();
3305
3306 assert_eq!(outcome.accepted_data_points, 3);
3307 assert_eq!(outcome.rejected_data_points, 0);
3308 assert!(outcome.error_message.is_none());
3309 assert_eq!(requests.inserts.len(), 3);
3310 let semantics = decode(&semantic_index);
3311 assert!(semantics.contains_key("temperature"));
3312 assert!(semantics.contains_key("latency"));
3313 assert_eq!(
3314 semantics["delta_latency"][SEMANTIC_METRIC_TEMPORALITY],
3315 "delta"
3316 );
3317
3318 let empty = metrics_request(vec![exponential_metric(
3319 "empty",
3320 vec![],
3321 AggregationTemporality::Cumulative,
3322 )]);
3323 let outcome = to_grpc_insert_requests(empty, &mut OtlpMetricCtx::default())
3324 .unwrap()
3325 .outcome;
3326 assert_eq!(outcome.rejected_data_points, 0);
3327 assert_eq!(outcome.error_message, None);
3328 }
3329
3330 #[test]
3331 fn test_exponential_histogram_cannot_share_table_with_scalar_metric() {
3332 let request = metrics_request(vec![
3333 Metric {
3334 name: "latency".to_string(),
3335 data: Some(metric::Data::Gauge(Gauge {
3336 data_points: vec![NumberDataPoint {
3337 value: Some(number_data_point::Value::AsDouble(1.0)),
3338 ..Default::default()
3339 }],
3340 })),
3341 ..Default::default()
3342 },
3343 exponential_metric(
3344 "latency",
3345 vec![exponential_point()],
3346 AggregationTemporality::Cumulative,
3347 ),
3348 ]);
3349 let mut ctx = OtlpMetricCtx::default();
3350
3351 let conversion = to_grpc_insert_requests(request, &mut ctx).unwrap();
3352 assert_eq!(conversion.outcome.accepted_data_points, 0);
3353 assert_eq!(conversion.outcome.rejected_data_points, 2);
3354 assert!(conversion.requests.inserts.is_empty());
3355 }
3356
3357 #[test]
3358 fn test_histogram_cannot_replace_exponential_histogram_table() {
3359 let request = metrics_request(vec![
3360 exponential_metric(
3361 "latency_bucket",
3362 vec![exponential_point()],
3363 AggregationTemporality::Cumulative,
3364 ),
3365 histogram_metric("latency"),
3366 ]);
3367 let mut ctx = OtlpMetricCtx::default();
3368
3369 let conversion = to_grpc_insert_requests(request, &mut ctx).unwrap();
3370 assert_eq!(conversion.outcome.accepted_data_points, 0);
3371 assert_eq!(conversion.outcome.rejected_data_points, 2);
3372 assert!(conversion.requests.inserts.is_empty());
3373 }
3374
3375 #[test]
3376 fn test_histograms_with_same_name_across_resources_are_merged() {
3377 let metric = histogram_metric("latency");
3378 let request = ExportMetricsServiceRequest {
3379 resource_metrics: ["service-a", "service-b"]
3380 .into_iter()
3381 .map(|service| ResourceMetrics {
3382 resource: Some(Resource {
3383 attributes: vec![keyvalue("service.name", service)],
3384 ..Default::default()
3385 }),
3386 scope_metrics: vec![ScopeMetrics {
3387 metrics: vec![metric.clone()],
3388 ..Default::default()
3389 }],
3390 ..Default::default()
3391 })
3392 .collect(),
3393 };
3394
3395 let MetricsConversion {
3396 requests,
3397 rows,
3398 outcome,
3399 ..
3400 } = to_grpc_insert_requests(request, &mut OtlpMetricCtx::default()).unwrap();
3401
3402 assert_eq!(outcome.accepted_data_points, 2);
3403 assert_eq!(rows, 6);
3404 assert_eq!(requests.inserts.len(), 3);
3405 for request in requests.inserts {
3406 assert_eq!(
3407 request.rows.unwrap().rows.len(),
3408 2,
3409 "{}",
3410 request.table_name
3411 );
3412 }
3413 }
3414
3415 #[test]
3416 fn test_exponential_histogram_rejects_temporality_before_stale_point() {
3417 let stale = ExponentialHistogramDataPoint {
3418 flags: DataPointFlags::NoRecordedValueMask as u32,
3419 ..Default::default()
3420 };
3421 let request = metrics_request(vec![exponential_metric(
3422 "latency",
3423 vec![stale],
3424 AggregationTemporality::Unspecified,
3425 )]);
3426 let mut ctx = OtlpMetricCtx::default();
3427 let MetricsConversion {
3428 requests,
3429 rows,
3430 semantic_index,
3431 outcome,
3432 ..
3433 } = to_grpc_insert_requests(request, &mut ctx).unwrap();
3434
3435 assert_eq!(outcome.accepted_data_points, 0);
3436 assert_eq!(outcome.rejected_data_points, 1);
3437 assert_eq!(rows, 0);
3438 assert!(requests.inserts.is_empty());
3439 assert!(semantic_index.is_empty());
3440 }
3441
3442 #[test]
3443 fn test_exponential_histogram_storage_paths_share_struct() {
3444 use common_query::prelude::greptime_native_histogram;
3445
3446 let mut point = exponential_point();
3447 point.sum = Some(42.0);
3448 let request = metrics_request(vec![exponential_metric(
3449 "request.duration",
3450 vec![point],
3451 AggregationTemporality::Cumulative,
3452 )]);
3453 let mut ordinary_ctx = OtlpMetricCtx::default();
3454 let ordinary_requests = to_grpc_insert_requests(request.clone(), &mut ordinary_ctx)
3455 .unwrap()
3456 .requests;
3457 let mut metric_ctx = OtlpMetricCtx {
3458 with_metric_engine: true,
3459 ..Default::default()
3460 };
3461 let metric_requests = to_grpc_insert_requests(request, &mut metric_ctx)
3462 .unwrap()
3463 .requests;
3464
3465 let ordinary_insert = &ordinary_requests.inserts[0];
3466 let metric_insert = &metric_requests.inserts[0];
3467 assert_eq!(ordinary_insert.table_name, "request_duration");
3468 assert_eq!(metric_insert.table_name, "request_duration");
3469 let ordinary_rows = ordinary_insert.rows.as_ref().unwrap();
3470 let metric_rows = metric_insert.rows.as_ref().unwrap();
3471 let field = greptime_native_histogram();
3472 let ordinary_histogram = ordinary_rows.rows[0].values[ordinary_rows
3473 .schema
3474 .iter()
3475 .position(|column| column.column_name == field)
3476 .unwrap()]
3477 .clone();
3478 let metric_histogram = metric_rows.rows[0].values[metric_rows
3479 .schema
3480 .iter()
3481 .position(|column| column.column_name == field)
3482 .unwrap()]
3483 .clone();
3484 assert_eq!(ordinary_histogram, metric_histogram);
3485 assert!(matches!(
3486 ordinary_rows.rows[0].values[ordinary_rows
3487 .schema
3488 .iter()
3489 .position(|column| column.column_name == greptime_timestamp())
3490 .unwrap()]
3491 .value_data,
3492 Some(ValueData::TimestampMillisecondValue(2))
3493 ));
3494 assert!(matches!(
3495 metric_rows.rows[0].values[metric_rows
3496 .schema
3497 .iter()
3498 .position(|column| column.column_name == greptime_timestamp())
3499 .unwrap()]
3500 .value_data,
3501 Some(ValueData::TimestampNanosecondValue(2_000_000))
3502 ));
3503 }
3504
3505 #[test]
3506 fn test_rejection_message_is_bounded() {
3507 let request = metrics_request(vec![exponential_metric(
3508 "x".repeat(1_000),
3509 vec![ExponentialHistogramDataPoint::default()],
3510 AggregationTemporality::Unspecified,
3511 )]);
3512 let outcome = to_grpc_insert_requests(request, &mut OtlpMetricCtx::default())
3513 .unwrap()
3514 .outcome;
3515
3516 assert_eq!(outcome.rejected_data_points, 1);
3517 assert!(outcome.error_message.unwrap().len() <= MAX_REJECTION_MESSAGE_BYTES);
3518 }
3519
3520 #[test]
3521 fn test_rejection_message_reason_is_lazy_after_cap() {
3522 let mut outcome = MetricsIngestOutcome {
3523 error_message: Some("x".repeat(MAX_REJECTION_MESSAGE_BYTES)),
3524 ..Default::default()
3525 };
3526 let mut reason_built = false;
3527
3528 reject_data_points(&mut outcome, 1, || {
3529 reason_built = true;
3530 "unused".to_string()
3531 })
3532 .unwrap();
3533
3534 assert_eq!(outcome.rejected_data_points, 1);
3535 assert!(!reason_built);
3536 }
3537}