Skip to main content

risingwave_meta/manager/
diagnose.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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            // is_streaming and is_serving are only meaningful for ComputeNode
324            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            // is_iceberg_compactor is only meaningful for Compactor worker type
339            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                    // FIXME: why chaining levels iter leads to segmentation fault?
609                    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}