Skip to main content

risingwave_meta/dashboard/
mod.rs

1// Copyright 2022 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
15mod 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        // Get table statistics from hummock manager
179        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    /// In the ddl backpressure graph, we want to compute the backpressure between relations.
315    /// So we need to know which are the fragments which are connected to external relations.
316    /// These fragments form the vertices of the graph.
317    /// We can get collection of backpressure values, keyed by vertex_id-vertex_id.
318    /// This function will return a map of fragment vertex id to relation id.
319    /// We can convert `fragment_id-fragment_id` to `relation_id-relation_id` using that.
320    /// Finally, we have a map of `relation_id-relation_id` to backpressure values.
321    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    /// Provides a hierarchy of relation ids to fragments to actors.
343    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        /// Parse the `format` parameter to [`ActorTracesFormat`].
479        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        // In dashboard, await tree is usually for engineer to debug, so we use human-readable text format by default here.
495        "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    /// NOTE(kwannoel): Although we fetch the BP for the entire graph via this API,
695    /// the workload should be reasonable.
696    /// In most cases, we can safely assume each node has most 2 outgoing edges (e.g. join).
697    /// In such a scenario, the number of edges is linear to the number of nodes.
698    /// So the workload is proportional to the relation id graph we fetch in `get_relation_id_infos`.
699    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            // Aggregate fragment_stats
729            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            // Aggregate relation_stats
739            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            // Aggregate channel_stats
749            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        /// Unix timestamp in seconds for the evaluation time. If not set, defaults to current Prometheus server time.
767        #[serde(default)]
768        at: Option<i64>,
769        /// Time offset for throughput and backpressure rate calculation in seconds. If not set, defaults to 60s.
770        #[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        // Get fragment and relation stats from workers
785        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            // Aggregate fragment_stats
810            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            // Aggregate relation_stats
820            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        // Get channel delta stats from Prometheus
831        if let Some(ref client) = srv.prometheus_client {
832            // Query channel delta stats: throughput and backpressure rate
833            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            // Execute all queries concurrently with optional time parameter
848            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                // Set the evaluation time if provided
858                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            // Process channel delta stats
873            let mut channel_data = HashMap::new();
874
875            // Collect input throughput
876            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            // Collect output throughput
903            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            // Collect backpressure rate
930            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; // Convert ns to seconds
951                    }
952                }
953            }
954
955            // Set actor count for channels (using fragment actor count as approximation)
956            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            // /monitor/await_tree/{worker_id}/?format={text or json}
1019            .route("/monitor/await_tree/{worker_id}", get(dump_await_tree))
1020            // /monitor/await_tree/?format={text or json}
1021            .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            // /monitor/diagnose/?format={text or json}
1033            .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}