1use 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
74fn 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 #[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 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 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 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 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 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 fn record_batches_to_data(
254 batches: RecordBatches,
255 metric_name: Option<String>,
256 result_type: ValueType,
257 ) -> Result<PrometheusResponse> {
258 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 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 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 let mut buffer = IndexMap::<Vec<(&str, &str)>, PromSeriesSamples>::new();
323
324 let mut last_entry_index = None;
332
333 let schema = batches.schema();
334 for batch in batches.iter() {
335 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 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 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 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 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 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 }
489 }
490 });
491
492 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 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 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 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 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 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 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 assert_eq!(
880 series
881 .iter()
882 .map(|series| series.metric["host"].as_str())
883 .collect::<Vec<_>>(),
884 vec!["a", "b", "c"]
885 );
886 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 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}