Skip to main content

frontend/instance/
promql.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
15use std::collections::HashSet;
16use std::sync::Arc;
17use std::time::SystemTime;
18
19use auth::PermissionTableTarget;
20use catalog::information_schema::TABLES;
21use client::OutputData;
22use common_catalog::consts::INFORMATION_SCHEMA_NAME;
23use common_catalog::format_full_table_name;
24use common_recordbatch::{RecordBatch, util};
25use common_telemetry::tracing;
26use datafusion_expr::LogicalPlan;
27use datatypes::arrow::array::{Array, UInt32Array};
28use futures::StreamExt;
29use promql_parser::label::{Matcher, Matchers};
30use query::promql;
31use query::promql::planner::PromPlanner;
32use servers::prometheus;
33use session::context::QueryContextRef;
34use snafu::{OptionExt, ResultExt};
35use store_api::metric_engine_consts::DATA_SCHEMA_TABLE_ID_COLUMN_NAME;
36use store_api::storage::TableId;
37use table::TableRef;
38
39use crate::error::{
40    CatalogSnafu, CollectRecordbatchSnafu, ExecLogicalPlanSnafu,
41    PrometheusLabelValuesQueryPlanSnafu, PrometheusMetricNamesQueryPlanSnafu, ReadTableSnafu,
42    Result, TableNotFoundSnafu, TableSnafu, UnexpectedColumnTypeSnafu,
43};
44use crate::instance::Instance;
45
46/// Strips the output sort a PromQL plan ends with, keeping the plan schema intact.
47///
48/// Only the sort the caller would observe is removed: recursion stops at any other
49/// node, so ordering consumed by windows, limits or PromQL extension nodes stays.
50pub(super) fn remove_output_sort(plan: LogicalPlan) -> LogicalPlan {
51    match plan {
52        LogicalPlan::Sort(sort) if sort.fetch.is_none() => Arc::unwrap_or_clone(sort.input),
53        LogicalPlan::Projection(mut projection) => {
54            projection.input = Arc::new(remove_output_sort(Arc::unwrap_or_clone(projection.input)));
55            LogicalPlan::Projection(projection)
56        }
57        plan => plan,
58    }
59}
60
61impl Instance {
62    /// Handles metric names query request, returns the names.
63    #[tracing::instrument(skip_all)]
64    pub(crate) async fn handle_query_metric_names(
65        &self,
66        matchers: Vec<Matcher>,
67        schema: &str,
68        ctx: &QueryContextRef,
69    ) -> Result<Vec<String>> {
70        let _timer = crate::metrics::PROMQL_QUERY_METRICS_ELAPSED
71            .with_label_values(&[ctx.get_db_string().as_str()])
72            .start_timer();
73
74        let table = self
75            .catalog_manager
76            .table(
77                ctx.current_catalog(),
78                INFORMATION_SCHEMA_NAME,
79                TABLES,
80                Some(ctx),
81            )
82            .await
83            .context(CatalogSnafu)?
84            .with_context(|| TableNotFoundSnafu {
85                table_name: "greptime.information_schema.tables",
86            })?;
87
88        let dataframe = self
89            .query_engine
90            .read_table(table)
91            .with_context(|_| ReadTableSnafu {
92                table_name: "greptime.information_schema.tables",
93            })?;
94
95        let logical_plan =
96            prometheus::metric_name_matchers_to_plan(dataframe, matchers, schema, ctx)
97                .context(PrometheusMetricNamesQueryPlanSnafu)?;
98
99        let results = self
100            .query_engine
101            .execute(logical_plan, ctx.clone())
102            .await
103            .context(ExecLogicalPlanSnafu)?;
104
105        let batches = match results.data {
106            OutputData::Stream(stream) => util::collect(stream)
107                .await
108                .context(CollectRecordbatchSnafu)?,
109            OutputData::RecordBatches(rbs) => rbs.take(),
110            _ => unreachable!("should not happen"),
111        };
112
113        let mut results = Vec::with_capacity(batches.iter().map(|b| b.num_rows()).sum());
114
115        for batch in batches {
116            // Only one column the results, ensured by `prometheus::metric_name_matchers_to_plan`.
117            batch
118                .iter_column_as_string(0)
119                .flatten()
120                .for_each(|x| results.push(x))
121        }
122
123        Ok(results)
124    }
125
126    /// Handles a metric names query constrained by matchers on ordinary labels.
127    ///
128    /// [`Instance::handle_query_metric_names`] answers from table metadata, which
129    /// cannot resolve a label matcher: whether a metric carries `pod="abc"` is a
130    /// property of its data. The metric engine multiplexes the logical tables of
131    /// one physical table into a region holding the union of their label columns
132    /// alongside `__table_id`, so a single distinct scan per physical table
133    /// resolves the matchers for all of its logical tables at once.
134    ///
135    /// Only metric engine tables are covered. Tables on other engines share no
136    /// column space to scan, and a scan per table does not scale to the table
137    /// counts this API is expected to answer over.
138    #[tracing::instrument(skip_all)]
139    pub(crate) async fn handle_query_metric_names_by_labels(
140        &self,
141        matchers: Vec<Matcher>,
142        schema: &str,
143        start: SystemTime,
144        end: SystemTime,
145        ctx: &QueryContextRef,
146    ) -> Result<Vec<String>> {
147        let _timer = crate::metrics::PROMQL_QUERY_METRICS_ELAPSED
148            .with_label_values(&[ctx.get_db_string().as_str()])
149            .start_timer();
150
151        let catalog = ctx.current_catalog();
152        let mut table_ids = HashSet::new();
153        for physical in self.physical_metric_tables(catalog, schema, ctx).await? {
154            table_ids.extend(
155                self.scan_matching_table_ids(physical, &matchers, start, end, ctx)
156                    .await?,
157            );
158        }
159
160        // Batch-resolve only the ids the scan produced. An id dropped between the
161        // scan and here simply has no entry.
162        let table_ids = table_ids.into_iter().collect::<Vec<_>>();
163        let mut names = self
164            .catalog_manager
165            .tables_by_ids(catalog, schema, &table_ids)
166            .await
167            .context(CatalogSnafu)?
168            .into_iter()
169            .map(|table| table.table_info().name.clone())
170            .collect::<Vec<_>>();
171        names.sort_unstable();
172        Ok(names)
173    }
174
175    /// Returns the metric engine physical tables of a schema.
176    ///
177    /// Their data regions carry the union of their logical tables' label columns,
178    /// so scanning these covers every metric of the schema.
179    async fn physical_metric_tables(
180        &self,
181        catalog: &str,
182        schema: &str,
183        ctx: &QueryContextRef,
184    ) -> Result<Vec<TableRef>> {
185        let mut tables = self.catalog_manager.tables(catalog, schema, Some(ctx));
186        let mut physical_tables = Vec::new();
187
188        while let Some(table) = tables.next().await {
189            let table = table.context(CatalogSnafu)?;
190            if table.table_info().is_physical_table() {
191                physical_tables.push(table);
192            }
193        }
194
195        Ok(physical_tables)
196    }
197
198    /// Scans `table` for the distinct values of `column` that match `matchers`
199    /// within the time range.
200    ///
201    /// Callers look the table up and decode the batches; the plan between is the
202    /// same whether the projected column is a label or `__table_id`.
203    async fn scan_distinct_column(
204        &self,
205        table: TableRef,
206        matchers: Vec<Matcher>,
207        column: String,
208        start: SystemTime,
209        end: SystemTime,
210        ctx: &QueryContextRef,
211    ) -> Result<Vec<RecordBatch>> {
212        let dataframe = self
213            .query_engine
214            .read_table(table.clone())
215            .with_context(|_| ReadTableSnafu {
216                table_name: table.table_info().full_table_name(),
217            })?;
218
219        let scan_plan = dataframe.into_unoptimized_plan();
220        let conditions = PromPlanner::matchers_to_expr(Matchers::new(matchers), scan_plan.schema())
221            .context(PrometheusLabelValuesQueryPlanSnafu)?;
222        let logical_plan = promql::label_values::rewrite_label_values_query(
223            table, scan_plan, conditions, column, start, end,
224        )
225        .context(PrometheusLabelValuesQueryPlanSnafu)?;
226
227        let results = self
228            .query_engine
229            .execute(logical_plan, ctx.clone())
230            .await
231            .context(ExecLogicalPlanSnafu)?;
232
233        match results.data {
234            OutputData::Stream(stream) => {
235                util::collect(stream).await.context(CollectRecordbatchSnafu)
236            }
237            OutputData::RecordBatches(rbs) => Ok(rbs.take()),
238            _ => unreachable!("should not happen"),
239        }
240    }
241
242    /// Returns the `__table_id`s of `physical` carrying a row that matches every
243    /// matcher within the time range.
244    async fn scan_matching_table_ids(
245        &self,
246        physical: TableRef,
247        matchers: &[Matcher],
248        start: SystemTime,
249        end: SystemTime,
250        ctx: &QueryContextRef,
251    ) -> Result<Vec<TableId>> {
252        // `__table_id` attributes a row to its logical table, and a physical
253        // table that never took a column from one does not expose it. Skipping
254        // such a table can only miss a metric carrying no labels at all.
255        if physical
256            .schema()
257            .column_schema_by_name(DATA_SCHEMA_TABLE_ID_COLUMN_NAME)
258            .is_none()
259        {
260            return Ok(Vec::new());
261        }
262
263        let table_name = physical.table_info().full_table_name();
264        let batches = self
265            .scan_distinct_column(
266                physical,
267                matchers.to_vec(),
268                DATA_SCHEMA_TABLE_ID_COLUMN_NAME.to_string(),
269                start,
270                end,
271                ctx,
272            )
273            .await?;
274
275        let mut table_ids = Vec::new();
276        for batch in batches {
277            // Only one column in results, ensured by `rewrite_label_values_query`.
278            let column = batch.column(0);
279            let ids = column
280                .as_any()
281                .downcast_ref::<UInt32Array>()
282                .with_context(|| UnexpectedColumnTypeSnafu {
283                    table_name: table_name.clone(),
284                    column: DATA_SCHEMA_TABLE_ID_COLUMN_NAME,
285                    data_type: column.data_type().to_string(),
286                })?;
287            table_ids.extend(ids.iter().flatten());
288        }
289
290        Ok(table_ids)
291    }
292
293    /// Handles label values query request, returns the values.
294    #[tracing::instrument(skip_all)]
295    pub(crate) async fn handle_query_label_values(
296        &self,
297        target: PermissionTableTarget,
298        label_name: String,
299        matchers: Vec<Matcher>,
300        start: SystemTime,
301        end: SystemTime,
302        ctx: &QueryContextRef,
303    ) -> Result<Vec<String>> {
304        let full_table_name =
305            format_full_table_name(&target.catalog, &target.schema, &target.table);
306        let table = self
307            .catalog_manager
308            .table(&target.catalog, &target.schema, &target.table, Some(ctx))
309            .await
310            .context(CatalogSnafu)?
311            .with_context(|| TableNotFoundSnafu {
312                table_name: full_table_name.clone(),
313            })?;
314
315        // Check label column existence before building the query plan so a missing label can be
316        // reported as `TableColumnNotFound` and handled like Prometheus expects.
317        if table.schema().column_schema_by_name(&label_name).is_none() {
318            return table::error::ColumnNotExistsSnafu {
319                column_name: label_name,
320                table_name: full_table_name,
321            }
322            .fail()
323            .context(TableSnafu);
324        }
325
326        let batches = self
327            .scan_distinct_column(table, matchers, label_name, start, end, ctx)
328            .await?;
329
330        let mut results = Vec::with_capacity(batches.iter().map(|b| b.num_rows()).sum());
331        for batch in batches {
332            // Only one column in results, ensured by `prometheus::label_values_matchers_to_plan`.
333            batch
334                .iter_column_as_string(0)
335                .flatten()
336                .for_each(|x| results.push(x))
337        }
338
339        Ok(results)
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use datafusion::arrow::array::Int64Array;
346    use datafusion::prelude::{SessionConfig, SessionContext};
347    use datafusion_expr::{LogicalPlanBuilder, Sort, col, lit};
348
349    use super::*;
350
351    #[tokio::test]
352    async fn remove_output_sort_keeps_schema_and_row_selection() {
353        let input =
354            LogicalPlanBuilder::values(vec![vec![lit(3_i64)], vec![lit(1_i64)], vec![lit(2_i64)]])
355                .unwrap()
356                .build()
357                .unwrap();
358        let sort = Sort {
359            expr: vec![col("column1").sort(true, false)],
360            input: Arc::new(input),
361            fetch: None,
362        };
363        let projected = LogicalPlanBuilder::from(LogicalPlan::Sort(sort.clone()))
364            .project(vec![col("column1").alias("sample")])
365            .unwrap()
366            .project(vec![col("sample")])
367            .unwrap()
368            .build()
369            .unwrap();
370        // The root sort is removed, but the sort feeding the limit selects the rows.
371        let limited = LogicalPlanBuilder::from(LogicalPlan::Sort(sort.clone()))
372            .limit(0, Some(2))
373            .unwrap()
374            .sort(vec![col("column1").sort(false, false)])
375            .unwrap()
376            .build()
377            .unwrap();
378        let fetched = LogicalPlan::Sort(Sort {
379            fetch: Some(2),
380            ..sort.clone()
381        });
382
383        let context =
384            SessionContext::new_with_config(SessionConfig::new().with_target_partitions(1));
385        for (name, plan, expected) in [
386            ("root", LogicalPlan::Sort(sort), vec![3, 1, 2]),
387            ("projection", projected, vec![3, 1, 2]),
388            ("sort below limit", limited, vec![1, 2]),
389            ("fetch", fetched, vec![1, 2]),
390        ] {
391            let schema = plan.schema().clone();
392            let plan = remove_output_sort(plan);
393            assert_eq!(plan.schema(), &schema, "{name}");
394            let output = context
395                .execute_logical_plan(plan)
396                .await
397                .unwrap()
398                .collect()
399                .await
400                .unwrap();
401            let values = output
402                .iter()
403                .flat_map(|batch| {
404                    batch
405                        .column(0)
406                        .as_any()
407                        .downcast_ref::<Int64Array>()
408                        .unwrap()
409                        .values()
410                        .iter()
411                        .copied()
412                })
413                .collect::<Vec<_>>();
414            assert_eq!(values, expected, "{name}");
415        }
416    }
417}