Skip to main content

servers/http/result/
prometheus_resp.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
15//! prom supply the prometheus HTTP API Server compliance
16use std::cmp::Ordering;
17use std::collections::{BTreeMap, HashMap};
18
19use arrow::array::{Array, AsArray, StructArray};
20use arrow::datatypes::{Float64Type, TimestampMillisecondType};
21use arrow_schema::DataType;
22use axum::Json;
23use axum::http::HeaderValue;
24use axum::response::{IntoResponse, Response};
25use common_error::ext::ErrorExt;
26use common_error::status_code::StatusCode;
27use common_query::native_histogram::{
28    NativeHistogram, is_native_histogram_value_type, read_histogram,
29};
30use common_query::prometheus::{format_prometheus_float, is_prometheus_stale_nan};
31use common_query::promql_annotations::{
32    PromqlAnnotationCollector, get_promql_annotation_collector,
33};
34use common_query::{Output, OutputData};
35use common_recordbatch::RecordBatches;
36use datatypes::arrow_array::string_array_value_at_index;
37use datatypes::prelude::ConcreteDataType;
38use indexmap::IndexMap;
39use promql_parser::label::METRIC_NAME;
40use promql_parser::parser::value::ValueType;
41use ryu::Buffer;
42use serde::{Deserialize, Serialize};
43use serde_json::Value;
44use snafu::{OptionExt, ResultExt};
45
46use crate::error::{
47    ArrowSnafu, CollectRecordbatchSnafu, DataFusionSnafu, Result, UnexpectedResultSnafu,
48    status_code_to_http_status,
49};
50use crate::http::header::{GREPTIME_DB_HEADER_METRICS, collect_plan_metrics};
51use crate::http::prometheus::{
52    PromData, PromNativeHistogram, PromQueryResult, PromSeriesMatrix, PromSeriesVector,
53    PrometheusResponse,
54};
55
56#[derive(Default)]
57struct PromSeriesSamples {
58    values: Vec<(f64, String)>,
59    histograms: Vec<(f64, PromNativeHistogram)>,
60}
61
62fn prometheus_native_histogram(histogram: &NativeHistogram) -> Result<PromNativeHistogram> {
63    Ok(PromNativeHistogram {
64        count: format_prometheus_float(histogram.count),
65        sum: format_prometheus_float(histogram.sum),
66        buckets: histogram
67            .to_prometheus_buckets()
68            .context(UnexpectedResultSnafu {
69                reason: "native histogram cannot be converted to Prometheus buckets",
70            })?,
71    })
72}
73
74/// Formats a sample value for the Prometheus HTTP API.
75///
76/// Finite sample strings use ryu's shortest-roundtrip representation and may
77/// differ textually from previous Rust/Prometheus wire formatting; values parse
78/// identically. Non-finite values retain Rust's `f64::to_string()` output.
79fn format_prometheus_sample_value(value: f64) -> String {
80    if value.is_finite() {
81        Buffer::new().format_finite(value).to_string()
82    } else {
83        value.to_string()
84    }
85}
86
87#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
88pub struct PrometheusJsonResponse {
89    pub status: String,
90    #[serde(skip_serializing_if = "PrometheusResponse::is_none")]
91    #[serde(default)]
92    pub data: PrometheusResponse,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub error: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    #[serde(rename = "errorType")]
97    pub error_type: Option<String>,
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub warnings: Option<Vec<String>>,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub infos: Option<Vec<String>>,
102
103    #[serde(skip)]
104    pub status_code: Option<StatusCode>,
105    // placeholder for header value
106    #[serde(skip)]
107    #[serde(default)]
108    pub resp_metrics: HashMap<String, Value>,
109}
110
111impl IntoResponse for PrometheusJsonResponse {
112    fn into_response(self) -> Response {
113        let metrics = if self.resp_metrics.is_empty() {
114            None
115        } else {
116            serde_json::to_string(&self.resp_metrics).ok()
117        };
118
119        let http_code = self.status_code.map(|c| status_code_to_http_status(&c));
120
121        let mut resp = Json(self).into_response();
122
123        if let Some(http_code) = http_code {
124            *resp.status_mut() = http_code;
125        }
126
127        if let Some(m) = metrics.and_then(|m| HeaderValue::from_str(&m).ok()) {
128            resp.headers_mut().insert(&GREPTIME_DB_HEADER_METRICS, m);
129        }
130
131        resp
132    }
133}
134
135impl PrometheusJsonResponse {
136    pub fn error<S1>(error_type: StatusCode, reason: S1) -> Self
137    where
138        S1: Into<String>,
139    {
140        PrometheusJsonResponse {
141            status: "error".to_string(),
142            data: PrometheusResponse::None,
143            error: Some(reason.into()),
144            error_type: Some(error_type.to_string()),
145            warnings: None,
146            infos: None,
147            resp_metrics: Default::default(),
148            status_code: Some(error_type),
149        }
150    }
151
152    pub fn success(data: PrometheusResponse) -> Self {
153        PrometheusJsonResponse {
154            status: "success".to_string(),
155            data,
156            error: None,
157            error_type: None,
158            warnings: None,
159            infos: None,
160            resp_metrics: Default::default(),
161            status_code: None,
162        }
163    }
164
165    /// Adds collected PromQL warnings and infos to the response.
166    fn append_promql_annotations(&mut self, collector: &PromqlAnnotationCollector) {
167        let mut warnings = self.warnings.take().unwrap_or_default();
168        let mut infos = self.infos.take().unwrap_or_default();
169        collector.append_to(&mut warnings, &mut infos);
170        self.warnings = (!warnings.is_empty()).then_some(warnings);
171        self.infos = (!infos.is_empty()).then_some(infos);
172    }
173
174    /// Merges data and annotations from another expanded PromQL query response.
175    pub(crate) fn append_query_response(&mut self, mut other: Self) {
176        self.data.append(other.data);
177        merge_annotations(&mut self.warnings, other.warnings.take());
178        merge_annotations(&mut self.infos, other.infos.take());
179    }
180
181    /// Convert from `Result<Output>`
182    pub async fn from_query_result(
183        result: Result<Output>,
184        metric_name: Option<String>,
185        result_type: ValueType,
186        query_id: Option<&str>,
187    ) -> Self {
188        // Hold the collector while a streaming result is consumed.
189        let collector = query_id.and_then(get_promql_annotation_collector);
190        let response: Result<Self> = try {
191            let result = result?;
192            let mut resp =
193                match result.data {
194                    OutputData::RecordBatches(batches) => Self::success(
195                        Self::record_batches_to_data(batches, metric_name, result_type)?,
196                    ),
197                    OutputData::Stream(stream) => {
198                        let record_batches = RecordBatches::try_collect(stream)
199                            .await
200                            .context(CollectRecordbatchSnafu)?;
201                        Self::success(Self::record_batches_to_data(
202                            record_batches,
203                            metric_name,
204                            result_type,
205                        )?)
206                    }
207                    OutputData::AffectedRows(_) => Self::error(
208                        StatusCode::Unexpected,
209                        "expected data result, but got affected rows",
210                    ),
211                };
212
213            if let Some(physical_plan) = result.meta.plan {
214                let mut result_map = HashMap::new();
215                let mut tmp = vec![&mut result_map];
216                collect_plan_metrics(&physical_plan, &mut tmp);
217
218                let re = result_map
219                    .into_iter()
220                    .map(|(k, v)| (k, Value::from(v)))
221                    .collect();
222                resp.resp_metrics = re;
223            }
224
225            resp
226        };
227
228        let result_type_string = result_type.to_string();
229
230        let mut response = match response {
231            Ok(resp) => resp,
232            Err(err) => {
233                // Prometheus won't report error if querying nonexist label and metric
234                if err.status_code() == StatusCode::TableNotFound
235                    || err.status_code() == StatusCode::TableColumnNotFound
236                {
237                    Self::success(PrometheusResponse::PromData(PromData {
238                        result_type: result_type_string,
239                        ..Default::default()
240                    }))
241                } else {
242                    Self::error(err.status_code(), err.output_msg())
243                }
244            }
245        };
246        if let Some(collector) = collector {
247            response.append_promql_annotations(&collector);
248        }
249        response
250    }
251
252    /// Convert [RecordBatches] to [PromData]
253    fn record_batches_to_data(
254        batches: RecordBatches,
255        metric_name: Option<String>,
256        result_type: ValueType,
257    ) -> Result<PrometheusResponse> {
258        // Return empty result if no batches
259        if batches.iter().next().is_none() {
260            return Ok(PrometheusResponse::PromData(PromData {
261                result_type: result_type.to_string(),
262                ..Default::default()
263            }));
264        }
265
266        // infer semantic type of each column from schema.
267        // TODO(ruihang): wish there is a better way to do this.
268        let mut timestamp_column_index = None;
269        let mut tag_column_indices = Vec::new();
270        let mut first_field_column_index = None;
271        let mut native_histogram_column_index = None;
272
273        let mut num_label_columns = 0;
274
275        for (i, column) in batches.schema().column_schemas().iter().enumerate() {
276            match column.data_type {
277                ConcreteDataType::Timestamp(datatypes::types::TimestampType::Millisecond(_))
278                    if timestamp_column_index.is_none() =>
279                {
280                    timestamp_column_index = Some(i);
281                }
282                // Treat all value types as field
283                ConcreteDataType::Float32(_)
284                | ConcreteDataType::Float64(_)
285                | ConcreteDataType::Int8(_)
286                | ConcreteDataType::Int16(_)
287                | ConcreteDataType::Int32(_)
288                | ConcreteDataType::Int64(_)
289                | ConcreteDataType::UInt8(_)
290                | ConcreteDataType::UInt16(_)
291                | ConcreteDataType::UInt32(_)
292                | ConcreteDataType::UInt64(_)
293                    if first_field_column_index.is_none() =>
294                {
295                    first_field_column_index = Some(i);
296                }
297                _ if native_histogram_column_index.is_none()
298                    && is_native_histogram_value_type(&column.data_type) =>
299                {
300                    native_histogram_column_index = Some(i);
301                }
302                ConcreteDataType::String(_) => {
303                    tag_column_indices.push(i);
304                    num_label_columns += 1;
305                }
306                _ => {}
307            }
308        }
309
310        let timestamp_column_index = timestamp_column_index.context(UnexpectedResultSnafu {
311            reason: "no timestamp column found".to_string(),
312        })?;
313        if first_field_column_index.is_none() && native_histogram_column_index.is_none() {
314            return UnexpectedResultSnafu {
315                reason: "no value column found".to_string(),
316            }
317            .fail();
318        }
319
320        // Preserves the order of output tags.
321        // Tag order matters, e.g., after sorc and sort_desc, the output order must be kept.
322        let mut buffer = IndexMap::<Vec<(&str, &str)>, PromSeriesSamples>::new();
323
324        // Query output is clustered by series (the range plan sorts by series
325        // key + timestamp), so consecutive rows usually belong to the same
326        // series. Remember the index of the previous row's entry in `buffer`,
327        // and reuse it directly when its tags are unchanged. This avoids
328        // building and hashing the label vector on every row; the worst case
329        // adds one `Vec` comparison per series transition before falling back
330        // to the map lookup.
331        let mut last_entry_index = None;
332
333        let schema = batches.schema();
334        for batch in batches.iter() {
335            // prepare things...
336            let tag_columns = tag_column_indices
337                .iter()
338                .map(|i| batch.column(*i))
339                .collect::<Vec<_>>();
340            let tag_names = tag_column_indices
341                .iter()
342                .map(|c| schema.column_name_by_index(*c))
343                .collect::<Vec<_>>();
344            let timestamp_column = batch
345                .column(timestamp_column_index)
346                .as_primitive::<TimestampMillisecondType>();
347
348            let field_array = first_field_column_index
349                .map(|index| arrow::compute::cast(batch.column(index), &DataType::Float64))
350                .transpose()
351                .context(ArrowSnafu)?;
352            let field_column = field_array
353                .as_ref()
354                .map(|array| array.as_primitive::<Float64Type>());
355            let native_histogram_column = native_histogram_column_index
356                .map(|index| {
357                    batch
358                        .column(index)
359                        .as_any()
360                        .downcast_ref::<StructArray>()
361                        .with_context(|| UnexpectedResultSnafu {
362                            reason: "native histogram column is not a struct array",
363                        })
364                })
365                .transpose()?;
366
367            // assemble rows
368            for row_index in 0..batch.num_rows() {
369                let value = field_column.and_then(|field_column| {
370                    if !field_column.is_valid(row_index) {
371                        return None;
372                    }
373                    let value = field_column.value(row_index);
374                    (!is_prometheus_stale_nan(value))
375                        .then_some((timestamp_column.value(row_index), value))
376                });
377                let histogram = native_histogram_column
378                    .and_then(|column| {
379                        read_histogram(column, row_index)
380                            .context(DataFusionSnafu)
381                            .transpose()
382                    })
383                    .transpose()?
384                    .filter(|histogram| !is_prometheus_stale_nan(histogram.sum))
385                    .map(|histogram| {
386                        prometheus_native_histogram(&histogram)
387                            .map(|histogram| (timestamp_column.value(row_index), histogram))
388                    })
389                    .transpose()?;
390
391                if value.is_none() && histogram.is_none() {
392                    continue;
393                }
394
395                // retrieve tags
396                let mut tags = Vec::with_capacity(num_label_columns + 1);
397                if let Some(metric_name) = &metric_name {
398                    tags.push((METRIC_NAME, metric_name.as_str()));
399                }
400                for (tag_column, tag_name) in tag_columns.iter().zip(tag_names.iter()) {
401                    if let Some(tag_value) = string_array_value_at_index(tag_column, row_index) {
402                        tags.push((tag_name, tag_value));
403                    }
404                }
405
406                let reuse = last_entry_index.filter(|index| {
407                    buffer
408                        .get_index(*index)
409                        .is_some_and(|(key, _)| key == &tags)
410                });
411                let samples = if let Some(index) = reuse {
412                    buffer
413                        .get_index_mut(index)
414                        .map(|(_, samples)| samples)
415                        .with_context(|| UnexpectedResultSnafu {
416                            reason: "reused series entry must exist",
417                        })?
418                } else {
419                    let entry = buffer.entry(tags);
420                    last_entry_index = Some(entry.index());
421                    entry.or_default()
422                };
423                if let Some((timestamp_millis, histogram)) = histogram {
424                    samples
425                        .histograms
426                        .push((timestamp_millis as f64 / 1000.0, histogram));
427                } else if let Some((timestamp_millis, value)) = value {
428                    samples.values.push((
429                        timestamp_millis as f64 / 1000.0,
430                        format_prometheus_sample_value(value),
431                    ));
432                }
433            }
434        }
435
436        // initialize result to return
437        let mut result = match result_type {
438            ValueType::Vector => PromQueryResult::Vector(vec![]),
439            ValueType::Matrix => PromQueryResult::Matrix(vec![]),
440            ValueType::Scalar => PromQueryResult::Scalar(None),
441            ValueType::String => PromQueryResult::String(None),
442        };
443
444        // accumulate data into result
445        buffer.into_iter().for_each(|(tags, mut samples)| {
446            let metric = tags
447                .into_iter()
448                .map(|(k, v)| (k.to_string(), v.to_string()))
449                .collect::<BTreeMap<_, _>>();
450            match result {
451                PromQueryResult::Vector(ref mut v) => {
452                    let histogram = samples.histograms.pop();
453                    let value = if histogram.is_none() {
454                        samples.values.pop()
455                    } else {
456                        None
457                    };
458                    v.push(PromSeriesVector {
459                        metric,
460                        value,
461                        histogram,
462                    });
463                }
464                PromQueryResult::Matrix(ref mut v) => {
465                    // sort values by timestamp
466                    if !samples.values.is_sorted_by(|a, b| a.0 <= b.0) {
467                        samples
468                            .values
469                            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
470                    }
471                    if !samples.histograms.is_sorted_by(|a, b| a.0 <= b.0) {
472                        samples
473                            .histograms
474                            .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
475                    }
476
477                    v.push(PromSeriesMatrix {
478                        metric,
479                        values: samples.values,
480                        histograms: samples.histograms,
481                    });
482                }
483                PromQueryResult::Scalar(ref mut v) => {
484                    *v = samples.values.pop();
485                }
486                PromQueryResult::String(ref mut _v) => {
487                    // TODO(ruihang): Not supported yet
488                }
489            }
490        });
491
492        // sort matrix by metric
493        // see: https://prometheus.io/docs/prometheus/3.5/querying/api/#range-vectors
494        if let PromQueryResult::Matrix(ref mut v) = result {
495            v.sort_by(|a, b| a.metric.cmp(&b.metric));
496        }
497
498        let result_type_string = result_type.to_string();
499        let data = PrometheusResponse::PromData(PromData {
500            result_type: result_type_string,
501            result,
502        });
503
504        Ok(data)
505    }
506}
507
508fn merge_annotations(target: &mut Option<Vec<String>>, source: Option<Vec<String>>) {
509    let Some(source) = source else {
510        return;
511    };
512    let target = target.get_or_insert_default();
513    target.extend(source);
514    target.sort();
515    target.dedup();
516}
517
518#[cfg(test)]
519mod tests {
520    use std::sync::Arc;
521
522    use arrow::array::StringViewArray;
523    use common_query::native_histogram::{
524        CUSTOM_BUCKETS_SCHEMA, CounterResetHint, NativeHistogram, Span, build_histogram_array,
525        native_histogram_value_type,
526    };
527    use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS;
528    use common_query::promql_annotations::{
529        PromqlAnnotationCollector, promql_annotation_collector,
530    };
531    use common_recordbatch::{RecordBatch, RecordBatches};
532    use datatypes::data_type::ConcreteDataType;
533    use datatypes::schema::{ColumnSchema, Schema};
534    use datatypes::vectors::{
535        Float64Vector, StringVector, StructVector, TimestampMillisecondVector, VectorRef,
536    };
537
538    use super::*;
539
540    #[tokio::test]
541    async fn query_response_preserves_and_merges_promql_annotations() {
542        let query_id = "query_response_preserves_and_merges_promql_annotations";
543        let left = promql_annotation_collector(query_id);
544        left.record_warning("shared warning");
545        left.record_info("left info");
546        let mut response = PrometheusJsonResponse::from_query_result(
547            Ok(Output::new_with_record_batches(RecordBatches::empty())),
548            None,
549            ValueType::Vector,
550            Some(query_id),
551        )
552        .await;
553
554        let right = PromqlAnnotationCollector::default();
555        right.record_warning("shared warning");
556        right.record_info("right info");
557        let mut other = PrometheusJsonResponse::success(PrometheusResponse::None);
558        other.append_promql_annotations(&right);
559        response.append_query_response(other);
560
561        assert_eq!(response.warnings, Some(vec!["shared warning".to_string()]));
562        assert_eq!(
563            response.infos,
564            Some(vec!["left info".to_string(), "right info".to_string()])
565        );
566        let json = serde_json::to_value(response).unwrap();
567        assert_eq!(json["warnings"], serde_json::json!(["shared warning"]));
568        assert_eq!(
569            json["infos"],
570            serde_json::json!(["left info", "right info"])
571        );
572    }
573
574    fn sample_histogram() -> NativeHistogram {
575        NativeHistogram {
576            schema: 0,
577            zero_threshold: 0.001,
578            sum: 3.0,
579            reset_hint: CounterResetHint::Unknown,
580            start_timestamp: Some(0),
581            custom_values: vec![],
582            positive_spans: vec![Span {
583                offset: 0,
584                length: 1,
585            }],
586            negative_spans: vec![],
587            count: 2.0,
588            zero_count: 1.0,
589            positive_buckets: vec![1.0],
590            negative_buckets: vec![],
591        }
592    }
593
594    fn histogram_vector(values: &[Option<NativeHistogram>]) -> VectorRef {
595        let histogram_array = build_histogram_array(values);
596        let histogram_array = histogram_array
597            .as_any()
598            .downcast_ref::<StructArray>()
599            .unwrap()
600            .clone();
601        let ConcreteDataType::Struct(histogram_type) = native_histogram_value_type().clone() else {
602            unreachable!("native histogram type must be a struct")
603        };
604        Arc::new(StructVector::try_new(histogram_type, histogram_array).unwrap())
605    }
606
607    #[test]
608    fn format_prometheus_sample_value_uses_ryu_for_finite_values() {
609        let values = [
610            1.5,
611            0.1,
612            1.0,
613            0.0,
614            -0.0,
615            100.0,
616            1e-6,
617            1e-7,
618            1e21,
619            1e30,
620            f64::MAX,
621            f64::MIN_POSITIVE,
622        ];
623
624        for value in values {
625            let output = format_prometheus_sample_value(value);
626            assert_eq!(output.parse::<f64>().unwrap().to_bits(), value.to_bits());
627        }
628
629        // Representative integral values use ryu's explicit .0 form.
630        assert_eq!(format_prometheus_sample_value(1.0), "1.0");
631        assert_eq!(format_prometheus_sample_value(-0.0), "-0.0");
632        assert_eq!(format_prometheus_sample_value(100.0), "100.0");
633        assert_eq!(format_prometheus_sample_value(1e-6), "1e-6");
634        assert_eq!(format_prometheus_sample_value(1e-7), "1e-7");
635        assert_eq!(format_prometheus_sample_value(1e21), "1e21");
636
637        // These known shortest-roundtrip tie cases have different text but
638        // remain numerically equivalent to Rust's representation.
639        for value in [
640            f64::from_bits(0x42374876e8000400),
641            f64::from_bits(0x3ff0000800000000),
642            f64::from_bits(0x430a8e5672bc7312),
643        ] {
644            let ryu_output = format_prometheus_sample_value(value);
645            let std_output = value.to_string();
646            assert_ne!(ryu_output, std_output);
647            assert_eq!(ryu_output.parse::<f64>().unwrap(), value);
648            assert_eq!(std_output.parse::<f64>().unwrap(), value);
649        }
650    }
651
652    #[test]
653    fn format_prometheus_sample_value_preserves_nonfinite_values() {
654        assert_eq!(format_prometheus_sample_value(f64::NAN), "NaN");
655        assert_eq!(format_prometheus_sample_value(f64::INFINITY), "inf");
656        assert_eq!(format_prometheus_sample_value(f64::NEG_INFINITY), "-inf");
657
658        // Parsing a NaN does not preserve its payload bits, so NaN is checked
659        // by its required semantic spelling rather than by to_bits().
660        assert!(
661            format_prometheus_sample_value(f64::NAN)
662                .parse::<f64>()
663                .unwrap()
664                .is_nan()
665        );
666    }
667
668    #[test]
669    fn matrix_response_preserves_ordinary_nan_and_filters_stale_markers() {
670        let schema = Arc::new(Schema::new(vec![
671            ColumnSchema::new(
672                "timestamp",
673                ConcreteDataType::timestamp_millisecond_datatype(),
674                false,
675            ),
676            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
677        ]));
678        let batch = RecordBatch::new(
679            schema.clone(),
680            vec![
681                Arc::new(TimestampMillisecondVector::from_vec(vec![
682                    1_000, 2_000, 3_000, 4_000,
683                ])) as _,
684                Arc::new(Float64Vector::from(vec![
685                    Some(1.0),
686                    Some(f64::from_bits(0x7ff8_0000_0000_0000)),
687                    Some(f64::from_bits(0x7ff0_0000_0000_0002)),
688                    None,
689                ])) as _,
690            ],
691        )
692        .unwrap();
693        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
694
695        let response =
696            PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
697                .unwrap();
698        let PrometheusResponse::PromData(data) = response else {
699            panic!("expected Prometheus data response");
700        };
701        let PromQueryResult::Matrix(series) = data.result else {
702            panic!("expected matrix result");
703        };
704
705        assert_eq!(series.len(), 1);
706        assert_eq!(
707            series[0].values,
708            vec![(1.0, "1.0".to_string()), (2.0, "NaN".to_string())]
709        );
710    }
711
712    #[test]
713    fn record_batches_to_data_formats_values_with_ryu() {
714        let schema = Arc::new(Schema::new(vec![
715            ColumnSchema::new(
716                "timestamp",
717                ConcreteDataType::timestamp_millisecond_datatype(),
718                false,
719            ),
720            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
721        ]));
722        let batch = RecordBatch::new(
723            schema.clone(),
724            vec![
725                Arc::new(TimestampMillisecondVector::from_vec(vec![
726                    1_000, 2_000, 3_000, 4_000, 5_000, 6_000,
727                ])) as _,
728                Arc::new(Float64Vector::from(vec![
729                    Some(0.0),
730                    Some(-0.0),
731                    Some(1.25),
732                    Some(1e30),
733                    Some(1e-7),
734                    Some(f64::MAX),
735                ])) as _,
736            ],
737        )
738        .unwrap();
739        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
740
741        let response =
742            PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
743                .unwrap();
744        let PrometheusResponse::PromData(PromData {
745            result: PromQueryResult::Matrix(series),
746            ..
747        }) = response
748        else {
749            panic!("expected matrix response");
750        };
751
752        assert_eq!(series.len(), 1);
753        let input_values = [0.0, -0.0, 1.25, 1e30, 1e-7, f64::MAX];
754        // Keep this expected-value generation independent from the production
755        // formatter while still asserting the Arrow/batch-to-Prometheus path.
756        let expected = input_values
757            .into_iter()
758            .enumerate()
759            .map(|(index, value)| {
760                let expected_value = if value.is_finite() {
761                    let mut buffer = Buffer::new();
762                    buffer.format_finite(value).to_string()
763                } else {
764                    value.to_string()
765                };
766                ((index + 1) as f64, expected_value)
767            })
768            .collect::<Vec<_>>();
769        assert_eq!(series[0].values, expected);
770    }
771
772    #[test]
773    fn record_batches_to_data_preserves_infinity_output() {
774        // NaN and infinities use Rust's `f64::to_string()` output.
775        let schema = Arc::new(Schema::new(vec![
776            ColumnSchema::new(
777                "timestamp",
778                ConcreteDataType::timestamp_millisecond_datatype(),
779                false,
780            ),
781            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
782        ]));
783        let batch = RecordBatch::new(
784            schema.clone(),
785            vec![
786                Arc::new(TimestampMillisecondVector::from_vec(vec![
787                    1_000, 2_000, 3_000,
788                ])) as _,
789                Arc::new(Float64Vector::from(vec![
790                    Some(f64::INFINITY),
791                    Some(f64::NEG_INFINITY),
792                    Some(f64::NAN),
793                ])) as _,
794            ],
795        )
796        .unwrap();
797        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
798
799        let response =
800            PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
801                .unwrap();
802        let PrometheusResponse::PromData(PromData {
803            result: PromQueryResult::Matrix(series),
804            ..
805        }) = response
806        else {
807            panic!("expected matrix response");
808        };
809
810        assert_eq!(series.len(), 1);
811        assert_eq!(
812            series[0].values,
813            vec![
814                (1.0, "inf".to_string()),
815                (2.0, "-inf".to_string()),
816                (3.0, "NaN".to_string()),
817            ]
818        );
819    }
820
821    #[test]
822    fn record_batches_to_data_reuses_entries_for_clustered_series() {
823        // Rows are clustered by series (a, b, a, a, b, c): consecutive rows of
824        // the same series exercise the entry-reuse fast path, while series
825        // transitions fall back to the map lookup. The result must keep the
826        // first-occurrence order and accumulate values per series as before.
827        let schema = Arc::new(Schema::new(vec![
828            ColumnSchema::new(
829                "timestamp",
830                ConcreteDataType::timestamp_millisecond_datatype(),
831                false,
832            ),
833            ColumnSchema::new("host", ConcreteDataType::string_datatype(), false),
834            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
835        ]));
836        let batch = RecordBatch::new(
837            schema.clone(),
838            vec![
839                Arc::new(TimestampMillisecondVector::from_vec(vec![
840                    1_000, 2_000, 3_000, 4_000, 5_000, 6_000,
841                ])) as _,
842                Arc::new(StringVector::from(vec![
843                    Some("a"),
844                    Some("b"),
845                    Some("a"),
846                    Some("a"),
847                    Some("b"),
848                    Some("c"),
849                ])) as _,
850                Arc::new(Float64Vector::from(vec![
851                    Some(1.0),
852                    Some(2.0),
853                    Some(3.0),
854                    Some(4.0),
855                    Some(5.0),
856                    Some(6.0),
857                ])) as _,
858            ],
859        )
860        .unwrap();
861        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
862
863        let response = PrometheusJsonResponse::record_batches_to_data(
864            batches,
865            Some("metric".to_string()),
866            ValueType::Vector,
867        )
868        .unwrap();
869        let PrometheusResponse::PromData(PromData {
870            result: PromQueryResult::Vector(series),
871            ..
872        }) = response
873        else {
874            panic!("expected vector response");
875        };
876
877        assert_eq!(series.len(), 3);
878        // Output order is first-occurrence order: a, b, c.
879        assert_eq!(
880            series
881                .iter()
882                .map(|series| series.metric["host"].as_str())
883                .collect::<Vec<_>>(),
884            vec!["a", "b", "c"]
885        );
886        // Vector results keep the last sample of each series.
887        assert_eq!(series[0].value, Some((4.0, "4.0".to_string())));
888        assert_eq!(series[1].value, Some((5.0, "5.0".to_string())));
889        assert_eq!(series[2].value, Some((6.0, "6.0".to_string())));
890    }
891
892    #[test]
893    fn record_batches_to_data_preserves_mixed_float_and_histogram_rows() {
894        let schema = Arc::new(Schema::new(vec![
895            ColumnSchema::new(
896                "timestamp",
897                ConcreteDataType::timestamp_millisecond_datatype(),
898                false,
899            ),
900            ColumnSchema::new("kind", ConcreteDataType::string_datatype(), false),
901            ColumnSchema::new("float", ConcreteDataType::float64_datatype(), true),
902            ColumnSchema::new("histogram", native_histogram_value_type().clone(), true),
903        ]));
904        let batch = RecordBatch::new(
905            schema.clone(),
906            vec![
907                Arc::new(TimestampMillisecondVector::from_values([1_000, 1_000])) as _,
908                Arc::new(StringVector::from(vec![Some("float"), Some("histogram")])) as _,
909                Arc::new(Float64Vector::from(vec![Some(1.25), None])) as _,
910                histogram_vector(&[None, Some(sample_histogram())]),
911            ],
912        )
913        .unwrap();
914        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
915
916        let response = PrometheusJsonResponse::record_batches_to_data(
917            batches,
918            Some("mixed_metric".to_string()),
919            ValueType::Vector,
920        )
921        .unwrap();
922        let PrometheusResponse::PromData(PromData {
923            result: PromQueryResult::Vector(series),
924            ..
925        }) = response
926        else {
927            panic!("expected vector response");
928        };
929
930        assert_eq!(series.len(), 2);
931        let float = series
932            .iter()
933            .find(|series| series.metric["kind"] == "float")
934            .unwrap();
935        assert_eq!(float.value, Some((1.0, "1.25".to_string())));
936        assert!(float.histogram.is_none());
937
938        let histogram = series
939            .iter()
940            .find(|series| series.metric["kind"] == "histogram")
941            .unwrap();
942        assert!(histogram.value.is_none());
943        let (timestamp, histogram) = histogram.histogram.as_ref().unwrap();
944        assert_eq!(*timestamp, 1.0);
945        assert_eq!(histogram.count, "2");
946        assert_eq!(histogram.sum, "3");
947    }
948
949    #[test]
950    fn label_replace_with_utf8view_labels_does_not_panic() {
951        // A PromQL `label_replace` query produces its new label through DataFusion's
952        // `regexp_replace`, whose output materializes as a `Utf8View` array even when
953        // the source label is a plain `Utf8`. Serializing such labels must not assume
954        // the column is a `StringArray`.
955        let schema = Arc::new(Schema::new(vec![
956            ColumnSchema::new(
957                "timestamp",
958                ConcreteDataType::timestamp_millisecond_datatype(),
959                false,
960            ),
961            ColumnSchema::new("host", ConcreteDataType::string_datatype(), false),
962            ColumnSchema::new("host_copy", ConcreteDataType::utf8_view_datatype(), false),
963            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
964        ]));
965        let batch = RecordBatch::new(
966            schema.clone(),
967            vec![
968                Arc::new(TimestampMillisecondVector::from_values([1_000])) as _,
969                Arc::new(StringVector::from(vec![Some("server-01")])) as _,
970                Arc::new(StringVector::from(StringViewArray::from(vec![Some(
971                    "server-01",
972                )]))) as _,
973                Arc::new(Float64Vector::from(vec![Some(1.0)])) as _,
974            ],
975        )
976        .unwrap();
977        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
978
979        let response = PrometheusJsonResponse::record_batches_to_data(
980            batches,
981            Some("label_replace_repro".to_string()),
982            ValueType::Vector,
983        )
984        .unwrap();
985        let PrometheusResponse::PromData(PromData {
986            result: PromQueryResult::Vector(series),
987            ..
988        }) = response
989        else {
990            panic!("expected vector response");
991        };
992
993        assert_eq!(series.len(), 1);
994        assert_eq!(series[0].metric["__name__"], "label_replace_repro");
995        assert_eq!(series[0].metric["host"], "server-01");
996        assert_eq!(series[0].metric["host_copy"], "server-01");
997        assert_eq!(series[0].value, Some((1.0, "1.0".to_string())));
998    }
999
1000    #[test]
1001    fn matrix_response_preserves_ordinary_histogram_nan_and_filters_stale_marker() {
1002        let schema = Arc::new(Schema::new(vec![
1003            ColumnSchema::new(
1004                "timestamp",
1005                ConcreteDataType::timestamp_millisecond_datatype(),
1006                false,
1007            ),
1008            ColumnSchema::new("histogram", native_histogram_value_type().clone(), true),
1009        ]));
1010        let mut ordinary_nan = sample_histogram();
1011        ordinary_nan.sum = f64::NAN;
1012        let mut stale = sample_histogram();
1013        stale.sum = f64::from_bits(PROMETHEUS_STALE_NAN_BITS);
1014        let batch = RecordBatch::new(
1015            schema.clone(),
1016            vec![
1017                Arc::new(TimestampMillisecondVector::from_values([1_000, 2_000])) as _,
1018                histogram_vector(&[Some(ordinary_nan), Some(stale)]),
1019            ],
1020        )
1021        .unwrap();
1022        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
1023
1024        let response =
1025            PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
1026                .unwrap();
1027        let PrometheusResponse::PromData(PromData {
1028            result: PromQueryResult::Matrix(series),
1029            ..
1030        }) = response
1031        else {
1032            panic!("expected matrix response");
1033        };
1034
1035        assert_eq!(series.len(), 1);
1036        assert_eq!(series[0].histograms.len(), 1);
1037        assert_eq!(series[0].histograms[0].0, 1.0);
1038        assert_eq!(series[0].histograms[0].1.sum, "NaN");
1039    }
1040
1041    #[test]
1042    fn native_histogram_json_closes_custom_bucket_zero() {
1043        let mut histogram = sample_histogram();
1044        histogram.schema = CUSTOM_BUCKETS_SCHEMA;
1045        histogram.zero_threshold = 0.0;
1046        histogram.custom_values = vec![1.0];
1047        histogram.positive_spans = vec![Span {
1048            offset: 0,
1049            length: 1,
1050        }];
1051        histogram.count = 1.0;
1052        histogram.sum = 0.0;
1053        histogram.zero_count = 0.0;
1054
1055        let json = serde_json::to_value(prometheus_native_histogram(&histogram).unwrap()).unwrap();
1056        assert_eq!(json["buckets"], serde_json::json!([[3, "-Inf", "1", "1"]]));
1057    }
1058
1059    #[test]
1060    fn native_histogram_json_preserves_terminal_finite_bucket() {
1061        let mut histogram = sample_histogram();
1062        histogram.positive_spans = vec![Span {
1063            offset: 1024,
1064            length: 2,
1065        }];
1066        histogram.positive_buckets = vec![1.0, 1.0];
1067        histogram.zero_count = 0.0;
1068
1069        let json = serde_json::to_value(prometheus_native_histogram(&histogram).unwrap()).unwrap();
1070        assert_eq!(
1071            json["buckets"],
1072            serde_json::json!([
1073                [0, 2.0_f64.powi(1023).to_string(), f64::MAX.to_string(), "1"],
1074                [0, f64::MAX.to_string(), "+Inf", "1"]
1075            ])
1076        );
1077    }
1078}