1use 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
25pub struct MonitorClient<'a> {
32 client: &'a AzureHttpClient,
33 _ops: MonitorOps<'a>,
37}
38
39impl<'a> MonitorClient<'a> {
40 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 pub async fn list_metric_definitions(
68 &self,
69 resource_uri: &str,
70 metric_namespace: &str,
71 ) -> Result<MetricDefinitionCollection> {
72 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 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={}×pan={}",
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 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 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 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 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 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.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}