diff --git a/prom-client/src/builders.rs b/prom-client/src/builders.rs index 1dd1d84..4713669 100644 --- a/prom-client/src/builders.rs +++ b/prom-client/src/builders.rs @@ -1,7 +1,10 @@ +//! Builder for QueryRangeRequest + use super::QueryRangeRequest; use chrono::{DateTime, TimeDelta, Utc}; use std::{ops::Range, time::Duration}; +/// Builder for constructing a QueryRangeRequest pub struct QueryRangeRequestBuilder { query: String, range: Option>>, @@ -10,6 +13,7 @@ pub struct QueryRangeRequestBuilder { } impl QueryRangeRequestBuilder { + /// Create a new builder with the given PromQL query pub fn new(query: String) -> Self { Self { query, @@ -19,6 +23,7 @@ impl QueryRangeRequestBuilder { } } + /// Set the time range for the query pub fn range(mut self, range: Range>) -> Self { if self.range.is_some() { panic!("already set range: {:?}", self.range); @@ -27,6 +32,7 @@ impl QueryRangeRequestBuilder { self } + /// Set the time range to be from `time` ago until now pub fn since(mut self, time: Duration) -> Self { if self.range.is_some() { panic!("already set range: {:?}", self.range); @@ -38,6 +44,7 @@ impl QueryRangeRequestBuilder { self } + /// Set the step interval between data points pub fn step(mut self, step: Duration) -> Self { if self.step.is_some() { panic!("already set step: {:?}", self.step); @@ -46,11 +53,13 @@ impl QueryRangeRequestBuilder { self } + /// Set the target number of data points (used to compute step if not set) pub fn count(mut self, count: usize) -> Self { self.count = count; self } + /// Build the QueryRangeRequest pub fn build(self) -> QueryRangeRequest { let query = self.query; let range = self.range.unwrap(); diff --git a/prom-client/src/error.rs b/prom-client/src/error.rs index 980c7d5..5ff10f1 100644 --- a/prom-client/src/error.rs +++ b/prom-client/src/error.rs @@ -1,6 +1,9 @@ +//! Error types for prom-client + use displaydoc::Display; use url::ParseError; +/// Errors that can occur when making Prometheus API requests #[derive(Debug, Display)] pub enum Error { /// URL: {0} diff --git a/prom-client/src/labels.rs b/prom-client/src/labels.rs index 17530d8..87ac0c4 100644 --- a/prom-client/src/labels.rs +++ b/prom-client/src/labels.rs @@ -9,12 +9,18 @@ pub type Labels = BTreeMap; /// Common labels extracted from a very generic sequence of metric labels (key value pairs) pub struct ExtractLabels { + /// The metric name (from __name__ label) pub name: String, + /// Labels that are common to all metrics in the set pub common_labels: Labels, + /// Labels specific to each metric (excluding common labels) pub specific_labels: Vec, } impl ExtractLabels { + /// Extract common and specific labels from an iterator of metric label sets. + /// + /// Labels in `skip_labels` are excluded from both common and specific labels. pub fn new<'a, I, KV, K, V>(src: I, skip_labels: &[String]) -> Self where I: Iterator, diff --git a/prom-client/src/lib.rs b/prom-client/src/lib.rs index 9ee84a0..aa8fbd7 100644 --- a/prom-client/src/lib.rs +++ b/prom-client/src/lib.rs @@ -1,4 +1,6 @@ -//! API for getting time series data from prometheus +#![deny(missing_docs)] + +//! Minimal API for getting time series data from prometheus //! //! To use it, instantiate one of the request objects, //! e.g. QueryRequest or QueryRangeRequest. When it's helpful a builder is provided. @@ -12,6 +14,9 @@ //! In prometheus, metric labels are just a set of key-value pairs. However, //! if you are expecting certain structure, you may use any KV object that implements //! `serde::Deserialize` in the `PromData` that results from the call. +//! +//! When `plot` feature is active, the plot module can be used to plot the timeseries data. +//! This is mostly intended to be used as previews in alert messages. use chrono::{DateTime, Utc}; use serde::{Serialize, de::DeserializeOwned}; @@ -41,7 +46,9 @@ pub use traits::PromRequest; /// Query parameters for /api/v1/query prometheus request #[derive(Clone, Debug, Serialize)] pub struct QueryRequest { + /// The PromQL query string pub query: String, + /// Optional evaluation timestamp (defaults to current time) pub time: Option>, } @@ -76,6 +83,7 @@ impl PromRequest for QueryRangeRequest { #[derive(Clone, Debug, Serialize)] #[serde(transparent)] pub struct SeriesRequest { + /// Series selector arguments pub matches: MatchList, } @@ -88,6 +96,7 @@ impl PromRequest for SeriesRequest { #[derive(Clone, Debug, Serialize)] #[serde(transparent)] pub struct LabelsRequest { + /// Series selector arguments to filter which labels are returned pub matches: MatchList, } diff --git a/prom-client/src/messages.rs b/prom-client/src/messages.rs index 5e6dbdb..5f1be44 100644 --- a/prom-client/src/messages.rs +++ b/prom-client/src/messages.rs @@ -1,33 +1,38 @@ -// The prometheus http interface (at port 9090) renders graphs in JS and gets the raw data using API requests like this: -// -// GET http://localhost:9090/api/v1/query_range?query=tick_time{quantile="0.99"}&step=14&start=1762534433.802&end=1762538033.802 -// -// Response is: -// -// { -// status: "success" -// data: { -// resultType: "matrix", -// result: [ -// { -// metric: { __name__: "tick_time", instance: "x.y.z.w", job: "ec2", .. }, -// values: [ -// [ 1762534433.802, "1.8974293514080933" ], -// [ 1762534447.802, "2.029724353457351" ], -// .. -// ] -// } -// ] -// } -// } -// -// For more detail see: -// https://prometheus.io/docs/prometheus/latest/querying/api/ +//! Message types for Prometheus API responses +//! +//! The prometheus http interface (at port 9090) renders graphs in JS and gets the raw data using API requests like this: +//! +//! GET http://localhost:9090/api/v1/query_range?query=tick_time{quantile="0.99"}&step=14&start=1762534433.802&end=1762538033.802 +//! +//! Response is: +//! +//! { +//! status: "success" +//! data: { +//! resultType: "matrix", +//! result: [ +//! { +//! metric: { __name__: "tick_time", instance: "x.y.z.w", job: "ec2", .. }, +//! values: [ +//! [ 1762534433.802, "1.8974293514080933" ], +//! [ 1762534447.802, "2.029724353457351" ], +//! .. +//! ] +//! } +//! ] +//! } +//! } +//! +//! For more detail see: +//! https://prometheus.io/docs/prometheus/latest/querying/api/ use crate::Error; use chrono::{DateTime, Utc}; use serde::{Deserialize, Deserializer, de::DeserializeOwned}; -use std::{collections::HashMap, fmt::{self, Debug, Display}}; +use std::{ + collections::HashMap, + fmt::{self, Debug, Display}, +}; use tracing::warn; /// Wrapper for f64 that deserializes from a string (prometheus returns numeric values as strings) @@ -58,30 +63,37 @@ impl<'de> Deserialize<'de> for MetricVal { } } +/// A single metric value from an instant query #[derive(Clone, Debug, Deserialize)] #[serde(bound = "KV: DeserializeOwned")] pub struct MetricValue> where KV: Clone + Debug, { + /// The metric labels pub metric: KV, + /// The timestamp and value (if present) #[serde(default)] pub value: Option<(f64, MetricVal)>, // TODO: Include histograms } +/// A metric timeseries from a range query #[derive(Clone, Debug, Deserialize)] #[serde(bound = "KV: DeserializeOwned")] pub struct MetricTimeseries> where KV: Clone + Debug, { + /// The metric labels pub metric: KV, + /// The timestamp/value pairs #[serde(default)] pub values: Vec<(f64, MetricVal)>, // TODO: Include histograms } +/// The data payload from a Prometheus query response #[derive(Clone, Debug, Deserialize)] #[serde(bound = "KV: DeserializeOwned")] #[serde(tag = "resultType", content = "result", rename_all = "camelCase")] @@ -89,7 +101,9 @@ pub enum PromData> where KV: Clone + Debug, { + /// Result from a range query (multiple values per series) Matrix(Vec>), + /// Result from an instant query (single value per series) Vector(Vec>), } @@ -97,6 +111,7 @@ impl PromData where KV: Clone + Debug, { + /// Convert to matrix result, returning error if it was a vector pub fn into_matrix(self) -> Result>, Error> { match self { Self::Matrix(data) => Ok(data), @@ -104,6 +119,7 @@ where } } + /// Convert to vector result, returning error if it was a matrix pub fn into_vector(self) -> Result>, Error> { match self { Self::Vector(data) => Ok(data), @@ -114,14 +130,14 @@ where #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] -pub enum Status { +pub(crate) enum Status { Success, Error, } #[derive(Clone, Debug, Deserialize)] #[serde(bound = "T: DeserializeOwned", rename_all = "camelCase")] -pub struct PromResponse +pub(crate) struct PromResponse where T: Clone + Debug, { @@ -155,32 +171,44 @@ where } } +/// Response from /api/v1/alerts endpoint #[derive(Clone, Debug, Deserialize)] #[serde(bound = "KV: DeserializeOwned")] pub struct AlertsResponse> where KV: Clone + Debug, { + /// List of alerts pub alerts: Vec>, } +/// Information about a single alert #[derive(Clone, Debug, Deserialize)] #[serde(bound = "KV: DeserializeOwned", rename_all = "camelCase")] pub struct AlertInfo> where KV: Clone + Debug, { + /// When the alert became active pub active_at: DateTime, + /// Alert annotations pub annotations: KV, + /// Alert labels pub labels: KV, + /// Current state of the alert pub state: AlertStatus, + /// The value that triggered the alert pub value: String, } +/// The state of an alert #[derive(Clone, Copy, Debug, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AlertStatus { + /// Alert condition met but for duration not yet satisfied Pending, + /// Alert is actively firing Firing, + /// Alert has been resolved Resolved, } diff --git a/prom-client/src/plot.rs b/prom-client/src/plot.rs index 91c3ad7..8e299d1 100644 --- a/prom-client/src/plot.rs +++ b/prom-client/src/plot.rs @@ -1,3 +1,5 @@ +//! Make a simple plot of time-series data from prometheus + use crate::{ExtractLabels, MetricTimeseries}; use chrono::{DateTime, FixedOffset, TimeZone, Utc}; use plotters::prelude::*; @@ -9,18 +11,31 @@ use std::{ path::Path, }; +/// Styling options for the plot pub struct PlotStyle { + /// The pixel size of the plot pub drawing_area: (u32, u32), + /// The background color pub background: RGBAColor, + /// The grid color pub grid: RGBAColor, + /// The axis color pub axis: RGBAColor, + /// The text color pub text_color: RGBAColor, + /// The text font pub text_font: String, + /// The text size pub text_size: u32, + /// The caption size pub caption_size: u32, + /// The colors to use for lines. If there are more lines than this, then colors will be repeated. pub data_colors: Vec, + /// The colors to use for a "threshold" such as used in a PromQL alerting rule pub threshold_color: RGBAColor, + /// Labels to skip rendering of pub skip_labels: Vec, + /// UTC offset to use when labelling the timestamps being plotted pub utc_offset_hours: i32, /// Optional title override - used when prometheus aggregations remove __name__ pub title: Option, @@ -52,25 +67,42 @@ impl Default for PlotStyle { .collect(), threshold_color: RED.mix(0.2), skip_labels: vec!["job".into(), "instance".into()], - utc_offset_hours: -7, + utc_offset_hours: 0, title: None, } } } impl PlotStyle { + /// Set the drawing_area + pub fn with_drawing_area(mut self, drawing_area: impl Into<(u32, u32)>) -> Self { + self.drawing_area = drawing_area.into(); + self + } + + /// Override the title of the plot pub fn with_title(mut self, title: impl Into) -> Self { self.title = Some(title.into()); self } + + /// Set the UTC offset (timezone) used in the plot, in hours + pub fn with_utc_offset(mut self, offset: i32) -> Self { + self.utc_offset_hours = offset; + self + } } +/// A shaded region appearing on the plot to indicate values that would trigger an alert pub enum PlotThreshold { + /// Shade values greater than this threshold GreaterThan(f64), + /// Shade values less than this threshold LessThan(f64), } impl PlotStyle { + /// Use a dark color scheme for the plot pub fn dark_mode(mut self) -> Self { self.background = BLACK.into(); self.grid = RGBAColor(100, 100, 100, 0.5); @@ -133,7 +165,7 @@ impl PlotStyle { }; // Add timezone offset to the caption - write!(&mut caption, " UTC{o}", o = self.utc_offset_hours)?; + write!(&mut caption, " UTC{o:+}", o = self.utc_offset_hours)?; // Actually start writing the file let root_area = BitMapBackend::new(&path, self.drawing_area).into_drawing_area(); diff --git a/prom-client/src/traits.rs b/prom-client/src/traits.rs index 99058b8..a56ba82 100644 --- a/prom-client/src/traits.rs +++ b/prom-client/src/traits.rs @@ -1,13 +1,19 @@ +//! Trait for Prometheus API requests + use crate::{Error, PromResponse}; use reqwest::{Client, Url}; use serde::{Serialize, de::DeserializeOwned}; use std::fmt::Debug; +/// Trait for types that can be sent as Prometheus API requests #[async_trait::async_trait] pub trait PromRequest: Serialize { + /// The API path for this request type (e.g., "/api/v1/query") const PATH: &str; + /// The output type returned by this request type Output: Clone + Debug + DeserializeOwned; + /// Send the request to the given prometheus host URL async fn send(&self, host: &str) -> Result, Error> where KV: Clone + Debug + DeserializeOwned, @@ -15,6 +21,7 @@ pub trait PromRequest: Serialize { self.send_with_client(&Client::new(), host).await } + /// Send the request using the provided reqwest client async fn send_with_client( &self, client: &Client,