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:
Ruihang Xia
2023-02-15 17:15:44 +08:00
committed by GitHub
parent 5d1f231004
commit dfe7bfb07f
17 changed files with 270 additions and 46 deletions

View File

@@ -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" }

View File

@@ -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(),

View File

@@ -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);
}
}