deny missing docs in prometheus-client

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