Skip to content

Commit 1dabf7b

Browse files
committed
feat(dashboard): complete embedded dashboard telemetry
1 parent 9294756 commit 1dabf7b

16 files changed

Lines changed: 1399 additions & 120 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Embedded dashboard snapshots now include request-stage counters, route topology groups, health endpoint summary, and replay admin API discovery metadata.
13+
- Dashboard UI adds route group/method/tag filters plus a replay browser that reuses the existing `ReplayLayer` admin API for list, detail, and diff workflows.
14+
- Replay admin list endpoint now accepts UI-friendly pagination and filters: `offset`, `status_max`, `from`, `to`, `tag`, and `order`.
15+
16+
### Documentation
17+
18+
- Added a cookbook recipe and SVG preview for the embedded dashboard and replay browser workflow, including the inspection-first state rewind model and disabled-feature performance budget.
19+
1020
## [0.1.410] - 2026-03-09
1121

1222
### Added

api/public/rustapi-rs.all-features.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,19 @@ pub use rustapi_rs::core::serve_dir
227227
pub use rustapi_rs::core::sse_response
228228
pub mod rustapi_rs::dashboard
229229
pub use rustapi_rs::dashboard::DashboardConfig
230+
pub use rustapi_rs::dashboard::DashboardHealthEndpointSnapshot
231+
pub use rustapi_rs::dashboard::DashboardHealthSummary
232+
pub use rustapi_rs::dashboard::DashboardLiveCountersSnapshot
230233
pub use rustapi_rs::dashboard::DashboardMetrics
234+
pub use rustapi_rs::dashboard::DashboardReplayIndexSnapshot
231235
pub use rustapi_rs::dashboard::DashboardSnapshot
236+
pub use rustapi_rs::dashboard::DashboardStageSnapshot
237+
pub use rustapi_rs::dashboard::ExecutionPath
238+
pub use rustapi_rs::dashboard::RequestStage
239+
pub use rustapi_rs::dashboard::RouteGraphSnapshot
240+
pub use rustapi_rs::dashboard::RouteGroupSnapshot
241+
pub use rustapi_rs::dashboard::RouteInventoryItem
242+
pub use rustapi_rs::dashboard::RouteMetricsSnapshot
232243
pub mod rustapi_rs::extras
233244
pub mod rustapi_rs::extras::api_key
234245
pub use rustapi_rs::extras::api_key::api_key

crates/rustapi-core/src/app.rs

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::response::IntoResponse;
88
use crate::router::{MethodRouter, Router};
99
use crate::server::Server;
1010
use std::collections::BTreeMap;
11+
#[cfg(feature = "dashboard")]
12+
use std::collections::BTreeSet;
1113
use std::future::Future;
1214
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
1315

@@ -1274,18 +1276,42 @@ impl RustApi {
12741276
None => return,
12751277
};
12761278

