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