Skip to main content

azure_lite_rs/api/
monitor.rs

1//! Azure Monitor API client.
2//!
3//! Wraps the ARM management plane operations for Azure Monitor: metric
4//! definitions, metric values, alert rules, and activity logs.
5//!
6//! Different Azure Monitor operation groups require different API versions:
7//! - Metrics / MetricDefinitions: `2023-10-01` (via generated ops)
8//! - MetricAlerts: `2018-03-01` (direct HTTP calls — codegen version is wrong)
9//! - Activity Logs: `2015-04-01` (direct HTTP calls — codegen version is wrong)
10//!
11//! `subscription_id` is auto-injected from the parent `AzureHttpClient`.
12
13use crate::{
14    AzureError, AzureHttpClient, Result,
15    ops::monitor::MonitorOps,
16    types::monitor::{
17        EventDataCollection, MetricAlertCreateRequest, MetricAlertResource,
18        MetricAlertResourceCollection, MetricDefinitionCollection, MetricsResponse,
19    },
20};
21
22const ALERTS_API_VERSION: &str = "2018-03-01";
23const ACTIVITY_LOGS_API_VERSION: &str = "2015-04-01";
24
25/// Client for the Azure Monitor ARM management plane.
26///
27/// All methods use direct HTTP calls with operation-appropriate API versions:
28/// - Metrics / MetricDefinitions: `2023-10-01`
29/// - MetricAlerts: `2018-03-01`
30/// - Activity Logs: `2015-04-01`
31pub struct MonitorClient<'a> {
32    client: &'a AzureHttpClient,
33    // Kept to silence dead_code on the generated MonitorOps::new().
34    // Direct HTTP calls are used instead of the ops methods because
35    // different Monitor operations require different api-versions.
36    _ops: MonitorOps<'a>,
37}
38
39impl<'a> MonitorClient<'a> {
40    /// Create a new Azure Monitor API client.
41    pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
42        Self {
43            client,
44            _ops: MonitorOps::new(client),
45        }
46    }
47
48    fn base_url(&self) -> &str {
49        "https://management.azure.com"
50    }
51
52    fn parse_json<T: serde::de::DeserializeOwned>(&self, bytes: &[u8], op: &str) -> Result<T> {
53        serde_json::from_slice(bytes).map_err(|e| AzureError::InvalidResponse {
54            message: format!("Failed to parse {op} response: {e}"),
55            body: Some(String::from_utf8_lossy(bytes).to_string()),
56        })
57    }
58
59    // --- Metric operations (api-version 2023-10-01 via generated ops) ---
60
61    /// Lists the metric definitions for a resource.
62    ///
63    /// `resource_uri` is the ARM resource path without the leading `/`,
64    /// e.g. `subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{vm}`.
65    ///
66    /// `metric_namespace` is required by the Azure API, e.g. `Microsoft.Compute/virtualMachines`.
67    pub async fn list_metric_definitions(
68        &self,
69        resource_uri: &str,
70        metric_namespace: &str,
71    ) -> Result<MetricDefinitionCollection> {
72        // Append metricnamespace — the generated op URL is missing it
73        let sub_url = format!(
74            "/{}/providers/microsoft.insights/metricDefinitions?api-version=2023-10-01&metricnamespace={}",
75            resource_uri,
76            urlencoding::encode(metric_namespace),
77        );
78        let url = format!("{}{}", self.base_url(), sub_url);
79        let resp = self.client.get(&url).await?;
80        let resp = resp.error_for_status().await?;
81        let bytes = resp.bytes().await?;
82        self.parse_json(&bytes, "list_metric_definitions")
83    }
84
85    /// Lists the metric values for a resource.
86    ///
87    /// `resource_uri` is the ARM resource path without the leading `/`.
88    /// `metric_names` is a comma-separated list of metric names.
89    /// `timespan` is ISO 8601 duration, e.g. `PT1H` for last hour.
90    pub async fn get_metrics(
91        &self,
92        resource_uri: &str,
93        metric_names: &str,
94        timespan: &str,
95    ) -> Result<MetricsResponse> {
96        let url = format!(
97            "{}/{}/providers/microsoft.insights/metrics?api-version=2023-10-01&metricnames={}&timespan={}",
98            self.base_url(),
99            resource_uri,
100            urlencoding::encode(metric_names),
101            urlencoding::encode(timespan),
102        );
103        let resp = self.client.get(&url).await?;
104        let resp = resp.error_for_status().await?;
105        let bytes = resp.bytes().await?;
106        self.parse_json(&bytes, "get_metrics")
107    }
108
109    // --- Alert rule operations (api-version 2018-03-01 — direct HTTP) ---
110
111    /// Retrieve alert rule definitions in a resource group.
112    pub async fn list_alert_rules(
113        &self,
114        resource_group_name: &str,
115    ) -> Result<MetricAlertResourceCollection> {
116        let url = format!(
117            "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Insights/metricAlerts?api-version={}",
118            self.base_url(),
119            urlencoding::encode(self.client.subscription_id()),
120            urlencoding::encode(resource_group_name),
121            ALERTS_API_VERSION,
122        );
123        let resp = self.client.get(&url).await?;
124        let resp = resp.error_for_status().await?;
125        let bytes = resp.bytes().await?;
126        self.parse_json(&bytes, "list_alert_rules")
127    }
128
129    /// Retrieve an alert rule definition.
130    pub async fn get_alert_rule(
131        &self,
132        resource_group_name: &str,
133        rule_name: &str,
134    ) -> Result<MetricAlertResource> {
135        let url = format!(
136            "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Insights/metricAlerts/{}?api-version={}",
137            self.base_url(),
138            urlencoding::encode(self.client.subscription_id()),
139            urlencoding::encode(resource_group_name),
140            urlencoding::encode(rule_name),
141            ALERTS_API_VERSION,
142        );
143        let resp = self.client.get(&url).await?;
144        let resp = resp.error_for_status().await?;
145        let bytes = resp.bytes().await?;
146        self.parse_json(&bytes, "get_alert_rule")
147    }
148
149    /// Create or update an alert rule definition.
150    pub async fn create_alert_rule(
151        &self,
152        resource_group_name: &str,
153        rule_name: &str,
154        body: &MetricAlertCreateRequest,
155    ) -> Result<MetricAlertResource> {
156        let url = format!(
157            "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Insights/metricAlerts/{}?api-version={}",
158            self.base_url(),
159            urlencoding::encode(self.client.subscription_id()),
160            urlencoding::encode(resource_group_name),
161            urlencoding::encode(rule_name),
162            ALERTS_API_VERSION,
163        );
164        let body_bytes = serde_json::to_vec(body).map_err(|e| AzureError::InvalidResponse {
165            message: format!("Failed to serialize create_alert_rule body: {e}"),
166            body: None,
167        })?;
168        let resp = self.client.put(&url, &body_bytes).await?;
169        let resp = resp.error_for_status().await?;
170        let bytes = resp.bytes().await?;
171        self.parse_json(&bytes, "create_alert_rule")
172    }
173
174    /// Delete an alert rule definition.
175    pub async fn delete_alert_rule(
176        &self,
177        resource_group_name: &str,
178        rule_name: &str,
179    ) -> Result<()> {
180        let url = format!(
181            "{}/subscriptions/{}/resourceGroups/{}/providers/Microsoft.Insights/metricAlerts/{}?api-version={}",
182            self.base_url(),
183            urlencoding::encode(self.client.subscription_id()),
184            urlencoding::encode(resource_group_name),
185            urlencoding::encode(rule_name),
186            ALERTS_API_VERSION,
187        );
188        self.client.delete(&url).await?;
189        Ok(())
190    }
191
192    // --- Activity log operations (api-version 2015-04-01 — direct HTTP) ---
193
194    /// Provides the list of records from the activity logs.
195    ///
196    /// `filter` is an OData filter string, e.g. `"eventTimestamp ge '2024-01-01T00:00:00Z'"`.
197    pub async fn list_activity_logs(&self, filter: &str) -> Result<EventDataCollection> {
198        let url = format!(
199            "{}/subscriptions/{}/providers/microsoft.insights/eventtypes/management/values?api-version={}&$filter={}",
200            self.base_url(),
201            urlencoding::encode(self.client.subscription_id()),
202            ACTIVITY_LOGS_API_VERSION,
203            urlencoding::encode(filter),
204        );
205        let resp = self.client.get(&url).await?;
206        let resp = resp.error_for_status().await?;
207        let bytes = resp.bytes().await?;
208        self.parse_json(&bytes, "list_activity_logs")
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use crate::MockClient;
216
217    const SUB_ID: &str = "test-subscription-id";
218    const RG: &str = "test-rg";
219    const RULE_NAME: &str = "cloud-lite-test-alert-rule";
220    const RESOURCE_URI: &str = "subscriptions/test-subscription-id/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm";
221
222    fn make_client(mock: MockClient) -> AzureHttpClient {
223        AzureHttpClient::from_mock(mock)
224    }
225
226    fn alert_rule_json() -> serde_json::Value {
227        serde_json::json!({
228            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Insights/metricAlerts/{RULE_NAME}"),
229            "name": RULE_NAME,
230            "type": "Microsoft.Insights/metricAlerts",
231            "location": "global",
232            "properties": {
233                "description": "Test alert rule",
234                "severity": 3,
235                "enabled": true,
236                "evaluationFrequency": "PT1M",
237                "windowSize": "PT5M",
238                "provisioningState": "Succeeded"
239            }
240        })
241    }
242
243    #[tokio::test]
244    async fn list_alert_rules_returns_list() {
245        let mut mock = MockClient::new();
246        mock.expect_get(&format!(
247            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Insights/metricAlerts"
248        ))
249        .returning_json(serde_json::json!({ "value": [alert_rule_json()] }));
250        let client = make_client(mock);
251        let result = client
252            .monitor()
253            .list_alert_rules(RG)
254            .await
255            .expect("list_alert_rules failed");
256        assert_eq!(result.value.len(), 1);
257        assert_eq!(result.value[0].name.as_deref(), Some(RULE_NAME));
258    }
259
260    #[tokio::test]
261    async fn get_alert_rule_deserializes_properties() {
262        let mut mock = MockClient::new();
263        mock.expect_get(
264            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Insights/metricAlerts/{RULE_NAME}"),
265        )
266        .returning_json(alert_rule_json());
267        let client = make_client(mock);
268        let rule = client
269            .monitor()
270            .get_alert_rule(RG, RULE_NAME)
271            .await
272            .expect("get_alert_rule failed");
273        assert_eq!(rule.name.as_deref(), Some(RULE_NAME));
274        let props = rule.properties.as_ref().unwrap();
275        assert_eq!(props.severity, Some(3));
276        assert_eq!(props.enabled, Some(true));
277        assert_eq!(props.provisioning_state.as_deref(), Some("Succeeded"));
278    }
279
280    #[tokio::test]
281    async fn create_alert_rule_sends_body() {
282        let mut mock = MockClient::new();
283        mock.expect_put(
284            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Insights/metricAlerts/{RULE_NAME}"),
285        )
286        .returning_json(alert_rule_json());
287        let client = make_client(mock);
288        let body = MetricAlertCreateRequest {
289            location: "global".into(),
290            ..Default::default()
291        };
292        let rule = client
293            .monitor()
294            .create_alert_rule(RG, RULE_NAME, &body)
295            .await
296            .expect("create_alert_rule failed");
297        assert_eq!(rule.name.as_deref(), Some(RULE_NAME));
298    }
299
300    #[tokio::test]
301    async fn delete_alert_rule_succeeds() {
302        let mut mock = MockClient::new();
303        mock.expect_delete(
304            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Insights/metricAlerts/{RULE_NAME}"),
305        )
306        .returning_json(serde_json::json!({}));
307        let client = make_client(mock);
308        client
309            .monitor()
310            .delete_alert_rule(RG, RULE_NAME)
311            .await
312            .expect("delete_alert_rule failed");
313    }
314
315    #[tokio::test]
316    async fn list_activity_logs_returns_events() {
317        let mut mock = MockClient::new();
318        mock.expect_get(
319            &format!("/subscriptions/{SUB_ID}/providers/microsoft.insights/eventtypes/management/values"),
320        )
321        .returning_json(serde_json::json!({
322            "value": [
323                {
324                    "id": "/subscriptions/test-subscription-id/resourceGroups/test-rg/providers/microsoft.insights/eventtypes/management/values/event1",
325                    "resourceGroupName": "test-rg",
326                    "level": "Informational",
327                    "caller": "user@example.com",
328                    "description": "A test event"
329                }
330            ]
331        }));
332        let client = make_client(mock);
333        let result = client
334            .monitor()
335            .list_activity_logs("eventTimestamp ge '2024-01-01T00:00:00Z'")
336            .await
337            .expect("list_activity_logs failed");
338        assert_eq!(result.value.len(), 1);
339        assert_eq!(result.value[0].level.as_deref(), Some("Informational"));
340        assert_eq!(result.value[0].caller.as_deref(), Some("user@example.com"));
341    }
342
343    #[tokio::test]
344    async fn list_metric_definitions_returns_list() {
345        let mut mock = MockClient::new();
346        // Mock strips query params and just matches path
347        mock.expect_get(&format!(
348            "/{RESOURCE_URI}/providers/microsoft.insights/metricDefinitions"
349        ))
350        .returning_json(serde_json::json!({
351            "value": [
352                {
353                    "id": "metric-def-id",
354                    "name": "Percentage CPU",
355                    "namespace": "Microsoft.Compute/virtualMachines",
356                    "unit": "Percent",
357                    "primaryAggregationType": "Average"
358                }
359            ]
360        }));
361        let client = make_client(mock);
362        let result = client
363            .monitor()
364            .list_metric_definitions(RESOURCE_URI, "Microsoft.Compute/virtualMachines")
365            .await
366            .expect("list_metric_definitions failed");
367        assert_eq!(result.value.len(), 1);
368        assert_eq!(result.value[0].name.as_deref(), Some("Percentage CPU"));
369        assert_eq!(result.value[0].unit.as_deref(), Some("Percent"));
370    }
371
372    #[tokio::test]
373    async fn get_metrics_returns_response() {
374        let mut mock = MockClient::new();
375        mock.expect_get(&format!(
376            "/{RESOURCE_URI}/providers/microsoft.insights/metrics"
377        ))
378        .returning_json(serde_json::json!({
379            "cost": 0,
380            "timespan": "2024-01-01T00:00:00Z/2024-01-01T01:00:00Z",
381            "interval": "PT1M",
382            "namespace": "Microsoft.Compute/virtualMachines",
383            "value": [
384                {
385                    "id": "/metric/id",
386                    "type": "Microsoft.Insights/metrics",
387                    "name": "Percentage CPU",
388                    "unit": "Percent",
389                    "timeseries": [
390                        {
391                            "data": [
392                                { "timeStamp": "2024-01-01T00:00:00Z", "average": 5.0 }
393                            ]
394                        }
395                    ]
396                }
397            ]
398        }));
399        let client = make_client(mock);
400        let result = client
401            .monitor()
402            .get_metrics(RESOURCE_URI, "Percentage CPU", "PT1H")
403            .await
404            .expect("get_metrics failed");
405        assert_eq!(result.value.len(), 1);
406        assert_eq!(result.value[0].unit, "Percent");
407        let ts = &result.value[0].timeseries;
408        assert_eq!(ts.len(), 1);
409        let data = &ts[0].data;
410        assert_eq!(data[0].average, Some(5.0));
411    }
412}