Skip to content
Snippets Groups Projects
Commit 221b1eef authored by Maaz Ahmed's avatar Maaz Ahmed
Browse files

Initial implementation

parents
No related branches found
No related tags found
No related merge requests found
/target
This diff is collapsed.
[package]
name = "mquery"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
prometheus-http-query = "0.7.1"
reqwest = "0.11.22"
url = "2.5.0"
[dev-dependencies]
tokio = { version = "1.34.0", default-features = false, features = ["rt", "rt-multi-thread"]}
dotenv = "*"
# mquery (placeholder name)
A Rust library for handling PROMQL (and possibly MetricsQL) queries.
# Roadmap
- [x] Basic raw queries (Instant and Ranged)
- [ ] Query Builder
- [ ] Basic queries
- [ ] Operators
- [ ] Functions
- [ ] Compile time syntax checking
- [ ] Deserialize to custom data types
- [ ] Encapsulated raw queries with methods for constructing interpolated versions
//! A wrapper around prometheus_http_query with additional features for ease of use
//!
//! All HTTP requests are made using the `reqwest` client
use prometheus_http_query::{response::PromqlResult, Client, Error};
use reqwest::header::HeaderValue;
use url::Url;
/// The primary way to send queries and get deserialized responses from the metrics server
///
/// The default instance uses no authentication, initializes it's own reqwest client and the
/// server url is set to "http://127.0.0.1:9090" and sends requests using the GET HTTP method.
#[derive(Default, Clone)]
pub struct QueryManager {
auth: Auth,
client: Client,
timeout: Option<i64>,
method: Method,
}
impl QueryManager {
/// Build a new QueryManager using a valid base URL (without Prometheus API path)
pub fn new(url: Url) -> Self {
QueryManager {
#[allow(clippy::unwrap_used)] // url is already validated
client: Client::try_from(url.as_str()).expect("unexpected failure"),
..Default::default()
}
}
/// Build a new QueryManager using a valid base URL (without Prometheus API path) and an existing reqwest Client
pub fn from_client(url: Url, client: reqwest::Client) -> Self {
QueryManager {
auth: Default::default(),
#[allow(clippy::unwrap_used)] // url is already validated
client: Client::from(client, url.as_str()).expect("unexpected failure"),
..Default::default()
}
}
/// Set authentication method (no auth is used by default)
///
/// Currently only supports bearer tokens
pub fn auth(&mut self, auth: Auth) -> &mut Self {
self.auth = auth;
self
}
/// Set timeout for Prometheus queries
pub fn timeout(&mut self, timeout: i64) -> &mut Self {
self.timeout.replace(timeout);
self
}
/// Set HTTP method to use to send queries to the server
pub fn method(&mut self, method: Method) -> &mut Self {
self.method = method;
self
}
/// Send a query to the Prometheus server and retreived deserialized data
pub async fn query<Q: AsRef<str>>(&self, query: Q) -> Result<PromqlResult, Error> {
let mut builder = self.client.query(query.as_ref());
if let Some((name, val)) = self.auth.get_header() {
builder = builder.header(name, val);
}
if let Some(timeout) = self.timeout.as_ref() {
builder = builder.timeout(*timeout);
}
match self.method {
Method::Get => builder.get().await,
Method::Post => builder.post().await,
}
}
/// Send a ranged query to the Prometheus server and retreived deserialized data
pub async fn query_range<Q: AsRef<str>>(
&self,
query: Q,
start: i64,
end: i64,
step: f64,
) -> Result<PromqlResult, Error> {
let mut builder = self.client.query_range(query.as_ref(), start, end, step);
if let Some((name, val)) = self.auth.get_header() {
builder = builder.header(name, val);
}
if let Some(timeout) = self.timeout.as_ref() {
builder = builder.timeout(*timeout);
}
match self.method {
Method::Get => builder.get().await,
Method::Post => builder.post().await,
}
}
}
// TODO: Add support for basic auth as well
#[derive(Default, Clone, Debug)]
pub enum Auth {
#[default]
None,
Bearer(String),
}
impl Auth {
const AUTH_HEADER: &'static str = "Authorization";
fn get_header(&self) -> Option<(&'static str, HeaderValue)> {
match self {
Auth::None => None,
Auth::Bearer(token) => Some((
Self::AUTH_HEADER,
#[allow(clippy::unwrap_used)] // building from valid chars
format!("Bearer {}", token)
.parse()
.expect("unexpected failure"),
)),
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum Method {
#[default]
Get,
Post,
}
use mquery::{Auth, QueryManager};
use tokio::runtime::Runtime;
// TODO: Create proper tests with local server
#[test]
fn deserialize_response() {
let (url, token) = get_url_token();
let rt = Runtime::new().unwrap();
let query = format!(
"sum(M40c82231{{uuid=\"{}\"}}[10d])",
"1bed9384-5d24-11e3-abe4-2d513ee73a00"
);
let _ = rt
.block_on(
QueryManager::new(url.parse().unwrap())
.auth(Auth::Bearer(token))
.query(query),
)
.unwrap();
}
fn get_url_token() -> (String, String) {
dotenv::dotenv().expect("No .env file found in working dir");
(
std::env::var("VM_URL").expect("VM URL not found in env"),
std::env::var("VM_TOKEN").expect("VM URL not found in env"),
)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment