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};
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::prometheus::is_prometheus_stale_nan;
28use common_query::{Output, OutputData};
29use common_recordbatch::RecordBatches;
30use datatypes::prelude::ConcreteDataType;
31use indexmap::IndexMap;
32use promql_parser::label::METRIC_NAME;
33use promql_parser::parser::value::ValueType;
34use serde::{Deserialize, Serialize};
35use serde_json::Value;
36use snafu::{OptionExt, ResultExt};
37
38use crate::error::{
39    ArrowSnafu, CollectRecordbatchSnafu, Result, UnexpectedResultSnafu, status_code_to_http_status,
40};
41use crate::http::header::{GREPTIME_DB_HEADER_METRICS, collect_plan_metrics};
42use crate::http::prometheus::{
43    PromData, PromQueryResult, PromSeriesMatrix, PromSeriesVector, PrometheusResponse,
44};
45
46#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
47pub struct PrometheusJsonResponse {
48    pub status: String,
49    #[serde(skip_serializing_if = "PrometheusResponse::is_none")]
50    #[serde(default)]
51    pub data: PrometheusResponse,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub error: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[serde(rename = "errorType")]
56    pub error_type: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub warnings: Option<Vec<String>>,
59
60    #[serde(skip)]
61    pub status_code: Option<StatusCode>,
62    // placeholder for header value
63    #[serde(skip)]
64    #[serde(default)]
65    pub resp_metrics: HashMap<String, Value>,
66}
67
68impl IntoResponse for PrometheusJsonResponse {
69    fn into_response(self) -> Response {
70        let metrics = if self.resp_metrics.is_empty() {
71            None
72        } else {
73            serde_json::to_string(&self.resp_metrics).ok()
74        };
75
76        let http_code = self.status_code.map(|c| status_code_to_http_status(&c));
77
78        let mut resp = Json(self).into_response();
79
80        if let Some(http_code) = http_code {
81            *resp.status_mut() = http_code;
82        }
83
84        if let Some(m) = metrics.and_then(|m| HeaderValue::from_str(&m).ok()) {
85            resp.headers_mut().insert(&GREPTIME_DB_HEADER_METRICS, m);
86        }
87
88        resp
89    }
90}
91
92impl PrometheusJsonResponse {
93    pub fn error<S1>(error_type: StatusCode, reason: S1) -> Self
94    where
95        S1: Into<String>,
96    {
97        PrometheusJsonResponse {
98            status: "error".to_string(),
99            data: PrometheusResponse::None,
100            error: Some(reason.into()),
101            error_type: Some(error_type.to_string()),
102            warnings: None,
103            resp_metrics: Default::default(),
104            status_code: Some(error_type),
105        }
106    }
107
108    pub fn success(data: PrometheusResponse) -> Self {
109        PrometheusJsonResponse {
110            status: "success".to_string(),
111            data,
112            error: None,
113            error_type: None,
114            warnings: None,
115            resp_metrics: Default::default(),
116            status_code: None,
117        }
118    }
119
120    /// Convert from `Result<Output>`
121    pub async fn from_query_result(
122        result: Result<Output>,
123        metric_name: Option<String>,
124        result_type: ValueType,
125    ) -> Self {
126        let response: Result<Self> = try {
127            let result = result?;
128            let mut resp =
129                match result.data {
130                    OutputData::RecordBatches(batches) => Self::success(
131                        Self::record_batches_to_data(batches, metric_name, result_type)?,
132                    ),
133                    OutputData::Stream(stream) => {
134                        let record_batches = RecordBatches::try_collect(stream)
135                            .await
136                            .context(CollectRecordbatchSnafu)?;
137                        Self::success(Self::record_batches_to_data(
138                            record_batches,
139                            metric_name,
140                            result_type,
141                        )?)
142                    }
143                    OutputData::AffectedRows(_) => Self::error(
144                        StatusCode::Unexpected,
145                        "expected data result, but got affected rows",
146                    ),
147                };
148
149            if let Some(physical_plan) = result.meta.plan {
150                let mut result_map = HashMap::new();
151                let mut tmp = vec![&mut result_map];
152                collect_plan_metrics(&physical_plan, &mut tmp);
153
154                let re = result_map
155                    .into_iter()
156                    .map(|(k, v)| (k, Value::from(v)))
157                    .collect();
158                resp.resp_metrics = re;
159            }
160
161            resp
162        };
163
164        let result_type_string = result_type.to_string();
165
166        match response {
167            Ok(resp) => resp,
168            Err(err) => {
169                // Prometheus won't report error if querying nonexist label and metric
170                if err.status_code() == StatusCode::TableNotFound
171                    || err.status_code() == StatusCode::TableColumnNotFound
172                {
173                    Self::success(PrometheusResponse::PromData(PromData {
174                        result_type: result_type_string,
175                        ..Default::default()
176                    }))
177                } else {
178                    Self::error(err.status_code(), err.output_msg())
179                }
180            }
181        }
182    }
183
184    /// Convert [RecordBatches] to [PromData]
185    fn record_batches_to_data(
186        batches: RecordBatches,
187        metric_name: Option<String>,
188        result_type: ValueType,
189    ) -> Result<PrometheusResponse> {
190        // Return empty result if no batches
191        if batches.iter().next().is_none() {
192            return Ok(PrometheusResponse::PromData(PromData {
193                result_type: result_type.to_string(),
194                ..Default::default()
195            }));
196        }
197
198        // infer semantic type of each column from schema.
199        // TODO(ruihang): wish there is a better way to do this.
200        let mut timestamp_column_index = None;
201        let mut tag_column_indices = Vec::new();
202        let mut first_field_column_index = None;
203
204        let mut num_label_columns = 0;
205
206        for (i, column) in batches.schema().column_schemas().iter().enumerate() {
207            match column.data_type {
208                ConcreteDataType::Timestamp(datatypes::types::TimestampType::Millisecond(_))
209                    if timestamp_column_index.is_none() =>
210                {
211                    timestamp_column_index = Some(i);
212                }
213                // Treat all value types as field
214                ConcreteDataType::Float32(_)
215                | ConcreteDataType::Float64(_)
216                | ConcreteDataType::Int8(_)
217                | ConcreteDataType::Int16(_)
218                | ConcreteDataType::Int32(_)
219                | ConcreteDataType::Int64(_)
220                | ConcreteDataType::UInt8(_)
221                | ConcreteDataType::UInt16(_)
222                | ConcreteDataType::UInt32(_)
223                | ConcreteDataType::UInt64(_)
224                    if first_field_column_index.is_none() =>
225                {
226                    first_field_column_index = Some(i);
227                }
228                ConcreteDataType::String(_) => {
229                    tag_column_indices.push(i);
230                    num_label_columns += 1;
231                }
232                _ => {}
233            }
234        }
235
236        let timestamp_column_index = timestamp_column_index.context(UnexpectedResultSnafu {
237            reason: "no timestamp column found".to_string(),
238        })?;
239        let first_field_column_index = first_field_column_index.context(UnexpectedResultSnafu {
240            reason: "no value column found".to_string(),
241        })?;
242
243        // Preserves the order of output tags.
244        // Tag order matters, e.g., after sorc and sort_desc, the output order must be kept.
245        let mut buffer = IndexMap::<Vec<(&str, &str)>, Vec<(f64, String)>>::new();
246
247        let schema = batches.schema();
248        for batch in batches.iter() {
249            // prepare things...
250            let tag_columns = tag_column_indices
251                .iter()
252                .map(|i| batch.column(*i).as_string::<i32>())
253                .collect::<Vec<_>>();
254            let tag_names = tag_column_indices
255                .iter()
256                .map(|c| schema.column_name_by_index(*c))
257                .collect::<Vec<_>>();
258            let timestamp_column = batch
259                .column(timestamp_column_index)
260                .as_primitive::<TimestampMillisecondType>();
261
262            let array =
263                arrow::compute::cast(batch.column(first_field_column_index), &DataType::Float64)
264                    .context(ArrowSnafu)?;
265            let field_column = array.as_primitive::<Float64Type>();
266
267            // assemble rows
268            for row_index in 0..batch.num_rows() {
269                // retrieve value
270                if field_column.is_valid(row_index) {
271                    let v = field_column.value(row_index);
272                    // Ignore Prometheus stale markers.
273                    if is_prometheus_stale_nan(v) {
274                        continue;
275                    }
276
277                    // retrieve tags
278                    let mut tags = Vec::with_capacity(num_label_columns + 1);
279                    if let Some(metric_name) = &metric_name {
280                        tags.push((METRIC_NAME, metric_name.as_str()));
281                    }
282                    for (tag_column, tag_name) in tag_columns.iter().zip(tag_names.iter()) {
283                        // TODO(ruihang): add test for NULL tag
284                        if tag_column.is_valid(row_index) {
285                            tags.push((tag_name, tag_column.value(row_index)));
286                        }
287                    }
288
289                    // retrieve timestamp
290                    let timestamp_millis = timestamp_column.value(row_index);
291                    let timestamp = timestamp_millis as f64 / 1000.0;
292
293                    buffer
294                        .entry(tags)
295                        .or_default()
296                        .push((timestamp, Into::<f64>::into(v).to_string()));
297                };
298            }
299        }
300
301        // initialize result to return
302        let mut result = match result_type {
303            ValueType::Vector => PromQueryResult::Vector(vec![]),
304            ValueType::Matrix => PromQueryResult::Matrix(vec![]),
305            ValueType::Scalar => PromQueryResult::Scalar(None),
306            ValueType::String => PromQueryResult::String(None),
307        };
308
309        // accumulate data into result
310        buffer.into_iter().for_each(|(tags, mut values)| {
311            let metric = tags
312                .into_iter()
313                .map(|(k, v)| (k.to_string(), v.to_string()))
314                .collect::<BTreeMap<_, _>>();
315            match result {
316                PromQueryResult::Vector(ref mut v) => {
317                    v.push(PromSeriesVector {
318                        metric,
319                        value: values.pop(),
320                    });
321                }
322                PromQueryResult::Matrix(ref mut v) => {
323                    // sort values by timestamp
324                    if !values.is_sorted_by(|a, b| a.0 <= b.0) {
325                        values.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
326                    }
327
328                    v.push(PromSeriesMatrix { metric, values });
329                }
330                PromQueryResult::Scalar(ref mut v) => {
331                    *v = values.pop();
332                }
333                PromQueryResult::String(ref mut _v) => {
334                    // TODO(ruihang): Not supported yet
335                }
336            }
337        });
338
339        // sort matrix by metric
340        // see: https://prometheus.io/docs/prometheus/3.5/querying/api/#range-vectors
341        if let PromQueryResult::Matrix(ref mut v) = result {
342            v.sort_by(|a, b| a.metric.cmp(&b.metric));
343        }
344
345        let result_type_string = result_type.to_string();
346        let data = PrometheusResponse::PromData(PromData {
347            result_type: result_type_string,
348            result,
349        });
350
351        Ok(data)
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use std::sync::Arc;
358
359    use common_recordbatch::{RecordBatch, RecordBatches};
360    use datatypes::data_type::ConcreteDataType;
361    use datatypes::schema::{ColumnSchema, Schema};
362    use datatypes::vectors::{Float64Vector, TimestampMillisecondVector};
363
364    use super::*;
365
366    #[test]
367    fn matrix_response_preserves_ordinary_nan_and_filters_stale_markers() {
368        let schema = Arc::new(Schema::new(vec![
369            ColumnSchema::new(
370                "timestamp",
371                ConcreteDataType::timestamp_millisecond_datatype(),
372                false,
373            ),
374            ColumnSchema::new("value", ConcreteDataType::float64_datatype(), true),
375        ]));
376        let batch = RecordBatch::new(
377            schema.clone(),
378            vec![
379                Arc::new(TimestampMillisecondVector::from_vec(vec![
380                    1_000, 2_000, 3_000, 4_000,
381                ])) as _,
382                Arc::new(Float64Vector::from(vec![
383                    Some(1.0),
384                    Some(f64::from_bits(0x7ff8_0000_0000_0000)),
385                    Some(f64::from_bits(0x7ff0_0000_0000_0002)),
386                    None,
387                ])) as _,
388            ],
389        )
390        .unwrap();
391        let batches = RecordBatches::try_new(schema, vec![batch]).unwrap();
392
393        let response =
394            PrometheusJsonResponse::record_batches_to_data(batches, None, ValueType::Matrix)
395                .unwrap();
396        let PrometheusResponse::PromData(data) = response else {
397            panic!("expected Prometheus data response");
398        };
399        let PromQueryResult::Matrix(series) = data.result else {
400            panic!("expected matrix result");
401        };
402
403        assert_eq!(series.len(), 1);
404        assert_eq!(
405            series[0].values,
406            vec![(1.0, "1".to_string()), (2.0, "NaN".to_string())]
407        );
408    }
409}