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::{Output, OutputData};
32use common_recordbatch::RecordBatches;
33use datatypes::arrow_array::string_array_value_at_index;
34use datatypes::prelude::ConcreteDataType;
35use indexmap::IndexMap;
36use promql_parser::label::METRIC_NAME;
37use promql_parser::parser::value::ValueType;
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40use snafu::{OptionExt, ResultExt};
41
42use crate::error::{
43 ArrowSnafu, CollectRecordbatchSnafu, DataFusionSnafu, Result, UnexpectedResultSnafu,
44 status_code_to_http_status,
45};
46use crate::http::header::{GREPTIME_DB_HEADER_METRICS, collect_plan_metrics};
47use crate::http::prometheus::{
48 PromData, PromNativeHistogram, PromQueryResult, PromSeriesMatrix, PromSeriesVector,
49 PrometheusResponse,
50};
51
52#[derive(Default)]
53struct PromSeriesSamples {
54 values: Vec<(f64, String)>,
55 histograms: Vec<(f64, PromNativeHistogram)>,
56}
57
58fn prometheus_native_histogram(histogram: &NativeHistogram) -> Result<PromNativeHistogram> {
59 Ok(PromNativeHistogram {
60 count: format_prometheus_float(histogram.count),
61 sum: format_prometheus_float(histogram.sum),
62 buckets: histogram
63 .to_prometheus_buckets()
64 .context(UnexpectedResultSnafu {
65 reason: "native histogram cannot be converted to Prometheus buckets",
66 })?,
67 })
68}
69
70#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
71pub struct PrometheusJsonResponse {
72 pub status: String,
73 #[serde(skip_serializing_if = "PrometheusResponse::is_none")]
74 #[serde(default)]
75 pub data: PrometheusResponse,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub error: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 #[serde(rename = "errorType")]
80 pub error_type: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub warnings: Option<Vec<String>>,
83
84 #[serde(skip)]
85 pub status_code: Option<StatusCode>,
86 #[serde(skip)]
88 #[serde(default)]
89 pub resp_metrics: HashMap<String, Value>,
90}
91
92impl IntoResponse for PrometheusJsonResponse {
93 fn into_response(self) -> Response {
94 let metrics = if self.resp_metrics.is_empty() {
95 None
96 } else {
97 serde_json::to_string(&self.resp_metrics).ok()
98 };
99
100 let http_code = self.status_code.map(|c| status_code_to_http_status(&c));
101
102 let mut resp = Json(self).into_response();
103
104 if let Some(http_code) = http_code {
105 *resp.status_mut() = http_code;
106 }
107
108 if let Some(m) = metrics.and_then(|m| HeaderValue::from_str(&m).ok()) {
109 resp.headers_mut().insert(&GREPTIME_DB_HEADER_METRICS, m);
110 }
111
112 resp
113 }
114}
115
116impl PrometheusJsonResponse {
117 pub fn error<S1>(error_type: StatusCode, reason: S1) -> Self
118 where
119 S1: Into<String>,
120 {
121 PrometheusJsonResponse {
122 status: "error".to_string(),
123 data: PrometheusResponse::None,
124 error: Some(reason.into()),
125 error_type: Some(error_type.to_string()),
126 warnings: None,
127 resp_metrics: Default::default(),
128 status_code: Some(error_type),
129 }
130 }
131
132 pub fn success(data: PrometheusResponse) -> Self {
133 PrometheusJsonResponse {
134 status: "success".to_string(),
135 data,
136 error: None,
137 error_type: None,
138 warnings: None,
139 resp_metrics: Default::default(),
140 status_code: None,
141 }
142 }
143
144 pub async fn from_query_result(
146 result: Result<Output>,
147 metric_name: Option<String>,
148 result_type: ValueType,
149 ) -> Self {
150 let response: Result<Self> = try {
151 let result = result?;
152 let mut resp =
153 match result.data {
154 OutputData::RecordBatches(batches) => Self::success(
155 Self::record_batches_to_data(batches, metric_name, result_type)?,
156 ),
157 OutputData::Stream(stream) => {
158 let record_batches = RecordBatches::try_collect(stream)
159 .await
160 .context(CollectRecordbatchSnafu)?;
161 Self::success(Self::record_batches_to_data(
162 record_batches,
163 metric_name,
164 result_type,
165 )?)
166 }
167 OutputData::AffectedRows(_) => Self::error(
168 StatusCode::Unexpected,
169 "expected data result, but got affected rows",
170 ),
171 };
172
173 if let Some(physical_plan) = result.meta.plan {
174 let mut result_map = HashMap::new();
175 let mut tmp = vec![&mut result_map];
176 collect_plan_metrics(&physical_plan, &mut tmp);
177
178 let re = result_map
179 .into_iter()
180 .map(|(k, v)| (k, Value::from(v)))
181 .collect();
182 resp.resp_metrics = re;
183 }
184
185 resp
186 };
187
188 let result_type_string = result_type.to_string();
189
190 match response {
191 Ok(resp) => resp,
192 Err(err) => {
193 if err.status_code() == StatusCode::TableNotFound
195 || err.status_code() == StatusCode::TableColumnNotFound
196 {
197 Self::success(PrometheusResponse::PromData(PromData {
198 result_type: result_type_string,
199 ..Default::default()
200 }))
201 } else {
202 Self::error(err.status_code(), err.output_msg())
203 }
204 }
205 }
206 }
207
208 fn record_batches_to_data(
210 batches: RecordBatches,
211 metric_name: Option<String>,
212 result_type: ValueType,
213 ) -> Result<PrometheusResponse> {
214 if batches.iter().next().is_none() {
216 return Ok(PrometheusResponse::PromData(PromData {
217 result_type: result_type.to_string(),
218 ..Default::default()
219 }));
220 }
221
222 let mut timestamp_column_index = None;
225 let mut tag_column_indices = Vec::new();
226 let mut first_field_column_index = None;
227 let mut native_histogram_column_index = None;
228
229 let mut num_label_columns = 0;
230
231 for (i, column) in batches.schema().column_schemas().iter().enumerate() {
232 match column.data_type {
233 ConcreteDataType::Timestamp(datatypes::types::TimestampType::Millisecond(_))
234 if timestamp_column_index.is_none() =>
235 {
236 timestamp_column_index = Some(i);
237 }
238 ConcreteDataType::Float32(_)
240 | ConcreteDataType::Float64(_)
241 | ConcreteDataType::Int8(_)
242 | ConcreteDataType::Int16(_)
243 | ConcreteDataType::Int32(_)
244 | ConcreteDataType::Int64(_)
245 | ConcreteDataType::UInt8(_)
246 | ConcreteDataType::UInt16(_)
247 | ConcreteDataType::UInt32(_)
248 | ConcreteDataType::UInt64(_)
249 if first_field_column_index.is_none() =>
250 {
251 first_field_column_index = Some(i);
252 }
253 _ if native_histogram_column_index.is_none()
254 && is_native_histogram_value_type(&column.data_type) =>
255 {
256 native_histogram_column_index = Some(i);
257 }
258 ConcreteDataType::String(_) => {
259 tag_column_indices.push(i);
260 num_label_columns += 1;
261 }
262 _ => {}
263 }
264 }
265
266 let timestamp_column_index = timestamp_column_index.context(UnexpectedResultSnafu {
267 reason: "no timestamp column found".to_string(),
268 })?;
269 if first_field_column_index.is_none() && native_histogram_column_index.is_none() {
270 return UnexpectedResultSnafu {
271 reason: "no value column found".to_string(),
272 }
273 .fail();
274 }
275
276 let mut buffer = IndexMap::<Vec<(&str, &str)>, PromSeriesSamples>::new();
279
280 let schema = batches.schema();
281 for batch in batches.iter() {
282 let tag_columns = tag_column_indices
284 .iter()
285 .map(|i| batch.column(*i))
286 .collect::<Vec<_>>();
287 let tag_names = tag_column_indices
288 .iter()
289 .map(|c| schema.column_name_by_index(*c))
290 .collect::<Vec<_>>();
291 let timestamp_column = batch
292 .column(timestamp_column_index)
293 .as_primitive::<TimestampMillisecondType>();
294
295 let field_array = first_field_column_index
296 .map(|index| arrow::compute::cast(batch.column(index), &DataType::Float64))
297 .transpose()
298 .context(ArrowSnafu)?;
299 let field_column = field_array
300 .as_ref()
301 .map(|array| array.as_primitive::<Float64Type>());
302 let native_histogram_column = native_histogram_column_index
303 .map(|index| {
304 batch
305 .column(index)
306 .as_any()
307 .downcast_ref::<StructArray>()
308 .with_context(|| UnexpectedResultSnafu {
309 reason: "native histogram column is not a struct array",
310 })
311 })
312 .transpose()?;
313
314 for row_index in 0..batch.num_rows() {
316 let value = field_column.and_then(|field_column| {
317 if !field_column.is_valid(row_index) {
318 return None;
319 }
320 let value = field_column.value(row_index);
321 (!is_prometheus_stale_nan(value))
322 .then_some((timestamp_column.value(row_index), value))
323 });
324 let histogram = native_histogram_column
325 .and_then(|column| {
326 read_histogram(column, row_index)
327 .context(DataFusionSnafu)
328 .transpose()
329 })
330 .transpose()?
331 .filter(|histogram| !is_prometheus_stale_nan(histogram.sum))
332 .map(|histogram| {
333 prometheus_native_histogram(&histogram)
334 .map(|histogram| (timestamp_column.value(row_index), histogram))
335 })
336 .transpose()?;
337
338 if value.is_none() && histogram.is_none() {
339 continue;
340 }
341
342 let mut tags = Vec::with_capacity(num_label_columns + 1);
344 if let Some(metric_name) = &metric_name {
345 tags.push((METRIC_NAME, metric_name.as_str()));
346 }
347 for (tag_column, tag_name) in tag_columns.iter().zip(tag_names.iter()) {
348 if let Some(tag_value) = string_array_value_at_index(tag_column, row_index) {
349 tags.push((tag_name, tag_value));
350 }
351 }
352
353 let entry = buffer.entry(tags).or_default();
354 if let Some((timestamp_millis, histogram)) = histogram {
355 entry
356 .histograms
357 .push((timestamp_millis as f64 / 1000.0, histogram));
358 } else if let Some((timestamp_millis, value)) = value {
359 entry
360 .values
361 .push((timestamp_millis as f64 / 1000.0, value.to_string()));
362 }
363 }
364 }
365
366 let mut result = match result_type {
368 ValueType::Vector => PromQueryResult::Vector(vec![]),
369 ValueType::Matrix => PromQueryResult::Matrix(vec![]),
370 ValueType::Scalar => PromQueryResult::Scalar(None),
371 ValueType::String => PromQueryResult::String(None),
372 };
373
374 buffer.into_iter().for_each(|(tags, mut samples)| {
376 let metric = tags
377 .into_iter()
378 .map(|(k, v)| (k.to_string(), v.to_string()))
379 .collect::<BTreeMap<_, _>>();
380 match result {
381 PromQueryResult::Vector(ref mut v) => {
382 let histogram = samples.histograms.pop();
383 let value = if histogram.is_none() {
384 samples.values.pop()
385 } else {
386 None
387 };
388 v.push(PromSeriesVector {
389 metric,
390 value,
391 histogram,
392 });
393 }
394 PromQueryResult::Matrix(ref mut v) => {
395 if !samples.values.is_sorted_by(|a, b| a.0 <= b.0) {
397 samples
398 .values
399 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
400 }
401 if !samples.histograms.is_sorted_by(|a, b| a.0 <= b.0) {
402 samples
403 .histograms
404 .sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
405 }
406
407 v.push(PromSeriesMatrix {
408 metric,
409 values: samples.values,
410 histograms: samples.histograms,
411 });
412 }
413 PromQueryResult::Scalar(ref mut v) => {
414 *v = samples.values.pop();
415 }
416 PromQueryResult::String(ref mut _v) => {
417 }
419 }
420 });
421
422 if let PromQueryResult::Matrix(ref mut v) = result {
425 v.sort_by(|a, b| a.metric.cmp(&b.metric));
426 }
427
428 let result_type_string = result_type.to_string();
429 let data = PrometheusResponse::PromData(PromData {
430 result_type: result_type_string,
431 result,
432 });
433
434 Ok(data)
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use std::sync::Arc;
441
442 use common_query::native_histogram::{
443 CUSTOM_BUCKETS_SCHEMA, CounterResetHint, NativeHistogram, Span, build_histogram_array,
444 native_histogram_value_type,
445 };
446 use common_query::prometheus::PROMETHEUS_STALE_NAN_BITS;
447 use common_recordbatch::{RecordBatch, RecordBatches};
448 use datatypes::data_type::ConcreteDataType;
449 use datatypes::schema::{ColumnSchema, Schema};
450 use datatypes::vectors::{
451 Float64Vector, StringVector, StructVector, TimestampMillisecondVector, VectorRef,
452 };
453
454 use super::*;
455
456 fn sample_histogram() -> NativeHistogram {
457 NativeHistogram {
458 schema: 0,
459 zero_threshold: 0.001,
460 sum: 3.0,
461 reset_hint: CounterResetHint::Unknown,
462 start_timestamp: Some(0),
463 custom_values: vec![],
464 positive_spans: vec![Span {
465 offset: 0,
466 length: 1,
467 }],
468 negative_spans: vec![],
469 count: 2.0,
470 zero_count: 1.0,
471 positive_buckets: vec![1.0],
472 negative_buckets: vec![],
473 }
474 }
475
476 fn histogram_vector(values: &[Option<NativeHistogram>]) -> VectorRef {
477 let histogram_array = build_histogram_array(values);
478 let histogram_array = histogram_array
479 .as_any()
480 .downcast_ref::<StructArray>()
481 .unwrap()
482 .clone();
483 let ConcreteDataType::Struct(histogram_type) = native_histogram_value_type().clone() else {
484 unreachable!("native histogram type must be a struct")
485 };
486 Arc::new(StructVector::try_new(histogram_type, histogram_array).unwrap())
487 }
488
489 #[test]
490 fn matrix_response_preserves_ordinary_nan_and_filters_stale_markers() {
491 let schema = Arc::new(Schema::new(vec![
492 ColumnSchema::new(
493 "timestamp",
494 ConcreteDataType::timestamp_millisecond_datatype(),
495 false,
496 ),
497 ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
498 ]));
499 let batch = RecordBatch::new(
500 schema.clone(),
501 vec![
502 Arc::new(TimestampMillisecondVector::from_vec(vec![
503 1_000, 2_000, 3_000, 4_000,
504 ])) as _,
505 Arc::new(Float64Vector::from(vec![
506 Some(1.0),
507 Some(f64::from_bits(0x7ff8_0000_0000_0000)),
508 Some(f64::from_bits(0x7ff0_0000_0000_0002)),
509 None,
510 ])) as _,
511 ],
512 )
513 .unwrap();
514 let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
515
516 let response =
517 PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
518 .unwrap();
519 let PrometheusResponse::PromData(data) = response else {
520 panic!("expected Prometheus data response");
521 };
522 let PromQueryResult::Matrix(series) = data.result else {
523 panic!("expected matrix result");
524 };
525
526 assert_eq!(series.len(), 1);
527 assert_eq!(
528 series[0].values,
529 vec![(1.0, "1".to_string()), (2.0, "NaN".to_string())]
530 );
531 }
532
533 #[test]
534 fn record_batches_to_data_preserves_mixed_float_and_histogram_rows() {
535 let schema = Arc::new(Schema::new(vec![
536 ColumnSchema::new(
537 "timestamp",
538 ConcreteDataType::timestamp_millisecond_datatype(),
539 false,
540 ),
541 ColumnSchema::new("kind", ConcreteDataType::string_datatype(), false),
542 ColumnSchema::new("float", ConcreteDataType::float64_datatype(), true),
543 ColumnSchema::new("histogram", native_histogram_value_type().clone(), true),
544 ]));
545 let batch = RecordBatch::new(
546 schema.clone(),
547 vec![
548 Arc::new(TimestampMillisecondVector::from_values([1_000, 1_000])) as _,
549 Arc::new(StringVector::from(vec![Some("float"), Some("histogram")])) as _,
550 Arc::new(Float64Vector::from(vec![Some(1.25), None])) as _,
551 histogram_vector(&[None, Some(sample_histogram())]),
552 ],
553 )
554 .unwrap();
555 let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
556
557 let response = PrometheusJsonResponse::record_batches_to_data(
558 batches,
559 Some("mixed_metric".to_string()),
560 ValueType::Vector,
561 )
562 .unwrap();
563 let PrometheusResponse::PromData(PromData {
564 result: PromQueryResult::Vector(series),
565 ..
566 }) = response
567 else {
568 panic!("expected vector response");
569 };
570
571 assert_eq!(series.len(), 2);
572 let float = series
573 .iter()
574 .find(|series| series.metric["kind"] == "float")
575 .unwrap();
576 assert_eq!(float.value, Some((1.0, "1.25".to_string())));
577 assert!(float.histogram.is_none());
578
579 let histogram = series
580 .iter()
581 .find(|series| series.metric["kind"] == "histogram")
582 .unwrap();
583 assert!(histogram.value.is_none());
584 let (timestamp, histogram) = histogram.histogram.as_ref().unwrap();
585 assert_eq!(*timestamp, 1.0);
586 assert_eq!(histogram.count, "2");
587 assert_eq!(histogram.sum, "3");
588 }
589
590 #[test]
591 fn matrix_response_preserves_ordinary_histogram_nan_and_filters_stale_marker() {
592 let schema = Arc::new(Schema::new(vec![
593 ColumnSchema::new(
594 "timestamp",
595 ConcreteDataType::timestamp_millisecond_datatype(),
596 false,
597 ),
598 ColumnSchema::new("histogram", native_histogram_value_type().clone(), true),
599 ]));
600 let mut ordinary_nan = sample_histogram();
601 ordinary_nan.sum = f64::NAN;
602 let mut stale = sample_histogram();
603 stale.sum = f64::from_bits(PROMETHEUS_STALE_NAN_BITS);
604 let batch = RecordBatch::new(
605 schema.clone(),
606 vec![
607 Arc::new(TimestampMillisecondVector::from_values([1_000, 2_000])) as _,
608 histogram_vector(&[Some(ordinary_nan), Some(stale)]),
609 ],
610 )
611 .unwrap();
612 let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
613
614 let response =
615 PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
616 .unwrap();
617 let PrometheusResponse::PromData(PromData {
618 result: PromQueryResult::Matrix(series),
619 ..
620 }) = response
621 else {
622 panic!("expected matrix response");
623 };
624
625 assert_eq!(series.len(), 1);
626 assert_eq!(series[0].histograms.len(), 1);
627 assert_eq!(series[0].histograms[0].0, 1.0);
628 assert_eq!(series[0].histograms[0].1.sum, "NaN");
629 }
630
631 #[test]
632 fn native_histogram_json_closes_custom_bucket_zero() {
633 let mut histogram = sample_histogram();
634 histogram.schema = CUSTOM_BUCKETS_SCHEMA;
635 histogram.zero_threshold = 0.0;
636 histogram.custom_values = vec![1.0];
637 histogram.positive_spans = vec![Span {
638 offset: 0,
639 length: 1,
640 }];
641 histogram.count = 1.0;
642 histogram.sum = 0.0;
643 histogram.zero_count = 0.0;
644
645 let json = serde_json::to_value(prometheus_native_histogram(&histogram).unwrap()).unwrap();
646 assert_eq!(json["buckets"], serde_json::json!([[3, "-Inf", "1", "1"]]));
647 }
648
649 #[test]
650 fn native_histogram_json_preserves_terminal_finite_bucket() {
651 let mut histogram = sample_histogram();
652 histogram.positive_spans = vec![Span {
653 offset: 1024,
654 length: 2,
655 }];
656 histogram.positive_buckets = vec![1.0, 1.0];
657 histogram.zero_count = 0.0;
658
659 let json = serde_json::to_value(prometheus_native_histogram(&histogram).unwrap()).unwrap();
660 assert_eq!(
661 json["buckets"],
662 serde_json::json!([
663 [0, 2.0_f64.powi(1023).to_string(), f64::MAX.to_string(), "1"],
664 [0, f64::MAX.to_string(), "+Inf", "1"]
665 ])
666 );
667 }
668}