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::{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    // placeholder for header value
87    #[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    /// Convert from `Result<Output>`
145    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                // Prometheus won't report error if querying nonexist label and metric
194                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    /// Convert [RecordBatches] to [PromData]
209    fn record_batches_to_data(
210        batches: RecordBatches,
211        metric_name: Option<String>,
212        result_type: ValueType,
213    ) -> Result<PrometheusResponse> {
214        // Return empty result if no batches
215        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        // infer semantic type of each column from schema.
223        // TODO(ruihang): wish there is a better way to do this.
224        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                // Treat all value types as field
239                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        // Preserves the order of output tags.
277        // Tag order matters, e.g., after sorc and sort_desc, the output order must be kept.
278        let mut buffer = IndexMap::<Vec<(&str, &str)>, PromSeriesSamples>::new();
279
280        let schema = batches.schema();
281        for batch in batches.iter() {
282            // prepare things...
283            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            // assemble rows
315            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                // retrieve tags
343                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        // initialize result to return
367        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        // accumulate data into result
375        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                    // sort values by timestamp
396                    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                    // TODO(ruihang): Not supported yet
418                }
419            }
420        });
421
422        // sort matrix by metric
423        // see: https://prometheus.io/docs/prometheus/3.5/querying/api/#range-vectors
424        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}