1use std::cmp::{Ordering, Reverse};
16use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet};
17use std::fmt::Write;
18use std::sync::Arc;
19
20use itertools::Itertools;
21use prometheus_http_query::response::Data::Vector;
22use risingwave_common::id::{DatabaseId, ObjectId};
23use risingwave_common::system_param::AdaptiveParallelismStrategy;
24use risingwave_common::system_param::adaptive_parallelism_strategy::parse_strategy;
25use risingwave_common::system_param::reader::SystemParamsRead;
26use risingwave_common::types::Timestamptz;
27use risingwave_common::util::StackTraceResponseExt;
28use risingwave_common::util::epoch::Epoch;
29use risingwave_hummock_sdk::level::Level;
30use risingwave_hummock_sdk::{CompactionGroupId, HummockSstableId};
31use risingwave_license::LicenseManager;
32use risingwave_meta_model::{JobStatus, StreamingParallelism};
33use risingwave_pb::catalog::table::PbTableType;
34use risingwave_pb::common::WorkerType;
35use risingwave_pb::meta::EventLog;
36use risingwave_pb::meta::event_log::Event;
37use risingwave_pb::monitor_service::StackTraceResponse;
38use risingwave_pb::monitor_service::stack_trace_request::ActorTracesFormat;
39use risingwave_sqlparser::ast::SqlOptionRedactionRef;
40use risingwave_sqlparser::parser::Parser;
41use serde_json::json;
42use thiserror_ext::AsReport;
43
44use crate::MetaResult;
45use crate::controller::system_param::SystemParamsControllerRef;
46use crate::hummock::HummockManagerRef;
47use crate::manager::MetadataManager;
48use crate::manager::event_log::EventLogManagerRef;
49use crate::manager::iceberg_compaction::IcebergCompactionManagerRef;
50use crate::rpc::await_tree::dump_cluster_await_tree;
51
52pub type DiagnoseCommandRef = Arc<DiagnoseCommand>;
53
54pub struct DiagnoseCommand {
55 metadata_manager: MetadataManager,
56 await_tree_reg: await_tree::Registry,
57 hummock_manger: HummockManagerRef,
58 iceberg_compaction_manager: IcebergCompactionManagerRef,
59 event_log_manager: EventLogManagerRef,
60 prometheus_client: Option<prometheus_http_query::Client>,
61 prometheus_selector: String,
62 sql_option_redaction: SqlOptionRedactionRef,
63 system_params_controller: SystemParamsControllerRef,
64}
65
66impl DiagnoseCommand {
67 pub fn new(
68 metadata_manager: MetadataManager,
69 await_tree_reg: await_tree::Registry,
70 hummock_manger: HummockManagerRef,
71 iceberg_compaction_manager: IcebergCompactionManagerRef,
72 event_log_manager: EventLogManagerRef,
73 prometheus_client: Option<prometheus_http_query::Client>,
74 prometheus_selector: String,
75 sql_option_redaction: SqlOptionRedactionRef,
76 system_params_controller: SystemParamsControllerRef,
77 ) -> Self {
78 Self {
79 metadata_manager,
80 await_tree_reg,
81 hummock_manger,
82 iceberg_compaction_manager,
83 event_log_manager,
84 prometheus_client,
85 prometheus_selector,
86 sql_option_redaction,
87 system_params_controller,
88 }
89 }
90
91 pub async fn report(
92 &self,
93 actor_traces_format: ActorTracesFormat,
94 database_id: Option<DatabaseId>,
95 ) -> String {
96 let mut report = String::new();
97 let _ = writeln!(
98 report,
99 "report created at: {}\nversion: {}",
100 chrono::DateTime::<chrono::offset::Utc>::from(std::time::SystemTime::now()),
101 risingwave_common::current_cluster_version(),
102 );
103 let _ = writeln!(report);
104 if let Some(database_id) = database_id {
105 let _ = writeln!(report, "database_id filter: {database_id}");
106 let _ = writeln!(
107 report,
108 "Catalog details, actors, Iceberg schedules, and events with database IDs are filtered. \
109 Catalog counts and other shared cluster diagnostics cover the entire cluster."
110 );
111 }
112 self.write_license(&mut report);
113 let _ = writeln!(report);
114 self.write_catalog(&mut report, database_id).await;
115 let _ = writeln!(report);
116 self.write_worker_nodes(&mut report).await;
117 let _ = writeln!(report);
118 self.write_streaming_prometheus(&mut report).await;
119 let _ = writeln!(report);
120 self.write_storage(&mut report).await;
121 let _ = writeln!(report);
122 self.write_iceberg_compaction_schedule(&mut report, database_id)
123 .await;
124 let _ = writeln!(report);
125 self.write_event_logs(&mut report, database_id);
126 let _ = writeln!(report);
127 self.write_params(&mut report).await;
128 let _ = writeln!(report);
129 self.write_await_tree(&mut report, actor_traces_format, database_id)
130 .await;
131
132 report
133 }
134
135 async fn write_catalog(&self, s: &mut String, database_id: Option<DatabaseId>) {
136 self.write_catalog_inner(s, database_id).await;
137 let _ = self
138 .write_table_definition(s, database_id)
139 .await
140 .inspect_err(|e| {
141 tracing::warn!(
142 error = e.to_report_string(),
143 "failed to display table definition"
144 )
145 });
146 }
147
148 async fn write_catalog_inner(&self, s: &mut String, database_id: Option<DatabaseId>) {
149 let stats = self.metadata_manager.catalog_controller.stats().await;
150
151 let stat = match stats {
152 Ok(stat) => stat,
153 Err(err) => {
154 tracing::warn!(error=?err.as_report(), "failed to get catalog stats");
155 return;
156 }
157 };
158 let _ = writeln!(s, "number of database: {}", stat.database_num);
159 let _ = writeln!(s, "number of fragment: {}", stat.streaming_job_num);
160 let _ = writeln!(s, "number of actor: {}", stat.actor_num);
161 let _ = writeln!(s, "number of source: {}", stat.source_num);
162 let _ = writeln!(s, "number of table: {}", stat.table_num);
163 let _ = writeln!(s, "number of materialized view: {}", stat.mview_num);
164 let _ = writeln!(s, "number of sink: {}", stat.sink_num);
165 let _ = writeln!(s, "number of index: {}", stat.index_num);
166 let _ = writeln!(s, "number of function: {}", stat.function_num);
167
168 self.write_databases(s, database_id).await;
169 self.write_schemas(s, database_id).await;
170 }
171
172 async fn write_databases(&self, s: &mut String, database_id: Option<DatabaseId>) {
173 let databases = match self
174 .metadata_manager
175 .catalog_controller
176 .list_databases()
177 .await
178 {
179 Ok(databases) => databases,
180 Err(err) => {
181 tracing::warn!(error=?err.as_report(), "failed to list databases");
182 return;
183 }
184 };
185
186 use comfy_table::{Row, Table};
187 let mut table = Table::new();
188 table.set_header({
189 let mut row = Row::new();
190 row.add_cell("id".into());
191 row.add_cell("name".into());
192 row.add_cell("resource_group".into());
193 row.add_cell("barrier_interval_ms".into());
194 row.add_cell("checkpoint_frequency".into());
195 row
196 });
197 for db in databases
198 .into_iter()
199 .filter(|db| database_id.is_none_or(|id| db.id == id))
200 {
201 let mut row = Row::new();
202 row.add_cell(db.id.into());
203 row.add_cell(db.name.into());
204 row.add_cell(db.resource_group.into());
205 row.add_cell(
206 db.barrier_interval_ms
207 .map(|v| v.to_string())
208 .unwrap_or("default".into())
209 .into(),
210 );
211 row.add_cell(
212 db.checkpoint_frequency
213 .map(|v| v.to_string())
214 .unwrap_or("default".into())
215 .into(),
216 );
217 table.add_row(row);
218 }
219
220 let _ = writeln!(s, "DATABASE");
221 let _ = writeln!(s, "{table}");
222 }
223
224 async fn write_schemas(&self, s: &mut String, database_id: Option<DatabaseId>) {
225 let schemas = match self
226 .metadata_manager
227 .catalog_controller
228 .list_schemas()
229 .await
230 {
231 Ok(schemas) => schemas,
232 Err(err) => {
233 tracing::warn!(error=?err.as_report(), "failed to list schemas");
234 return;
235 }
236 };
237
238 use comfy_table::{Row, Table};
239 let mut table = Table::new();
240 table.set_header({
241 let mut row = Row::new();
242 row.add_cell("id".into());
243 row.add_cell("database_id".into());
244 row.add_cell("name".into());
245 row
246 });
247 for schema in schemas
248 .into_iter()
249 .filter(|schema| database_id.is_none_or(|id| schema.database_id == id))
250 {
251 let mut row = Row::new();
252 row.add_cell(schema.id.into());
253 row.add_cell(schema.database_id.into());
254 row.add_cell(schema.name.into());
255 table.add_row(row);
256 }
257
258 let _ = writeln!(s, "SCHEMA");
259 let _ = writeln!(s, "{table}");
260 }
261
262 async fn write_worker_nodes(&self, s: &mut String) {
263 let Ok(worker_actor_count) = self.metadata_manager.worker_actor_count() else {
264 tracing::warn!("failed to get worker actor count");
265 return;
266 };
267
268 use comfy_table::{Row, Table};
269 let Ok(worker_nodes) = self.metadata_manager.list_worker_node(None, None).await else {
270 tracing::warn!("failed to get worker nodes");
271 return;
272 };
273 let mut table = Table::new();
274 table.set_header({
275 let mut row = Row::new();
276 row.add_cell("id".into());
277 row.add_cell("host".into());
278 row.add_cell("hostname".into());
279 row.add_cell("type".into());
280 row.add_cell("state".into());
281 row.add_cell("parallelism".into());
282 row.add_cell("resource_group".into());
283 row.add_cell("is_streaming".into());
284 row.add_cell("is_serving".into());
285 row.add_cell("is_iceberg_compactor".into());
286 row.add_cell("rw_version".into());
287 row.add_cell("total_memory_bytes".into());
288 row.add_cell("total_cpu_cores".into());
289 row.add_cell("started_at".into());
290 row.add_cell("actor_count".into());
291 row
292 });
293 for worker_node in worker_nodes {
294 let mut row = Row::new();
295 row.add_cell(worker_node.id.into());
296 try_add_cell(
297 &mut row,
298 worker_node
299 .host
300 .as_ref()
301 .map(|h| format!("{}:{}", h.host, h.port)),
302 );
303 try_add_cell(
304 &mut row,
305 worker_node.resource.as_ref().map(|r| r.hostname.clone()),
306 );
307 try_add_cell(
308 &mut row,
309 worker_node.get_type().ok().map(|t| t.as_str_name()),
310 );
311 try_add_cell(
312 &mut row,
313 worker_node.get_state().ok().map(|s| s.as_str_name()),
314 );
315 try_add_cell(&mut row, worker_node.parallelism());
316 try_add_cell(
317 &mut row,
318 worker_node
319 .property
320 .as_ref()
321 .map(|p| p.resource_group.clone().unwrap_or("".to_owned())),
322 );
323 let (is_streaming, is_serving) = {
325 if let Ok(t) = worker_node.get_type()
326 && t == WorkerType::ComputeNode
327 {
328 (
329 worker_node.property.as_ref().map(|p| p.is_streaming),
330 worker_node.property.as_ref().map(|p| p.is_serving),
331 )
332 } else {
333 (None, None)
334 }
335 };
336 try_add_cell(&mut row, is_streaming);
337 try_add_cell(&mut row, is_serving);
338 let is_iceberg_compactor = {
340 if let Ok(t) = worker_node.get_type()
341 && t == WorkerType::Compactor
342 {
343 worker_node
344 .property
345 .as_ref()
346 .map(|p| p.is_iceberg_compactor)
347 } else {
348 None
349 }
350 };
351 try_add_cell(&mut row, is_iceberg_compactor);
352 try_add_cell(
353 &mut row,
354 worker_node.resource.as_ref().map(|r| r.rw_version.clone()),
355 );
356 try_add_cell(
357 &mut row,
358 worker_node.resource.as_ref().map(|r| r.total_memory_bytes),
359 );
360 try_add_cell(
361 &mut row,
362 worker_node.resource.as_ref().map(|r| r.total_cpu_cores),
363 );
364 try_add_cell(
365 &mut row,
366 worker_node
367 .started_at
368 .and_then(|ts| Timestamptz::from_secs(ts as _).map(|t| t.to_string())),
369 );
370 let actor_count = {
371 if let Ok(t) = worker_node.get_type()
372 && t != WorkerType::ComputeNode
373 {
374 None
375 } else {
376 match worker_actor_count.get(&worker_node.id) {
377 None => Some(0),
378 Some(c) => Some(*c),
379 }
380 }
381 };
382 try_add_cell(&mut row, actor_count);
383 table.add_row(row);
384 }
385 let _ = writeln!(s, "{table}");
386 }
387
388 fn write_event_logs(&self, s: &mut String, database_id: Option<DatabaseId>) {
389 let event_logs = self
390 .event_log_manager
391 .list_event_logs()
392 .into_iter()
393 .sorted_by(|a, b| {
394 a.timestamp
395 .unwrap_or(0)
396 .cmp(&b.timestamp.unwrap_or(0))
397 .reverse()
398 })
399 .collect_vec();
400
401 let _ = writeln!(s, "latest barrier completions");
402 Self::write_event_logs_impl(
403 s,
404 event_logs.iter(),
405 |e| {
406 let Event::BarrierComplete(info) = e else {
407 return None;
408 };
409 if database_id.is_some_and(|id| info.database_id != id.as_raw_id()) {
410 return None;
411 }
412 Some(json!(info).to_string())
413 },
414 Some(10),
415 );
416
417 let _ = writeln!(s);
418 let _ = writeln!(s, "latest barrier collection failures");
419 Self::write_event_logs_impl(
420 s,
421 event_logs.iter(),
422 |e| {
423 let Event::CollectBarrierFail(info) = e else {
424 return None;
425 };
426 Some(json!(info).to_string())
427 },
428 Some(3),
429 );
430
431 let _ = writeln!(s);
432 let _ = writeln!(s, "latest barrier injection failures");
433 Self::write_event_logs_impl(
434 s,
435 event_logs.iter(),
436 |e| {
437 let Event::InjectBarrierFail(info) = e else {
438 return None;
439 };
440 Some(json!(info).to_string())
441 },
442 Some(3),
443 );
444
445 let _ = writeln!(s);
446 let _ = writeln!(s, "latest worker node panics");
447 Self::write_event_logs_impl(
448 s,
449 event_logs.iter(),
450 |e| {
451 let Event::WorkerNodePanic(info) = e else {
452 return None;
453 };
454 Some(json!(info).to_string())
455 },
456 Some(10),
457 );
458
459 let _ = writeln!(s);
460 let _ = writeln!(s, "latest create stream job failures");
461 Self::write_event_logs_impl(
462 s,
463 event_logs.iter(),
464 |e| {
465 let Event::CreateStreamJobFail(info) = e else {
466 return None;
467 };
468 Some(json!(info).to_string())
469 },
470 Some(3),
471 );
472
473 let _ = writeln!(s);
474 let _ = writeln!(s, "latest dirty stream job clear-ups");
475 Self::write_event_logs_impl(
476 s,
477 event_logs.iter(),
478 |e| {
479 let Event::DirtyStreamJobClear(info) = e else {
480 return None;
481 };
482 Some(json!(info).to_string())
483 },
484 Some(3),
485 );
486 }
487
488 fn write_event_logs_impl<'a, F>(
489 s: &mut String,
490 event_logs: impl Iterator<Item = &'a EventLog>,
491 get_event_info: F,
492 limit: Option<usize>,
493 ) where
494 F: Fn(&Event) -> Option<String>,
495 {
496 use comfy_table::{Row, Table};
497 let mut table = Table::new();
498 table.set_header({
499 let mut row = Row::new();
500 row.add_cell("created_at".into());
501 row.add_cell("info".into());
502 row
503 });
504 let mut row_count = 0;
505 for event_log in event_logs {
506 let Some(ref inner) = event_log.event else {
507 continue;
508 };
509 if let Some(limit) = limit
510 && row_count >= limit
511 {
512 break;
513 }
514 let mut row = Row::new();
515 let ts = event_log
516 .timestamp
517 .and_then(|ts| Timestamptz::from_millis(ts as _).map(|ts| ts.to_string()));
518 try_add_cell(&mut row, ts);
519 if let Some(info) = get_event_info(inner) {
520 row.add_cell(info.into());
521 row_count += 1;
522 } else {
523 continue;
524 }
525 table.add_row(row);
526 }
527 let _ = writeln!(s, "{table}");
528 }
529
530 async fn write_storage(&self, s: &mut String) {
531 let mut sst_num = 0;
532 let mut sst_total_file_size = 0;
533 let back_pressured_compaction_groups = self
534 .hummock_manger
535 .write_limits()
536 .await
537 .into_iter()
538 .filter_map(|(k, v)| {
539 if v.table_ids.is_empty() {
540 None
541 } else {
542 Some(k)
543 }
544 })
545 .join(",");
546 if !back_pressured_compaction_groups.is_empty() {
547 let _ = writeln!(
548 s,
549 "back pressured compaction groups: {back_pressured_compaction_groups}"
550 );
551 }
552
553 #[derive(PartialEq, Eq)]
554 struct SstableSort {
555 compaction_group_id: CompactionGroupId,
556 sst_id: HummockSstableId,
557 delete_ratio: u64,
558 }
559 impl PartialOrd for SstableSort {
560 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
561 Some(self.cmp(other))
562 }
563 }
564 impl Ord for SstableSort {
565 fn cmp(&self, other: &Self) -> Ordering {
566 self.delete_ratio.cmp(&other.delete_ratio)
567 }
568 }
569 fn top_k_sstables(
570 top_k: usize,
571 heap: &mut BinaryHeap<Reverse<SstableSort>>,
572 e: SstableSort,
573 ) {
574 if heap.len() < top_k {
575 heap.push(Reverse(e));
576 } else if let Some(mut p) = heap.peek_mut()
577 && e.delete_ratio > p.0.delete_ratio
578 {
579 *p = Reverse(e);
580 }
581 }
582
583 let top_k = 10;
584 let mut top_tombstone_delete_sst = BinaryHeap::with_capacity(top_k);
585 let compaction_group_num = self
586 .hummock_manger
587 .on_current_version(|version| {
588 for compaction_group in version.levels.values() {
589 let mut visit_level = |level: &Level| {
590 sst_num += level.table_infos.len();
591 sst_total_file_size +=
592 level.table_infos.iter().map(|t| t.sst_size).sum::<u64>();
593 for sst in &level.table_infos {
594 if sst.total_key_count == 0 {
595 continue;
596 }
597 let tombstone_delete_ratio =
598 sst.stale_key_count * 10000 / sst.total_key_count;
599 let e = SstableSort {
600 compaction_group_id: compaction_group.group_id,
601 sst_id: sst.sst_id,
602 delete_ratio: tombstone_delete_ratio,
603 };
604 top_k_sstables(top_k, &mut top_tombstone_delete_sst, e);
605 }
606 };
607 let l0 = &compaction_group.l0;
608 for level in &l0.sub_levels {
610 visit_level(level);
611 }
612 for level in &compaction_group.levels {
613 visit_level(level);
614 }
615 }
616 version.levels.len()
617 })
618 .await;
619
620 let _ = writeln!(s, "number of SSTables: {sst_num}");
621 let _ = writeln!(s, "total size of SSTables (byte): {sst_total_file_size}");
622 let _ = writeln!(s, "number of compaction groups: {compaction_group_num}");
623 use comfy_table::{Row, Table};
624 fn format_table(heap: BinaryHeap<Reverse<SstableSort>>) -> Table {
625 let mut table = Table::new();
626 table.set_header({
627 let mut row = Row::new();
628 row.add_cell("compaction group id".into());
629 row.add_cell("sstable id".into());
630 row.add_cell("delete ratio".into());
631 row
632 });
633 for sst in &heap.into_sorted_vec() {
634 let mut row = Row::new();
635 row.add_cell(sst.0.compaction_group_id.into());
636 row.add_cell(sst.0.sst_id.into());
637 row.add_cell(format!("{:.2}%", sst.0.delete_ratio as f64 / 100.0).into());
638 table.add_row(row);
639 }
640 table
641 }
642 let _ = writeln!(s);
643 let _ = writeln!(s, "top tombstone delete ratio");
644 let _ = writeln!(s, "{}", format_table(top_tombstone_delete_sst));
645 let _ = writeln!(s);
646
647 let _ = writeln!(s);
648 self.write_storage_prometheus(s).await;
649 }
650
651 async fn write_iceberg_compaction_schedule(
652 &self,
653 s: &mut String,
654 database_id: Option<DatabaseId>,
655 ) {
656 use comfy_table::{Row, Table};
657
658 let sink_names = match self.metadata_manager.catalog_controller.list_sinks().await {
659 Ok(sinks) => sinks
660 .into_iter()
661 .filter(|sink| database_id.is_none_or(|id| sink.database_id == id))
662 .map(|sink| (sink.id.as_raw_id(), sink.name))
663 .collect::<BTreeMap<u32, String>>(),
664 Err(err) => {
665 tracing::warn!(error=?err.as_report(), "failed to list sinks");
666 return;
667 }
668 };
669
670 let statuses = self.iceberg_compaction_manager.list_compaction_statuses();
671
672 let _ = writeln!(s, "ICEBERG COMPACTION SCHEDULE");
673
674 let mut table = Table::new();
675 table.set_header({
676 let mut row = Row::new();
677 row.add_cell("sink_id".into());
678 row.add_cell("sink_name".into());
679 row.add_cell("task_type".into());
680 row.add_cell("schedule_state".into());
681 row.add_cell("trigger_interval_sec".into());
682 row.add_cell("trigger_snapshot_count".into());
683 row.add_cell("pending_snapshot_count".into());
684 row.add_cell("next_compaction_after_sec".into());
685 row.add_cell("is_triggerable".into());
686 row.add_cell("wait_reason".into());
687 row.add_cell("retry_after_sec".into());
688 row.add_cell("blocked_age_sec".into());
689 row
690 });
691
692 for status in statuses {
693 if database_id.is_some() && !sink_names.contains_key(&status.sink_id.as_raw_id()) {
694 continue;
695 }
696 let mut row = Row::new();
697 row.add_cell(status.sink_id.as_raw_id().into());
698 try_add_cell(
699 &mut row,
700 sink_names.get(&status.sink_id.as_raw_id()).cloned(),
701 );
702 row.add_cell(status.task_type.into());
703 row.add_cell(status.schedule_state.into());
704 row.add_cell(status.trigger_interval_sec.into());
705 row.add_cell((status.trigger_snapshot_count as u64).into());
706 try_add_cell(
707 &mut row,
708 status.pending_snapshot_count.map(|count| count as u64),
709 );
710 try_add_cell(&mut row, status.next_compaction_after_sec);
711 row.add_cell(status.is_triggerable.into());
712 row.add_cell(status.wait_reason.into());
713 try_add_cell(&mut row, status.retry_after_sec);
714 try_add_cell(&mut row, status.blocked_age_sec);
715 table.add_row(row);
716 }
717
718 let _ = writeln!(s, "{table}");
719 }
720
721 async fn write_streaming_prometheus(&self, s: &mut String) {
722 let _ = writeln!(s, "top sources by throughput (rows/s)");
723 let query = format!(
724 "topk(3, sum(rate(stream_source_output_rows_counts{{{}}}[10m]))by (source_name))",
725 self.prometheus_selector
726 );
727 self.write_instant_vector_impl(s, &query, vec!["source_name"])
728 .await;
729
730 let _ = writeln!(s);
731 let _ = writeln!(s, "top materialized views by throughput (rows/s)");
732 let query = format!(
733 "topk(3, sum(rate(stream_mview_input_row_count{{{}}}[10m]))by (table_id))",
734 self.prometheus_selector
735 );
736 self.write_instant_vector_impl(s, &query, vec!["table_id"])
737 .await;
738
739 let _ = writeln!(s);
740 let _ = writeln!(s, "top join executor by matched rows");
741 let query = format!(
742 "topk(3, histogram_quantile(0.9, sum(rate(stream_join_matched_join_keys_bucket{{{}}}[10m])) by (le, fragment_id, table_id)))",
743 self.prometheus_selector
744 );
745 self.write_instant_vector_impl(s, &query, vec!["table_id", "fragment_id"])
746 .await;
747 }
748
749 async fn write_storage_prometheus(&self, s: &mut String) {
750 let _ = writeln!(s, "top Hummock Get by duration (second)");
751 let query = format!(
752 "topk(3, histogram_quantile(0.9, sum(rate(state_store_get_duration_bucket{{{}}}[10m])) by (le, table_id)))",
753 self.prometheus_selector
754 );
755 self.write_instant_vector_impl(s, &query, vec!["table_id"])
756 .await;
757
758 let _ = writeln!(s);
759 let _ = writeln!(s, "top Hummock Iter Init by duration (second)");
760 let query = format!(
761 "topk(3, histogram_quantile(0.9, sum(rate(state_store_iter_init_duration_bucket{{{}}}[10m])) by (le, table_id)))",
762 self.prometheus_selector
763 );
764 self.write_instant_vector_impl(s, &query, vec!["table_id"])
765 .await;
766
767 let _ = writeln!(s);
768 let _ = writeln!(s, "top table commit flush by size (byte)");
769 let query = format!(
770 "topk(3, sum(rate(storage_commit_write_throughput{{{}}}[10m])) by (table_id))",
771 self.prometheus_selector
772 );
773 self.write_instant_vector_impl(s, &query, vec!["table_id"])
774 .await;
775
776 let _ = writeln!(s);
777 let _ = writeln!(s, "object store read throughput (bytes/s)");
778 let query = format!(
779 "sum(rate(object_store_read_bytes{{{}}}[10m])) by (job, instance)",
780 merge_prometheus_selector([&self.prometheus_selector, "job=~\"compute|compactor\""])
781 );
782 self.write_instant_vector_impl(s, &query, vec!["job", "instance"])
783 .await;
784
785 let _ = writeln!(s);
786 let _ = writeln!(s, "object store write throughput (bytes/s)");
787 let query = format!(
788 "sum(rate(object_store_write_bytes{{{}}}[10m])) by (job, instance)",
789 merge_prometheus_selector([&self.prometheus_selector, "job=~\"compute|compactor\""])
790 );
791 self.write_instant_vector_impl(s, &query, vec!["job", "instance"])
792 .await;
793
794 let _ = writeln!(s);
795 let _ = writeln!(s, "object store operation rate");
796 let query = format!(
797 "sum(rate(object_store_operation_latency_count{{{}}}[10m])) by (le, type, job, instance)",
798 merge_prometheus_selector([
799 &self.prometheus_selector,
800 "job=~\"compute|compactor\", type!~\"streaming_upload_write_bytes|streaming_read_read_bytes|streaming_read\""
801 ])
802 );
803 self.write_instant_vector_impl(s, &query, vec!["type", "job", "instance"])
804 .await;
805
806 let _ = writeln!(s);
807 let _ = writeln!(s, "object store operation duration (second)");
808 let query = format!(
809 "histogram_quantile(0.9, sum(rate(object_store_operation_latency_bucket{{{}}}[10m])) by (le, type, job, instance))",
810 merge_prometheus_selector([
811 &self.prometheus_selector,
812 "job=~\"compute|compactor\", type!~\"streaming_upload_write_bytes|streaming_read\""
813 ])
814 );
815 self.write_instant_vector_impl(s, &query, vec!["type", "job", "instance"])
816 .await;
817 }
818
819 async fn write_instant_vector_impl(&self, s: &mut String, query: &str, labels: Vec<&str>) {
820 let Some(ref client) = self.prometheus_client else {
821 return;
822 };
823 if let Ok(Vector(instant_vec)) = client
824 .query(query)
825 .get()
826 .await
827 .map(|result| result.into_inner().0)
828 {
829 for i in instant_vec {
830 let l = labels
831 .iter()
832 .map(|label| {
833 format!(
834 "{}={}",
835 *label,
836 i.metric()
837 .get(*label)
838 .map(|s| s.as_str())
839 .unwrap_or_default()
840 )
841 })
842 .join(",");
843 let _ = writeln!(s, "{}: {:.3}", l, i.sample().value());
844 }
845 }
846 }
847
848 async fn write_await_tree(
849 &self,
850 s: &mut String,
851 actor_traces_format: ActorTracesFormat,
852 database_id: Option<DatabaseId>,
853 ) {
854 let actor_ids = if database_id.is_some() {
855 Some(
856 self.metadata_manager
857 .catalog_controller
858 .list_actor_info(database_id)
859 .await
860 .map(|actors| actors.into_iter().map(|(id, ..)| id.as_raw_id()).collect()),
861 )
862 } else {
863 None
864 };
865 let all = dump_cluster_await_tree(
866 &self.metadata_manager,
867 &self.await_tree_reg,
868 actor_traces_format,
869 )
870 .await;
871
872 if let Ok(all) = all {
873 write_await_tree_response(s, all, actor_ids);
874 } else {
875 tracing::warn!("failed to dump await tree");
876 }
877 }
878
879 async fn write_table_definition(
880 &self,
881 s: &mut String,
882 database_id: Option<DatabaseId>,
883 ) -> MetaResult<()> {
884 let sources = self
885 .metadata_manager
886 .catalog_controller
887 .list_sources()
888 .await?
889 .into_iter()
890 .filter(|s| database_id.is_none_or(|id| s.database_id == id))
891 .map(|s| {
892 (
893 s.id.into(),
894 (
895 s.name,
896 s.database_id,
897 s.schema_id,
898 s.definition,
899 s.created_at_epoch,
900 ),
901 )
902 })
903 .collect::<BTreeMap<_, _>>();
904 let mut user_tables = BTreeMap::new();
905 let mut mvs = BTreeMap::new();
906 let mut indexes = BTreeMap::new();
907 let mut internal_tables = BTreeMap::new();
908 {
909 let grouped = self
910 .metadata_manager
911 .catalog_controller
912 .list_all_state_tables()
913 .await?
914 .into_iter()
915 .filter(|t| database_id.is_none_or(|id| t.database_id == id))
916 .chunk_by(|t| t.table_type());
917 for (table_type, tables) in &grouped {
918 let tables = tables.into_iter().map(|t| {
919 (
920 t.id.into(),
921 (
922 t.name,
923 t.database_id,
924 t.schema_id,
925 t.definition,
926 t.created_at_epoch,
927 ),
928 )
929 });
930 match table_type {
931 PbTableType::Table => user_tables.extend(tables),
932 PbTableType::MaterializedView => mvs.extend(tables),
933 PbTableType::Index | PbTableType::VectorIndex => indexes.extend(tables),
934 PbTableType::Internal => internal_tables.extend(tables),
935 PbTableType::Unspecified => {
936 tracing::error!("unspecified table type: {:?}", tables.collect_vec());
937 }
938 }
939 }
940 }
941 let sinks = self
942 .metadata_manager
943 .catalog_controller
944 .list_sinks()
945 .await?
946 .into_iter()
947 .filter(|s| database_id.is_none_or(|id| s.database_id == id))
948 .map(|s| {
949 (
950 s.id.into(),
951 (
952 s.name,
953 s.database_id,
954 s.schema_id,
955 s.definition,
956 s.created_at_epoch,
957 ),
958 )
959 })
960 .collect::<BTreeMap<_, _>>();
961 let views = self
962 .metadata_manager
963 .catalog_controller
964 .list_views()
965 .await?
966 .into_iter()
967 .filter(|v| database_id.is_none_or(|id| v.database_id == id))
968 .map(|v| {
969 (
970 v.id.into(),
971 (v.name, v.database_id, v.schema_id, v.sql, None),
972 )
973 })
974 .collect::<BTreeMap<_, _>>();
975 let mut streaming_jobs = self
976 .metadata_manager
977 .catalog_controller
978 .list_streaming_job_infos()
979 .await?;
980 streaming_jobs.retain(|job| database_id.is_none_or(|id| job.database_id == id));
981 streaming_jobs.sort_by_key(|info| (info.obj_type as usize, info.job_id));
982 {
983 use comfy_table::{Row, Table};
984 let mut table = Table::new();
985 table.set_header({
986 let mut row = Row::new();
987 row.add_cell("job_id".into());
988 row.add_cell("name".into());
989 row.add_cell("obj_type".into());
990 row.add_cell("state".into());
991 row.add_cell("parallelism".into());
992 row.add_cell("max_parallelism".into());
993 row.add_cell("resource_group".into());
994 row.add_cell("database_id".into());
995 row.add_cell("schema_id".into());
996 row.add_cell("config_override".into());
997 row
998 });
999 for job in streaming_jobs {
1000 let mut row = Row::new();
1001 row.add_cell(job.job_id.into());
1002 row.add_cell(job.name.into());
1003 row.add_cell(job.obj_type.as_str().into());
1004 row.add_cell(format_job_status(job.job_status).into());
1005 row.add_cell(
1006 format_streaming_parallelism(
1007 &job.parallelism,
1008 job.adaptive_parallelism_strategy.as_deref(),
1009 )
1010 .into(),
1011 );
1012 row.add_cell(job.max_parallelism.into());
1013 row.add_cell(job.resource_group.into());
1014 row.add_cell(job.database_id.into());
1015 row.add_cell(job.schema_id.into());
1016 row.add_cell(job.config_override.into());
1017 table.add_row(row);
1018 }
1019 let _ = writeln!(s);
1020 let _ = writeln!(s, "STREAMING JOB");
1021 let _ = writeln!(s, "{table}");
1022 }
1023 let catalogs = [
1024 ("SOURCE", sources),
1025 ("TABLE", user_tables),
1026 ("MATERIALIZED VIEW", mvs),
1027 ("INDEX", indexes),
1028 ("SINK", sinks),
1029 ("VIEW", views),
1030 ("INTERNAL TABLE", internal_tables),
1031 ];
1032 let mut obj_id_to_name: HashMap<ObjectId, _> = HashMap::new();
1033 for (title, items) in catalogs {
1034 use comfy_table::{Row, Table};
1035 let mut table = Table::new();
1036 table.set_header({
1037 let mut row = Row::new();
1038 row.add_cell("id".into());
1039 row.add_cell("name".into());
1040 row.add_cell("database_id".into());
1041 row.add_cell("schema_id".into());
1042 row.add_cell("created_at".into());
1043 row.add_cell("definition".into());
1044 row
1045 });
1046 for (id, (name, database_id, schema_id, definition, created_at_epoch)) in items {
1047 obj_id_to_name.insert(id, name.clone());
1048 let mut row = Row::new();
1049 let may_redact = redact_sql(&definition, self.sql_option_redaction.clone())
1050 .unwrap_or_else(|| "[REDACTED]".into());
1051 let created_at = if let Some(created_at_epoch) = created_at_epoch {
1052 format!("{}", Epoch::from(created_at_epoch).as_timestamptz())
1053 } else {
1054 "".into()
1055 };
1056 row.add_cell(id.into());
1057 row.add_cell(name.into());
1058 row.add_cell(database_id.into());
1059 row.add_cell(schema_id.into());
1060 row.add_cell(created_at.into());
1061 row.add_cell(may_redact.into());
1062 table.add_row(row);
1063 }
1064 let _ = writeln!(s);
1065 let _ = writeln!(s, "{title}");
1066 let _ = writeln!(s, "{table}");
1067 }
1068
1069 let mut fragments = BTreeMap::new();
1070 for (actor_id, fragment_id, job_id, schema_id, obj_type) in self
1071 .metadata_manager
1072 .catalog_controller
1073 .list_actor_info(database_id)
1074 .await?
1075 {
1076 let entry = fragments
1077 .entry(fragment_id)
1078 .or_insert_with(|| (job_id, schema_id, obj_type, Vec::new()));
1079 entry.3.push(actor_id);
1080 }
1081
1082 use comfy_table::{Row, Table};
1083 let mut table = Table::new();
1084 table.set_header({
1085 let mut row = Row::new();
1086 row.add_cell("fragment_id".into());
1087 row.add_cell("job_id".into());
1088 row.add_cell("schema_id".into());
1089 row.add_cell("type".into());
1090 row.add_cell("name".into());
1091 row.add_cell("actor_count".into());
1092 row.add_cell("actors".into());
1093 row
1094 });
1095 for (fragment_id, (job_id, schema_id, obj_type, mut actor_ids)) in fragments {
1096 actor_ids.sort_unstable();
1097 let mut row = Row::new();
1098 row.add_cell(fragment_id.into());
1099 row.add_cell(job_id.into());
1100 row.add_cell(schema_id.into());
1101 row.add_cell(obj_type.as_str().into());
1102 row.add_cell(
1103 obj_id_to_name
1104 .get(&job_id)
1105 .cloned()
1106 .unwrap_or_default()
1107 .into(),
1108 );
1109 row.add_cell(actor_ids.len().into());
1110 row.add_cell(actor_ids.iter().join(", ").into());
1111 table.add_row(row);
1112 }
1113 let _ = writeln!(s);
1114 let _ = writeln!(s, "FRAGMENT");
1115 let _ = writeln!(s, "{table}");
1116 Ok(())
1117 }
1118
1119 fn write_license(&self, s: &mut String) {
1120 use comfy_table::presets::ASCII_BORDERS_ONLY;
1121 use comfy_table::{ContentArrangement, Row, Table};
1122
1123 let mut table = Table::new();
1124 table.load_style(ASCII_BORDERS_ONLY);
1125 table.set_content_arrangement(ContentArrangement::Dynamic);
1126 table.set_header({
1127 let mut row = Row::new();
1128 row.add_cell("field".into());
1129 row.add_cell("value".into());
1130 row
1131 });
1132
1133 match LicenseManager::get().license() {
1134 Ok(license) => {
1135 let fmt_option = |value: Option<u64>| match value {
1136 Some(v) => v.to_string(),
1137 None => "unlimited".to_owned(),
1138 };
1139
1140 let expires_at = if license.exp == u64::MAX {
1141 "never".to_owned()
1142 } else {
1143 let exp_i64 = license.exp as i64;
1144 chrono::DateTime::<chrono::Utc>::from_timestamp(exp_i64, 0)
1145 .map(|ts| ts.to_rfc3339())
1146 .unwrap_or_else(|| format!("invalid ({})", license.exp))
1147 };
1148
1149 let mut row = Row::new();
1150 row.add_cell("status".into());
1151 row.add_cell("valid".into());
1152 table.add_row(row);
1153
1154 let mut row = Row::new();
1155 row.add_cell("tier".into());
1156 row.add_cell(license.tier.name().into());
1157 table.add_row(row);
1158
1159 let mut row = Row::new();
1160 row.add_cell("expires_at".into());
1161 row.add_cell(expires_at.into());
1162 table.add_row(row);
1163
1164 let mut row = Row::new();
1165 row.add_cell("rwu_limit".into());
1166 row.add_cell(fmt_option(license.rwu_limit.map(|v| v.get())).into());
1167 table.add_row(row);
1168
1169 let mut row = Row::new();
1170 row.add_cell("cpu_core_limit".into());
1171 row.add_cell(fmt_option(license.cpu_core_limit()).into());
1172 table.add_row(row);
1173
1174 let mut row = Row::new();
1175 row.add_cell("memory_limit_bytes".into());
1176 row.add_cell(fmt_option(license.memory_limit()).into());
1177 table.add_row(row);
1178
1179 let mut features: Vec<_> = license
1180 .tier
1181 .available_features()
1182 .map(|f| f.name())
1183 .collect();
1184 features.sort_unstable();
1185 let feature_summary = format_features(&features);
1186
1187 let mut row = Row::new();
1188 row.add_cell("available_features".into());
1189 row.add_cell(feature_summary.into());
1190 table.add_row(row);
1191 }
1192 Err(error) => {
1193 let mut row = Row::new();
1194 row.add_cell("status".into());
1195 row.add_cell("invalid".into());
1196 table.add_row(row);
1197
1198 let mut row = Row::new();
1199 row.add_cell("error".into());
1200 row.add_cell(error.to_report_string().into());
1201 table.add_row(row);
1202 }
1203 }
1204
1205 let _ = writeln!(s, "LICENSE");
1206 let _ = writeln!(s, "{table}");
1207 }
1208
1209 async fn write_params(&self, s: &mut String) {
1210 let params = self.system_params_controller.get_params().await;
1211
1212 use comfy_table::{Row, Table};
1213 let mut table = Table::new();
1214 table.set_header({
1215 let mut row = Row::new();
1216 row.add_cell("key".into());
1217 row.add_cell("value".into());
1218 row
1219 });
1220
1221 let mut row = Row::new();
1222 row.add_cell("barrier_interval_ms".into());
1223 row.add_cell(params.barrier_interval_ms().to_string().into());
1224 table.add_row(row);
1225
1226 let mut row = Row::new();
1227 row.add_cell("checkpoint_frequency".into());
1228 row.add_cell(params.checkpoint_frequency().to_string().into());
1229 table.add_row(row);
1230
1231 let mut row = Row::new();
1232 row.add_cell("state_store".into());
1233 row.add_cell(params.state_store().to_string().into());
1234 table.add_row(row);
1235
1236 let mut row = Row::new();
1237 row.add_cell("data_directory".into());
1238 row.add_cell(params.data_directory().to_owned().into());
1239 table.add_row(row);
1240
1241 let mut row = Row::new();
1242 row.add_cell("max_concurrent_creating_streaming_jobs".into());
1243 row.add_cell(
1244 params
1245 .max_concurrent_creating_streaming_jobs()
1246 .to_string()
1247 .into(),
1248 );
1249 table.add_row(row);
1250
1251 let mut row = Row::new();
1252 row.add_cell("time_travel_retention_ms".into());
1253 row.add_cell(params.time_travel_retention_ms().to_string().into());
1254 table.add_row(row);
1255
1256 let mut row = Row::new();
1257 row.add_cell("per_database_isolation".into());
1258 row.add_cell(params.per_database_isolation().to_string().into());
1259 table.add_row(row);
1260
1261 let _ = writeln!(s, "SYSTEM PARAMS");
1262 let _ = writeln!(s, "{table}");
1263 }
1264}
1265
1266fn write_await_tree_response(
1267 s: &mut String,
1268 mut traces: StackTraceResponse,
1269 actor_ids: Option<MetaResult<HashSet<u32>>>,
1270) {
1271 match actor_ids {
1272 Some(Ok(actor_ids)) => traces.actor_traces.retain(|id, _| actor_ids.contains(id)),
1273 Some(Err(err)) => {
1274 tracing::warn!(error = ?err.as_report(), "failed to list actors for await tree filter");
1275 traces.actor_traces.clear();
1276 let _ = writeln!(
1277 s,
1278 "Actor traces omitted: failed to list actors for the database filter. \
1279 Shared cluster await-tree diagnostics are included below."
1280 );
1281 }
1282 None => {}
1283 }
1284 write!(s, "{}", traces.output()).unwrap();
1285}
1286
1287fn try_add_cell<T: Into<comfy_table::Cell>>(row: &mut comfy_table::Row, t: Option<T>) {
1288 match t {
1289 Some(t) => {
1290 row.add_cell(t.into());
1291 }
1292 None => {
1293 row.add_cell("".into());
1294 }
1295 }
1296}
1297
1298fn merge_prometheus_selector<'a>(selectors: impl IntoIterator<Item = &'a str>) -> String {
1299 selectors.into_iter().filter(|s| !s.is_empty()).join(",")
1300}
1301
1302fn redact_sql(sql: &str, redaction: SqlOptionRedactionRef) -> Option<String> {
1303 match Parser::parse_sql(sql) {
1304 Ok(sqls) => Some(
1305 sqls.into_iter()
1306 .map(|sql| sql.to_redacted_string(redaction.clone()))
1307 .join(";"),
1308 ),
1309 Err(_) => None,
1310 }
1311}
1312
1313fn format_features(features: &[&'static str]) -> String {
1314 if features.is_empty() {
1315 return "(none)".into();
1316 }
1317
1318 const PER_LINE: usize = 6;
1319 features
1320 .chunks(PER_LINE)
1321 .map(|chunk| format!(" {}", chunk.join(", ")))
1322 .collect::<Vec<_>>()
1323 .join("\n")
1324}
1325
1326fn format_job_status(status: JobStatus) -> &'static str {
1327 match status {
1328 JobStatus::Initial => "initial",
1329 JobStatus::Creating => "creating",
1330 JobStatus::Created => "created",
1331 }
1332}
1333
1334fn format_streaming_parallelism(
1335 parallelism: &StreamingParallelism,
1336 adaptive_parallelism_strategy: Option<&str>,
1337) -> String {
1338 match parallelism {
1339 StreamingParallelism::Adaptive => adaptive_parallelism_strategy
1340 .and_then(format_adaptive_parallelism_strategy)
1341 .unwrap_or_else(|| "adaptive".into()),
1342 StreamingParallelism::Fixed(n) => n.to_string(),
1343 StreamingParallelism::Custom => adaptive_parallelism_strategy
1344 .and_then(format_adaptive_parallelism_strategy)
1345 .unwrap_or_else(|| "custom".into()),
1346 }
1347}
1348
1349fn format_adaptive_parallelism_strategy(strategy: &str) -> Option<String> {
1350 parse_strategy(strategy)
1351 .ok()
1352 .map(|strategy| match strategy {
1353 AdaptiveParallelismStrategy::Auto | AdaptiveParallelismStrategy::Full => {
1354 "adaptive".to_owned()
1355 }
1356 AdaptiveParallelismStrategy::Bounded(n) => format!("bounded({n})"),
1357 AdaptiveParallelismStrategy::Ratio(r) => format!("ratio({r})"),
1358 })
1359}