1use anyhow::{bail, ensure, Result};
29use serde_json::{json, Map, Value};
30
31use crate::utils::sanitize;
32use crate::utils::truncate::truncate_with_suffix;
33
34pub const MAX_JSON_DEPTH: usize = 32;
36pub const MAX_TEXT_BYTES: usize = 2048;
38pub const MAX_LONG_TEXT_BYTES: usize = 8192;
40pub const MAX_SCALAR_LIST: usize = 1000;
42pub const MAX_ATTEMPTS: usize = 20;
44pub const MAX_PLAN_NODES: usize = 200;
46pub const MAX_METRICS: usize = 100;
48
49pub const APPLICATION_FIELDS: &[&str] = &[
50 "id",
51 "name",
52 "coresGranted",
53 "maxCores",
54 "coresPerExecutor",
55 "memoryPerExecutorMB",
56];
57pub const ATTEMPT_FIELDS: &[&str] = &[
58 "attemptId",
59 "startTime",
60 "endTime",
61 "lastUpdated",
62 "duration",
63 "sparkUser",
64 "completed",
65 "appSparkVersion",
66];
67pub const JOB_FIELDS: &[&str] = &[
68 "jobId",
69 "name",
70 "description",
71 "submissionTime",
72 "completionTime",
73 "stageIds",
74 "jobGroup",
75 "jobTags",
76 "status",
77 "numTasks",
78 "numActiveTasks",
79 "numCompletedTasks",
80 "numSkippedTasks",
81 "numFailedTasks",
82 "numKilledTasks",
83 "numCompletedIndices",
84 "numActiveStages",
85 "numCompletedStages",
86 "numSkippedStages",
87 "numFailedStages",
88];
89pub const STAGE_FIELDS: &[&str] = &[
90 "stageId",
91 "attemptId",
92 "status",
93 "name",
94 "description",
95 "numTasks",
96 "numActiveTasks",
97 "numCompleteTasks",
98 "numFailedTasks",
99 "numKilledTasks",
100 "numCompletedIndices",
101 "submissionTime",
102 "firstTaskLaunchedTime",
103 "completionTime",
104 "failureReason",
105 "executorDeserializeTime",
106 "executorRunTime",
107 "executorCpuTime",
108 "resultSize",
109 "jvmGcTime",
110 "peakExecutionMemory",
111 "inputBytes",
112 "inputRecords",
113 "outputBytes",
114 "outputRecords",
115 "shuffleReadBytes",
116 "shuffleReadRecords",
117 "shuffleWriteBytes",
118 "shuffleWriteRecords",
119 "shuffleFetchWaitTime",
120 "shuffleWriteTime",
121 "memoryBytesSpilled",
122 "diskBytesSpilled",
123 "schedulingPool",
124 "rddIds",
125 "resourceProfileId",
126];
127pub const EXECUTOR_FIELDS: &[&str] = &[
128 "id",
129 "hostPort",
130 "isActive",
131 "rddBlocks",
132 "memoryUsed",
133 "diskUsed",
134 "totalCores",
135 "maxTasks",
136 "activeTasks",
137 "failedTasks",
138 "completedTasks",
139 "totalTasks",
140 "totalDuration",
141 "totalGCTime",
142 "totalInputBytes",
143 "totalShuffleRead",
144 "totalShuffleWrite",
145 "isExcluded",
146 "maxMemory",
147 "addTime",
148 "removeTime",
149 "removeReason",
150 "resourceProfileId",
151];
152pub const SQL_FIELDS: &[&str] = &[
153 "id",
154 "status",
155 "description",
156 "submissionTime",
157 "duration",
158 "runningJobIds",
159 "successJobIds",
160 "failedJobIds",
161 "queryId",
162 "errorMessage",
163 "rootExecutionId",
164];
165
166const LONG_TEXT_FIELDS: &[&str] = &[
168 "failureReason",
169 "errorMessage",
170 "planDescription",
171 "removeReason",
172];
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum Resource {
177 Applications,
178 Jobs,
179 Stages,
180 TaskSummary,
181 Executors,
182 Environment,
183 Sql,
184 Version,
185}
186
187impl Resource {
188 pub fn of(operation: &str) -> Option<Resource> {
189 Some(match operation {
190 "list_applications" | "get_application" => Resource::Applications,
191 "list_jobs" | "get_job" => Resource::Jobs,
192 "list_stages" | "get_stage" => Resource::Stages,
193 "get_task_summary" => Resource::TaskSummary,
194 "list_executors" => Resource::Executors,
195 "get_environment" => Resource::Environment,
196 "list_sql_executions" | "get_sql_execution" => Resource::Sql,
197 "get_version" => Resource::Version,
198 _ => return None,
199 })
200 }
201}
202
203pub fn json_depth(bytes: &[u8]) -> usize {
206 let (mut depth, mut max, mut in_string, mut escaped) = (0usize, 0usize, false, false);
207 for &b in bytes {
208 if in_string {
209 match (escaped, b) {
210 (true, _) => escaped = false,
211 (false, b'\\') => escaped = true,
212 (false, b'"') => in_string = false,
213 _ => {}
214 }
215 continue;
216 }
217 match b {
218 b'"' => in_string = true,
219 b'[' | b'{' => {
220 depth += 1;
221 max = max.max(depth);
222 }
223 b']' | b'}' => depth = depth.saturating_sub(1),
224 _ => {}
225 }
226 }
227 max
228}
229
230pub fn parse(body: &[u8]) -> Result<Value> {
232 let depth = json_depth(body);
233 ensure!(
234 depth <= MAX_JSON_DEPTH,
235 "the response nests {depth} levels deep; at most {MAX_JSON_DEPTH} are read (Spark's own \
236 answers nest at most 4)"
237 );
238 serde_json::from_slice(body).map_err(|e| anyhow::anyhow!("the response is not JSON: {e}"))
239}
240
241pub fn text(s: &str, long: bool) -> String {
243 let clean = if long {
244 sanitize::multiline(s)
245 } else {
246 sanitize::line_field(s)
247 };
248 truncate_with_suffix(
249 &clean,
250 if long {
251 MAX_LONG_TEXT_BYTES
252 } else {
253 MAX_TEXT_BYTES
254 },
255 "...",
256 )
257}
258
259fn scalar(key: &str, v: &Value) -> Option<Value> {
261 match v {
262 Value::Null | Value::Bool(_) | Value::Number(_) => Some(v.clone()),
263 Value::String(s) => Some(Value::String(text(s, LONG_TEXT_FIELDS.contains(&key)))),
264 Value::Array(items) => Some(Value::Array(
265 items
266 .iter()
267 .filter(|x| !matches!(x, Value::Array(_) | Value::Object(_)))
268 .take(MAX_SCALAR_LIST)
269 .map(|x| match x {
270 Value::String(s) => Value::String(text(s, false)),
271 other => other.clone(),
272 })
273 .collect(),
274 )),
275 Value::Object(_) => None,
276 }
277}
278
279pub fn project(entry: &Value, fields: &[&str]) -> Value {
281 let mut out = Map::new();
282 if let Some(obj) = entry.as_object() {
283 for key in fields {
284 if let Some(v) = obj.get(*key).and_then(|v| scalar(key, v)) {
285 out.insert((*key).to_string(), v);
286 }
287 }
288 }
289 Value::Object(out)
290}
291
292fn application(entry: &Value) -> Value {
293 let mut out = project(entry, APPLICATION_FIELDS);
294 let attempts: Vec<Value> = entry
295 .get("attempts")
296 .and_then(Value::as_array)
297 .map(|a| {
298 a.iter()
299 .take(MAX_ATTEMPTS)
300 .map(|x| project(x, ATTEMPT_FIELDS))
301 .collect()
302 })
303 .unwrap_or_default();
304 out["attempts"] = Value::Array(attempts);
305 out
306}
307
308fn sql_execution(entry: &Value, with_plan: bool) -> Value {
309 let mut out = project(entry, SQL_FIELDS);
310 if with_plan {
311 if let Some(plan) = entry.get("planDescription").and_then(Value::as_str) {
312 out["planDescription"] = json!(text(plan, true));
313 }
314 let nodes = entry.get("nodes").and_then(Value::as_array);
315 out["nodes"] = Value::Array(
316 nodes
317 .map(|n| {
318 n.iter()
319 .take(MAX_PLAN_NODES)
320 .map(|x| project(x, &["nodeId", "nodeName"]))
321 .collect()
322 })
323 .unwrap_or_default(),
324 );
325 out["node_count"] = json!(nodes.map(Vec::len).unwrap_or(0));
326 }
327 out
328}
329
330fn entries<'a>(value: &'a Value, what: &str) -> Result<&'a [Value]> {
332 match value {
333 Value::Array(items) => {
334 ensure!(
335 items.iter().all(Value::is_object),
336 "Spark answers {what} as an array of objects; this array holds something else"
337 );
338 Ok(items.as_slice())
339 }
340 Value::Object(_) => Ok(std::slice::from_ref(value)),
341 _ => bail!("Spark answers {what} as a JSON array or object; this is neither"),
342 }
343}
344
345fn take(
347 out: &mut Map<String, Value>,
348 key: &str,
349 items: &[Value],
350 max_items: usize,
351 f: impl Fn(&Value) -> Value,
352) {
353 let total = items.len();
354 let kept: Vec<Value> = items.iter().take(max_items).map(f).collect();
355 out.insert("count".into(), json!(kept.len()));
356 out.insert("total".into(), json!(total));
357 out.insert("truncated".into(), json!(total > max_items));
358 out.insert(key.into(), Value::Array(kept));
359}
360
361pub fn decode(
363 resource: Resource,
364 operation: &str,
365 value: &Value,
366 max_items: usize,
367 context: &Map<String, Value>,
368) -> Result<Value> {
369 let mut out = context.clone();
370 out.insert("operation".into(), json!(operation));
371 match resource {
372 Resource::Applications => {
373 let items = entries(value, "applications")?;
374 take(&mut out, "applications", items, max_items, application);
375 }
376 Resource::Jobs => {
377 let items = entries(value, "jobs")?;
378 take(&mut out, "jobs", items, max_items, |x| {
379 project(x, JOB_FIELDS)
380 });
381 }
382 Resource::Stages => {
383 let items = entries(value, "stages")?;
384 take(&mut out, "stages", items, max_items, |x| {
385 project(x, STAGE_FIELDS)
386 });
387 }
388 Resource::Executors => {
389 let items = entries(value, "executors")?;
390 take(&mut out, "executors", items, max_items, |x| {
391 project(x, EXECUTOR_FIELDS)
392 });
393 }
394 Resource::Sql => {
395 let with_plan = operation == "get_sql_execution";
396 let items = entries(value, "SQL executions")?;
397 take(&mut out, "executions", items, max_items, |x| {
398 sql_execution(x, with_plan)
399 });
400 }
401 Resource::TaskSummary => {
402 let obj = value
403 .as_object()
404 .ok_or_else(|| anyhow::anyhow!("Spark answers a task summary as an object"))?;
405 let quantiles = obj
406 .get("quantiles")
407 .and_then(|q| scalar("quantiles", q))
408 .ok_or_else(|| anyhow::anyhow!("the task summary carries no quantiles"))?;
409 let mut metrics = Map::new();
410 for (key, v) in obj.iter().filter(|(k, _)| k.as_str() != "quantiles") {
411 if metrics.len() >= MAX_METRICS {
412 break;
413 }
414 match v {
415 Value::Array(_) => {
416 if let Some(list) = scalar(key, v) {
417 metrics.insert(key.clone(), list);
418 }
419 }
420 Value::Object(group) => {
423 let mut inner = Map::new();
424 for (k, list) in group.iter().take(MAX_METRICS) {
425 if let Value::Array(_) = list {
426 if let Some(list) = scalar(k, list) {
427 inner.insert(k.clone(), list);
428 }
429 }
430 }
431 metrics.insert(key.clone(), Value::Object(inner));
432 }
433 _ => {}
434 }
435 }
436 let n = metrics.len();
437 out.insert("quantiles".into(), quantiles);
438 out.insert("metrics".into(), Value::Object(metrics));
439 out.insert("count".into(), json!(n));
440 out.insert("total".into(), json!(n));
441 out.insert("truncated".into(), json!(false));
442 }
443 Resource::Environment => {
444 let obj = value
445 .as_object()
446 .ok_or_else(|| anyhow::anyhow!("Spark answers an environment as an object"))?;
447 let runtime = obj
448 .get("runtime")
449 .map(|r| project(r, &["javaVersion", "javaHome", "scalaVersion"]))
450 .unwrap_or_else(|| json!({}));
451 let props = obj
452 .get("sparkProperties")
453 .and_then(Value::as_array)
454 .cloned()
455 .unwrap_or_default();
456 let total = props.len();
457 let mut map = Map::new();
458 for pair in props.iter() {
459 if map.len() >= max_items {
460 break;
461 }
462 if let (Some(k), Some(v)) = (
463 pair.get(0).and_then(Value::as_str),
464 pair.get(1).and_then(Value::as_str),
465 ) {
466 map.insert(text(k, false), json!(text(v, false)));
467 }
468 }
469 let count_of = |key: &str| obj.get(key).and_then(Value::as_array).map_or(0, Vec::len);
470 out.insert("runtime".into(), runtime);
471 out.insert("count".into(), json!(map.len()));
472 out.insert("total".into(), json!(total));
473 out.insert("truncated".into(), json!(total > map.len()));
474 out.insert("spark_properties".into(), Value::Object(map));
475 out.insert(
476 "system_property_count".into(),
477 json!(count_of("systemProperties")),
478 );
479 out.insert(
480 "hadoop_property_count".into(),
481 json!(count_of("hadoopProperties")),
482 );
483 out.insert(
484 "classpath_entry_count".into(),
485 json!(count_of("classpathEntries")),
486 );
487 }
488 Resource::Version => {
489 let v = value
490 .get("spark")
491 .and_then(Value::as_str)
492 .ok_or_else(|| anyhow::anyhow!("the version answer carries no \"spark\" field"))?;
493 out.insert("spark_version".into(), json!(text(v, false)));
494 }
495 }
496 Ok(Value::Object(out))
497}