use chrono::{DateTime, Utc};
use http::Method;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use typed_builder::TypedBuilder;
use crate::client::*;
use crate::error::Result;
use crate::host::HostId;
use crate::service::ServiceName;
#[derive(PartialEq, Clone, Debug, TypedBuilder, Serialize, Deserialize)]
#[builder(field_defaults(setter(into)))]
#[serde(rename_all = "camelCase")]
pub struct HostMetricValue {
pub host_id: HostId,
pub name: String,
#[serde(flatten)]
pub value: MetricValue,
}
#[derive(PartialEq, Clone, Debug, TypedBuilder, Serialize, Deserialize)]
#[builder(field_defaults(setter(into)))]
pub struct ServiceMetricValue {
pub name: String,
#[serde(flatten)]
pub value: MetricValue,
}
#[derive(PartialEq, Clone, Debug, TypedBuilder, Serialize, Deserialize)]
#[builder(field_defaults(setter(into)))]
pub struct MetricValue {
#[serde(with = "chrono::serde::ts_seconds")]
pub time: DateTime<Utc>,
pub value: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json::json;
fn host_metric_example1() -> HostMetricValue {
HostMetricValue::builder()
.host_id("host1")
.name("loadavg.loadavg1")
.value(
MetricValue::builder()
.time(DateTime::from_timestamp(1700000000, 0).unwrap())
.value(1.2)
.build(),
)
.build()
}
fn host_metric_json_example1() -> serde_json::Value {
json!({
"hostId": "host1",
"name": "loadavg.loadavg1",
"time": 1700000000,
"value": 1.2,
})
}
fn service_metric_example1() -> ServiceMetricValue {
ServiceMetricValue::builder()
.name("custom.metric.name")
.value(
MetricValue::builder()
.time(DateTime::from_timestamp(1700000000, 0).unwrap())
.value(1.3)
.build(),
)
.build()
}
fn service_metric_json_example1() -> serde_json::Value {
json!({
"name": "custom.metric.name",
"time": 1700000000,
"value": 1.3,
})
}
#[rstest]
#[case(host_metric_example1(), host_metric_json_example1())]
#[case(service_metric_example1(), service_metric_json_example1())]
fn test_metric_value<
MetricValue: PartialEq + std::fmt::Debug + serde::ser::Serialize + serde::de::DeserializeOwned,
>(
#[case] metric_value: MetricValue,
#[case] json: serde_json::Value,
) {
assert_eq!(serde_json::to_value(&metric_value).unwrap(), json);
assert_eq!(metric_value, serde_json::from_value(json).unwrap());
}
}
impl Client {
pub async fn post_host_metric_values(
&self,
host_metric_values: impl IntoIterator<Item = HostMetricValue>,
) -> Result<()> {
self.request(
Method::POST,
"/api/v0/tsdb",
query_params![],
request_body!(host_metric_values.into_iter().collect::<Vec<_>>()),
response_body!(),
)
.await
}
pub async fn list_host_metric_values(
&self,
host_id: impl Into<HostId>,
metric_name: impl AsRef<str>,
from: impl Into<DateTime<Utc>>,
to: impl Into<DateTime<Utc>>,
) -> Result<Vec<MetricValue>> {
self.request(
Method::GET,
format_url!("/api/v0/hosts/{}/metrics", host_id),
query_params! {
name = metric_name.as_ref().to_owned(),
from = from.into().timestamp().to_string(),
to = to.into().timestamp().to_string(),
},
request_body![],
response_body! { metrics: Vec<MetricValue> },
)
.await
}
pub async fn list_latest_host_metric_values(
&self,
host_ids: impl IntoIterator<Item = impl Into<HostId>>,
metric_names: impl IntoIterator<Item = impl AsRef<str>>,
) -> Result<HashMap<HostId, HashMap<String, Option<MetricValue>>>> {
self.request(
Method::GET,
"/api/v0/tsdb/latest",
&host_ids
.into_iter()
.map(|host_id| ("hostId", host_id.into().to_string()))
.chain(
metric_names
.into_iter()
.map(|metric_name| ("name", metric_name.as_ref().to_owned())),
)
.collect::<Vec<_>>(),
request_body![],
response_body! { tsdbLatest: HashMap<HostId, HashMap<String, Option<MetricValue>>> },
)
.await
}
pub async fn post_service_metric_values(
&self,
service_name: impl Into<ServiceName>,
service_metric_values: impl IntoIterator<Item = ServiceMetricValue>,
) -> Result<()> {
self.request(
Method::POST,
format_url!("/api/v0/services/{}/tsdb", service_name),
query_params![],
request_body!(service_metric_values.into_iter().collect::<Vec<_>>()),
response_body!(),
)
.await
}
pub async fn list_service_metric_values(
&self,
service_name: impl Into<ServiceName>,
metric_name: impl AsRef<str>,
from: impl Into<DateTime<Utc>>,
to: impl Into<DateTime<Utc>>,
) -> Result<Vec<MetricValue>> {
self.request(
Method::GET,
format_url!("/api/v0/services/{}/metrics", service_name),
query_params! {
name = metric_name.as_ref().to_owned(),
from = from.into().timestamp().to_string(),
to = to.into().timestamp().to_string(),
},
request_body![],
response_body! { metrics: Vec<MetricValue> },
)
.await
}
}
#[cfg(test)]
mod client_tests {
use crate::metric::*;
use crate::tests::*;
#[async_std::test]
async fn post_host_metric_values() {
let server = test_server! {
method = POST,
path = "/api/v0/tsdb",
request = json!([
{
"hostId": "host0",
"name": "loadavg1",
"time": 1698894000,
"value": 1.0,
},
{
"hostId": "host0",
"name": "loadavg5",
"time": 1698894000,
"value": 1.1,
},
{
"hostId": "host0",
"name": "loadavg15",
"time": 1698894000,
"value": 1.2,
},
]),
response = json!({ "success": true }),
};
assert_eq!(
test_client!(server)
.post_host_metric_values([
HostMetricValue {
host_id: "host0".into(),
name: "loadavg1".to_owned(),
value: MetricValue {
time: DateTime::from_timestamp(1698894000, 0).unwrap(),
value: 1.0,
},
},
HostMetricValue {
host_id: "host0".into(),
name: "loadavg5".to_owned(),
value: MetricValue {
time: DateTime::from_timestamp(1698894000, 0).unwrap(),
value: 1.1,
},
},
HostMetricValue {
host_id: "host0".into(),
name: "loadavg15".to_owned(),
value: MetricValue {
time: DateTime::from_timestamp(1698894000, 0).unwrap(),
value: 1.2,
},
},
])
.await,
Ok(()),
);
}
#[async_std::test]
async fn list_host_metric_values() {
let server = test_server! {
method = GET,
path = "/api/v0/hosts/host0/metrics",
query_params = "name=loadavg5&from=1699999860&to=1700000000",
response = json!({
"metrics": [
{ "time": 1699999860, "value": 1.0 },
{ "time": 1699999920, "value": 1.1 },
{ "time": 1699999980, "value": 1.2 },
],
}),
};
assert_eq!(
test_client!(server)
.list_host_metric_values(
"host0",
"loadavg5",
DateTime::from_timestamp(1699999860, 0).unwrap(),
DateTime::from_timestamp(1700000000, 0).unwrap(),
)
.await,
Ok(vec![
MetricValue {
time: DateTime::from_timestamp(1699999860, 0).unwrap(),
value: 1.0,
},
MetricValue {
time: DateTime::from_timestamp(1699999920, 0).unwrap(),
value: 1.1,
},
MetricValue {
time: DateTime::from_timestamp(1699999980, 0).unwrap(),
value: 1.2,
},
]),
);
}
#[async_std::test]
async fn list_latest_host_metric_values() {
let server = test_server! {
method = GET,
path = "/api/v0/tsdb/latest",
query_params = "hostId=host0&hostId=host1&name=custom.host.metric0&name=custom.host.metric1",
response = json!({
"tsdbLatest": {
"host0": {
"custom.host.metric0": { "time": 1700000040, "value": 1.0 },
"custom.host.metric1": { "time": 1700000040, "value": 1.1 },
},
"host1": {
"custom.host.metric0": null,
"custom.host.metric1": null,
},
},
}),
};
assert_eq!(
test_client!(server)
.list_latest_host_metric_values(
["host0", "host1"],
["custom.host.metric0", "custom.host.metric1"],
)
.await,
Ok(HashMap::from([
(
HostId::from("host0"),
HashMap::from([
(
"custom.host.metric0".to_owned(),
Some(MetricValue {
time: DateTime::from_timestamp(1700000040, 0).unwrap(),
value: 1.0,
}),
),
(
"custom.host.metric1".to_owned(),
Some(MetricValue {
time: DateTime::from_timestamp(1700000040, 0).unwrap(),
value: 1.1,
})
),
])
),
(
HostId::from("host1"),
HashMap::from([
("custom.host.metric0".to_owned(), None),
("custom.host.metric1".to_owned(), None),
])
),
])),
);
}
#[async_std::test]
async fn post_service_metric_values() {
let server = test_server! {
method = POST,
path = "/api/v0/services/service0/tsdb",
request = json!([
{ "name": "custom.metric0", "time": 1698894000, "value": 1.0 },
{ "name": "custom.metric1", "time": 1698894000, "value": 1.1 },
{ "name": "custom.metric2", "time": 1698894000, "value": 1.2 },
]),
response = json!({ "success": true }),
};
assert_eq!(
test_client!(server)
.post_service_metric_values(
"service0",
[
ServiceMetricValue {
name: "custom.metric0".to_owned(),
value: MetricValue {
time: DateTime::from_timestamp(1698894000, 0).unwrap(),
value: 1.0,
},
},
ServiceMetricValue {
name: "custom.metric1".to_owned(),
value: MetricValue {
time: DateTime::from_timestamp(1698894000, 0).unwrap(),
value: 1.1,
},
},
ServiceMetricValue {
name: "custom.metric2".to_owned(),
value: MetricValue {
time: DateTime::from_timestamp(1698894000, 0).unwrap(),
value: 1.2,
},
}
]
)
.await,
Ok(()),
);
}
#[async_std::test]
async fn list_service_metric_values() {
let server = test_server! {
method = GET,
path = "/api/v0/services/service0/metrics",
query_params = "name=custom.metric&from=1699999860&to=1700000000",
response = json!({
"metrics": [
{ "time": 1699999860, "value": 1.0 },
{ "time": 1699999920, "value": 1.1 },
{ "time": 1699999980, "value": 1.2 },
],
}),
};
assert_eq!(
test_client!(server)
.list_service_metric_values(
"service0",
"custom.metric",
DateTime::from_timestamp(1699999860, 0).unwrap(),
DateTime::from_timestamp(1700000000, 0).unwrap(),
)
.await,
Ok(vec![
MetricValue {
time: DateTime::from_timestamp(1699999860, 0).unwrap(),
value: 1.0,
},
MetricValue {
time: DateTime::from_timestamp(1699999920, 0).unwrap(),
value: 1.1,
},
MetricValue {
time: DateTime::from_timestamp(1699999980, 0).unwrap(),
value: 1.2,
},
]),
);
}
}