1277-
// Build route inventory from currently registered routes
1278-
let inventory: Vec<RouteInventoryItem> = self
1279+
// Build route inventory from currently registered routes. This snapshot
1280+
// intentionally happens before dashboard routes are mounted so the UI
1281+
// represents application endpoints rather than the dashboard itself.
1282+
let mut inventory: Vec<RouteInventoryItem> = self
12791283
.router
12801284
.registered_routes()
12811285
.values()
1282-
.map(|info| RouteInventoryItem {
1283-
path: info.path.clone(),
1284-
methods: info.methods.iter().map(|m| m.to_string()).collect(),
1286+
.map(|info| {
1287+
let methods: Vec<String> = info.methods.iter().map(|m| m.to_string()).collect();
1288+
let health_eligible = self
1289+
.health_endpoint_config
1290+
.as_ref()
1291+
.map(|health| {
1292+
info.path == health.health_path
1293+
|| info.path == health.readiness_path
1294+
|| info.path == health.liveness_path
1295+
})
1296+
.unwrap_or(false);
1297+
1298+
RouteInventoryItem::new(info.path.clone(), methods)
1299+
.with_tags(openapi_tags_for_route(
1300+
&self.openapi_spec,
1301+
&info.path,
1302+
&info.methods,
1303+
))
1304+
.with_feature_gates(infer_route_feature_gates(&info.path))
1305+
.health_eligible(health_eligible)
1306+
.replay_eligible(is_dashboard_replay_eligible(&info.path, health_eligible))
12851307
})
12861308
.collect();
1309+
inventory.sort_by(|a, b| a.path.cmp(&b.path));
12871310

1288-
let metrics = Arc::new(DashboardMetrics::new(inventory));
1311+
let metrics = Arc::new(DashboardMetrics::new_with_replay_admin_path(
1312+
inventory,
1313+
config.replay_api_path.clone(),
1314+
));
12891315

12901316
// Insert metrics into router state using the public .state() API
12911317
let router = std::mem::take(&mut self.router);
@@ -1645,6 +1671,60 @@ impl RustApi {
16451671
}
16461672
}
16471673

1674+
#[cfg(feature = "dashboard")]
1675+
fn openapi_tags_for_route(
1676+
spec: &rustapi_openapi::OpenApiSpec,
1677+
path: &str,
1678+
methods: &[http::Method],
1679+
) -> Vec<String> {
1680+
let Some(path_item) = spec.paths.get(path) else {
1681+
return Vec::new();
1682+
};
1683+
1684+
let mut tags = BTreeSet::new();
1685+
for method in methods {
1686+
if let Some(operation) = operation_for_method(path_item, method) {
1687+
tags.extend(operation.tags.iter().cloned());
1688+
}
1689+
}
1690+
1691+
tags.into_iter().collect()
1692+
}
1693+
1694+
#[cfg(feature = "dashboard")]
1695+
fn operation_for_method<'a>(
1696+
path_item: &'a rustapi_openapi::PathItem,
1697+
method: &http::Method,
1698+
) -> Option<&'a rustapi_openapi::Operation> {
1699+
match *method {
1700+
http::Method::GET => path_item.get.as_ref(),
1701+
http::Method::POST => path_item.post.as_ref(),
1702+
http::Method::PUT => path_item.put.as_ref(),
1703+
http::Method::PATCH => path_item.patch.as_ref(),
1704+
http::Method::DELETE => path_item.delete.as_ref(),
1705+
http::Method::HEAD => path_item.head.as_ref(),
1706+
http::Method::OPTIONS => path_item.options.as_ref(),
1707+
http::Method::TRACE => path_item.trace.as_ref(),
1708+
_ => None,
1709+
}
1710+
}
1711+
1712+
#[cfg(feature = "dashboard")]
1713+
fn infer_route_feature_gates(path: &str) -> Vec<String> {
1714+
if path.contains("openapi") || path.contains("docs") {
1715+
vec!["core-openapi".to_string()]
1716+
} else if path.starts_with("/__rustapi/replays") {
1717+
vec!["extras-replay".to_string()]
1718+
} else {
1719+
Vec::new()
1720+
}
1721+
}
1722+
1723+
#[cfg(feature = "dashboard")]
1724+
fn is_dashboard_replay_eligible(path: &str, health_eligible: bool) -> bool {
1725+
!health_eligible && !path.starts_with("/__rustapi/")
1726+
}
1727+
16481728
fn add_path_params_to_operation(
16491729
path: &str,
16501730
op: &mut rustapi_openapi::Operation,

crates/rustapi-core/src/dashboard/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
/// let config = DashboardConfig::new()
1111
/// .admin_token("my-secret")
1212
/// .path("/__rustapi/dashboard")
13+
/// .replay_api_path("/__rustapi/replays")
1314
/// .title("My API Dashboard");
1415
/// ```
1516
#[derive(Debug, Clone)]
@@ -23,6 +24,10 @@ pub struct DashboardConfig {
2324

2425
/// Page title shown in the UI.
2526
pub title: String,
27+
28+
/// Replay admin API path used by the dashboard replay browser.
29+
/// Default: `"/__rustapi/replays"`.
30+
pub replay_api_path: String,
2631
}
2732

2833
impl Default for DashboardConfig {
@@ -38,6 +43,7 @@ impl DashboardConfig {
3843
admin_token: None,
3944
path: "/__rustapi/dashboard".to_string(),
4045
title: "RustAPI System Dashboard".to_string(),
46+
replay_api_path: "/__rustapi/replays".to_string(),
4147
}
4248
}
4349

@@ -63,4 +69,12 @@ impl DashboardConfig {
6369
self.title = title.into();
6470
self
6571
}
72+
73+
/// Override the replay admin API path used by the UI replay browser.
74+
///
75+
/// This should match `ReplayConfig::admin_route_prefix(...)` when replay is enabled.
76+
pub fn replay_api_path(mut self, path: impl Into<String>) -> Self {
77+
self.replay_api_path = path.into();
78+
self
79+
}
6680
}

0 commit comments

Comments
 (0)