prometheus_http_client/
messages.rs

1//! Message types for Prometheus API responses
2//!
3//! The prometheus http interface (at port 9090) renders graphs in JS and gets the raw data using API requests like this:
4//!
5//! GET http://localhost:9090/api/v1/query_range?query=tick_time{quantile="0.99"}&step=14&start=1762534433.802&end=1762538033.802
6//!
7//! Response is:
8//!
9//! {
10//!   status: "success"
11//!   data: {
12//!     resultType: "matrix",
13//!     result: [
14//!       {
15//!         metric: { __name__: "tick_time", instance: "x.y.z.w", job: "ec2", .. },
16//!         values: [
17//!            [ 1762534433.802, "1.8974293514080933" ],
18//!            [ 1762534447.802, "2.029724353457351" ],
19//!            ..
20//!         ]
21//!       }
22//!     ]
23//!   }
24//! }
25//!
26//! For more detail see:
27//! https://prometheus.io/docs/prometheus/latest/querying/api/
28
29use crate::Error;
30use chrono::{DateTime, Utc};
31use serde::{Deserialize, Deserializer, de::DeserializeOwned};
32use std::{
33    collections::HashMap,
34    fmt::{self, Debug, Display},
35};
36use tracing::warn;
37
38/// Wrapper for f64 that deserializes from a string (prometheus returns numeric values as strings)
39#[derive(Clone, Copy, Debug)]
40pub struct MetricVal(pub f64);
41
42impl Display for MetricVal {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        Display::fmt(&self.0, f)
45    }
46}
47
48impl AsRef<f64> for MetricVal {
49    fn as_ref(&self) -> &f64 {
50        &self.0
51    }
52}
53
54impl<'de> Deserialize<'de> for MetricVal {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: Deserializer<'de>,
58    {
59        let s = <&str>::deserialize(deserializer)?;
60        s.parse::<f64>()
61            .map(MetricVal)
62            .map_err(serde::de::Error::custom)
63    }
64}
65
66/// A single metric value from an instant query
67#[derive(Clone, Debug, Deserialize)]
68#[serde(bound = "KV: DeserializeOwned")]
69pub struct MetricValue<KV = HashMap<String, String>>
70where
71    KV: Clone + Debug,
72{
73    /// The metric labels
74    pub metric: KV,
75    /// The timestamp and value (if present)
76    #[serde(default)]
77    pub value: Option<(f64, MetricVal)>,
78    // TODO: Include histograms
79}
80
81/// A metric timeseries from a range query
82#[derive(Clone, Debug, Deserialize)]
83#[serde(bound = "KV: DeserializeOwned")]
84pub struct MetricTimeseries<KV = HashMap<String, String>>
85where
86    KV: Clone + Debug,
87{
88    /// The metric labels
89    pub metric: KV,
90    /// The timestamp/value pairs
91    #[serde(default)]
92    pub values: Vec<(f64, MetricVal)>,
93    // TODO: Include histograms
94}
95
96/// The data payload from a Prometheus query response
97#[derive(Clone, Debug, Deserialize)]
98#[serde(bound = "KV: DeserializeOwned")]
99#[serde(tag = "resultType", content = "result", rename_all = "camelCase")]
100pub enum PromData<KV = HashMap<String, String>>
101where
102    KV: Clone + Debug,
103{
104    /// Result from a range query (multiple values per series)
105    Matrix(Vec<MetricTimeseries<KV>>),
106    /// Result from an instant query (single value per series)
107    Vector(Vec<MetricValue<KV>>),
108}
109
110impl<KV> PromData<KV>
111where
112    KV: Clone + Debug,
113{
114    /// Convert to matrix result, returning error if it was a vector
115    pub fn into_matrix(self) -> Result<Vec<MetricTimeseries<KV>>, Error> {
116        match self {
117            Self::Matrix(data) => Ok(data),
118            _ => Err(Error::UnexpectedResultType(format!("{self:?}"))),
119        }
120    }
121
122    /// Convert to vector result, returning error if it was a matrix
123    pub fn into_vector(self) -> Result<Vec<MetricValue<KV>>, Error> {
124        match self {
125            Self::Vector(data) => Ok(data),
126            _ => Err(Error::UnexpectedResultType(format!("{self:?}"))),
127        }
128    }
129}
130
131#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
132#[serde(rename_all = "camelCase")]
133pub(crate) enum Status {
134    Success,
135    Error,
136}
137
138#[derive(Clone, Debug, Deserialize)]
139#[serde(bound = "T: DeserializeOwned", rename_all = "camelCase")]
140pub(crate) struct PromResponse<T>
141where
142    T: Clone + Debug,
143{
144    pub status: Status,
145    #[serde(default)]
146    pub data: Option<T>,
147    #[serde(default)]
148    pub error_type: Option<String>,
149    #[serde(default)]
150    pub error: Option<String>,
151    #[serde(default)]
152    pub warnings: Vec<String>,
153}
154
155impl<T> PromResponse<T>
156where
157    T: Clone + Debug,
158{
159    pub fn into_result(self) -> Result<T, Error> {
160        for warning in self.warnings {
161            warn!("Prometheus API response: {warning}");
162        }
163
164        match self.status {
165            Status::Success => Ok(self.data.ok_or(Error::MissingData)?),
166            Status::Error => Err(Error::API(
167                self.error_type.unwrap_or_default(),
168                self.error.unwrap_or_default(),
169            )),
170        }
171    }
172}
173
174/// Response from /api/v1/alerts endpoint
175#[derive(Clone, Debug, Deserialize)]
176#[serde(bound = "KV: DeserializeOwned")]
177pub struct AlertsResponse<KV = HashMap<String, String>>
178where
179    KV: Clone + Debug,
180{
181    /// List of alerts
182    pub alerts: Vec<AlertInfo<KV>>,
183}
184
185/// Information about a single alert
186#[derive(Clone, Debug, Deserialize)]
187#[serde(bound = "KV: DeserializeOwned", rename_all = "camelCase")]
188pub struct AlertInfo<KV = HashMap<String, String>>
189where
190    KV: Clone + Debug,
191{
192    /// When the alert became active
193    pub active_at: DateTime<Utc>,
194    /// Alert annotations
195    pub annotations: KV,
196    /// Alert labels
197    pub labels: KV,
198    /// Current state of the alert
199    pub state: AlertStatus,
200    /// The value that triggered the alert
201    pub value: String,
202}
203
204/// The state of an alert
205#[derive(Clone, Copy, Debug, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum AlertStatus {
208    /// Alert condition met but for duration not yet satisfied
209    Pending,
210    /// Alert is actively firing
211    Firing,
212    /// Alert has been resolved
213    Resolved,
214}