1use std::pin::Pin;
6
7use futures_util::{Stream, StreamExt};
8use reqwest::Method;
9use reqwest_eventsource::{Event, EventSource};
10use serde::Deserialize;
11
12use crate::client::Client;
13use crate::error::BoxxkiteError;
14
15#[derive(Debug, Clone, Deserialize)]
19pub struct AuditLogEntry {
20 pub id: String,
21 pub session_id: String,
22 pub source: String,
24 pub operation: String,
25 pub detail: serde_json::Value,
26 pub exit_code: Option<i64>,
27 pub output_truncated: Option<String>,
28 pub started_at: String,
29 pub duration_ms: i64,
30 pub row_hash: Option<String>,
33 pub prev_hash: Option<String>,
34}
35
36#[derive(Debug, Clone, Deserialize)]
38pub struct AuditLogResponse {
39 pub entries: Vec<AuditLogEntry>,
40 pub limit: i64,
41 pub offset: i64,
42 pub total: i64,
43}
44
45#[derive(Debug, Clone, Default)]
47pub struct GetLogOptions {
48 limit: Option<u32>,
49 offset: Option<u32>,
50}
51
52impl GetLogOptions {
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn limit(mut self, limit: u32) -> Self {
59 self.limit = Some(limit);
60 self
61 }
62
63 pub fn offset(mut self, offset: u32) -> Self {
65 self.offset = Some(offset);
66 self
67 }
68}
69
70impl Client {
71 pub async fn get_log(
73 &self,
74 session_id: &str,
75 options: GetLogOptions,
76 ) -> Result<AuditLogResponse, BoxxkiteError> {
77 let mut query = Vec::new();
78 if let Some(limit) = options.limit {
79 query.push(("limit", limit.to_string()));
80 }
81 if let Some(offset) = options.offset {
82 query.push(("offset", offset.to_string()));
83 }
84 let builder = self
85 .request(Method::GET, &format!("/v1/sandboxes/{session_id}/log"))
86 .query(&query);
87 self.send(builder).await
88 }
89
90 pub fn watch(
115 &self,
116 session_id: &str,
117 ) -> Pin<Box<dyn Stream<Item = Result<AuditLogEntry, BoxxkiteError>> + Send + 'static>> {
118 let request_builder =
119 self.request(Method::GET, &format!("/v1/sandboxes/{session_id}/watch"));
120
121 Box::pin(async_stream::stream! {
122 let mut event_source = match EventSource::new(request_builder) {
123 Ok(event_source) => event_source,
124 Err(err) => {
125 yield Err(BoxxkiteError::Config(format!("failed to build watch request: {err}")));
126 return;
127 }
128 };
129
130 while let Some(event) = event_source.next().await {
131 match event {
132 Ok(Event::Open) => continue,
133 Ok(Event::Message(message)) => {
134 yield serde_json::from_str::<AuditLogEntry>(&message.data).map_err(BoxxkiteError::from);
135 }
136 Err(reqwest_eventsource::Error::StreamEnded) => break,
137 Err(reqwest_eventsource::Error::InvalidStatusCode(status, response)) => {
138 let bytes = response.bytes().await.unwrap_or_default();
139 yield Err(crate::error::api_error_from_bytes(status.as_u16(), &bytes));
140 break;
141 }
142 Err(err) => {
143 yield Err(BoxxkiteError::from(err));
144 break;
145 }
146 }
147 }
148 event_source.close();
149 })
150 }
151}