mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-05-27 10:20:38 +00:00
feat: handle PromQL HTTP API parameters (#985)
* feat: impl EvalStmt parser Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * fix compile errors Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * update test result Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * add integration test Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * fix clippy Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * resolve CR comments Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * impl From<PromqlQuery> for PromQuery Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * move format into with_context Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * update test result Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * shorthand compound error Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * use rfc3339 error to report float parsing error Signed-off-by: Ruihang Xia <waynestxia@gmail.com> * remove CompoundError Signed-off-by: Ruihang Xia <waynestxia@gmail.com> --------- Signed-off-by: Ruihang Xia <waynestxia@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@ license.workspace = true
|
||||
arc-swap = "1.0"
|
||||
async-trait = "0.1"
|
||||
catalog = { path = "../catalog" }
|
||||
chrono.workspace = true
|
||||
common-base = { path = "../common/base" }
|
||||
common-catalog = { path = "../common/catalog" }
|
||||
common-error = { path = "../common/error" }
|
||||
|
||||
@@ -74,6 +74,20 @@ pub enum Error {
|
||||
|
||||
#[snafu(display("Failed to convert datatype: {}", source))]
|
||||
Datatype { source: datatypes::error::Error },
|
||||
|
||||
#[snafu(display("Failed to parse timestamp `{}`: {}", raw, source))]
|
||||
ParseTimestamp {
|
||||
raw: String,
|
||||
source: chrono::ParseError,
|
||||
backtrace: Backtrace,
|
||||
},
|
||||
|
||||
#[snafu(display("Failed to parse float number `{}`: {}", raw, source))]
|
||||
ParseFloat {
|
||||
raw: String,
|
||||
source: std::num::ParseFloatError,
|
||||
backtrace: Backtrace,
|
||||
},
|
||||
}
|
||||
|
||||
impl ErrorExt for Error {
|
||||
@@ -85,7 +99,9 @@ impl ErrorExt for Error {
|
||||
UnsupportedExpr { .. }
|
||||
| CatalogNotFound { .. }
|
||||
| SchemaNotFound { .. }
|
||||
| TableNotFound { .. } => StatusCode::InvalidArguments,
|
||||
| TableNotFound { .. }
|
||||
| ParseTimestamp { .. }
|
||||
| ParseFloat { .. } => StatusCode::InvalidArguments,
|
||||
QueryAccessDenied { .. } => StatusCode::AccessDenied,
|
||||
Catalog { source } => source.status_code(),
|
||||
VectorComputation { source } => source.status_code(),
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use chrono::DateTime;
|
||||
use common_error::ext::PlainError;
|
||||
use common_error::prelude::BoxedError;
|
||||
use common_error::status_code::StatusCode;
|
||||
@@ -24,15 +25,27 @@ use sql::dialect::GenericDialect;
|
||||
use sql::parser::ParserContext;
|
||||
use sql::statements::statement::Statement;
|
||||
|
||||
use crate::error::{MultipleStatementsSnafu, QueryParseSnafu, Result};
|
||||
use crate::error::{
|
||||
MultipleStatementsSnafu, ParseFloatSnafu, ParseTimestampSnafu, QueryParseSnafu, Result,
|
||||
};
|
||||
use crate::metric::{METRIC_PARSE_PROMQL_ELAPSED, METRIC_PARSE_SQL_ELAPSED};
|
||||
|
||||
const DEFAULT_LOOKBACK: u64 = 5 * 60; // 5m
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum QueryStatement {
|
||||
Sql(Statement),
|
||||
Promql(EvalStmt),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PromQuery {
|
||||
pub query: String,
|
||||
pub start: String,
|
||||
pub end: String,
|
||||
pub step: String,
|
||||
}
|
||||
|
||||
pub struct QueryLanguageParser {}
|
||||
|
||||
impl QueryLanguageParser {
|
||||
@@ -54,25 +67,75 @@ impl QueryLanguageParser {
|
||||
}
|
||||
|
||||
// TODO(ruihang): implement this method when parser is ready.
|
||||
pub fn parse_promql(promql: &str) -> Result<QueryStatement> {
|
||||
pub fn parse_promql(query: &PromQuery) -> Result<QueryStatement> {
|
||||
let _timer = timer!(METRIC_PARSE_PROMQL_ELAPSED);
|
||||
|
||||
let prom_expr = promql_parser::parser::parse(promql)
|
||||
let expr = promql_parser::parser::parse(&query.query)
|
||||
.map_err(|msg| BoxedError::new(PlainError::new(msg, StatusCode::InvalidArguments)))
|
||||
.context(QueryParseSnafu { query: promql })?;
|
||||
.context(QueryParseSnafu {
|
||||
query: &query.query,
|
||||
})?;
|
||||
|
||||
let start = Self::parse_promql_timestamp(&query.start)
|
||||
.map_err(BoxedError::new)
|
||||
.context(QueryParseSnafu {
|
||||
query: &query.query,
|
||||
})?;
|
||||
|
||||
let end = Self::parse_promql_timestamp(&query.end)
|
||||
.map_err(BoxedError::new)
|
||||
.context(QueryParseSnafu {
|
||||
query: &query.query,
|
||||
})?;
|
||||
|
||||
let step = promql_parser::util::parse_duration(&query.step)
|
||||
.map_err(|msg| BoxedError::new(PlainError::new(msg, StatusCode::InvalidArguments)))
|
||||
.context(QueryParseSnafu {
|
||||
query: &query.query,
|
||||
})?;
|
||||
|
||||
let eval_stmt = EvalStmt {
|
||||
expr: prom_expr,
|
||||
start: std::time::UNIX_EPOCH,
|
||||
end: std::time::UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(100))
|
||||
.unwrap(),
|
||||
interval: Duration::from_secs(5),
|
||||
lookback_delta: Duration::from_secs(1),
|
||||
expr,
|
||||
start,
|
||||
end,
|
||||
interval: step,
|
||||
// TODO(ruihang): provide a way to adjust this parameter.
|
||||
lookback_delta: Duration::from_secs(DEFAULT_LOOKBACK),
|
||||
};
|
||||
|
||||
Ok(QueryStatement::Promql(eval_stmt))
|
||||
}
|
||||
|
||||
fn parse_promql_timestamp(timestamp: &str) -> Result<SystemTime> {
|
||||
// try rfc3339 format
|
||||
let rfc3339_result = DateTime::parse_from_rfc3339(timestamp)
|
||||
.context(ParseTimestampSnafu { raw: timestamp })
|
||||
.map(Into::<SystemTime>::into);
|
||||
|
||||
// shorthand
|
||||
if rfc3339_result.is_ok() {
|
||||
return rfc3339_result;
|
||||
}
|
||||
|
||||
// try float format
|
||||
timestamp
|
||||
.parse::<f64>()
|
||||
.context(ParseFloatSnafu { raw: timestamp })
|
||||
.map(|float| {
|
||||
let duration = Duration::from_secs_f64(float);
|
||||
SystemTime::UNIX_EPOCH
|
||||
.checked_add(duration)
|
||||
.unwrap_or(max_system_timestamp())
|
||||
})
|
||||
// also report rfc3339 error if float parsing fails
|
||||
.map_err(|_| rfc3339_result.unwrap_err())
|
||||
}
|
||||
}
|
||||
|
||||
fn max_system_timestamp() -> SystemTime {
|
||||
SystemTime::UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs(std::i64::MAX as u64))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -111,4 +174,78 @@ mod test {
|
||||
|
||||
assert_eq!(format!("{stmt:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_promql_timestamp() {
|
||||
let cases = vec![
|
||||
(
|
||||
"1435781451.781",
|
||||
SystemTime::UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs_f64(1435781451.781))
|
||||
.unwrap(),
|
||||
),
|
||||
("0.000", SystemTime::UNIX_EPOCH),
|
||||
("00", SystemTime::UNIX_EPOCH),
|
||||
(
|
||||
// i64::MAX + 1
|
||||
"9223372036854775808.000",
|
||||
max_system_timestamp(),
|
||||
),
|
||||
(
|
||||
"2015-07-01T20:10:51.781Z",
|
||||
SystemTime::UNIX_EPOCH
|
||||
.checked_add(Duration::from_secs_f64(1435781451.781))
|
||||
.unwrap(),
|
||||
),
|
||||
("1970-01-01T00:00:00.000Z", SystemTime::UNIX_EPOCH),
|
||||
];
|
||||
|
||||
for (input, expected) in cases {
|
||||
let result = QueryLanguageParser::parse_promql_timestamp(input).unwrap();
|
||||
|
||||
let result = result
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
let expected = expected
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
|
||||
// assert difference < 0.1 second
|
||||
assert!(result.abs_diff(expected) < 100);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_promql_simple() {
|
||||
let promql = PromQuery {
|
||||
query: "http_request".to_string(),
|
||||
start: "2022-02-13T17:14:00Z".to_string(),
|
||||
end: "2023-02-13T17:14:00Z".to_string(),
|
||||
step: "1d".to_string(),
|
||||
};
|
||||
|
||||
let expected = String::from(
|
||||
"\
|
||||
Promql(EvalStmt { \
|
||||
expr: VectorSelector(VectorSelector { \
|
||||
name: Some(\"http_request\"), \
|
||||
matchers: Matchers { \
|
||||
matchers: {Matcher { \
|
||||
op: Equal, \
|
||||
name: \"__name__\", \
|
||||
value: \"http_request\" \
|
||||
}} }, \
|
||||
offset: None, at: None }), \
|
||||
start: SystemTime { tv_sec: 1644772440, tv_nsec: 0 }, \
|
||||
end: SystemTime { tv_sec: 1676308440, tv_nsec: 0 }, \
|
||||
interval: 86400s, \
|
||||
lookback_delta: 300s \
|
||||
})",
|
||||
);
|
||||
|
||||
let result = QueryLanguageParser::parse_promql(&promql).unwrap();
|
||||
assert_eq!(format!("{result:?}"), expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user