Skip to main content

servers/otlp/
metrics.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
63/// the default column count for table writer
64const 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
73// see: https://prometheus.io/docs/guides/opentelemetry/#promoting-resource-attributes
74const 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/// Result of converting one OTLP metrics request.
109#[derive(Debug)]
110pub struct MetricsConversion {
111    pub requests: RowInsertRequests,
112    /// Row count of `requests`; the resource descriptor is not counted.
113    pub rows: usize,
114    /// Per-table semantic index for the auto-create path to stamp as table
115    /// options; covers the descriptor table too.
116    pub semantic_index: SemanticIndex,
117    /// The synthesized resource descriptor, written separately from the
118    /// metric data. See [`resource_info`].
119    pub resource_info: Option<RowInsertRequests>,
120    pub outcome: MetricsIngestOutcome,
121}
122
123/// Convert OpenTelemetry metrics to GreptimeDB insert requests
124///
125/// See
126/// <https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/metrics/v1/metrics.proto>
127/// for data structure of OTLP metrics.
128pub 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    // The metric and the descriptor would fight over the same table across
180    // two engines. The user's metric wins.
181    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
249/// The tables a metric emits and their per-table `metric.type`. Histogram fans
250/// out into `_bucket` (the histogram) plus `_sum`/`_count` counters; summary
251/// fans out into the quantile table plus `_count`/`_sum` counters (legacy
252/// summary stays a single table).
253fn 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
283/// Maps OTLP `aggregation_temporality` to the semantic value, or `None` when the
284/// instrument has no temporality (gauge/summary) or it is unspecified.
285fn 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
299/// Records the declared metric-level semantic keys for every table this metric
300/// emits.
301fn 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    // `original_name` is meaningful only when translation renamed the metric.
315    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
352/// Non-scalar values (arrays, maps, bytes) are not representable as tags.
353fn 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
363/// Prometheus-style `(job, instance)` identity. Per the OTel Prometheus
364/// compatibility spec `job` folds in `service.namespace` when present; a
365/// resource without `service.name` gets no job rather than a fabricated one.
366/// Both fields are optional strings, so a named type keeps a swapped
367/// destructuring from type-checking at the call sites.
368pub(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    // remap the service identity attributes to job and instance
407    let ServiceIdentity { job, instance } = service_identity(attrs);
408
409    // if promote all, then exclude the list, else, include the list
410    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    // persist scope attrs with name, version and schema_url
437    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        // Stamp semantic metadata only after at least one row was accepted.
543        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
719/// Whole-metric acceptance, decided once for the encoder and for the resource
720/// descriptor, which must not describe a resource whose data was rejected.
721/// Individual points can still fail [`exponential_histogram_value`].
722/// Raw-delta storage currently supports sums and explicit histograms; exponential
723/// histograms still require cumulative input. Delta-to-cumulative conversion is out of scope.
724pub(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        // Bucket deltas continue across span gaps; omitted zero buckets do not reset them.
924        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        // TODO(sunng87): allow different type of values
967        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    // Keep the full nanosecond precision whenever the request is headed for
1005    // the metric engine: `Inserter::handle_metric_row_inserts` converts the
1006    // timestamps to the physical table's time index unit (which may be
1007    // micro/nanosecond). Only the non-metric prometheus-compatible path is
1008    // fixed to milliseconds, to keep auto-created mito tables on the
1009    // millisecond time index.
1010    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            // we coerce all values to f64
1038            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        // TODO(shuiyisong): check `__type__` and `__unit__` tags in prometheus
1095        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
1117/// encode this gauge metric
1118///
1119/// note that there can be multiple data points in the request, it's going to be
1120/// stored as multiple rows
1121fn 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
1154/// Encodes sums, preserving delta points as interval values with a temporality tag.
1155/// Ingestion is stateless: values are never accumulated across timestamps.
1156fn 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
1204/// Encode histogram data. This function returns 3 insert requests for 3 tables.
1205///
1206/// The implementation has been following Prometheus histogram table format:
1207///
1208/// - A `%metric%_bucket` table including `greptime_le` tag that stores bucket upper
1209///   limit, and `greptime_value` for bucket count
1210/// - A `%metric%_sum` table storing sum of samples
1211/// -  A `%metric%_count` table storing count of samples.
1212///
1213/// By its Prometheus compatibility, we hope to be able to use prometheus
1214/// quantile functions on this table.
1215/// Delta points retain their temporality tag and interval values. Bucket counts
1216/// are prefix-summed within each point, never accumulated across timestamps.
1217fn 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            // OTLP count-only histograms map to one implicit Prometheus infinity bucket.
1265            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        // 1. quantile table
1459        // 2. count table
1460        // 3. sum table
1461
1462        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        // descriptor keeps raw OTel keys while the metric table's labels went
1653        // through the (default underscore-escaping) translation strategy
1654        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        // host.id is not in the promote list: only the descriptor keeps it
1663        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        // legacy tables predate job/instance and the promote filter; adding
1692        // either would alter the schema of tables already in use
1693        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        // the metric itself still goes through the main path
1708        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        // no service.name: no job is fabricated
1738        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        // bucket table
2065        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        // Name unchanged by translation -> no original_name.
2222        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        // Summary has no temporality.
2296        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        // Compaction must not hide invalid source indexes, even for empty buckets.
2450        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        // The metric engine path keeps the full nanosecond precision here;
2719        // `Inserter::handle_metric_row_inserts` converts the timestamps to
2720        // the physical table's time index unit afterwards.
2721        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        // The non-metric prometheus-compatible path stays millisecond so
2746        // auto-created mito tables keep the millisecond time index.
2747        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        // Other conversion tests update the same process-global counters.
2781        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}