├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── img └── proq.png ├── src ├── api.rs ├── errors.rs ├── lib.rs ├── query_types.rs ├── result_types.rs └── value_types.rs └── tests ├── query.rs └── serialization.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build_and_test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | toolchain: 11 | - x86_64-unknown-linux-gnu 12 | version: 13 | - stable 14 | - nightly 15 | include: 16 | - toolchain: x86_64-unknown-linux-gnu 17 | os: ubuntu-latest 18 | 19 | name: ${{ matrix.version }} - ${{ matrix.toolchain }} 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | 25 | - name: Install ${{ matrix.version }} 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: ${{ matrix.version }}-${{ matrix.toolchain }} 29 | default: true 30 | 31 | - name: check nightly 32 | if: matrix.version == 'nightly' 33 | uses: actions-rs/cargo@v1 34 | with: 35 | command: check 36 | args: --all --benches --bins --examples --tests 37 | 38 | - name: check stable 39 | if: matrix.version == 'stable' 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: check 43 | args: --all --bins --examples --tests 44 | 45 | - name: tests 46 | env: 47 | PROMETHEUS_VERSION: "v2.15.1" 48 | run: | 49 | CONTAINER="$(docker run --detach --publish 9090:9090 prom/prometheus:$PROMETHEUS_VERSION)" 50 | cargo test --all 51 | docker stop --time 0 "${CONTAINER}" 52 | 53 | check_fmt_and_docs: 54 | name: Checking fmt and docs 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@master 58 | 59 | - name: Setup 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: stable 63 | default: true 64 | components: rustfmt 65 | 66 | - name: fmt 67 | run: cargo fmt --all -- --check 68 | 69 | - name: doc 70 | run: cargo doc 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | **/target 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | /target 14 | **/*.rs.bk 15 | 16 | *.bc 17 | 18 | bcs 19 | 20 | # Intellij stuff 21 | .idea/ 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proq" 3 | version = "0.1.1-alpha.0" 4 | authors = [ 5 | "Mahmut Bulut ", 6 | "Patrice Billaut " 7 | ] 8 | keywords = ["prometheus", "metrics-gathering", "metrics", "aggregation", "async"] 9 | categories = ["asynchronous", "api-bindings", "web-programming"] 10 | homepage = "https://github.com/vertexclique/proq" 11 | repository = "https://github.com/vertexclique/proq" 12 | description = "Idiomatic Async Prometheus Query (PromQL) Client for Rust." 13 | documentation = "https://docs.rs/proq" 14 | readme = "README.md" 15 | license = "Apache-2.0/MIT" 16 | edition = "2018" 17 | exclude = [ 18 | ".github/*", 19 | "examples/*", 20 | "graphstore/*", 21 | "tests/*", 22 | "img/*", 23 | "ci/*", 24 | "benches/*", 25 | "doc/*", 26 | "*.png", 27 | "*.dot", 28 | "*.yml", 29 | "*.toml", 30 | "*.md" 31 | ] 32 | 33 | [badges] 34 | maintenance = { status = "actively-developed" } 35 | 36 | [dependencies] 37 | chrono = "0.4.10" 38 | failure = "0.1.6" 39 | http = "0.1.21" 40 | serde = { version = "1.0", features = ["derive"] } 41 | serde_json = "1.0.44" 42 | serde_urlencoded = "0.6.1" 43 | surf = "1.0.3" 44 | url = "1.7" 45 | url_serde = "0.2.0" 46 | 47 | [dev-dependencies] 48 | futures = "0.3.1" 49 | once_cell = "1.2.0" 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |
5 | 6 | Proq – Idiomatic Async Prometheus Query (PromQL) Client for Rust. 7 | 8 |
9 | 10 | [![Build Status](https://github.com/vertexclique/proq/workflows/CI/badge.svg)](https://github.com/vertexclique/proq/actions) 11 | [![Latest Version](https://img.shields.io/crates/v/proq.svg)](https://crates.io/crates/proq) 12 | [![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/proq/) 13 |
14 | 15 | This crate provides async client for Prometheus Query API. 16 | All queries can be written with PromQL notation. 17 | Timeout and protocol configuration can be passed at the client initiation time. 18 | 19 | #### Adding as dependency 20 | ```toml 21 | [dependencies] 22 | proq = "0.1" 23 | ``` 24 | 25 | #### Basic Usage 26 | ```rust 27 | use proq::prelude::*; 28 | use std::time::Duration; 29 | 30 | fn main() { 31 | let client = ProqClient::new( 32 | "localhost:9090", 33 | Some(Duration::from_secs(5)), 34 | ).unwrap(); 35 | 36 | futures::executor::block_on(async { 37 | let end = Utc::now(); 38 | let start = Some(end - chrono::Duration::minutes(1)); 39 | let step = Some(Duration::from_secs_f64(1.5)); 40 | 41 | let rangeq = client.range_query("up", start, Some(end), step).await; 42 | }); 43 | } 44 | ``` 45 | 46 | For more information please head to the [Documentation](https://docs.rs/proq/). -------------------------------------------------------------------------------- /img/proq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertexclique/proq/ad319b0c293c7e4be05380d47392915f964cc120/img/proq.png -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Proq client API 3 | //! 4 | //! This module provides Prometheus Query API related methods. 5 | 6 | use std::str::FromStr; 7 | use std::time::Duration; 8 | 9 | use ::url::Url; 10 | use chrono::offset::Utc; 11 | use chrono::DateTime; 12 | use http::{uri, Uri}; 13 | use serde::Serialize; 14 | use surf::*; 15 | 16 | use crate::query_types::*; 17 | use crate::result_types::ApiResult; 18 | 19 | use super::errors::*; 20 | 21 | const PROQ_INSTANT_QUERY_URL: &str = "/api/v1/query"; 22 | const PROQ_RANGE_QUERY_URL: &str = "/api/v1/query_range"; 23 | const PROQ_SERIES_URL: &str = "/api/v1/series"; 24 | const PROQ_LABELS_URL: &str = "/api/v1/labels"; 25 | const PROQ_TARGETS_URL: &str = "/api/v1/targets"; 26 | const PROQ_RULES_URL: &str = "/api/v1/rules"; 27 | const PROQ_ALERTS_URL: &str = "/api/v1/alerts"; 28 | const PROQ_ALERT_MANAGERS_URL: &str = "/api/v1/alertmanagers"; 29 | const PROQ_STATUS_CONFIG_URL: &str = "/api/v1/status/config"; 30 | const PROQ_STATUS_FLAGS_URL: &str = "/api/v1/status/config"; 31 | macro_rules! PROQ_LABEL_VALUES_URL { 32 | () => { 33 | "/api/v1/label/{}/values" 34 | }; 35 | } 36 | 37 | /// 38 | /// Protocol type for the client 39 | #[derive(PartialEq)] 40 | pub enum ProqProtocol { 41 | /// HTTP transport 42 | HTTP, 43 | /// HTTPS transport 44 | HTTPS, 45 | } 46 | 47 | /// 48 | /// Main client structure. 49 | pub struct ProqClient { 50 | host: Url, 51 | protocol: ProqProtocol, 52 | query_timeout: Option, 53 | } 54 | 55 | impl ProqClient { 56 | /// 57 | /// Get a HTTPS using Proq client. 58 | /// 59 | /// # Arguments 60 | /// 61 | /// * `host` - host port combination string: e.g. `localhost:9090` 62 | /// * `query_timeout` - Maximum query timeout for the client 63 | /// 64 | /// # Example 65 | /// 66 | /// ```rust 67 | /// use proq::prelude::*; 68 | ///# use chrono::Utc; 69 | ///# use std::time::Duration; 70 | /// 71 | ///# fn main() { 72 | /// let client = ProqClient::new( 73 | /// "localhost:9090", 74 | /// Some(Duration::from_secs(5)), 75 | /// ).unwrap(); 76 | ///# } 77 | /// ``` 78 | pub fn new(host: &str, query_timeout: Option) -> ProqResult { 79 | Self::new_with_proto(host, ProqProtocol::HTTPS, query_timeout) 80 | } 81 | 82 | /// 83 | /// Get a Proq client with specified protocol. 84 | /// 85 | /// # Arguments 86 | /// 87 | /// * `host` - host port combination string: e.g. `localhost:9090` 88 | /// * `protocol` - [ProqProtocol] Currently either HTTP or HTTPS 89 | /// * `query_timeout` - Maximum query timeout for the client 90 | /// 91 | /// # Example 92 | /// 93 | /// ```rust 94 | /// use proq::prelude::*; 95 | ///# use chrono::Utc; 96 | ///# use std::time::Duration; 97 | /// 98 | ///# fn main() { 99 | /// let client = ProqClient::new_with_proto( 100 | /// "localhost:9090", 101 | /// ProqProtocol::HTTP, 102 | /// Some(Duration::from_secs(5)), 103 | /// ).unwrap(); 104 | ///# } 105 | /// ``` 106 | pub fn new_with_proto( 107 | host: &str, 108 | protocol: ProqProtocol, 109 | query_timeout: Option, 110 | ) -> ProqResult { 111 | let host = Url::from_str(host).map_err(ProqError::UrlParseError)?; 112 | 113 | Ok(Self { 114 | host, 115 | query_timeout, 116 | protocol, 117 | }) 118 | } 119 | 120 | async fn get_basic(&self, url: Url) -> ProqResult { 121 | surf::get(url) 122 | .recv_json() 123 | .await 124 | .map_err(|e| ProqError::GenericError(e.to_string())) 125 | } 126 | 127 | async fn get_query(&self, endpoint: &str, query: &impl Serialize) -> ProqResult { 128 | let url: Url = Url::from_str(self.get_slug(&endpoint)?.to_string().as_str())?; 129 | surf::get(url) 130 | .set_query(&query) 131 | .map_err(|e| ProqError::HTTPClientError(Box::new(e)))? 132 | .recv_json() 133 | .await 134 | .map_err(|e| ProqError::GenericError(e.to_string())) 135 | } 136 | 137 | async fn post(&self, endpoint: &str, payload: String) -> ProqResult { 138 | let url: Url = Url::from_str(self.get_slug(&endpoint)?.to_string().as_str())?; 139 | surf::post(url) 140 | .body_string(payload) 141 | .set_mime(mime::APPLICATION_WWW_FORM_URLENCODED) 142 | .recv_json() 143 | .await 144 | .map_err(|e| ProqError::GenericError(e.to_string())) 145 | } 146 | 147 | /// 148 | /// Make an instant query to Prometheus. 149 | /// Get all timeseries at that point. 150 | /// 151 | /// # Arguments 152 | /// 153 | /// * `query` - query string 154 | /// * `eval_time` - instant query timestamp to query 155 | /// 156 | /// # Example 157 | /// 158 | /// ```rust 159 | /// use proq::prelude::*; 160 | ///# use chrono::Utc; 161 | ///# use std::time::Duration; 162 | /// 163 | ///# fn main() { 164 | ///# let client = ProqClient::new_with_proto( 165 | ///# "localhost:9090", 166 | ///# ProqProtocol::HTTP, 167 | ///# Some(Duration::from_secs(5)), 168 | ///# ).unwrap(); 169 | ///# 170 | ///# futures::executor::block_on(async { 171 | /// let instantq = client.instant_query("up", None).await; 172 | ///# }); 173 | ///# } 174 | /// ``` 175 | pub async fn instant_query( 176 | &self, 177 | query: &str, 178 | eval_time: Option>, 179 | ) -> ProqResult { 180 | let query = InstantQuery { 181 | query: query.into(), 182 | time: eval_time.as_ref().map(|et| DateTime::timestamp(et)), 183 | timeout: self.query_timeout.map(|t| t.as_secs().to_string()), 184 | }; 185 | self.get_query(PROQ_INSTANT_QUERY_URL, &query).await 186 | } 187 | 188 | /// 189 | /// Make a range query to Prometheus. 190 | /// 191 | /// # Arguments 192 | /// 193 | /// * `query` - query string 194 | /// * `start` - start time of the query 195 | /// * `end` - end time of the query 196 | /// * `step` - step duration between start and end range 197 | /// 198 | /// # Example 199 | /// 200 | /// ```rust 201 | /// use proq::prelude::*; 202 | ///# use chrono::Utc; 203 | ///# use std::time::Duration; 204 | /// 205 | ///# fn main() { 206 | ///# let client = ProqClient::new_with_proto( 207 | ///# "localhost:9090", 208 | ///# ProqProtocol::HTTP, 209 | ///# Some(Duration::from_secs(5)), 210 | ///# ).unwrap(); 211 | ///# 212 | ///# futures::executor::block_on(async { 213 | /// let end = Utc::now(); 214 | /// let start = Some(end - chrono::Duration::minutes(1)); 215 | /// let step = Some(Duration::from_secs_f64(1.5)); 216 | /// 217 | /// let rangeq = client.range_query("up", start, Some(end), step).await; 218 | ///# }); 219 | ///# } 220 | /// ``` 221 | pub async fn range_query( 222 | &self, 223 | query: &str, 224 | start_time: Option>, 225 | end_time: Option>, 226 | step: Option, 227 | ) -> ProqResult { 228 | let query = RangeQuery { 229 | query: query.into(), 230 | start: start_time.as_ref().map(|et| DateTime::timestamp(et)), 231 | end: end_time.as_ref().map(|et| DateTime::timestamp(et)), 232 | step: step.map(|s| s.as_secs_f64()), 233 | timeout: self.query_timeout.map(|t| t.as_secs().to_string()), 234 | }; 235 | self.get_query(PROQ_RANGE_QUERY_URL, &query).await 236 | } 237 | 238 | /// 239 | /// Get series from Prometheus 240 | /// 241 | /// # Arguments 242 | /// 243 | /// * `selectors` - vector of selectors 244 | /// * `start` - start time of the query 245 | /// * `end` - end time of the query 246 | /// 247 | /// # Example 248 | /// 249 | /// ```rust 250 | /// use proq::prelude::*; 251 | ///# use chrono::Utc; 252 | ///# use std::time::Duration; 253 | /// 254 | ///# fn main() { 255 | ///# let client = ProqClient::new_with_proto( 256 | ///# "localhost:9090", 257 | ///# ProqProtocol::HTTP, 258 | ///# Some(Duration::from_secs(5)), 259 | ///# ).unwrap(); 260 | ///# 261 | ///# futures::executor::block_on(async { 262 | /// let end = Utc::now(); 263 | /// let start = Some(end - chrono::Duration::hours(1)); 264 | /// 265 | /// let selectors = vec!["up", "process_start_time_seconds{job=\"prometheus\"}"]; 266 | /// let series = client.series(selectors, start, Some(end)).await; 267 | ///# }); 268 | ///# } 269 | /// ``` 270 | pub async fn series( 271 | &self, 272 | selectors: Vec<&str>, 273 | start_time: Option>, 274 | end_time: Option>, 275 | ) -> ProqResult { 276 | let query = SeriesRequest { 277 | selectors: selectors.iter().map(|s| (*s).to_string()).collect(), 278 | start: start_time.as_ref().map(|et| DateTime::timestamp(et)), 279 | end: end_time.as_ref().map(|et| DateTime::timestamp(et)), 280 | timeout: self.query_timeout.map(|t| t.as_secs().to_string()), 281 | }; 282 | 283 | let mut uencser = url::form_urlencoded::Serializer::new(String::new()); 284 | // TODO: Remove the allocation overhead from AsRef. 285 | for s in query.selectors { 286 | uencser.append_pair("match[]", s.as_str()); 287 | } 288 | query 289 | .start 290 | .map(|s| uencser.append_pair("start", s.to_string().as_str())); 291 | query 292 | .end 293 | .map(|s| uencser.append_pair("end", s.to_string().as_str())); 294 | let query = uencser.finish(); 295 | 296 | self.post(PROQ_SERIES_URL, query).await 297 | } 298 | 299 | /// 300 | /// Get all label names from Prometheus. 301 | /// 302 | /// # Example 303 | /// 304 | /// ```rust 305 | /// use proq::prelude::*; 306 | ///# use std::time::Duration; 307 | /// 308 | ///# fn main() { 309 | ///# let client = ProqClient::new_with_proto( 310 | ///# "localhost:9090", 311 | ///# ProqProtocol::HTTP, 312 | ///# Some(Duration::from_secs(5)), 313 | ///# ).unwrap(); 314 | ///# 315 | ///# futures::executor::block_on(async { 316 | /// let label_names = client.label_names().await; 317 | ///# }); 318 | ///# } 319 | /// ``` 320 | pub async fn label_names(&self) -> ProqResult { 321 | let url: Url = Url::from_str(self.get_slug(PROQ_LABELS_URL)?.to_string().as_str())?; 322 | self.get_basic(url).await 323 | } 324 | 325 | /// 326 | /// Get all label values for a given label from Prometheus. 327 | /// 328 | /// # Arguments 329 | /// 330 | /// * `label_name` - Label name to get label values 331 | /// 332 | /// # Example 333 | /// 334 | /// ```rust 335 | /// use proq::prelude::*; 336 | ///# use std::time::Duration; 337 | /// 338 | ///# fn main() { 339 | ///# let client = ProqClient::new_with_proto( 340 | ///# "localhost:9090", 341 | ///# ProqProtocol::HTTP, 342 | ///# Some(Duration::from_secs(5)), 343 | ///# ).unwrap(); 344 | ///# 345 | ///# futures::executor::block_on(async { 346 | /// let label_name = "version"; 347 | /// let label_values = client.label_values(label_name).await; 348 | ///# }); 349 | ///# } 350 | /// ``` 351 | pub async fn label_values(&self, label_name: &str) -> ProqResult { 352 | let slug = format!(PROQ_LABEL_VALUES_URL!(), label_name); 353 | let url: Url = Url::from_str(self.get_slug(slug.as_str())?.to_string().as_str())?; 354 | self.get_basic(url).await 355 | } 356 | 357 | /// 358 | /// Get all Prometheus targets. 359 | /// 360 | /// # Example 361 | /// 362 | /// ```rust 363 | /// use proq::prelude::*; 364 | ///# use std::time::Duration; 365 | /// 366 | ///# fn main() { 367 | ///# let client = ProqClient::new_with_proto( 368 | ///# "localhost:9090", 369 | ///# ProqProtocol::HTTP, 370 | ///# Some(Duration::from_secs(5)), 371 | ///# ).unwrap(); 372 | ///# 373 | ///# futures::executor::block_on(async { 374 | /// let targets = client.targets().await; 375 | ///# }); 376 | ///# } 377 | /// ``` 378 | pub async fn targets(&self) -> ProqResult { 379 | let url: Url = Url::from_str(self.get_slug(PROQ_TARGETS_URL)?.to_string().as_str())?; 380 | self.get_basic(url).await 381 | } 382 | 383 | /// 384 | /// Get Prometheus targets filtered by the given target state. 385 | /// 386 | /// Target state can be `ACTIVE`, `DROPPED` or `ANY` which are defined in [ProqTargetStates]. 387 | /// 388 | /// # Arguments 389 | /// 390 | /// * `state` - [ProqTargetStates] : State to filter 391 | /// 392 | /// # Example 393 | /// 394 | /// ```rust 395 | /// use proq::prelude::*; 396 | ///# use std::time::Duration; 397 | /// 398 | ///# fn main() { 399 | ///# let client = ProqClient::new_with_proto( 400 | ///# "localhost:9090", 401 | ///# ProqProtocol::HTTP, 402 | ///# Some(Duration::from_secs(5)), 403 | ///# ).unwrap(); 404 | ///# 405 | ///# futures::executor::block_on(async { 406 | /// let filtered_targets = client.targets_with_state(ProqTargetStates::DROPPED).await; 407 | ///# }); 408 | ///# } 409 | /// ``` 410 | pub async fn targets_with_state(&self, state: ProqTargetStates) -> ProqResult { 411 | let query = TargetsWithStatesRequest { state }; 412 | self.get_query(PROQ_TARGETS_URL, &query).await 413 | } 414 | 415 | /// 416 | /// Get all rules from Prometheus. 417 | /// 418 | /// # Example 419 | /// 420 | /// ```rust 421 | /// use proq::prelude::*; 422 | ///# use std::time::Duration; 423 | /// 424 | ///# fn main() { 425 | ///# let client = ProqClient::new_with_proto( 426 | ///# "localhost:9090", 427 | ///# ProqProtocol::HTTP, 428 | ///# Some(Duration::from_secs(5)), 429 | ///# ).unwrap(); 430 | ///# 431 | ///# futures::executor::block_on(async { 432 | /// let rules = client.rules().await; 433 | ///# }); 434 | ///# } 435 | /// ``` 436 | pub async fn rules(&self) -> ProqResult { 437 | let url: Url = Url::from_str(self.get_slug(PROQ_RULES_URL)?.to_string().as_str())?; 438 | self.get_basic(url).await 439 | } 440 | 441 | /// 442 | /// Get rules filtered by given type. 443 | /// 444 | /// Type can be either `ALERT` or `RECORD` from the [ProqRulesType]. 445 | /// 446 | /// # Arguments 447 | /// 448 | /// * `rule_type` - [ProqRulesType] : Rule type to filter 449 | /// 450 | /// # Example 451 | /// 452 | /// ```rust 453 | /// use proq::prelude::*; 454 | ///# use std::time::Duration; 455 | /// 456 | ///# fn main() { 457 | ///# let client = ProqClient::new_with_proto( 458 | ///# "localhost:9090", 459 | ///# ProqProtocol::HTTP, 460 | ///# Some(Duration::from_secs(5)), 461 | ///# ).unwrap(); 462 | ///# 463 | ///# futures::executor::block_on(async { 464 | /// let filtered_rules = client.rules_with_type(ProqRulesType::ALERT).await; 465 | ///# }); 466 | ///# } 467 | /// ``` 468 | pub async fn rules_with_type(&self, rule_type: ProqRulesType) -> ProqResult { 469 | let query = RulesWithTypeRequest { rule_type }; 470 | self.get_query(PROQ_RULES_URL, &query).await 471 | } 472 | 473 | /// 474 | /// Get current alerts Prometheus has. 475 | /// 476 | /// # Example 477 | /// 478 | /// ```rust 479 | /// use proq::prelude::*; 480 | ///# use std::time::Duration; 481 | /// 482 | ///# fn main() { 483 | ///# let client = ProqClient::new_with_proto( 484 | ///# "localhost:9090", 485 | ///# ProqProtocol::HTTP, 486 | ///# Some(Duration::from_secs(5)), 487 | ///# ).unwrap(); 488 | ///# 489 | ///# futures::executor::block_on(async { 490 | /// let alerts = client.alerts().await; 491 | ///# }); 492 | ///# } 493 | /// ``` 494 | pub async fn alerts(&self) -> ProqResult { 495 | let url: Url = Url::from_str(self.get_slug(PROQ_ALERTS_URL)?.to_string().as_str())?; 496 | self.get_basic(url).await 497 | } 498 | 499 | /// 500 | /// Get alert managers currently Prometheus has. 501 | /// 502 | /// # Example 503 | /// 504 | /// ```rust 505 | /// use proq::prelude::*; 506 | ///# use std::time::Duration; 507 | /// 508 | ///# fn main() { 509 | ///# let client = ProqClient::new_with_proto( 510 | ///# "localhost:9090", 511 | ///# ProqProtocol::HTTP, 512 | ///# Some(Duration::from_secs(5)), 513 | ///# ).unwrap(); 514 | ///# 515 | ///# futures::executor::block_on(async { 516 | /// let alert_managers = client.alert_managers().await; 517 | ///# }); 518 | ///# } 519 | /// ``` 520 | pub async fn alert_managers(&self) -> ProqResult { 521 | let url: Url = Url::from_str(self.get_slug(PROQ_ALERT_MANAGERS_URL)?.to_string().as_str())?; 522 | self.get_basic(url).await 523 | } 524 | 525 | /// 526 | /// Query config that Prometheus configured 527 | /// 528 | /// # Example 529 | /// 530 | /// ```rust 531 | /// use proq::prelude::*; 532 | ///# use std::time::Duration; 533 | /// 534 | ///# fn main() { 535 | ///# let client = ProqClient::new_with_proto( 536 | ///# "localhost:9090", 537 | ///# ProqProtocol::HTTP, 538 | ///# Some(Duration::from_secs(5)), 539 | ///# ).unwrap(); 540 | ///# 541 | ///# futures::executor::block_on(async { 542 | /// let config = client.config().await; 543 | ///# }); 544 | ///# } 545 | /// ``` 546 | pub async fn config(&self) -> ProqResult { 547 | let url: Url = Url::from_str(self.get_slug(PROQ_STATUS_CONFIG_URL)?.to_string().as_str())?; 548 | self.get_basic(url).await 549 | } 550 | 551 | /// 552 | /// Query flag values that Prometheus configured with 553 | /// 554 | /// # Example 555 | /// 556 | /// ```rust 557 | /// use proq::prelude::*; 558 | ///# use std::time::Duration; 559 | /// 560 | ///# fn main() { 561 | ///# let client = ProqClient::new_with_proto( 562 | ///# "localhost:9090", 563 | ///# ProqProtocol::HTTP, 564 | ///# Some(Duration::from_secs(5)), 565 | ///# ).unwrap(); 566 | ///# 567 | ///# futures::executor::block_on(async { 568 | /// let flags = client.flags().await; 569 | ///# }); 570 | ///# } 571 | /// ``` 572 | pub async fn flags(&self) -> ProqResult { 573 | let url: Url = Url::from_str(self.get_slug(PROQ_STATUS_FLAGS_URL)?.to_string().as_str())?; 574 | self.get_basic(url).await 575 | } 576 | 577 | pub(crate) fn get_slug(&self, slug: &str) -> ProqResult { 578 | let proto = if self.protocol == ProqProtocol::HTTP { 579 | "http" 580 | } else { 581 | "https" 582 | }; 583 | 584 | uri::Builder::new() 585 | .scheme(proto) 586 | .authority(self.host.as_str()) 587 | .path_and_query(slug) 588 | .build() 589 | .map_err(ProqError::UrlBuildError) 590 | } 591 | } 592 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Proq client related error listing 3 | //! 4 | //! All errors are aggregated here and exposed by the Proq will be seen here. 5 | 6 | use failure::*; 7 | use std::result; 8 | use url::ParseError; 9 | 10 | /// Alias type for Result with Proq errors. 11 | pub type ProqResult = result::Result; 12 | 13 | /// Error types of Proq 14 | #[derive(Fail, Debug)] 15 | pub enum ProqError { 16 | /// Generic Error raised from Proq. 17 | #[fail(display = "Generic Error: {}", _0)] 18 | GenericError(String), 19 | /// URL parsing error. 20 | #[fail(display = "Failed to parse URL: {}", _0)] 21 | UrlParseError(ParseError), 22 | /// URL building error. 23 | #[fail(display = "Failed to build URL: {}", _0)] 24 | UrlBuildError(http::Error), 25 | /// HTTP Client error raised from underlying HTTP client. 26 | #[fail(display = "Http client Error: {}", _0)] 27 | HTTPClientError(surf::Exception), 28 | } 29 | 30 | impl From for ProqError { 31 | fn from(e: ParseError) -> Self { 32 | ProqError::UrlParseError(e) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Idiomatic Async Prometheus Query (PromQL) Client for Rust. 3 | //! 4 | //! This crate provides general client API for Prometheus Query API. 5 | //! All queries can be written with PromQL notation. 6 | //! 7 | //! 8 | //! # Basic Usage 9 | //! ```rust 10 | //! use proq::prelude::*; 11 | //!# use chrono::Utc; 12 | //!# use std::time::Duration; 13 | //! 14 | //!fn main() { 15 | //! let client = ProqClient::new( 16 | //! "localhost:9090", 17 | //! Some(Duration::from_secs(5)), 18 | //! ).unwrap(); 19 | //! 20 | //! futures::executor::block_on(async { 21 | //! let end = Utc::now(); 22 | //! let start = Some(end - chrono::Duration::minutes(1)); 23 | //! let step = Some(Duration::from_secs_f64(1.5)); 24 | //! 25 | //! let rangeq = client.range_query("up", start, Some(end), step).await; 26 | //! }); 27 | //!} 28 | //! ``` 29 | //! 30 | //! **For extensive documentation about which methods are available and what they are doing you can see 31 | //! the [api::ProqClient] documentation.** 32 | //! 33 | 34 | #![doc(html_logo_url = "https://github.com/vertexclique/proq/raw/master/img/proq.png")] 35 | // Force missing implementations 36 | //#![warn(missing_docs)] 37 | //#![warn(missing_debug_implementations)] 38 | #![forbid(unsafe_code)] 39 | 40 | pub mod api; 41 | pub mod errors; 42 | pub mod query_types; 43 | pub mod result_types; 44 | pub mod value_types; 45 | 46 | pub mod prelude { 47 | //! 48 | //! Prelude of the Proq package. 49 | //! 50 | //! Includes all request response types to client itself. 51 | pub use super::api::*; 52 | pub use super::errors::*; 53 | pub use super::query_types::*; 54 | pub use super::result_types::*; 55 | pub use super::value_types::prometheus_types::*; 56 | pub use chrono::prelude::*; 57 | } 58 | -------------------------------------------------------------------------------- /src/query_types.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Request types that are sent by the Proq to different endpoints. 3 | use serde::*; 4 | 5 | /// 6 | /// Instant query request struct 7 | #[derive(Serialize, Deserialize, Debug, Clone)] 8 | pub struct InstantQuery { 9 | /// PromQL Query which will be sent to API 10 | pub query: String, 11 | /// Evaluation timestamp in unix timestamp format 12 | pub time: Option, 13 | /// Timeout duration for evaluating the result 14 | pub timeout: Option, 15 | } 16 | 17 | /// 18 | /// Range query request struct 19 | #[derive(Serialize, Deserialize, Debug, Clone)] 20 | pub struct RangeQuery { 21 | /// PromQL Query which will be sent to API 22 | pub query: String, 23 | /// Start timestamp for the range query 24 | pub start: Option, 25 | /// End timestamp for the range query 26 | pub end: Option, 27 | /// Step as duration in the range in seconds as 64-bit floating point format 28 | pub step: Option, 29 | /// Timeout duration for evaluating the result 30 | pub timeout: Option, 31 | } 32 | 33 | /// 34 | /// Series query request struct 35 | #[derive(Serialize, Deserialize, Debug, Clone)] 36 | pub struct SeriesRequest { 37 | /// List of series selectors 38 | #[serde(rename(serialize = "match[]"))] 39 | pub selectors: Vec, 40 | /// Start timestamp for the range query 41 | pub start: Option, 42 | /// End timestamp for the range query 43 | pub end: Option, 44 | /// Timeout duration for evaluating the result 45 | pub timeout: Option, 46 | } 47 | 48 | /// 49 | /// Possible Prometheus target states. 50 | #[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] 51 | #[serde(rename_all = "lowercase")] 52 | pub enum ProqTargetStates { 53 | /// Target state filtered by Active state 54 | ACTIVE, 55 | /// Target state filtered by Dropped state 56 | DROPPED, 57 | /// Target state without any filtering 58 | ANY, 59 | } 60 | 61 | /// 62 | /// Target with filtered state request. 63 | #[derive(Serialize, Deserialize, Debug, Clone)] 64 | pub struct TargetsWithStatesRequest { 65 | /// Requested target state filter 66 | pub state: ProqTargetStates, 67 | } 68 | 69 | /// 70 | /// Possible Prometheus rule types. 71 | #[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] 72 | #[serde(rename_all = "lowercase")] 73 | pub enum ProqRulesType { 74 | /// Rule type filtered by Alert 75 | ALERT, 76 | /// Rule type filtered by Record 77 | RECORD, 78 | } 79 | 80 | /// 81 | /// Rules with filtered state request. 82 | #[derive(Serialize, Deserialize, Debug, Clone)] 83 | pub struct RulesWithTypeRequest { 84 | /// Requested target state filter 85 | #[serde(rename = "type")] 86 | pub rule_type: ProqRulesType, 87 | } 88 | -------------------------------------------------------------------------------- /src/result_types.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Response types to Proq from Prometheus. 3 | //! 4 | //! Return types are mostly borrowed from: 5 | //! https://github.com/allengeorge/prometheus-query/blob/master/src/messages.rs 6 | //! 7 | //! extended with filtered and unfiltered methods and new beta endpoints. 8 | use std::collections::HashMap; 9 | use std::fmt::Formatter; 10 | use std::fmt::Result as FmtResult; 11 | use std::result::Result as StdResult; 12 | use std::str::FromStr; 13 | 14 | use chrono::{DateTime, FixedOffset}; 15 | use serde::{ 16 | de, 17 | de::{MapAccess, SeqAccess, Unexpected, Visitor}, 18 | ser::{SerializeStruct, SerializeTuple}, 19 | {Deserialize, Deserializer, Serialize, Serializer}, 20 | }; 21 | use url::Url; 22 | use url_serde::{De, Ser}; 23 | 24 | use crate::value_types::prometheus_types::*; 25 | 26 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 27 | #[serde(tag = "status")] 28 | pub enum ApiResult { 29 | #[serde(rename = "success")] 30 | ApiOk(ApiOk), 31 | #[serde(rename = "error")] 32 | ApiErr(ApiErr), 33 | } 34 | 35 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 36 | pub struct ApiOk { 37 | #[serde(default)] 38 | pub data: Option, 39 | #[serde(default)] 40 | pub warnings: Vec, 41 | } 42 | 43 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 44 | pub struct ApiErr { 45 | #[serde(rename = "errorType")] 46 | pub error_type: String, 47 | #[serde(rename = "error")] 48 | pub error_message: String, 49 | #[serde(default)] 50 | pub data: Option, 51 | #[serde(default)] 52 | pub warnings: Vec, 53 | } 54 | 55 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 56 | #[serde(untagged)] 57 | pub enum Data { 58 | Expression(Expression), 59 | Series(Series), 60 | LabelsOrValues(LabelsOrValues), 61 | Targets(Targets), 62 | Rules(Rules), 63 | Alerts(Alerts), 64 | AlertManagers(AlertManagers), 65 | Config(Config), 66 | Snapshot(Snapshot), 67 | // IMPORTANT: this must *always* be the final variant. 68 | // For untagged enums serde will attempt deserialization using 69 | // each variant in order and accept the first one that is successful. 70 | // Since `Flags` is a map, it captures any other map-like 71 | // types, including `Config`, `Snapshot`, etc. To give those 72 | // variants a chance to be matches this variant must be the last 73 | Flags(HashMap), 74 | } 75 | 76 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 77 | #[serde(tag = "resultType", content = "result")] 78 | pub enum Expression { 79 | #[serde(rename = "scalar")] 80 | Scalar(Sample), 81 | #[serde(rename = "string")] 82 | String(StringSample), 83 | #[serde(rename = "vector")] 84 | Instant(Vec), 85 | #[serde(rename = "matrix")] 86 | Range(Vec), 87 | } 88 | 89 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 90 | pub struct Instant { 91 | pub metric: Metric, 92 | #[serde(rename = "value")] 93 | pub sample: Sample, 94 | } 95 | 96 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 97 | pub struct Range { 98 | pub metric: Metric, 99 | #[serde(rename = "values")] 100 | pub samples: Vec, 101 | } 102 | 103 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 104 | pub struct Metric { 105 | #[serde(flatten)] 106 | pub labels: HashMap, 107 | } 108 | 109 | #[derive(Clone, Debug, PartialEq)] 110 | pub struct Sample { 111 | pub epoch: f64, 112 | pub value: f64, 113 | } 114 | 115 | impl<'de> Deserialize<'de> for Sample { 116 | fn deserialize(deserializer: D) -> StdResult 117 | where 118 | D: Deserializer<'de>, 119 | { 120 | struct VisitorImpl; 121 | 122 | impl<'de> Visitor<'de> for VisitorImpl { 123 | type Value = Sample; 124 | 125 | fn expecting(&self, formatter: &mut Formatter) -> FmtResult { 126 | formatter.write_str("Prometheus sample") 127 | } 128 | 129 | fn visit_seq(self, mut seq: A) -> StdResult 130 | where 131 | A: SeqAccess<'de>, 132 | { 133 | let epoch = seq 134 | .next_element::()? 135 | .ok_or_else(|| de::Error::missing_field("sample time"))?; 136 | let value = seq 137 | .next_element::<&str>()? 138 | .ok_or_else(|| de::Error::missing_field("sample value"))?; 139 | 140 | let value = match value { 141 | PROQ_INFINITY => std::f64::INFINITY, 142 | PROQ_NEGATIVE_INFINITY => std::f64::NEG_INFINITY, 143 | PROQ_NAN => std::f64::NAN, 144 | _ => value 145 | .parse::() 146 | .map_err(|_| de::Error::invalid_value(Unexpected::Str(value), &self))?, 147 | }; 148 | 149 | Ok(Sample { epoch, value }) 150 | } 151 | } 152 | 153 | deserializer.deserialize_seq(VisitorImpl) 154 | } 155 | } 156 | 157 | impl Serialize for Sample { 158 | fn serialize(&self, serializer: S) -> StdResult 159 | where 160 | S: Serializer, 161 | { 162 | let mut s = serializer.serialize_tuple(2)?; 163 | s.serialize_element(&self.epoch)?; 164 | s.serialize_element(&self.value)?; 165 | s.end() 166 | } 167 | } 168 | 169 | #[derive(Clone, Debug, PartialEq)] 170 | pub struct StringSample { 171 | pub epoch: f64, 172 | pub value: String, 173 | } 174 | 175 | impl<'de> Deserialize<'de> for StringSample { 176 | fn deserialize(deserializer: D) -> StdResult 177 | where 178 | D: Deserializer<'de>, 179 | { 180 | struct VisitorImpl; 181 | 182 | impl<'de> Visitor<'de> for VisitorImpl { 183 | type Value = StringSample; 184 | 185 | fn expecting(&self, formatter: &mut Formatter) -> FmtResult { 186 | formatter.write_str("Prometheus string sample") 187 | } 188 | 189 | fn visit_seq(self, mut seq: A) -> StdResult 190 | where 191 | A: SeqAccess<'de>, 192 | { 193 | let epoch = seq 194 | .next_element::()? 195 | .ok_or_else(|| de::Error::missing_field("sample time"))?; 196 | let value = seq 197 | .next_element::()? 198 | .ok_or_else(|| de::Error::missing_field("sample value"))?; 199 | 200 | Ok(StringSample { epoch, value }) 201 | } 202 | } 203 | 204 | deserializer.deserialize_seq(VisitorImpl) 205 | } 206 | } 207 | 208 | impl Serialize for StringSample { 209 | fn serialize(&self, serializer: S) -> StdResult 210 | where 211 | S: Serializer, 212 | { 213 | let mut s = serializer.serialize_tuple(2)?; 214 | s.serialize_element(&self.epoch)?; 215 | s.serialize_element(&self.value)?; 216 | s.end() 217 | } 218 | } 219 | 220 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 221 | pub struct Series(pub Vec); 222 | 223 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 224 | pub struct LabelsOrValues(pub Vec); 225 | 226 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 227 | #[serde(deny_unknown_fields)] 228 | pub struct Targets { 229 | #[serde(default, rename = "activeTargets")] 230 | pub active: Vec, 231 | #[serde(default, rename = "droppedTargets")] 232 | pub dropped: Vec, 233 | } 234 | 235 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 236 | #[serde(rename_all = "camelCase")] 237 | pub struct ActiveTarget { 238 | pub discovered_labels: HashMap, 239 | pub labels: HashMap, 240 | #[serde(with = "url_serde")] 241 | pub scrape_url: Url, 242 | #[serde( 243 | deserialize_with = "empty_string_is_none", 244 | serialize_with = "none_to_empty_string" 245 | )] 246 | pub last_error: Option, 247 | #[serde( 248 | deserialize_with = "rfc3339_to_date_time", 249 | serialize_with = "date_time_to_rfc3339" 250 | )] 251 | pub last_scrape: DateTime, 252 | #[serde( 253 | deserialize_with = "deserialize_health", 254 | serialize_with = "serialize_health" 255 | )] 256 | pub health: TargetHealth, 257 | } 258 | 259 | #[derive(Clone, Debug, PartialEq)] 260 | pub enum TargetHealth { 261 | Up, 262 | Down, 263 | Unknown, 264 | } 265 | 266 | fn empty_string_is_none<'de, D: Deserializer<'de>>(d: D) -> StdResult, D::Error> { 267 | let o: Option = Option::deserialize(d)?; 268 | Ok(o.filter(|s| !s.is_empty())) 269 | } 270 | 271 | fn none_to_empty_string( 272 | s: &Option, 273 | serializer: S, 274 | ) -> StdResult { 275 | if let Some(v) = s { 276 | serializer.serialize_str(&v) 277 | } else { 278 | serializer.serialize_str("") 279 | } 280 | } 281 | 282 | fn rfc3339_to_date_time<'de, D: Deserializer<'de>>( 283 | d: D, 284 | ) -> StdResult, D::Error> { 285 | let s = String::deserialize(d)?; 286 | DateTime::from_str(&s).map_err(de::Error::custom) 287 | } 288 | 289 | fn date_time_to_rfc3339( 290 | v: &DateTime, 291 | serializer: S, 292 | ) -> StdResult { 293 | serializer.serialize_str(&v.to_rfc3339()) 294 | } 295 | 296 | fn deserialize_health<'de, D: Deserializer<'de>>(d: D) -> StdResult { 297 | let o: Option = Option::deserialize(d)?; 298 | Ok(o.map_or(TargetHealth::Unknown, |s| match s.as_str() { 299 | "up" => TargetHealth::Up, 300 | "down" => TargetHealth::Down, 301 | _ => TargetHealth::Unknown, 302 | })) 303 | } 304 | 305 | fn serialize_health( 306 | health: &TargetHealth, 307 | serializer: S, 308 | ) -> StdResult { 309 | let value = match health { 310 | TargetHealth::Up => "up", 311 | TargetHealth::Down => "down", 312 | TargetHealth::Unknown => "unknown", 313 | }; 314 | 315 | serializer.serialize_str(value) 316 | } 317 | 318 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 319 | #[serde(rename_all = "camelCase")] 320 | pub struct DroppedTarget { 321 | pub discovered_labels: HashMap, 322 | } 323 | 324 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 325 | #[serde(deny_unknown_fields)] 326 | pub struct AlertManagers { 327 | #[serde(default, rename = "activeAlertmanagers")] 328 | pub active: Vec, 329 | #[serde(default, rename = "droppedAlertmanagers")] 330 | pub dropped: Vec, 331 | } 332 | 333 | #[derive(Clone, Debug, PartialEq)] 334 | pub struct AlertManager { 335 | pub url: Url, 336 | } 337 | 338 | impl<'de> Deserialize<'de> for AlertManager { 339 | fn deserialize(deserializer: D) -> StdResult 340 | where 341 | D: Deserializer<'de>, 342 | { 343 | // variant of: https://serde.rs/deserialize-struct.html 344 | 345 | struct VisitorImpl; 346 | 347 | #[derive(Deserialize)] 348 | #[serde(field_identifier, rename_all = "lowercase")] 349 | enum Field { 350 | Url, 351 | }; 352 | 353 | const FIELDS: &[&str] = &["url"]; 354 | 355 | impl<'de> Visitor<'de> for VisitorImpl { 356 | type Value = AlertManager; 357 | 358 | fn expecting(&self, formatter: &mut Formatter) -> FmtResult { 359 | formatter.write_str("AlertManager") 360 | } 361 | 362 | fn visit_map(self, mut map: V) -> StdResult 363 | where 364 | V: MapAccess<'de>, 365 | { 366 | let mut url: Option = None; 367 | while let Some(key) = map.next_key()? { 368 | match key { 369 | Field::Url => { 370 | if url.is_some() { 371 | return Err(de::Error::duplicate_field("url")); 372 | } 373 | url = De::into_inner(map.next_value()?); // FIXME: how does this work??! 374 | } 375 | } 376 | } 377 | let url = url.ok_or_else(|| de::Error::missing_field("url"))?; 378 | Ok(AlertManager { url }) 379 | } 380 | } 381 | 382 | deserializer.deserialize_struct("AlertManager", &FIELDS, VisitorImpl) 383 | } 384 | } 385 | 386 | impl Serialize for AlertManager { 387 | fn serialize(&self, serializer: S) -> StdResult 388 | where 389 | S: Serializer, 390 | { 391 | let mut s = serializer.serialize_struct("AlertManager", 1)?; 392 | s.serialize_field("url", &Ser::new(&self.url))?; 393 | s.end() 394 | } 395 | } 396 | 397 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 398 | pub struct Snapshot { 399 | pub name: String, 400 | } 401 | 402 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 403 | pub struct Config { 404 | pub yaml: String, 405 | } 406 | 407 | #[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] 408 | #[serde(rename_all = "lowercase")] 409 | pub enum AlertState { 410 | INACTIVE, 411 | PENDING, 412 | FIRING, 413 | } 414 | 415 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 416 | #[serde(deny_unknown_fields)] 417 | pub struct Rules { 418 | pub groups: Vec, 419 | } 420 | 421 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 422 | pub struct RuleGroups { 423 | pub rules: Vec, 424 | pub file: String, 425 | pub interval: i64, 426 | pub name: String, 427 | } 428 | 429 | #[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] 430 | #[serde(rename_all = "lowercase")] 431 | pub enum RuleType { 432 | RECORDING, 433 | ALERTING, 434 | } 435 | 436 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 437 | pub struct Rule { 438 | pub alerts: Option>, 439 | pub annotations: Option>, 440 | pub duration: Option, 441 | pub labels: Option>, 442 | pub health: String, 443 | pub name: String, 444 | pub query: String, 445 | #[serde(rename = "type")] 446 | pub rule_type: RuleType, 447 | } 448 | 449 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 450 | pub struct Alert { 451 | #[serde(default, rename = "activeAt")] 452 | pub active_at: String, 453 | pub annotations: Option>, 454 | pub labels: Option>, 455 | pub state: AlertState, 456 | pub value: String, 457 | } 458 | 459 | #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] 460 | #[serde(deny_unknown_fields)] 461 | pub struct Alerts { 462 | pub alerts: Vec, 463 | } 464 | -------------------------------------------------------------------------------- /src/value_types.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! All value types which enables us to interpret various parts of Prometheus. 3 | 4 | pub mod prometheus_types { 5 | //! 6 | //! Constants that helps Proq to interpret Prometheus return types. 7 | pub const PROQ_INFINITY: &str = "Inf"; 8 | pub const PROQ_NEGATIVE_INFINITY: &str = "-Inf"; 9 | pub const PROQ_NAN: &str = "NaN"; 10 | } 11 | -------------------------------------------------------------------------------- /tests/query.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use once_cell::sync::OnceCell; 3 | use proq::api::{ProqClient, ProqProtocol}; 4 | use proq::query_types::{ProqRulesType, ProqTargetStates}; 5 | use proq::result_types::ApiResult::ApiOk; 6 | use std::sync::Once; 7 | use std::time::Duration; 8 | 9 | static CLIENT: OnceCell = OnceCell::new(); 10 | static BARRIER: Once = Once::new(); 11 | 12 | fn client() -> &'static ProqClient { 13 | BARRIER.call_once(|| { 14 | let c = ProqClient::new_with_proto( 15 | "localhost:9090", 16 | ProqProtocol::HTTP, 17 | Some(Duration::from_secs(5)), 18 | ) 19 | .unwrap(); 20 | let _ = CLIENT.set(c); 21 | }); 22 | 23 | CLIENT.get().unwrap() 24 | } 25 | 26 | #[test] 27 | fn proq_instant_query() { 28 | futures::executor::block_on(async { 29 | let x = match client().instant_query("up", None).await.unwrap() { 30 | ApiOk(r) => { 31 | dbg!(r); 32 | true 33 | } 34 | e => { 35 | dbg!(e); 36 | false 37 | } 38 | }; 39 | 40 | assert!(x) 41 | }); 42 | } 43 | 44 | #[test] 45 | fn proq_range_query() { 46 | futures::executor::block_on(async { 47 | let end = Utc::now(); 48 | let start = Some(end - chrono::Duration::minutes(1)); 49 | let step = Some(Duration::from_secs_f64(1.5)); 50 | 51 | let x = match client() 52 | .range_query("up", start, Some(end), step) 53 | .await 54 | .unwrap() 55 | { 56 | ApiOk(r) => { 57 | dbg!(r); 58 | true 59 | } 60 | e => { 61 | dbg!(e); 62 | false 63 | } 64 | }; 65 | 66 | assert!(x) 67 | }); 68 | } 69 | 70 | #[test] 71 | fn proq_series() { 72 | futures::executor::block_on(async { 73 | let end = Utc::now(); 74 | let start = Some(end - chrono::Duration::hours(1)); 75 | 76 | let selectors = vec!["up", "process_start_time_seconds{job=\"prometheus\"}"]; 77 | 78 | let x = match client().series(selectors, start, Some(end)).await.unwrap() { 79 | ApiOk(r) => { 80 | dbg!(r); 81 | true 82 | } 83 | e => { 84 | dbg!(e); 85 | false 86 | } 87 | }; 88 | 89 | assert!(x) 90 | }); 91 | } 92 | 93 | #[test] 94 | fn proq_labels() { 95 | futures::executor::block_on(async { 96 | let x = match client().label_names().await.unwrap() { 97 | ApiOk(r) => { 98 | dbg!(r); 99 | true 100 | } 101 | e => { 102 | dbg!(e); 103 | false 104 | } 105 | }; 106 | 107 | assert!(x) 108 | }); 109 | } 110 | 111 | #[test] 112 | fn proq_label_values() { 113 | futures::executor::block_on(async { 114 | let query_label = "version"; 115 | 116 | let x = match client().label_values(query_label).await.unwrap() { 117 | ApiOk(r) => { 118 | dbg!(r); 119 | true 120 | } 121 | e => { 122 | dbg!(e); 123 | false 124 | } 125 | }; 126 | 127 | assert!(x) 128 | }); 129 | } 130 | 131 | #[test] 132 | fn proq_targets() { 133 | futures::executor::block_on(async { 134 | let x = match client().targets().await.unwrap() { 135 | ApiOk(r) => { 136 | dbg!(r); 137 | true 138 | } 139 | e => { 140 | dbg!(e); 141 | false 142 | } 143 | }; 144 | 145 | assert!(x) 146 | }); 147 | } 148 | 149 | #[test] 150 | fn proq_targets_with_state() { 151 | futures::executor::block_on(async { 152 | let state_filer = ProqTargetStates::ACTIVE; 153 | 154 | let x = match client().targets_with_state(state_filer).await.unwrap() { 155 | ApiOk(r) => { 156 | dbg!(r); 157 | true 158 | } 159 | e => { 160 | dbg!(e); 161 | false 162 | } 163 | }; 164 | 165 | assert!(x) 166 | }); 167 | } 168 | 169 | #[test] 170 | fn proq_rules() { 171 | futures::executor::block_on(async { 172 | let x = match client().rules().await.unwrap() { 173 | ApiOk(r) => { 174 | dbg!(r); 175 | true 176 | } 177 | e => { 178 | dbg!(e); 179 | false 180 | } 181 | }; 182 | 183 | assert!(x) 184 | }); 185 | } 186 | 187 | #[test] 188 | fn proq_rules_with_type() { 189 | futures::executor::block_on(async { 190 | let rule_type = ProqRulesType::ALERT; 191 | 192 | let x = match client().rules_with_type(rule_type).await.unwrap() { 193 | ApiOk(r) => { 194 | dbg!(r); 195 | true 196 | } 197 | e => { 198 | dbg!(e); 199 | false 200 | } 201 | }; 202 | 203 | assert!(x) 204 | }); 205 | } 206 | 207 | #[test] 208 | fn proq_alerts() { 209 | futures::executor::block_on(async { 210 | let x = match client().alerts().await.unwrap() { 211 | ApiOk(r) => { 212 | dbg!(r); 213 | true 214 | } 215 | e => { 216 | dbg!(e); 217 | false 218 | } 219 | }; 220 | 221 | assert!(x) 222 | }); 223 | } 224 | 225 | #[test] 226 | fn proq_alert_manager() { 227 | futures::executor::block_on(async { 228 | let x = match client().alert_managers().await.unwrap() { 229 | ApiOk(r) => { 230 | dbg!(r); 231 | true 232 | } 233 | e => { 234 | dbg!(e); 235 | false 236 | } 237 | }; 238 | 239 | assert!(x) 240 | }); 241 | } 242 | 243 | #[test] 244 | fn proq_config() { 245 | futures::executor::block_on(async { 246 | let x = match client().config().await.unwrap() { 247 | ApiOk(r) => { 248 | dbg!(r); 249 | true 250 | } 251 | e => { 252 | dbg!(e); 253 | false 254 | } 255 | }; 256 | 257 | assert!(x) 258 | }); 259 | } 260 | 261 | #[test] 262 | fn proq_flags() { 263 | futures::executor::block_on(async { 264 | let x = match client().flags().await.unwrap() { 265 | ApiOk(r) => { 266 | dbg!(r); 267 | true 268 | } 269 | e => { 270 | dbg!(e); 271 | false 272 | } 273 | }; 274 | 275 | assert!(x) 276 | }); 277 | } 278 | -------------------------------------------------------------------------------- /tests/serialization.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::result::Result as StdResult; 3 | 4 | use chrono::{DateTime, FixedOffset}; 5 | use url::Url; 6 | 7 | use proq::result_types::{ 8 | ActiveTarget, Alert, AlertManager, AlertManagers, AlertState, ApiErr, ApiOk, ApiResult, Config, 9 | Data, DroppedTarget, Expression, Instant, LabelsOrValues, Metric, Range, Rule, RuleGroups, 10 | RuleType, Rules, Sample, Series, Snapshot, StringSample, TargetHealth, Targets, 11 | }; 12 | 13 | #[test] 14 | fn should_deserialize_json_error() -> StdResult<(), std::io::Error> { 15 | let j = r#" 16 | { 17 | "status": "error", 18 | "error": "Major", 19 | "errorType": "Seriously Bad" 20 | } 21 | "#; 22 | 23 | let res = serde_json::from_str::(j)?; 24 | assert_eq!( 25 | ApiResult::ApiErr(ApiErr { 26 | error_message: "Major".to_string(), 27 | error_type: "Seriously Bad".to_string(), 28 | data: None, 29 | warnings: Vec::new(), 30 | }), 31 | res 32 | ); 33 | 34 | Ok(()) 35 | } 36 | 37 | #[test] 38 | fn should_deserialize_json_error_with_instant_and_warnings() -> StdResult<(), std::io::Error> { 39 | let expected_json = r#" 40 | { 41 | "status": "error", 42 | "error": "This is a strange error", 43 | "errorType": "Weird", 44 | "warnings": [ 45 | "You timed out, foo" 46 | ], 47 | "data" : { 48 | "resultType" : "vector", 49 | "result" : [ 50 | { 51 | "metric" : { 52 | "__name__" : "up", 53 | "job" : "prometheus", 54 | "instance" : "localhost:9090" 55 | }, 56 | "value": [ 1435781451.781, "1" ] 57 | }, 58 | { 59 | "metric" : { 60 | "__name__" : "up", 61 | "job" : "node", 62 | "instance" : "localhost:9100" 63 | }, 64 | "value" : [ 1435781451.781, "0" ] 65 | } 66 | ] 67 | } 68 | } 69 | "#; 70 | let expected = serde_json::from_str::(expected_json)?; 71 | 72 | let mut metric_1: HashMap = HashMap::new(); 73 | metric_1.insert("__name__".to_owned(), "up".to_owned()); 74 | metric_1.insert("job".to_owned(), "prometheus".to_owned()); 75 | metric_1.insert("instance".to_owned(), "localhost:9090".to_owned()); 76 | 77 | let mut metric_2: HashMap = HashMap::new(); 78 | metric_2.insert("__name__".to_owned(), "up".to_owned()); 79 | metric_2.insert("job".to_owned(), "node".to_owned()); 80 | metric_2.insert("instance".to_owned(), "localhost:9100".to_owned()); 81 | 82 | let actual = ApiResult::ApiErr(ApiErr { 83 | error_type: "Weird".to_owned(), 84 | error_message: "This is a strange error".to_owned(), 85 | data: Some(Data::Expression(Expression::Instant(vec![ 86 | Instant { 87 | metric: Metric { 88 | labels: metric_1.clone(), 89 | }, 90 | sample: Sample { 91 | epoch: 1435781451.781, 92 | value: 1 as f64, 93 | }, 94 | }, 95 | Instant { 96 | metric: Metric { 97 | labels: metric_2.clone(), 98 | }, 99 | sample: Sample { 100 | epoch: 1435781451.781, 101 | value: 0 as f64, 102 | }, 103 | }, 104 | ]))), 105 | warnings: vec!["You timed out, foo".to_owned()], 106 | }); 107 | assert_eq!(actual, expected); 108 | 109 | Ok(()) 110 | } 111 | 112 | #[test] 113 | fn should_deserialize_json_prom_scalar() -> StdResult<(), std::io::Error> { 114 | let j = r#" 115 | { 116 | "status": "success", 117 | "data": { 118 | "resultType": "scalar", 119 | "result": [1435781451.781, "1"] 120 | } 121 | } 122 | "#; 123 | 124 | let res = serde_json::from_str::(j)?; 125 | assert_eq!( 126 | ApiResult::ApiOk(ApiOk { 127 | data: Some(Data::Expression(Expression::Scalar(Sample { 128 | epoch: 1435781451.781, 129 | value: 1 as f64, 130 | }))), 131 | warnings: Vec::new(), 132 | }), 133 | res 134 | ); 135 | 136 | Ok(()) 137 | } 138 | 139 | #[test] 140 | fn should_deserialize_json_prom_scalar_with_warnings() -> StdResult<(), std::io::Error> { 141 | let j = r#" 142 | { 143 | "warnings": ["You timed out, foo"], 144 | "status": "success", 145 | "data": { 146 | "resultType": "scalar", 147 | "result": [1435781451.781, "1"] 148 | } 149 | } 150 | "#; 151 | 152 | let res = serde_json::from_str::(j)?; 153 | assert_eq!( 154 | ApiResult::ApiOk(ApiOk { 155 | data: Some(Data::Expression(Expression::Scalar(Sample { 156 | epoch: 1435781451.781, 157 | value: 1 as f64, 158 | }))), 159 | warnings: vec!["You timed out, foo".to_owned()], 160 | }), 161 | res 162 | ); 163 | 164 | Ok(()) 165 | } 166 | 167 | #[test] 168 | fn should_deserialize_json_prom_string() -> StdResult<(), std::io::Error> { 169 | let j = r#" 170 | { 171 | "status": "success", 172 | "data": { 173 | "resultType": "string", 174 | "result": [1435781451.781, "foo"] 175 | } 176 | } 177 | "#; 178 | 179 | let res = serde_json::from_str::(j)?; 180 | assert_eq!( 181 | ApiResult::ApiOk(ApiOk { 182 | data: Some(Data::Expression(Expression::String(StringSample { 183 | epoch: 1435781451.781, 184 | value: "foo".to_owned(), 185 | }))), 186 | warnings: Vec::new(), 187 | }), 188 | res 189 | ); 190 | 191 | Ok(()) 192 | } 193 | 194 | #[test] 195 | fn should_deserialize_json_prom_vector() -> StdResult<(), std::io::Error> { 196 | let j = r#" 197 | { 198 | "status" : "success", 199 | "data" : { 200 | "resultType" : "vector", 201 | "result" : [ 202 | { 203 | "metric" : { 204 | "__name__" : "up", 205 | "job" : "prometheus", 206 | "instance" : "localhost:9090" 207 | }, 208 | "value": [ 1435781451.781, "1" ] 209 | }, 210 | { 211 | "metric" : { 212 | "__name__" : "up", 213 | "job" : "node", 214 | "instance" : "localhost:9100" 215 | }, 216 | "value" : [ 1435781451.781, "0" ] 217 | } 218 | ] 219 | } 220 | } 221 | "#; 222 | 223 | let mut metric_1: HashMap = HashMap::new(); 224 | metric_1.insert("__name__".to_owned(), "up".to_owned()); 225 | metric_1.insert("job".to_owned(), "prometheus".to_owned()); 226 | metric_1.insert("instance".to_owned(), "localhost:9090".to_owned()); 227 | 228 | let mut metric_2: HashMap = HashMap::new(); 229 | metric_2.insert("__name__".to_owned(), "up".to_owned()); 230 | metric_2.insert("job".to_owned(), "node".to_owned()); 231 | metric_2.insert("instance".to_owned(), "localhost:9100".to_owned()); 232 | 233 | let res = serde_json::from_str::(j)?; 234 | assert_eq!( 235 | ApiResult::ApiOk(ApiOk { 236 | data: Some(Data::Expression(Expression::Instant(vec!( 237 | Instant { 238 | metric: Metric { 239 | labels: metric_1.clone(), 240 | }, 241 | sample: Sample { 242 | epoch: 1435781451.781, 243 | value: 1 as f64, 244 | }, 245 | }, 246 | Instant { 247 | metric: Metric { 248 | labels: metric_2.clone(), 249 | }, 250 | sample: Sample { 251 | epoch: 1435781451.781, 252 | value: 0 as f64, 253 | }, 254 | }, 255 | )))), 256 | warnings: Vec::new(), 257 | }), 258 | res 259 | ); 260 | 261 | Ok(()) 262 | } 263 | 264 | #[test] 265 | fn should_deserialize_json_prom_matrix() -> StdResult<(), std::io::Error> { 266 | let j = r#" 267 | { 268 | "status" : "success", 269 | "data" : { 270 | "resultType" : "matrix", 271 | "result" : [ 272 | { 273 | "metric" : { 274 | "__name__" : "up", 275 | "job" : "prometheus", 276 | "instance" : "localhost:9090" 277 | }, 278 | "values" : [ 279 | [ 1435781430.781, "1" ], 280 | [ 1435781445.781, "1" ], 281 | [ 1435781460.781, "1" ] 282 | ] 283 | }, 284 | { 285 | "metric" : { 286 | "__name__" : "up", 287 | "job" : "node", 288 | "instance" : "localhost:9091" 289 | }, 290 | "values" : [ 291 | [ 1435781430.781, "0" ], 292 | [ 1435781445.781, "0" ], 293 | [ 1435781460.781, "1" ] 294 | ] 295 | } 296 | ] 297 | } 298 | } 299 | "#; 300 | 301 | let mut metric_1: HashMap = HashMap::new(); 302 | metric_1.insert("__name__".to_owned(), "up".to_owned()); 303 | metric_1.insert("job".to_owned(), "prometheus".to_owned()); 304 | metric_1.insert("instance".to_owned(), "localhost:9090".to_owned()); 305 | 306 | let mut metric_2: HashMap = HashMap::new(); 307 | metric_2.insert("__name__".to_owned(), "up".to_owned()); 308 | metric_2.insert("job".to_owned(), "node".to_owned()); 309 | metric_2.insert("instance".to_owned(), "localhost:9091".to_owned()); 310 | 311 | let res = serde_json::from_str::(j)?; 312 | assert_eq!( 313 | ApiResult::ApiOk(ApiOk { 314 | data: Some(Data::Expression(Expression::Range(vec!( 315 | Range { 316 | metric: Metric { 317 | labels: metric_1.clone(), 318 | }, 319 | samples: vec!( 320 | Sample { 321 | epoch: 1435781430.781, 322 | value: 1 as f64, 323 | }, 324 | Sample { 325 | epoch: 1435781445.781, 326 | value: 1 as f64, 327 | }, 328 | Sample { 329 | epoch: 1435781460.781, 330 | value: 1 as f64, 331 | }, 332 | ), 333 | }, 334 | Range { 335 | metric: Metric { 336 | labels: metric_2.clone(), 337 | }, 338 | samples: vec!( 339 | Sample { 340 | epoch: 1435781430.781, 341 | value: 0 as f64, 342 | }, 343 | Sample { 344 | epoch: 1435781445.781, 345 | value: 0 as f64, 346 | }, 347 | Sample { 348 | epoch: 1435781460.781, 349 | value: 1 as f64, 350 | }, 351 | ), 352 | }, 353 | )))), 354 | warnings: Vec::new(), 355 | }), 356 | res 357 | ); 358 | 359 | Ok(()) 360 | } 361 | 362 | #[test] 363 | fn should_deserialize_json_prom_labels() -> StdResult<(), std::io::Error> { 364 | let j = r#" 365 | { 366 | "status" : "success", 367 | "data" :[ 368 | "__name__", 369 | "call", 370 | "code", 371 | "config", 372 | "dialer_name", 373 | "endpoint", 374 | "event", 375 | "goversion", 376 | "handler", 377 | "instance", 378 | "interval", 379 | "job", 380 | "le", 381 | "listener_name", 382 | "name", 383 | "quantile", 384 | "reason", 385 | "role", 386 | "scrape_job", 387 | "slice", 388 | "version" 389 | ] 390 | } 391 | "#; 392 | 393 | let res = serde_json::from_str::(j)?; 394 | assert_eq!( 395 | ApiResult::ApiOk(ApiOk { 396 | data: Some(Data::LabelsOrValues(LabelsOrValues(vec![ 397 | "__name__".to_owned(), 398 | "call".to_owned(), 399 | "code".to_owned(), 400 | "config".to_owned(), 401 | "dialer_name".to_owned(), 402 | "endpoint".to_owned(), 403 | "event".to_owned(), 404 | "goversion".to_owned(), 405 | "handler".to_owned(), 406 | "instance".to_owned(), 407 | "interval".to_owned(), 408 | "job".to_owned(), 409 | "le".to_owned(), 410 | "listener_name".to_owned(), 411 | "name".to_owned(), 412 | "quantile".to_owned(), 413 | "reason".to_owned(), 414 | "role".to_owned(), 415 | "scrape_job".to_owned(), 416 | "slice".to_owned(), 417 | "version".to_owned(), 418 | ]))), 419 | warnings: Vec::new(), 420 | }), 421 | res 422 | ); 423 | 424 | Ok(()) 425 | } 426 | 427 | #[test] 428 | fn should_deserialize_json_prom_label_values() -> StdResult<(), std::io::Error> { 429 | let j = r#" 430 | { 431 | "status" : "success", 432 | "data" :[ 433 | "node", 434 | "prometheus" 435 | ] 436 | } 437 | "#; 438 | 439 | let res = serde_json::from_str::(j)?; 440 | assert_eq!( 441 | ApiResult::ApiOk(ApiOk { 442 | data: Some(Data::LabelsOrValues(LabelsOrValues(vec![ 443 | "node".to_owned(), 444 | "prometheus".to_owned(), 445 | ]))), 446 | warnings: Vec::new(), 447 | }), 448 | res 449 | ); 450 | 451 | Ok(()) 452 | } 453 | 454 | #[test] 455 | fn should_deserialize_json_prom_series() -> StdResult<(), std::io::Error> { 456 | let j = r#" 457 | { 458 | "status" : "success", 459 | "data" : [ 460 | { 461 | "__name__" : "up", 462 | "job" : "prometheus", 463 | "instance" : "localhost:9090" 464 | }, 465 | { 466 | "__name__" : "up", 467 | "job" : "node", 468 | "instance" : "localhost:9091" 469 | }, 470 | { 471 | "__name__" : "process_start_time_seconds", 472 | "job" : "prometheus", 473 | "instance" : "localhost:9090" 474 | } 475 | ] 476 | } 477 | "#; 478 | 479 | let mut metric_1: HashMap = HashMap::new(); 480 | metric_1.insert("__name__".to_owned(), "up".to_owned()); 481 | metric_1.insert("job".to_owned(), "prometheus".to_owned()); 482 | metric_1.insert("instance".to_owned(), "localhost:9090".to_owned()); 483 | 484 | let mut metric_2: HashMap = HashMap::new(); 485 | metric_2.insert("__name__".to_owned(), "up".to_owned()); 486 | metric_2.insert("job".to_owned(), "node".to_owned()); 487 | metric_2.insert("instance".to_owned(), "localhost:9091".to_owned()); 488 | 489 | let mut metric_3: HashMap = HashMap::new(); 490 | metric_3.insert( 491 | "__name__".to_owned(), 492 | "process_start_time_seconds".to_owned(), 493 | ); 494 | metric_3.insert("job".to_owned(), "prometheus".to_owned()); 495 | metric_3.insert("instance".to_owned(), "localhost:9090".to_owned()); 496 | 497 | let res = serde_json::from_str::(j)?; 498 | assert_eq!( 499 | ApiResult::ApiOk(ApiOk { 500 | data: Some(Data::Series(Series(vec![ 501 | Metric { labels: metric_1 }, 502 | Metric { labels: metric_2 }, 503 | Metric { labels: metric_3 }, 504 | ]))), 505 | warnings: Vec::new(), 506 | }), 507 | res 508 | ); 509 | 510 | Ok(()) 511 | } 512 | 513 | #[test] 514 | fn should_deserialize_json_prom_targets() -> StdResult<(), std::io::Error> { 515 | let j = r#" 516 | { 517 | "status": "success", 518 | "data": { 519 | "activeTargets": [ 520 | { 521 | "discoveredLabels": { 522 | "__address__": "127.0.0.1:9090", 523 | "__metrics_path__": "/metrics", 524 | "__scheme__": "http", 525 | "job": "prometheus" 526 | }, 527 | "labels": { 528 | "instance": "127.0.0.1:9090", 529 | "job": "prometheus" 530 | }, 531 | "scrapeUrl": "http://127.0.0.1:9090/metrics", 532 | "lastError": "", 533 | "lastScrape": "2017-01-17T15:07:44.723715405+01:00", 534 | "health": "up" 535 | } 536 | ], 537 | "droppedTargets": [ 538 | { 539 | "discoveredLabels": { 540 | "__address__": "127.0.0.1:9100", 541 | "__metrics_path__": "/metrics", 542 | "__scheme__": "http", 543 | "job": "node" 544 | } 545 | } 546 | ] 547 | } 548 | } 549 | "#; 550 | 551 | let mut active_discovered_labels: HashMap = HashMap::new(); 552 | active_discovered_labels.insert("__address__".to_owned(), "127.0.0.1:9090".to_owned()); 553 | active_discovered_labels.insert("__metrics_path__".to_owned(), "/metrics".to_owned()); 554 | active_discovered_labels.insert("__scheme__".to_owned(), "http".to_owned()); 555 | active_discovered_labels.insert("job".to_owned(), "prometheus".to_owned()); 556 | let active_discovered_labels = active_discovered_labels; 557 | 558 | let mut active_labels: HashMap = HashMap::new(); 559 | active_labels.insert("instance".to_owned(), "127.0.0.1:9090".to_owned()); 560 | active_labels.insert("job".to_owned(), "prometheus".to_owned()); 561 | let active_labels = active_labels; 562 | 563 | let mut dropped_discovered_labels: HashMap = HashMap::new(); 564 | dropped_discovered_labels.insert("__address__".to_owned(), "127.0.0.1:9100".to_owned()); 565 | dropped_discovered_labels.insert("__metrics_path__".to_owned(), "/metrics".to_owned()); 566 | dropped_discovered_labels.insert("__scheme__".to_owned(), "http".to_owned()); 567 | dropped_discovered_labels.insert("job".to_owned(), "node".to_owned()); 568 | let dropped_discovered_labels = dropped_discovered_labels; 569 | 570 | let last_scrape: DateTime = 571 | DateTime::parse_from_rfc3339("2017-01-17T15:07:44.723715405+01:00").unwrap(); 572 | 573 | let res = serde_json::from_str::(j)?; 574 | assert_eq!( 575 | res, 576 | ApiResult::ApiOk(ApiOk { 577 | data: Some(Data::Targets(Targets { 578 | active: vec![ActiveTarget { 579 | discovered_labels: active_discovered_labels, 580 | labels: active_labels, 581 | scrape_url: Url::parse("http://127.0.0.1:9090/metrics").unwrap(), 582 | last_error: None, 583 | last_scrape, 584 | health: TargetHealth::Up, 585 | },], 586 | dropped: vec![DroppedTarget { 587 | discovered_labels: dropped_discovered_labels 588 | },], 589 | })), 590 | warnings: Vec::new(), 591 | }) 592 | ); 593 | 594 | Ok(()) 595 | } 596 | 597 | #[test] 598 | fn should_deserialize_json_prom_alert_managers() -> StdResult<(), std::io::Error> { 599 | let j = r#" 600 | { 601 | "status": "success", 602 | "data": { 603 | "activeAlertmanagers": [ 604 | { 605 | "url": "http://127.0.0.1:9090/api/v1/alerts" 606 | } 607 | ], 608 | "droppedAlertmanagers": [ 609 | { 610 | "url": "http://127.0.0.1:9093/api/v1/alerts" 611 | } 612 | ] 613 | } 614 | } 615 | "#; 616 | 617 | let res = serde_json::from_str::(j)?; 618 | assert_eq!( 619 | ApiResult::ApiOk(ApiOk { 620 | data: Some(Data::AlertManagers(AlertManagers { 621 | active: vec![AlertManager { 622 | url: Url::parse("http://127.0.0.1:9090/api/v1/alerts").unwrap(), 623 | },], 624 | dropped: vec![AlertManager { 625 | url: Url::parse("http://127.0.0.1:9093/api/v1/alerts").unwrap(), 626 | },], 627 | })), 628 | warnings: Vec::new(), 629 | }), 630 | res 631 | ); 632 | 633 | Ok(()) 634 | } 635 | 636 | #[test] 637 | fn should_deserialize_json_prom_flags() -> StdResult<(), std::io::Error> { 638 | let j = r#" 639 | { 640 | "status": "success", 641 | "data": { 642 | "alertmanager.notification-queue-capacity": "10000", 643 | "alertmanager.timeout": "10s", 644 | "log.level": "info", 645 | "query.lookback-delta": "5m", 646 | "query.max-concurrency": "20" 647 | } 648 | } 649 | "#; 650 | 651 | let mut flags: HashMap = HashMap::new(); 652 | flags.insert( 653 | "alertmanager.notification-queue-capacity".to_owned(), 654 | "10000".to_owned(), 655 | ); 656 | flags.insert("alertmanager.timeout".to_owned(), "10s".to_owned()); 657 | flags.insert("log.level".to_owned(), "info".to_owned()); 658 | flags.insert("query.lookback-delta".to_owned(), "5m".to_owned()); 659 | flags.insert("query.max-concurrency".to_owned(), "20".to_owned()); 660 | let flags = flags; 661 | 662 | let res = serde_json::from_str::(j)?; 663 | assert_eq!( 664 | ApiResult::ApiOk(ApiOk { 665 | data: Some(Data::Flags(flags)), 666 | warnings: Vec::new(), 667 | }), 668 | res 669 | ); 670 | 671 | Ok(()) 672 | } 673 | 674 | #[test] 675 | fn should_deserialize_json_prom_snapshot() -> StdResult<(), std::io::Error> { 676 | let j = r#" 677 | { 678 | "status": "success", 679 | "data": { 680 | "name": "20171210T211224Z-2be650b6d019eb54" 681 | } 682 | } 683 | "#; 684 | 685 | let res = serde_json::from_str::(j)?; 686 | assert_eq!( 687 | ApiResult::ApiOk(ApiOk { 688 | data: Some(Data::Snapshot(Snapshot { 689 | name: "20171210T211224Z-2be650b6d019eb54".to_owned() 690 | })), 691 | warnings: Vec::new(), 692 | }), 693 | res 694 | ); 695 | 696 | Ok(()) 697 | } 698 | 699 | // FIXME: make this an actual tests 700 | #[test] 701 | fn should_serialize_rust_prom_snapshot() -> StdResult<(), std::io::Error> { 702 | let s = serde_json::to_string_pretty(&ApiResult::ApiOk(ApiOk { 703 | data: Some(Data::Snapshot(Snapshot { 704 | name: "20171210T211224Z-2be650b6d019eb54".to_owned(), 705 | })), 706 | warnings: Vec::new(), 707 | }))?; 708 | 709 | dbg!(s); 710 | 711 | Ok(()) 712 | } 713 | 714 | #[test] 715 | fn should_deserialize_json_prom_config() -> StdResult<(), std::io::Error> { 716 | let j = r#" 717 | { 718 | "status": "success", 719 | "data": { 720 | "yaml": "CONTENT" 721 | } 722 | } 723 | "#; 724 | 725 | let res = serde_json::from_str::(j)?; 726 | assert_eq!( 727 | ApiResult::ApiOk(ApiOk { 728 | data: Some(Data::Config(Config { 729 | yaml: "CONTENT".to_owned() 730 | })), 731 | warnings: Vec::new(), 732 | }), 733 | res 734 | ); 735 | 736 | Ok(()) 737 | } 738 | 739 | // FIXME: make this an actual tests 740 | #[test] 741 | fn should_serialize_rust_prom_config() -> StdResult<(), std::io::Error> { 742 | let s = serde_json::to_string_pretty(&ApiResult::ApiOk(ApiOk { 743 | data: Some(Data::Config(Config { 744 | yaml: "CONTENT".to_owned(), 745 | })), 746 | warnings: Vec::new(), 747 | }))?; 748 | 749 | dbg!(s); 750 | 751 | Ok(()) 752 | } 753 | 754 | #[test] 755 | fn should_deserialize_json_prom_rules() -> StdResult<(), std::io::Error> { 756 | let j = r#" 757 | { 758 | "data": { 759 | "groups": [ 760 | { 761 | "rules": [ 762 | { 763 | "alerts": [ 764 | { 765 | "activeAt": "2018-07-04T20:27:12.60602144+02:00", 766 | "annotations": { 767 | "summary": "High request latency" 768 | }, 769 | "labels": { 770 | "alertname": "HighRequestLatency", 771 | "severity": "page" 772 | }, 773 | "state": "firing", 774 | "value": "1e+00" 775 | } 776 | ], 777 | "annotations": { 778 | "summary": "High request latency" 779 | }, 780 | "duration": 600, 781 | "health": "ok", 782 | "labels": { 783 | "severity": "page" 784 | }, 785 | "name": "HighRequestLatency", 786 | "query": "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", 787 | "type": "alerting" 788 | }, 789 | { 790 | "health": "ok", 791 | "name": "job:http_inprogress_requests:sum", 792 | "query": "sum(http_inprogress_requests) by (job)", 793 | "type": "recording" 794 | } 795 | ], 796 | "file": "/rules.yaml", 797 | "interval": 60, 798 | "name": "example" 799 | } 800 | ] 801 | }, 802 | "status": "success" 803 | } 804 | "#; 805 | 806 | let res = serde_json::from_str::(j)?; 807 | 808 | let mut data_groups_rules_annotations = HashMap::::new(); 809 | data_groups_rules_annotations.insert( 810 | String::from("summary"), 811 | String::from("High request latency"), 812 | ); 813 | 814 | let mut data_groups_rules_labels = HashMap::new(); 815 | data_groups_rules_labels.insert(String::from("severity"), String::from("page")); 816 | 817 | let mut data_groups_rules_alert_labels = HashMap::new(); 818 | data_groups_rules_alert_labels.insert( 819 | String::from("alertname"), 820 | String::from("HighRequestLatency"), 821 | ); 822 | data_groups_rules_alert_labels.insert(String::from("severity"), String::from("page")); 823 | 824 | assert_eq!( 825 | ApiResult::ApiOk(ApiOk { 826 | data: Some(Data::Rules(Rules { 827 | groups: vec![RuleGroups { 828 | rules: vec![ 829 | Rule { 830 | alerts: Some(vec![Alert { 831 | active_at: String::from("2018-07-04T20:27:12.60602144+02:00"), 832 | annotations: Some(data_groups_rules_annotations.clone()), 833 | labels: Some(data_groups_rules_alert_labels), 834 | state: AlertState::FIRING, 835 | value: String::from("1e+00"), 836 | }]), 837 | annotations: Some(data_groups_rules_annotations), 838 | duration: Some(600), 839 | health: String::from("ok"), 840 | labels: Some(data_groups_rules_labels), 841 | name: String::from("HighRequestLatency"), 842 | query: String::from( 843 | "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5" 844 | ), 845 | rule_type: RuleType::ALERTING 846 | }, 847 | Rule { 848 | alerts: None, 849 | annotations: None, 850 | duration: None, 851 | health: String::from("ok"), 852 | labels: None, 853 | name: String::from("job:http_inprogress_requests:sum"), 854 | query: String::from("sum(http_inprogress_requests) by (job)"), 855 | rule_type: RuleType::RECORDING 856 | } 857 | ], 858 | file: String::from("/rules.yaml"), 859 | interval: 60, 860 | name: String::from("example") 861 | }] 862 | })), 863 | warnings: Vec::new(), 864 | }), 865 | res 866 | ); 867 | 868 | Ok(()) 869 | } 870 | --------------------------------------------------------------------------------