hypercall_admin/monitoring/
journal.rs1use axum::{extract::State, http::StatusCode, response::IntoResponse};
4use serde::Serialize;
5use sonic_rs::json;
6
7use crate::state::AdminState;
8use hypercall_db::{JournalCommandSummary, JournalFullRecord};
9use hypercall_runtime_api::sonic_json::SonicJson;
10
11#[derive(Debug, serde::Deserialize, utoipa::IntoParams)]
13pub struct JournalQuery {
14 #[serde(default = "default_journal_limit")]
16 pub limit: usize,
17}
18
19fn default_journal_limit() -> usize {
20 100
21}
22
23#[derive(Debug, Serialize, utoipa::ToSchema)]
25pub struct JournalListResponse {
26 pub count: usize,
28 #[schema(value_type = Vec<Object>)]
30 pub records: Vec<JournalCommandSummary>,
31}
32
33#[derive(Debug, Serialize, utoipa::ToSchema)]
35pub struct JournalRecordResponse {
36 pub found: bool,
38 #[schema(value_type = Option<Object>)]
40 pub record: Option<JournalFullRecord>,
41}
42
43#[utoipa::path(
51 get,
52 path = "/monitoring/engine/journal/commands",
53 params(JournalQuery),
54 responses(
55 (status = 200, description = "List of journal command summaries", body = JournalListResponse),
56 (status = 401, description = "Invalid or missing X-Admin-Key header"),
57 (status = 503, description = "Engine journal not available")
58 ),
59 tag = "Monitoring",
60 security(("admin_key" = []))
61)]
62pub async fn list_journal_commands(
63 State(app_state): State<AdminState>,
64 axum::extract::Query(params): axum::extract::Query<JournalQuery>,
65) -> impl IntoResponse {
66 let journal = match &app_state.engine_journal_reader {
67 Some(j) => j.clone(),
68 None => {
69 return (
70 StatusCode::SERVICE_UNAVAILABLE,
71 SonicJson(json!({
72 "error": "Engine journal not available"
73 })),
74 )
75 .into_response();
76 }
77 };
78
79 let limit = params.limit;
81 let result = tokio::task::spawn_blocking(move || journal.get_recent(limit)).await;
82
83 match result {
84 Ok(Ok(records)) => {
85 let count = records.len();
86 (
87 StatusCode::OK,
88 SonicJson(JournalListResponse { count, records }),
89 )
90 .into_response()
91 }
92 Ok(Err(e)) => (
93 StatusCode::INTERNAL_SERVER_ERROR,
94 SonicJson(json!({
95 "error": format!("Failed to query journal: {}", e)
96 })),
97 )
98 .into_response(),
99 Err(e) => (
100 StatusCode::INTERNAL_SERVER_ERROR,
101 SonicJson(json!({
102 "error": format!("Task failed: {}", e)
103 })),
104 )
105 .into_response(),
106 }
107}
108
109#[utoipa::path(
116 get,
117 path = "/monitoring/engine/journal/commands/{request_id}",
118 params(
119 ("request_id" = String, Path, description = "Request ID to look up")
120 ),
121 responses(
122 (status = 200, description = "Journal record for the request", body = JournalRecordResponse),
123 (status = 401, description = "Invalid or missing X-Admin-Key header"),
124 (status = 404, description = "No record found for request_id"),
125 (status = 503, description = "Engine journal not available")
126 ),
127 tag = "Monitoring",
128 security(("admin_key" = []))
129)]
130pub async fn get_journal_command_by_id(
131 State(app_state): State<AdminState>,
132 axum::extract::Path(request_id): axum::extract::Path<String>,
133) -> impl IntoResponse {
134 let journal = match &app_state.engine_journal_reader {
135 Some(j) => j.clone(),
136 None => {
137 return (
138 StatusCode::SERVICE_UNAVAILABLE,
139 SonicJson(json!({
140 "error": "Engine journal not available"
141 })),
142 )
143 .into_response();
144 }
145 };
146
147 let req_id = match uuid::Uuid::parse_str(&request_id) {
148 Ok(id) => id,
149 Err(e) => {
150 return (
151 StatusCode::BAD_REQUEST,
152 SonicJson(json!({
153 "error": format!("Invalid request_id UUID '{}': {}", request_id, e)
154 })),
155 )
156 .into_response();
157 }
158 };
159
160 let result = tokio::task::spawn_blocking(move || journal.get_by_request_id(&req_id)).await;
162
163 match result {
164 Ok(Ok(Some(record))) => (
165 StatusCode::OK,
166 SonicJson(JournalRecordResponse {
167 found: true,
168 record: Some(record),
169 }),
170 )
171 .into_response(),
172 Ok(Ok(None)) => (
173 StatusCode::NOT_FOUND,
174 SonicJson(json!({
175 "error": format!("No record found for request_id: {}", request_id)
176 })),
177 )
178 .into_response(),
179 Ok(Err(e)) => (
180 StatusCode::INTERNAL_SERVER_ERROR,
181 SonicJson(json!({
182 "error": format!("Failed to query journal: {}", e)
183 })),
184 )
185 .into_response(),
186 Err(e) => (
187 StatusCode::INTERNAL_SERVER_ERROR,
188 SonicJson(json!({
189 "error": format!("Task failed: {}", e)
190 })),
191 )
192 .into_response(),
193 }
194}