1mod prometheus;
16
17use std::net::SocketAddr;
18use std::sync::Arc;
19
20use anyhow::{Context as _, Result, anyhow};
21use axum::Router;
22use axum::extract::{Extension, Path};
23use axum::http::{Method, StatusCode};
24use axum::response::{IntoResponse, Response};
25use axum::routing::get;
26use risingwave_common_heap_profiling::ProfileServiceImpl;
27use risingwave_rpc_client::MonitorClientPool;
28use tokio::net::TcpListener;
29use tower::ServiceBuilder;
30use tower_http::add_extension::AddExtensionLayer;
31use tower_http::compression::CompressionLayer;
32use tower_http::cors::{self, CorsLayer};
33
34use crate::hummock::HummockManagerRef;
35use crate::manager::MetadataManager;
36use crate::manager::diagnose::DiagnoseCommandRef;
37
38#[derive(Clone)]
39pub struct DashboardService {
40 pub await_tree_reg: await_tree::Registry,
41 pub dashboard_addr: SocketAddr,
42 pub prometheus_client: Option<prometheus_http_query::Client>,
43 pub prometheus_selector: String,
44 pub metadata_manager: MetadataManager,
45 pub hummock_manager: HummockManagerRef,
46 pub monitor_clients: MonitorClientPool,
47 pub diagnose_command: DiagnoseCommandRef,
48 pub profile_service: ProfileServiceImpl,
49 pub trace_state: otlp_embedded::StateRef,
50}
51
52pub type Service = Arc<DashboardService>;
53
54pub(super) mod handlers {
55 use std::cmp::min;
56 use std::collections::HashMap;
57
58 use anyhow::Context;
59 use axum::Json;
60 use axum::extract::Query;
61 use futures::future::join_all;
62 use itertools::Itertools;
63 use risingwave_common::id::JobId;
64 use risingwave_meta_model::WorkerId;
65 use risingwave_pb::catalog::table::TableType;
66 use risingwave_pb::catalog::{
67 Index, PbDatabase, PbFunction, PbSchema, Sink, Source, Subscription, Table, View,
68 };
69 use risingwave_pb::common::{WorkerNode, WorkerType};
70 use risingwave_pb::hummock::TableStats;
71 use risingwave_pb::meta::{
72 ActorIds, FragmentIdToActorIdMap, FragmentToRelationMap, PbTableFragments, RelationIdInfos,
73 };
74 use risingwave_pb::monitor_service::stack_trace_request::ActorTracesFormat;
75 use risingwave_pb::monitor_service::{
76 AnalyzeHeapRequest, ChannelDeltaStats, GetStreamingPrometheusStatsResponse,
77 GetStreamingStatsResponse, HeapProfilingRequest, HeapProfilingResponse,
78 ListHeapProfilingRequest, ListHeapProfilingResponse, ProfilingRequest, StackTraceResponse,
79 };
80 use risingwave_pb::user::PbUserInfo;
81 use serde::{Deserialize, Serialize};
82 use serde_json::json;
83 use thiserror_ext::AsReport;
84 use tonic::Request;
85
86 use super::*;
87 use crate::controller::fragment::StreamingJobInfo;
88 use crate::rpc::await_tree::{dump_cluster_await_tree, dump_worker_node_await_tree};
89
90 #[derive(Serialize)]
91 pub struct TableWithStats {
92 #[serde(flatten)]
93 pub table: Table,
94 pub total_size_bytes: i64,
95 pub total_key_count: i64,
96 pub total_key_size: i64,
97 pub total_value_size: i64,
98 pub compressed_size: u64,
99 }
100
101 impl TableWithStats {
102 pub fn from_table_and_stats(table: Table, stats: Option<&TableStats>) -> Self {
103 match stats {
104 Some(stats) => Self {
105 total_size_bytes: stats.total_key_size + stats.total_value_size,
106 total_key_count: stats.total_key_count,
107 total_key_size: stats.total_key_size,
108 total_value_size: stats.total_value_size,
109 compressed_size: stats.total_compressed_size,
110 table,
111 },
112 None => Self {
113 total_size_bytes: 0,
114 total_key_count: 0,
115 total_key_size: 0,
116 total_value_size: 0,
117 compressed_size: 0,
118 table,
119 },
120 }
121 }
122 }
123
124 pub struct DashboardError(anyhow::Error);
125 pub type Result<T> = std::result::Result<T, DashboardError>;
126
127 pub fn err(err: impl Into<anyhow::Error>) -> DashboardError {
128 DashboardError(err.into())
129 }
130
131 impl From<anyhow::Error> for DashboardError {
132 fn from(value: anyhow::Error) -> Self {
133 DashboardError(value)
134 }
135 }
136
137 impl IntoResponse for DashboardError {
138 fn into_response(self) -> axum::response::Response {
139 let mut resp = Json(json!({
140 "error": self.0.to_report_string(),
141 }))
142 .into_response();
143 *resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
144 resp
145 }
146 }
147
148 pub async fn list_clusters(
149 Path(ty): Path<i32>,
150 Extension(srv): Extension<Service>,
151 ) -> Result<Json<Vec<WorkerNode>>> {
152 let worker_type = match WorkerType::try_from(ty) {
153 Ok(WorkerType::Unspecified) | Err(_) => {
154 return Err(err(anyhow!("invalid worker type: {ty}")));
155 }
156 Ok(worker_type) => worker_type,
157 };
158 let mut result = srv
159 .metadata_manager
160 .list_worker_node(Some(worker_type), None)
161 .await
162 .map_err(err)?;
163 result.sort_unstable_by_key(|n| n.id);
164 Ok(result.into())
165 }
166
167 async fn list_table_catalogs_inner(
168 metadata_manager: &MetadataManager,
169 hummock_manager: &HummockManagerRef,
170 table_type: TableType,
171 ) -> Result<Json<Vec<TableWithStats>>> {
172 let tables = metadata_manager
173 .catalog_controller
174 .list_tables_by_type(table_type.into())
175 .await
176 .map_err(err)?;
177
178 let version_stats = hummock_manager.get_version_stats().await;
180
181 let tables_with_stats = tables
182 .into_iter()
183 .map(|table| {
184 let stats = version_stats.table_stats.get(&table.id);
185 TableWithStats::from_table_and_stats(table, stats)
186 })
187 .collect();
188
189 Ok(Json(tables_with_stats))
190 }
191
192 pub async fn list_materialized_views(
193 Extension(srv): Extension<Service>,
194 ) -> Result<Json<Vec<TableWithStats>>> {
195 list_table_catalogs_inner(
196 &srv.metadata_manager,
197 &srv.hummock_manager,
198 TableType::MaterializedView,
199 )
200 .await
201 }
202
203 pub async fn list_tables(
204 Extension(srv): Extension<Service>,
205 ) -> Result<Json<Vec<TableWithStats>>> {
206 list_table_catalogs_inner(
207 &srv.metadata_manager,
208 &srv.hummock_manager,
209 TableType::Table,
210 )
211 .await
212 }
213
214 pub async fn list_index_tables(
215 Extension(srv): Extension<Service>,
216 ) -> Result<Json<Vec<TableWithStats>>> {
217 list_table_catalogs_inner(
218 &srv.metadata_manager,
219 &srv.hummock_manager,
220 TableType::Index,
221 )
222 .await
223 }
224
225 pub async fn list_indexes(Extension(srv): Extension<Service>) -> Result<Json<Vec<Index>>> {
226 let indexes = srv
227 .metadata_manager
228 .catalog_controller
229 .list_indexes()
230 .await
231 .map_err(err)?;
232
233 Ok(Json(indexes))
234 }
235
236 pub async fn list_subscription(
237 Extension(srv): Extension<Service>,
238 ) -> Result<Json<Vec<Subscription>>> {
239 let subscriptions = srv
240 .metadata_manager
241 .catalog_controller
242 .list_subscriptions()
243 .await
244 .map_err(err)?;
245
246 Ok(Json(subscriptions))
247 }
248
249 pub async fn list_internal_tables(
250 Extension(srv): Extension<Service>,
251 ) -> Result<Json<Vec<TableWithStats>>> {
252 list_table_catalogs_inner(
253 &srv.metadata_manager,
254 &srv.hummock_manager,
255 TableType::Internal,
256 )
257 .await
258 }
259
260 pub async fn list_sources(Extension(srv): Extension<Service>) -> Result<Json<Vec<Source>>> {
261 let sources = srv.metadata_manager.list_sources().await.map_err(err)?;
262
263 Ok(Json(sources))
264 }
265
266 pub async fn list_sinks(Extension(srv): Extension<Service>) -> Result<Json<Vec<Sink>>> {
267 let sinks = srv
268 .metadata_manager
269 .catalog_controller
270 .list_sinks()
271 .await
272 .map_err(err)?;
273
274 Ok(Json(sinks))
275 }
276
277 pub async fn list_views(Extension(srv): Extension<Service>) -> Result<Json<Vec<View>>> {
278 let views = srv
279 .metadata_manager
280 .catalog_controller
281 .list_views()
282 .await
283 .map_err(err)?;
284
285 Ok(Json(views))
286 }
287
288 pub async fn list_functions(
289 Extension(srv): Extension<Service>,
290 ) -> Result<Json<Vec<PbFunction>>> {
291 let functions = srv
292 .metadata_manager
293 .catalog_controller
294 .list_functions()
295 .await
296 .map_err(err)?;
297
298 Ok(Json(functions))
299 }
300
301 pub async fn list_streaming_jobs(
302 Extension(srv): Extension<Service>,
303 ) -> Result<Json<Vec<StreamingJobInfo>>> {
304 let streaming_jobs = srv
305 .metadata_manager
306 .catalog_controller
307 .list_streaming_job_infos()
308 .await
309 .map_err(err)?;
310
311 Ok(Json(streaming_jobs))
312 }
313
314 pub async fn get_fragment_to_relation_map(
322 Extension(srv): Extension<Service>,
323 ) -> Result<Json<FragmentToRelationMap>> {
324 let table_fragments = srv
325 .metadata_manager
326 .catalog_controller
327 .table_fragments()
328 .await
329 .map_err(err)?;
330 let mut fragment_to_relation_map = HashMap::new();
331 for (relation_id, (tf, _, _)) in table_fragments {
332 for fragment_id in tf.fragments.keys() {
333 fragment_to_relation_map.insert(*fragment_id, relation_id);
334 }
335 }
336 let map = FragmentToRelationMap {
337 fragment_to_relation_map,
338 };
339 Ok(Json(map))
340 }
341
342 pub async fn get_relation_id_infos(
344 Extension(srv): Extension<Service>,
345 ) -> Result<Json<RelationIdInfos>> {
346 let table_fragments = srv
347 .metadata_manager
348 .catalog_controller
349 .table_fragments()
350 .await
351 .map_err(err)?;
352 let mut map = HashMap::new();
353 for (id, (tf, fragment_actors, _actor_status)) in table_fragments {
354 let mut fragment_id_to_actor_ids = HashMap::new();
355 for fragment_id in tf.fragments.keys() {
356 let actor_ids = fragment_actors
357 .get(fragment_id)
358 .into_iter()
359 .flat_map(|actors| actors.iter().map(|a| a.actor_id))
360 .collect_vec();
361 fragment_id_to_actor_ids.insert(*fragment_id, ActorIds { ids: actor_ids });
362 }
363 map.insert(
364 id.as_raw_id(),
365 FragmentIdToActorIdMap {
366 map: fragment_id_to_actor_ids,
367 },
368 );
369 }
370 let relation_id_infos = RelationIdInfos { map };
371
372 Ok(Json(relation_id_infos))
373 }
374
375 pub async fn list_fragments_by_job_id(
376 Extension(srv): Extension<Service>,
377 Path(job_id): Path<u32>,
378 ) -> Result<Json<PbTableFragments>> {
379 let job_id = JobId::new(job_id);
380 let (table_fragments, fragment_actors, actor_status) = srv
381 .metadata_manager
382 .catalog_controller
383 .get_job_fragments_by_id(job_id)
384 .await
385 .map_err(err)?;
386 let upstream_fragments = srv
387 .metadata_manager
388 .catalog_controller
389 .upstream_fragments(table_fragments.fragment_ids())
390 .await
391 .map_err(err)?;
392 let dispatchers = srv
393 .metadata_manager
394 .catalog_controller
395 .get_fragment_actor_dispatchers(
396 table_fragments.fragment_ids().map(|id| id as _).collect(),
397 )
398 .await
399 .map_err(err)?;
400 Ok(Json(table_fragments.to_protobuf(
401 &fragment_actors,
402 &upstream_fragments,
403 &dispatchers,
404 actor_status,
405 )))
406 }
407
408 pub async fn list_users(Extension(srv): Extension<Service>) -> Result<Json<Vec<PbUserInfo>>> {
409 let users = srv
410 .metadata_manager
411 .catalog_controller
412 .list_users()
413 .await
414 .map_err(err)?;
415
416 Ok(Json(users))
417 }
418
419 pub async fn list_databases(
420 Extension(srv): Extension<Service>,
421 ) -> Result<Json<Vec<PbDatabase>>> {
422 let databases = srv
423 .metadata_manager
424 .catalog_controller
425 .list_databases()
426 .await
427 .map_err(err)?;
428
429 Ok(Json(databases))
430 }
431
432 pub async fn list_schemas(Extension(srv): Extension<Service>) -> Result<Json<Vec<PbSchema>>> {
433 let schemas = srv
434 .metadata_manager
435 .catalog_controller
436 .list_schemas()
437 .await
438 .map_err(err)?;
439
440 Ok(Json(schemas))
441 }
442
443 #[derive(Serialize)]
444 #[serde(rename_all = "camelCase")]
445 pub struct DashboardObjectDependency {
446 pub object_id: u32,
447 pub referenced_object_id: u32,
448 }
449
450 pub async fn list_object_dependencies(
451 Extension(srv): Extension<Service>,
452 ) -> Result<Json<Vec<DashboardObjectDependency>>> {
453 let object_dependencies = srv
454 .metadata_manager
455 .catalog_controller
456 .list_all_object_dependencies()
457 .await
458 .map_err(err)?;
459
460 let result = object_dependencies
461 .into_iter()
462 .map(|dependency| DashboardObjectDependency {
463 object_id: dependency.object_id.as_raw_id(),
464 referenced_object_id: dependency.referenced_object_id.as_raw_id(),
465 })
466 .collect();
467
468 Ok(Json(result))
469 }
470
471 #[derive(Debug, Deserialize)]
472 pub struct AwaitTreeDumpParams {
473 #[serde(default = "await_tree_default_format")]
474 format: String,
475 }
476
477 impl AwaitTreeDumpParams {
478 pub fn actor_traces_format(&self) -> Result<ActorTracesFormat> {
480 Ok(match self.format.as_str() {
481 "text" => ActorTracesFormat::Text,
482 "json" => ActorTracesFormat::Json,
483 _ => {
484 return Err(err(anyhow!(
485 "Unsupported format `{}`, only `text` and `json` are supported for now",
486 self.format
487 )));
488 }
489 })
490 }
491 }
492
493 fn await_tree_default_format() -> String {
494 "text".to_owned()
496 }
497
498 pub async fn dump_await_tree_all(
499 Query(params): Query<AwaitTreeDumpParams>,
500 Extension(srv): Extension<Service>,
501 ) -> Result<Json<StackTraceResponse>> {
502 let actor_traces_format = params.actor_traces_format()?;
503
504 let res = dump_cluster_await_tree(
505 &srv.metadata_manager,
506 &srv.await_tree_reg,
507 actor_traces_format,
508 )
509 .await
510 .map_err(err)?;
511
512 Ok(res.into())
513 }
514
515 pub async fn dump_await_tree(
516 Path(worker_id): Path<WorkerId>,
517 Query(params): Query<AwaitTreeDumpParams>,
518 Extension(srv): Extension<Service>,
519 ) -> Result<Json<StackTraceResponse>> {
520 let actor_traces_format = params.actor_traces_format()?;
521
522 let worker_node = srv
523 .metadata_manager
524 .get_worker_by_id(worker_id)
525 .await
526 .map_err(err)?
527 .context("worker node not found")
528 .map_err(err)?;
529
530 let res = dump_worker_node_await_tree(std::iter::once(&worker_node), actor_traces_format)
531 .await
532 .map_err(err)?;
533
534 Ok(res.into())
535 }
536
537 pub async fn heap_profile(
538 Path(worker_id): Path<WorkerId>,
539 Extension(srv): Extension<Service>,
540 ) -> Result<Json<HeapProfilingResponse>> {
541 if worker_id == crate::manager::META_NODE_ID {
542 let result = srv
543 .profile_service
544 .heap_profiling(Request::new(HeapProfilingRequest { dir: "".to_owned() }))
545 .map_err(err)?
546 .into_inner();
547 return Ok(result.into());
548 }
549
550 let worker_node = srv
551 .metadata_manager
552 .get_worker_by_id(worker_id)
553 .await
554 .map_err(err)?
555 .context("worker node not found")
556 .map_err(err)?;
557
558 let client = srv.monitor_clients.get(&worker_node).await.map_err(err)?;
559
560 let result = client.heap_profile("".to_owned()).await.map_err(err)?;
561
562 Ok(result.into())
563 }
564
565 pub async fn cpu_profile(
566 Path((worker_id, duration_secs)): Path<(WorkerId, u64)>,
567 Extension(srv): Extension<Service>,
568 ) -> Result<Response> {
569 if duration_secs == 0 {
570 return Err(err(anyhow!(
571 "CPU profiling duration must be greater than zero"
572 )));
573 }
574
575 let flamegraph = if worker_id == crate::manager::META_NODE_ID {
576 srv.profile_service
577 .profiling(Request::new(ProfilingRequest {
578 sleep_s: duration_secs,
579 }))
580 .await
581 .map_err(err)?
582 .into_inner()
583 .result
584 } else {
585 let worker_node = srv
586 .metadata_manager
587 .get_worker_by_id(worker_id)
588 .await
589 .map_err(err)?
590 .context("worker node not found")
591 .map_err(err)?;
592
593 let client = srv.monitor_clients.get(&worker_node).await.map_err(err)?;
594 client.profile(duration_secs).await.map_err(err)?.result
595 };
596
597 Response::builder()
598 .header("Content-Type", "image/svg+xml")
599 .body(flamegraph.into())
600 .map_err(err)
601 }
602
603 pub async fn list_heap_profile(
604 Path(worker_id): Path<WorkerId>,
605 Extension(srv): Extension<Service>,
606 ) -> Result<Json<ListHeapProfilingResponse>> {
607 if worker_id == crate::manager::META_NODE_ID {
608 let result = srv
609 .profile_service
610 .list_heap_profiling(Request::new(ListHeapProfilingRequest {}))
611 .map_err(err)?
612 .into_inner();
613 return Ok(result.into());
614 }
615
616 let worker_node = srv
617 .metadata_manager
618 .get_worker_by_id(worker_id)
619 .await
620 .map_err(err)?
621 .context("worker node not found")
622 .map_err(err)?;
623
624 let client = srv.monitor_clients.get(&worker_node).await.map_err(err)?;
625
626 let result = client.list_heap_profile().await.map_err(err)?;
627 Ok(result.into())
628 }
629
630 pub async fn analyze_heap(
631 Path((worker_id, file_path)): Path<(WorkerId, String)>,
632 Extension(srv): Extension<Service>,
633 ) -> Result<Response> {
634 let file_path =
635 String::from_utf8(base64_url::decode(&file_path).map_err(err)?).map_err(err)?;
636
637 let collapsed_bin = if worker_id == crate::manager::META_NODE_ID {
638 srv.profile_service
639 .analyze_heap(Request::new(AnalyzeHeapRequest {
640 path: file_path.clone(),
641 }))
642 .await
643 .map_err(err)?
644 .into_inner()
645 .result
646 } else {
647 let worker_node = srv
648 .metadata_manager
649 .get_worker_by_id(worker_id)
650 .await
651 .map_err(err)?
652 .context("worker node not found")
653 .map_err(err)?;
654
655 let client = srv.monitor_clients.get(&worker_node).await.map_err(err)?;
656 client
657 .analyze_heap(file_path.clone())
658 .await
659 .map_err(err)?
660 .result
661 };
662 let collapsed_str = String::from_utf8_lossy(&collapsed_bin).to_string();
663
664 let response = Response::builder()
665 .header("Content-Type", "application/octet-stream")
666 .body(collapsed_str.into());
667
668 response.map_err(err)
669 }
670
671 #[derive(Debug, Deserialize)]
672 pub struct DiagnoseParams {
673 #[serde(default = "await_tree_default_format")]
674 actor_traces_format: String,
675 }
676
677 pub async fn diagnose(
678 Query(params): Query<DiagnoseParams>,
679 Extension(srv): Extension<Service>,
680 ) -> Result<String> {
681 let actor_traces_format = match params.actor_traces_format.as_str() {
682 "text" => ActorTracesFormat::Text,
683 "json" => ActorTracesFormat::Json,
684 _ => {
685 return Err(err(anyhow!(
686 "Unsupported actor_traces_format `{}`, only `text` and `json` are supported for now",
687 params.actor_traces_format
688 )));
689 }
690 };
691 Ok(srv.diagnose_command.report(actor_traces_format).await)
692 }
693
694 pub async fn get_streaming_stats(
700 Extension(srv): Extension<Service>,
701 ) -> Result<Json<GetStreamingStatsResponse>> {
702 let worker_nodes = srv
703 .metadata_manager
704 .list_active_streaming_compute_nodes()
705 .await
706 .map_err(err)?;
707
708 let mut futures = Vec::new();
709
710 for worker_node in worker_nodes {
711 let client = srv.monitor_clients.get(&worker_node).await.map_err(err)?;
712 let client = Arc::new(client);
713 let fut = async move {
714 let result = client.get_streaming_stats().await.map_err(err)?;
715 Ok::<_, DashboardError>(result)
716 };
717 futures.push(fut);
718 }
719 let results = join_all(futures).await;
720
721 let mut all = GetStreamingStatsResponse::default();
722
723 for result in results {
724 let result = result
725 .map_err(|_| anyhow!("Failed to get back pressure"))
726 .map_err(err)?;
727
728 for (fragment_id, fragment_stats) in result.fragment_stats {
730 if let Some(s) = all.fragment_stats.get_mut(&fragment_id) {
731 s.actor_count += fragment_stats.actor_count;
732 s.current_epoch = min(s.current_epoch, fragment_stats.current_epoch);
733 } else {
734 all.fragment_stats.insert(fragment_id, fragment_stats);
735 }
736 }
737
738 for (relation_id, relation_stats) in result.relation_stats {
740 if let Some(s) = all.relation_stats.get_mut(&relation_id) {
741 s.actor_count += relation_stats.actor_count;
742 s.current_epoch = min(s.current_epoch, relation_stats.current_epoch);
743 } else {
744 all.relation_stats.insert(relation_id, relation_stats);
745 }
746 }
747
748 for (key, channel_stats) in result.channel_stats {
750 if let Some(s) = all.channel_stats.get_mut(&key) {
751 s.actor_count += channel_stats.actor_count;
752 s.output_blocking_duration += channel_stats.output_blocking_duration;
753 s.recv_row_count += channel_stats.recv_row_count;
754 s.send_row_count += channel_stats.send_row_count;
755 } else {
756 all.channel_stats.insert(key, channel_stats);
757 }
758 }
759 }
760
761 Ok(all.into())
762 }
763
764 #[derive(Debug, Deserialize)]
765 pub struct StreamingStatsPrometheusParams {
766 #[serde(default)]
768 at: Option<i64>,
769 #[serde(default = "streaming_stats_default_time_offset")]
771 time_offset: i64,
772 }
773
774 fn streaming_stats_default_time_offset() -> i64 {
775 60
776 }
777
778 pub async fn get_streaming_stats_from_prometheus(
779 Query(params): Query<StreamingStatsPrometheusParams>,
780 Extension(srv): Extension<Service>,
781 ) -> Result<Json<GetStreamingPrometheusStatsResponse>> {
782 let mut all = GetStreamingPrometheusStatsResponse::default();
783
784 let worker_nodes = srv
786 .metadata_manager
787 .list_active_streaming_compute_nodes()
788 .await
789 .map_err(err)?;
790
791 let mut futures = Vec::new();
792
793 for worker_node in worker_nodes {
794 let client = srv.monitor_clients.get(&worker_node).await.map_err(err)?;
795 let client = Arc::new(client);
796 let fut = async move {
797 let result = client.get_streaming_stats().await.map_err(err)?;
798 Ok::<_, DashboardError>(result)
799 };
800 futures.push(fut);
801 }
802 let results = join_all(futures).await;
803
804 for result in results {
805 let result = result
806 .map_err(|_| anyhow!("Failed to get streaming stats from worker"))
807 .map_err(err)?;
808
809 for (fragment_id, fragment_stats) in result.fragment_stats {
811 if let Some(s) = all.fragment_stats.get_mut(&fragment_id) {
812 s.actor_count += fragment_stats.actor_count;
813 s.current_epoch = min(s.current_epoch, fragment_stats.current_epoch);
814 } else {
815 all.fragment_stats.insert(fragment_id, fragment_stats);
816 }
817 }
818
819 for (relation_id, relation_stats) in result.relation_stats {
821 if let Some(s) = all.relation_stats.get_mut(&relation_id) {
822 s.actor_count += relation_stats.actor_count;
823 s.current_epoch = min(s.current_epoch, relation_stats.current_epoch);
824 } else {
825 all.relation_stats.insert(relation_id, relation_stats);
826 }
827 }
828 }
829
830 if let Some(ref client) = srv.prometheus_client {
832 let channel_input_throughput_query = format!(
834 "sum(rate(stream_actor_in_record_cnt{{{}}}[{}s])) by (fragment_id, upstream_fragment_id)",
835 srv.prometheus_selector, params.time_offset
836 );
837 let channel_output_throughput_query = format!(
838 "sum(rate(stream_actor_out_record_cnt{{{}}}[{}s])) by (fragment_id, upstream_fragment_id)",
839 srv.prometheus_selector, params.time_offset
840 );
841 let channel_backpressure_query = format!(
842 "sum(rate(stream_actor_output_buffer_blocking_duration_ns{{{}}}[{}s])) by (fragment_id, downstream_fragment_id) \
843 / ignoring (downstream_fragment_id) group_left sum(stream_actor_count) by (fragment_id)",
844 srv.prometheus_selector, params.time_offset
845 );
846
847 let (
849 channel_input_throughput_result,
850 channel_output_throughput_result,
851 channel_backpressure_result,
852 ) = {
853 let mut input_query = client.query(channel_input_throughput_query);
854 let mut output_query = client.query(channel_output_throughput_query);
855 let mut backpressure_query = client.query(channel_backpressure_query);
856
857 if let Some(at_time) = params.at {
859 input_query = input_query.at(at_time);
860 output_query = output_query.at(at_time);
861 backpressure_query = backpressure_query.at(at_time);
862 }
863
864 tokio::try_join!(
865 input_query.get(),
866 output_query.get(),
867 backpressure_query.get(),
868 )
869 .map_err(err)?
870 };
871
872 let mut channel_data = HashMap::new();
874
875 if let Some(channel_input_throughput_data) =
877 channel_input_throughput_result.data().as_vector()
878 {
879 for sample in channel_input_throughput_data {
880 if let Some(fragment_id_str) = sample.metric().get("fragment_id")
881 && let Some(upstream_fragment_id_str) =
882 sample.metric().get("upstream_fragment_id")
883 && let (Ok(fragment_id), Ok(upstream_fragment_id)) = (
884 fragment_id_str.parse::<u32>(),
885 upstream_fragment_id_str.parse::<u32>(),
886 )
887 {
888 let key = format!("{}_{}", upstream_fragment_id, fragment_id);
889 channel_data
890 .entry(key)
891 .or_insert_with(|| ChannelDeltaStats {
892 actor_count: 0,
893 backpressure_rate: 0.0,
894 recv_throughput: 0.0,
895 send_throughput: 0.0,
896 })
897 .recv_throughput = sample.sample().value();
898 }
899 }
900 }
901
902 if let Some(channel_output_throughput_data) =
904 channel_output_throughput_result.data().as_vector()
905 {
906 for sample in channel_output_throughput_data {
907 if let Some(fragment_id_str) = sample.metric().get("fragment_id")
908 && let Some(upstream_fragment_id_str) =
909 sample.metric().get("upstream_fragment_id")
910 && let (Ok(fragment_id), Ok(upstream_fragment_id)) = (
911 fragment_id_str.parse::<u32>(),
912 upstream_fragment_id_str.parse::<u32>(),
913 )
914 {
915 let key = format!("{}_{}", upstream_fragment_id, fragment_id);
916 channel_data
917 .entry(key)
918 .or_insert_with(|| ChannelDeltaStats {
919 actor_count: 0,
920 backpressure_rate: 0.0,
921 recv_throughput: 0.0,
922 send_throughput: 0.0,
923 })
924 .send_throughput = sample.sample().value();
925 }
926 }
927 }
928
929 if let Some(channel_backpressure_data) = channel_backpressure_result.data().as_vector()
931 {
932 for sample in channel_backpressure_data {
933 if let Some(fragment_id_str) = sample.metric().get("fragment_id")
934 && let Some(downstream_fragment_id_str) =
935 sample.metric().get("downstream_fragment_id")
936 && let (Ok(fragment_id), Ok(downstream_fragment_id)) = (
937 fragment_id_str.parse::<u32>(),
938 downstream_fragment_id_str.parse::<u32>(),
939 )
940 {
941 let key = format!("{}_{}", fragment_id, downstream_fragment_id);
942 channel_data
943 .entry(key)
944 .or_insert_with(|| ChannelDeltaStats {
945 actor_count: 0,
946 backpressure_rate: 0.0,
947 recv_throughput: 0.0,
948 send_throughput: 0.0,
949 })
950 .backpressure_rate = sample.sample().value() / 1_000_000_000.0; }
952 }
953 }
954
955 for (key, channel_stats) in &mut channel_data {
957 let parts: Vec<&str> = key.split('_').collect();
958 if parts.len() == 2
959 && let Ok(fragment_id) = parts[1].parse::<u32>()
960 && let Some(fragment_stats) = all.fragment_stats.get(&fragment_id)
961 {
962 channel_stats.actor_count = fragment_stats.actor_count;
963 }
964 }
965
966 all.channel_stats = channel_data;
967
968 Ok(Json(all))
969 } else {
970 Err(err(anyhow!("Prometheus endpoint is not set")))
971 }
972 }
973
974 pub async fn get_version(Extension(_srv): Extension<Service>) -> Result<Json<String>> {
975 Ok(Json(risingwave_common::current_cluster_version()))
976 }
977}
978
979impl DashboardService {
980 pub async fn serve(self) -> Result<()> {
981 use handlers::*;
982 let srv = Arc::new(self);
983
984 let cors_layer = CorsLayer::new()
985 .allow_origin(cors::Any)
986 .allow_methods(vec![Method::GET]);
987
988 let api_router = Router::new()
989 .route("/version", get(get_version))
990 .route("/clusters/{ty}", get(list_clusters))
991 .route("/streaming_jobs", get(list_streaming_jobs))
992 .route("/fragments/job_id/{job_id}", get(list_fragments_by_job_id))
993 .route("/relation_id_infos", get(get_relation_id_infos))
994 .route(
995 "/fragment_to_relation_map",
996 get(get_fragment_to_relation_map),
997 )
998 .route("/views", get(list_views))
999 .route("/functions", get(list_functions))
1000 .route("/materialized_views", get(list_materialized_views))
1001 .route("/tables", get(list_tables))
1002 .route("/indexes", get(list_index_tables))
1003 .route("/index_items", get(list_indexes))
1004 .route("/subscriptions", get(list_subscription))
1005 .route("/internal_tables", get(list_internal_tables))
1006 .route("/sources", get(list_sources))
1007 .route("/sinks", get(list_sinks))
1008 .route("/users", get(list_users))
1009 .route("/databases", get(list_databases))
1010 .route("/schemas", get(list_schemas))
1011 .route("/object_dependencies", get(list_object_dependencies))
1012 .route("/metrics/cluster", get(prometheus::list_prometheus_cluster))
1013 .route("/metrics/streaming_stats", get(get_streaming_stats))
1014 .route(
1015 "/metrics/streaming_stats_prometheus",
1016 get(get_streaming_stats_from_prometheus),
1017 )
1018 .route("/monitor/await_tree/{worker_id}", get(dump_await_tree))
1020 .route("/monitor/await_tree/", get(dump_await_tree_all))
1022 .route(
1023 "/monitor/dump_cpu_profile/{worker_id}/{duration_secs}",
1024 get(cpu_profile),
1025 )
1026 .route("/monitor/dump_heap_profile/{worker_id}", get(heap_profile))
1027 .route(
1028 "/monitor/list_heap_profile/{worker_id}",
1029 get(list_heap_profile),
1030 )
1031 .route("/monitor/analyze/{worker_id}/{*path}", get(analyze_heap))
1032 .route("/monitor/diagnose/", get(diagnose))
1034 .layer(
1035 ServiceBuilder::new()
1036 .layer(AddExtensionLayer::new(srv.clone()))
1037 .into_inner(),
1038 )
1039 .layer(cors_layer);
1040
1041 let trace_ui_router = otlp_embedded::ui_app(srv.trace_state.clone(), "/trace/");
1042 let dashboard_router = risingwave_meta_dashboard::router();
1043
1044 let app = Router::new()
1045 .fallback_service(dashboard_router)
1046 .nest("/api", api_router)
1047 .nest("/trace", trace_ui_router)
1048 .layer(CompressionLayer::new());
1049
1050 let listener = TcpListener::bind(&srv.dashboard_addr)
1051 .await
1052 .context("failed to bind dashboard address")?;
1053 axum::serve(listener, app)
1054 .await
1055 .context("failed to serve dashboard service")?;
1056
1057 Ok(())
1058 }
1059}