Files
signal-gateway/prometheus-http-client/src/messages.rs
T

215 lines
5.6 KiB
Rust

//! 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 tracing::warn;
/// Wrapper for f64 that deserializes from a string (prometheus returns numeric values as strings)
#[derive(Clone, Copy, Debug)]
pub struct MetricVal(pub f64);
impl Display for MetricVal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl AsRef<f64> for MetricVal {
fn as_ref(&self) -> &f64 {
&self.0
}
}
impl<'de> Deserialize<'de> for MetricVal {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
s.parse::<f64>()
.map(MetricVal)
.map_err(serde::de::Error::custom)
}
}
/// A single metric value from an instant query
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "KV: DeserializeOwned")]
pub struct MetricValue<KV = HashMap<String, String>>
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<KV = HashMap<String, String>>
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")]
pub enum PromData<KV = HashMap<String, String>>
where
KV: Clone + Debug,
{
/// Result from a range query (multiple values per series)
Matrix(Vec<MetricTimeseries<KV>>),
/// Result from an instant query (single value per series)
Vector(Vec<MetricValue<KV>>),
}
impl<KV> PromData<KV>
where
KV: Clone + Debug,
{
/// Convert to matrix result, returning error if it was a vector
pub fn into_matrix(self) -> Result<Vec<MetricTimeseries<KV>>, Error> {
match self {
Self::Matrix(data) => Ok(data),
_ => Err(Error::UnexpectedResultType(format!("{self:?}"))),
}
}
/// Convert to vector result, returning error if it was a matrix
pub fn into_vector(self) -> Result<Vec<MetricValue<KV>>, Error> {
match self {
Self::Vector(data) => Ok(data),
_ => Err(Error::UnexpectedResultType(format!("{self:?}"))),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) enum Status {
Success,
Error,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "T: DeserializeOwned", rename_all = "camelCase")]
pub(crate) struct PromResponse<T>
where
T: Clone + Debug,
{
pub status: Status,
#[serde(default)]
pub data: Option<T>,
#[serde(default)]
pub error_type: Option<String>,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub warnings: Vec<String>,
}
impl<T> PromResponse<T>
where
T: Clone + Debug,
{
pub fn into_result(self) -> Result<T, Error> {
for warning in self.warnings {
warn!("Prometheus API response: {warning}");
}
match self.status {
Status::Success => Ok(self.data.ok_or(Error::MissingData)?),
Status::Error => Err(Error::API(
self.error_type.unwrap_or_default(),
self.error.unwrap_or_default(),
)),
}
}
}
/// Response from /api/v1/alerts endpoint
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "KV: DeserializeOwned")]
pub struct AlertsResponse<KV = HashMap<String, String>>
where
KV: Clone + Debug,
{
/// List of alerts
pub alerts: Vec<AlertInfo<KV>>,
}
/// Information about a single alert
#[derive(Clone, Debug, Deserialize)]
#[serde(bound = "KV: DeserializeOwned", rename_all = "camelCase")]
pub struct AlertInfo<KV = HashMap<String, String>>
where
KV: Clone + Debug,
{
/// When the alert became active
pub active_at: DateTime<Utc>,
/// 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,
}