Skip to main content

dd_api/
metrics.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::client::Client;
5use crate::error::Result;
6
7/// v2 "query timeseries data across multiple products". `from`/`to`/`interval`
8/// are epoch **milliseconds**. Replaces the legacy v1 `/api/v1/query`, which
9/// scoped application keys are not authorized for.
10pub const TIMESERIES_PATH: &str = "api/v2/query/timeseries";
11
12// ---- Request --------------------------------------------------------------
13
14#[derive(Debug, Clone, Serialize)]
15pub struct TimeseriesRequest {
16    pub data: RequestData,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct RequestData {
21    pub r#type: String,
22    pub attributes: RequestAttributes,
23}
24
25#[derive(Debug, Clone, Serialize)]
26pub struct RequestAttributes {
27    pub from: i64,
28    pub to: i64,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub interval: Option<i64>,
31    pub queries: Vec<MetricQuery>,
32    pub formulas: Vec<Formula>,
33}
34
35#[derive(Debug, Clone, Serialize)]
36pub struct MetricQuery {
37    pub data_source: String,
38    pub query: String,
39    pub name: String,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct Formula {
44    pub formula: String,
45}
46
47// ---- Response -------------------------------------------------------------
48
49#[derive(Debug, Clone, Default, Deserialize, Serialize)]
50pub struct TimeseriesResponse {
51    #[serde(default)]
52    pub data: Option<ResponseData>,
53}
54
55#[derive(Debug, Clone, Default, Deserialize, Serialize)]
56pub struct ResponseData {
57    #[serde(default)]
58    pub attributes: ResponseAttributes,
59}
60
61#[derive(Debug, Clone, Default, Deserialize, Serialize)]
62pub struct ResponseAttributes {
63    /// One entry per returned series, in the same order as `values`.
64    #[serde(default)]
65    pub series: Vec<SeriesMeta>,
66    /// Shared bucket timestamps (epoch milliseconds) for every series.
67    #[serde(default)]
68    pub times: Vec<i64>,
69    /// `values[i][j]` is series `i` at `times[j]`; `null` where there's no data.
70    #[serde(default)]
71    pub values: Vec<Vec<Option<f64>>>,
72}
73
74#[derive(Debug, Clone, Default, Deserialize, Serialize)]
75pub struct SeriesMeta {
76    /// Tag values for the grouping, e.g. `["feed:positions"]`.
77    #[serde(default)]
78    pub group_tags: Vec<String>,
79    #[serde(default)]
80    pub query_index: Option<i64>,
81    #[serde(default)]
82    pub unit: Value,
83}
84
85impl Client {
86    /// Query timeseries data for a single metric query string.
87    ///
88    /// `from_ms`/`to_ms`/`interval_ms` are epoch / duration **milliseconds**.
89    /// `interval_ms` is optional; when `None`, Datadog picks a rollup (or the
90    /// query's own `.rollup(...)` applies).
91    pub async fn metrics_timeseries(
92        &self,
93        from_ms: i64,
94        to_ms: i64,
95        interval_ms: Option<i64>,
96        query: &str,
97    ) -> Result<TimeseriesResponse> {
98        let req = TimeseriesRequest {
99            data: RequestData {
100                r#type: "timeseries_request".into(),
101                attributes: RequestAttributes {
102                    from: from_ms,
103                    to: to_ms,
104                    interval: interval_ms,
105                    queries: vec![MetricQuery {
106                        data_source: "metrics".into(),
107                        query: query.into(),
108                        name: "q1".into(),
109                    }],
110                    formulas: vec![Formula {
111                        formula: "q1".into(),
112                    }],
113                },
114            },
115        };
116        self.post_json(TIMESERIES_PATH, &req).await
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn request_serializes_to_v2_shape() {
126        let req = TimeseriesRequest {
127            data: RequestData {
128                r#type: "timeseries_request".into(),
129                attributes: RequestAttributes {
130                    from: 1_747_000_000_000,
131                    to: 1_747_864_000_000,
132                    interval: Some(86_400_000),
133                    queries: vec![MetricQuery {
134                        data_source: "metrics".into(),
135                        query: "sum:bridgeft.import.records{*} by {feed}".into(),
136                        name: "q1".into(),
137                    }],
138                    formulas: vec![Formula {
139                        formula: "q1".into(),
140                    }],
141                },
142            },
143        };
144        let v: Value = serde_json::to_value(&req).unwrap();
145        assert_eq!(v["data"]["type"], "timeseries_request");
146        assert_eq!(v["data"]["attributes"]["from"], 1_747_000_000_000i64);
147        assert_eq!(v["data"]["attributes"]["queries"][0]["data_source"], "metrics");
148        assert_eq!(v["data"]["attributes"]["formulas"][0]["formula"], "q1");
149    }
150
151    #[test]
152    fn parses_timeseries_response() {
153        let raw = r#"{
154            "data": {
155                "type": "timeseries_response",
156                "attributes": {
157                    "series": [
158                        {"group_tags": ["feed:positions"], "query_index": 0},
159                        {"group_tags": ["feed:account_balances"], "query_index": 0}
160                    ],
161                    "times": [1747000000000, 1747086400000, 1747172800000],
162                    "values": [
163                        [2901, 2950, null],
164                        [1483, 1490, 0]
165                    ]
166                }
167            }
168        }"#;
169        let resp: TimeseriesResponse = serde_json::from_str(raw).unwrap();
170        let attrs = resp.data.unwrap().attributes;
171        assert_eq!(attrs.series.len(), 2);
172        assert_eq!(attrs.series[0].group_tags, vec!["feed:positions"]);
173        assert_eq!(attrs.times.len(), 3);
174        assert_eq!(attrs.values[0][0], Some(2901.0));
175        assert_eq!(attrs.values[0][2], None); // null preserved (≠ zero)
176        assert_eq!(attrs.values[1][2], Some(0.0)); // real zero stays zero
177    }
178}