Skip to main content

azure_lite_rs/api/
loganalytics.rs

1//! Azure Log Analytics API client.
2//!
3//! Wraps the ARM management plane operations for Azure Log Analytics
4//! (Microsoft.OperationalInsights): workspace management, KQL query execution,
5//! and saved searches.
6//!
7//! `subscription_id` is auto-injected from the parent `AzureHttpClient`.
8
9use crate::{
10    AzureHttpClient, Result,
11    ops::loganalytics::LoganalyticsOps,
12    types::loganalytics::{
13        LogQueryBody, LogQueryResult, SavedSearchListResult, Workspace, WorkspaceCreateRequest,
14        WorkspaceListResult,
15    },
16};
17
18/// Client for Azure Log Analytics ARM management plane.
19pub struct LogAnalyticsClient<'a> {
20    ops: LoganalyticsOps<'a>,
21    client: &'a AzureHttpClient,
22}
23
24impl<'a> LogAnalyticsClient<'a> {
25    /// Create a new Log Analytics API client.
26    pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
27        Self {
28            ops: LoganalyticsOps::new(client),
29            client,
30        }
31    }
32
33    /// Gets the workspaces in a subscription.
34    pub async fn list_workspaces(&self) -> Result<WorkspaceListResult> {
35        self.ops
36            .list_workspaces(self.client.subscription_id())
37            .await
38    }
39
40    /// Gets a workspace instance.
41    pub async fn get_workspace(
42        &self,
43        resource_group_name: &str,
44        workspace_name: &str,
45    ) -> Result<Workspace> {
46        self.ops
47            .get_workspace(
48                self.client.subscription_id(),
49                resource_group_name,
50                workspace_name,
51            )
52            .await
53    }
54
55    /// Create or update a workspace.
56    pub async fn create_workspace(
57        &self,
58        resource_group_name: &str,
59        workspace_name: &str,
60        body: &WorkspaceCreateRequest,
61    ) -> Result<Workspace> {
62        self.ops
63            .create_workspace(
64                self.client.subscription_id(),
65                resource_group_name,
66                workspace_name,
67                body,
68            )
69            .await
70    }
71
72    /// Deletes a workspace resource instance.
73    pub async fn delete_workspace(
74        &self,
75        resource_group_name: &str,
76        workspace_name: &str,
77    ) -> Result<()> {
78        self.ops
79            .delete_workspace(
80                self.client.subscription_id(),
81                resource_group_name,
82                workspace_name,
83            )
84            .await
85    }
86
87    /// Execute a KQL query against a Log Analytics workspace.
88    pub async fn query_logs(
89        &self,
90        resource_group_name: &str,
91        workspace_name: &str,
92        body: &LogQueryBody,
93    ) -> Result<LogQueryResult> {
94        self.ops
95            .query_logs(
96                self.client.subscription_id(),
97                resource_group_name,
98                workspace_name,
99                body,
100            )
101            .await
102    }
103
104    /// Gets the saved searches for a given Log Analytics workspace.
105    pub async fn list_saved_searches(
106        &self,
107        resource_group_name: &str,
108        workspace_name: &str,
109    ) -> Result<SavedSearchListResult> {
110        self.ops
111            .list_saved_searches(
112                self.client.subscription_id(),
113                resource_group_name,
114                workspace_name,
115            )
116            .await
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::MockClient;
124    use crate::types::loganalytics::WorkspaceCreateRequest;
125
126    const SUB_ID: &str = "test-subscription-id";
127    const RG: &str = "cloud-lite-test-rg";
128    const WS_NAME: &str = "cloud-lite-test-ralph-workspace";
129
130    fn make_client(mock: MockClient) -> AzureHttpClient {
131        AzureHttpClient::from_mock(mock)
132    }
133
134    fn workspace_json() -> serde_json::Value {
135        serde_json::json!({
136            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.OperationalInsights/workspaces/{WS_NAME}"),
137            "name": WS_NAME,
138            "type": "Microsoft.OperationalInsights/workspaces",
139            "location": "eastus",
140            "properties": {
141                "customerId": "aae30729-30f7-4237-aec7-59447782acbb",
142                "provisioningState": "Succeeded",
143                "retentionInDays": 30,
144                "sku": { "name": "PerGB2018" }
145            }
146        })
147    }
148
149    #[tokio::test]
150    async fn list_workspaces_returns_list() {
151        let mut mock = MockClient::new();
152        mock.expect_get(&format!(
153            "/subscriptions/{SUB_ID}/providers/Microsoft.OperationalInsights/workspaces"
154        ))
155        .returning_json(serde_json::json!({ "value": [workspace_json()] }));
156        let client = make_client(mock);
157        let result = client
158            .log_analytics()
159            .list_workspaces()
160            .await
161            .expect("list_workspaces failed");
162        assert_eq!(result.value.len(), 1);
163        assert_eq!(result.value[0].name.as_deref(), Some(WS_NAME));
164    }
165
166    #[tokio::test]
167    async fn get_workspace_deserializes_properties() {
168        let mut mock = MockClient::new();
169        mock.expect_get(
170            &format!("/subscriptions/{SUB_ID}/resourcegroups/{RG}/providers/Microsoft.OperationalInsights/workspaces/{WS_NAME}"),
171        )
172        .returning_json(workspace_json());
173        let client = make_client(mock);
174        let ws = client
175            .log_analytics()
176            .get_workspace(RG, WS_NAME)
177            .await
178            .expect("get_workspace failed");
179        assert_eq!(ws.name.as_deref(), Some(WS_NAME));
180        let props = ws.properties.as_ref().unwrap();
181        assert_eq!(
182            props.customer_id.as_deref(),
183            Some("aae30729-30f7-4237-aec7-59447782acbb")
184        );
185        assert_eq!(props.provisioning_state.as_deref(), Some("Succeeded"));
186        assert_eq!(props.retention_in_days, Some(30));
187    }
188
189    #[tokio::test]
190    async fn create_workspace_returns_workspace() {
191        let mut mock = MockClient::new();
192        mock.expect_put(
193            &format!("/subscriptions/{SUB_ID}/resourcegroups/{RG}/providers/Microsoft.OperationalInsights/workspaces/{WS_NAME}"),
194        )
195        .returning_json(workspace_json());
196        let client = make_client(mock);
197        let body = WorkspaceCreateRequest {
198            location: "eastus".into(),
199            ..Default::default()
200        };
201        let ws = client
202            .log_analytics()
203            .create_workspace(RG, WS_NAME, &body)
204            .await
205            .expect("create_workspace failed");
206        assert_eq!(ws.name.as_deref(), Some(WS_NAME));
207    }
208
209    #[tokio::test]
210    async fn delete_workspace_succeeds() {
211        let mut mock = MockClient::new();
212        mock.expect_delete(
213            &format!("/subscriptions/{SUB_ID}/resourcegroups/{RG}/providers/Microsoft.OperationalInsights/workspaces/{WS_NAME}"),
214        )
215        .returning_json(serde_json::json!({}));
216        let client = make_client(mock);
217        client
218            .log_analytics()
219            .delete_workspace(RG, WS_NAME)
220            .await
221            .expect("delete_workspace failed");
222    }
223
224    #[tokio::test]
225    async fn query_logs_returns_tables() {
226        let mut mock = MockClient::new();
227        mock.expect_post(
228            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.OperationalInsights/workspaces/{WS_NAME}/query"),
229        )
230        .returning_json(serde_json::json!({
231            "tables": [
232                {
233                    "name": "PrimaryResult",
234                    "columns": [
235                        { "name": "TimeGenerated", "type": "datetime" },
236                        { "name": "Category", "type": "string" }
237                    ],
238                    "rows": [
239                        ["2024-01-01T00:00:00Z", "Administrative"]
240                    ]
241                }
242            ]
243        }));
244        let client = make_client(mock);
245        let body = LogQueryBody {
246            query: "AzureActivity | limit 5".into(),
247            timespan: Some("PT1H".into()),
248            ..Default::default()
249        };
250        let result = client
251            .log_analytics()
252            .query_logs(RG, WS_NAME, &body)
253            .await
254            .expect("query_logs failed");
255        assert_eq!(result.tables.len(), 1);
256        assert_eq!(result.tables[0].name.as_deref(), Some("PrimaryResult"));
257        assert_eq!(result.tables[0].columns.len(), 2);
258        assert_eq!(
259            result.tables[0].columns[0].name.as_deref(),
260            Some("TimeGenerated")
261        );
262        assert_eq!(result.tables[0].rows.len(), 1);
263    }
264
265    #[tokio::test]
266    async fn list_saved_searches_returns_list() {
267        let mut mock = MockClient::new();
268        mock.expect_get(
269            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.OperationalInsights/workspaces/{WS_NAME}/savedSearches"),
270        )
271        .returning_json(serde_json::json!({ "value": [] }));
272        let client = make_client(mock);
273        let result = client
274            .log_analytics()
275            .list_saved_searches(RG, WS_NAME)
276            .await
277            .expect("list_saved_searches failed");
278        assert_eq!(result.value.len(), 0);
279    }
280}