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