chore(agent-data-plane): register health API dynamically#1259
chore(agent-data-plane): register health API dynamically#1259tobz wants to merge 2 commits intotobz/dynamic-api-endpoint-routesfrom
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
This PR moves the health registry functionality into saluki-core and updates the agent data plane to expose the health API via the dynamic API routing mechanism (dataspace-published routes).
Changes:
- Removed the standalone
saluki-healthcrate and rehomed health types undersaluki_core::health. - Added
HealthRegistryWorkerto run the health registry loop and dynamically publish the health HTTP routes. - Switched the agent-data-plane unprivileged API server to
DynamicAPIBuilderand updated dependencies accordingly (including enablingtonic’srouterfeature insaluki-api).
Reviewed changes
Copilot reviewed 24 out of 31 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/saluki-core/src/health/worker.rs | New worker that runs the health registry and publishes health routes dynamically via DataspaceRegistry. |
| bin/agent-data-plane/src/internal/control_plane.rs | Replaces the unprivileged API worker with DynamicAPIBuilder and wires in the new health worker. |
| lib/saluki-core/src/health/mod.rs | Exposes health module + worker API; removes direct spawn API and adjusts tests/visibility. |
| lib/saluki-api/Cargo.toml | Enables tonic router feature to support route-to-axum conversion used by dynamic APIs. |
| lib/saluki-env/, bin/agent-data-plane/, lib/saluki-core/** | Updates imports/dependencies from saluki_health::* to saluki_core::health::*. |
| Cargo.toml, Cargo.lock | Removes saluki-health from workspace/deps and updates lockfile. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let dataspace_registry = DataspaceRegistry::global(); | ||
| dataspace_registry.assert(health_routes, Handle::current_process()); | ||
|
|
||
| runner.run(process_shutdown).await; | ||
|
|
||
| // TODO: OK, yeah, this is clunky. We should just make it a drop guard. | ||
| dataspace_registry.retract::<DynamicRoute>(Handle::current_process()); | ||
|
|
There was a problem hiding this comment.
DynamicRoute is asserted but only retracted on the normal completion path. If runner.run(process_shutdown).await panics (or this future is cancelled), the route can remain published in the DataspaceRegistry, leaving the dynamic API serving a stale health handler even though the registry loop is no longer running. Consider using an RAII/drop guard (or scopeguard) so the retraction happens on all exits, including unwinding/cancellation.
| let dataspace_registry = DataspaceRegistry::global(); | |
| dataspace_registry.assert(health_routes, Handle::current_process()); | |
| runner.run(process_shutdown).await; | |
| // TODO: OK, yeah, this is clunky. We should just make it a drop guard. | |
| dataspace_registry.retract::<DynamicRoute>(Handle::current_process()); | |
| // | |
| // Use an RAII guard so the route is retracted even if this future is cancelled | |
| // or unwinds due to a panic while awaiting `runner.run`. | |
| struct RouteRetractionGuard<'a> { | |
| dataspace_registry: &'a DataspaceRegistry, | |
| handle: Handle, | |
| } | |
| impl<'a> Drop for RouteRetractionGuard<'a> { | |
| fn drop(&mut self) { | |
| self.dataspace_registry.retract::<DynamicRoute>(self.handle); | |
| } | |
| } | |
| let dataspace_registry = DataspaceRegistry::global(); | |
| dataspace_registry.assert(health_routes, Handle::current_process()); | |
| let _route_guard = RouteRetractionGuard { | |
| dataspace_registry: &dataspace_registry, | |
| handle: Handle::current_process(), | |
| }; | |
| runner.run(process_shutdown).await; |
| let health_routes = DynamicRoute::http(EndpointType::Unprivileged, self.health_registry.api_handler()); | ||
|
|
||
| Ok(Box::pin(async move { | ||
| // Publish our health endpoint route and retract it after the registry task completes, | ||
| // regardless of whether it failed or not. | ||
| let dataspace_registry = DataspaceRegistry::global(); | ||
| dataspace_registry.assert(health_routes, Handle::current_process()); | ||
|
|
||
| runner.run(process_shutdown).await; | ||
|
|
||
| // TODO: OK, yeah, this is clunky. We should just make it a drop guard. | ||
| dataspace_registry.retract::<DynamicRoute>(Handle::current_process()); | ||
|
|
There was a problem hiding this comment.
Using Handle::current_process() for the DynamicRoute assertion means this process can only safely publish a single DynamicRoute value at a time (same type+handle key). If any other dynamic route is asserted under the same process handle later, it will overwrite this one and retract::<DynamicRoute>(Handle::current_process()) would retract whichever route is currently stored. Consider using a dedicated Handle::new_global() stored in the worker so the health route has a stable, collision-free key.
Binary Size Analysis (Agent Data Plane)Target: 568e97e (baseline) vs 372618f (comparison) diff
|
| Module | File Size | Symbols |
|---|---|---|
h2 |
+142.85 KiB | 355 |
core |
+133.06 KiB | 8969 |
hyper |
+70.77 KiB | 347 |
saluki_core::runtime::supervisor |
+69.31 KiB | 66 |
tokio |
+59.85 KiB | 2352 |
hyper_util |
+59.22 KiB | 80 |
[sections] |
+49.96 KiB | 9 |
agent_data_plane::internal::control_plane |
-42.83 KiB | 16 |
saluki_app::dynamic_api::DynamicAPIBuilder |
+37.41 KiB | 6 |
saluki_health |
-30.01 KiB | 24 |
saluki_core::health::worker |
+25.30 KiB | 4 |
saluki_io::net::server |
+25.02 KiB | 39 |
agent_data_plane::internal::initialize_and_launch_runtime |
-18.37 KiB | 1 |
agent_data_plane::internal::create_internal_supervisor |
+16.49 KiB | 1 |
saluki_app::memory::MemoryBoundsConfiguration |
-13.70 KiB | 5 |
std |
-10.77 KiB | 211 |
http |
+10.59 KiB | 339 |
tracing_core |
+9.24 KiB | 475 |
tokio_rustls |
+8.86 KiB | 29 |
saluki_common::task::instrument |
+8.57 KiB | 91 |
Detailed Symbol Changes
FILE SIZE VM SIZE
-------------- --------------
[NEW] +1.79Mi [NEW] +1.79Mi std::thread::local::LocalKey<T>::with::hb17e908acf04879d
+5.6% +710Ki +5.8% +610Ki [22642 Others]
[NEW] +114Ki [NEW] +114Ki agent_data_plane::cli::run::create_topology::_{{closure}}::ha8f300467dd1d69a
[NEW] +64.1Ki [NEW] +64.0Ki saluki_components::common::datadog::io::run_endpoint_io_loop::_{{closure}}::hb106ea2c152b09e3
[NEW] +63.6Ki [NEW] +63.4Ki agent_data_plane::cli::run::handle_run_command::_{{closure}}::h2d979578028f7e69
[NEW] +58.9Ki [NEW] +58.6Ki _<agent_data_plane::internal::control_plane::PrivilegedApiWorker as saluki_core::runtime::supervisor::Supervisable>::initialize::_{{closure}}::h41b755b25f8fd10b
[NEW] +49.5Ki [NEW] +49.3Ki saluki_app::bootstrap::AppBootstrapper::bootstrap::_{{closure}}::h160d59e5f17d49c0
[NEW] +46.4Ki [NEW] +46.3Ki h2::proto::connection::Connection<T,P,B>::poll::h70805a8e402f8fb4
[NEW] +46.2Ki [NEW] +46.0Ki _<saluki_components::forwarders::otlp::OtlpForwarder as saluki_core::components::forwarders::Forwarder>::run::_{{closure}}::h13de10e3cdb6b0d8
[NEW] +45.7Ki [NEW] +45.5Ki _<saluki_components::destinations::prometheus::Prometheus as saluki_core::components::destinations::Destination>::run::_{{closure}}::hf2c7a477f8eea3fb
[NEW] +44.3Ki [NEW] +44.1Ki _<saluki_components::transforms::aggregate::Aggregate as saluki_core::components::transforms::Transform>::run::_{{closure}}::h58d13fc6b4dd4d3f
[DEL] -44.4Ki [DEL] -44.2Ki _<saluki_components::transforms::aggregate::Aggregate as saluki_core::components::transforms::Transform>::run::_{{closure}}::h4183d8718b35855c
[DEL] -45.7Ki [DEL] -45.5Ki _<saluki_components::destinations::prometheus::Prometheus as saluki_core::components::destinations::Destination>::run::_{{closure}}::he34c6c99f535251c
[DEL] -46.1Ki [DEL] -45.9Ki _<saluki_components::forwarders::otlp::OtlpForwarder as saluki_core::components::forwarders::Forwarder>::run::_{{closure}}::h3fc056b8c3b1ef5a
[DEL] -46.4Ki [DEL] -46.3Ki h2::proto::connection::Connection<T,P,B>::poll::h882eac246abc63c7
[DEL] -49.5Ki [DEL] -49.3Ki saluki_app::bootstrap::AppBootstrapper::bootstrap::_{{closure}}::he5d18b68ad8674e3
[DEL] -59.9Ki [DEL] -59.7Ki agent_data_plane::cli::run::handle_run_command::_{{closure}}::h18a09eec15b3cc76
[DEL] -64.2Ki [DEL] -64.0Ki saluki_components::common::datadog::io::run_endpoint_io_loop::_{{closure}}::hbbac7c9eda9e917d
[DEL] -84.6Ki [DEL] -84.5Ki agent_data_plane::internal::control_plane::spawn_control_plane::_{{closure}}::h10ad1a803e89e510
[DEL] -114Ki [DEL] -114Ki agent_data_plane::cli::run::create_topology::_{{closure}}::hd2940cf47ffc187f
[DEL] -1.79Mi [DEL] -1.79Mi std::thread::local::LocalKey<T>::with::h9949df9476fbdca7
+2.6% +688Ki +2.5% +588Ki TOTAL
Regression Detector (Agent Data Plane)Regression Detector ResultsRun ID: 6adc7a0d-44df-4f5d-b332-10fdb87c3c5f Baseline: 568e97e Optimization Goals: ✅ No significant changes detected
|
| perf | experiment | goal | Δ mean % | Δ mean % CI | trials | links |
|---|---|---|---|---|---|---|
| ➖ | otlp_ingest_logs_5mb_throughput | ingress throughput | +0.00 | [-0.12, +0.13] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_cpu | % cpu utilization | -0.06 | [-5.19, +5.07] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_memory | memory utilization | -3.20 | [-3.96, -2.43] | 1 | (metrics) (profiles) (logs) |
Fine details of change detection per experiment
| perf | experiment | goal | Δ mean % | Δ mean % CI | trials | links |
|---|---|---|---|---|---|---|
| ➖ | dsd_uds_512kb_3k_contexts_cpu | % cpu utilization | +7.81 | [-52.15, +67.77] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_metrics_5mb_cpu | % cpu utilization | +4.65 | [-3.10, +12.41] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_metrics_5mb_memory | memory utilization | +2.30 | [+2.06, +2.55] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_1mb_3k_contexts_cpu | % cpu utilization | +1.68 | [-53.80, +57.17] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_500mb_3k_contexts_memory | memory utilization | +0.98 | [+0.82, +1.14] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_low | memory utilization | +0.97 | [+0.78, +1.16] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_512kb_3k_contexts_memory | memory utilization | +0.70 | [+0.52, +0.87] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_100mb_3k_contexts_memory | memory utilization | +0.64 | [+0.45, +0.82] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_medium | memory utilization | +0.56 | [+0.37, +0.76] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_10mb_3k_contexts_cpu | % cpu utilization | +0.45 | [-31.89, +32.80] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_filtering_5mb_memory | memory utilization | +0.39 | [+0.05, +0.73] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_10mb_3k_contexts_memory | memory utilization | +0.38 | [+0.19, +0.56] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_ultraheavy | memory utilization | +0.34 | [+0.20, +0.47] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_500mb_3k_contexts_cpu | % cpu utilization | +0.31 | [-1.10, +1.73] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_1mb_3k_contexts_memory | memory utilization | +0.25 | [+0.07, +0.42] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_dsd_heavy | memory utilization | +0.18 | [+0.04, +0.32] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_transform_5mb_memory | memory utilization | +0.10 | [-0.15, +0.35] | 1 | (metrics) (profiles) (logs) |
| ➖ | quality_gates_rss_idle | memory utilization | +0.04 | [-0.01, +0.09] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_100mb_3k_contexts_cpu | % cpu utilization | +0.03 | [-6.17, +6.23] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_throughput | ingress throughput | +0.00 | [-0.12, +0.13] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_filtering_5mb_throughput | ingress throughput | +0.00 | [-0.02, +0.02] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_1mb_3k_contexts_throughput | ingress throughput | -0.00 | [-0.06, +0.06] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_512kb_3k_contexts_throughput | ingress throughput | -0.00 | [-0.05, +0.05] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_5mb_throughput | ingress throughput | -0.00 | [-0.02, +0.02] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_transform_5mb_throughput | ingress throughput | -0.00 | [-0.02, +0.02] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_100mb_3k_contexts_throughput | ingress throughput | -0.01 | [-0.04, +0.03] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_10mb_3k_contexts_throughput | ingress throughput | -0.01 | [-0.15, +0.13] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_metrics_5mb_throughput | ingress throughput | -0.03 | [-0.15, +0.10] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_cpu | % cpu utilization | -0.06 | [-5.19, +5.07] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_5mb_memory | memory utilization | -0.34 | [-0.60, -0.09] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_transform_5mb_cpu | % cpu utilization | -1.42 | [-3.62, +0.78] | 1 | (metrics) (profiles) (logs) |
| ➖ | dsd_uds_500mb_3k_contexts_throughput | ingress throughput | -1.95 | [-2.07, -1.83] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_ottl_filtering_5mb_cpu | % cpu utilization | -2.12 | [-4.53, +0.28] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_traces_5mb_cpu | % cpu utilization | -2.56 | [-4.67, -0.46] | 1 | (metrics) (profiles) (logs) |
| ➖ | otlp_ingest_logs_5mb_memory | memory utilization | -3.20 | [-3.96, -2.43] | 1 | (metrics) (profiles) (logs) |
Bounds Checks: ✅ Passed
| perf | experiment | bounds_check_name | replicates_passed | observed_value | links |
|---|---|---|---|---|---|
| ✅ | quality_gates_rss_dsd_heavy | memory_usage | 10/10 | 112.30MiB ≤ 140MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_dsd_low | memory_usage | 10/10 | 33.95MiB ≤ 50MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_dsd_medium | memory_usage | 10/10 | 53.29MiB ≤ 75MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_dsd_ultraheavy | memory_usage | 10/10 | 167.71MiB ≤ 200MiB | (metrics) (profiles) (logs) |
| ✅ | quality_gates_rss_idle | memory_usage | 10/10 | 21.42MiB ≤ 40MiB | (metrics) (profiles) (logs) |
Explanation
Confidence level: 90.00%
Effect size tolerance: |Δ mean %| ≥ 5.00%
Performance changes are noted in the perf column of each table:
- ✅ = significantly better comparison variant performance
- ❌ = significantly worse comparison variant performance
- ➖ = no significant change in performance
A regression test is an A/B test of target performance in a repeatable rig, where "performance" is measured as "comparison variant minus baseline variant" for an optimization goal (e.g., ingress throughput). Due to intrinsic variability in measuring that goal, we can only estimate its mean value for each experiment; we report uncertainty in that value as a 90.00% confidence interval denoted "Δ mean % CI".
For each experiment, we decide whether a change in performance is a "regression" -- a change worth investigating further -- if all of the following criteria are true:
-
Its estimated |Δ mean %| ≥ 5.00%, indicating the change is big enough to merit a closer look.
-
Its 90.00% confidence interval "Δ mean % CI" does not contain zero, indicating that if our statistical model is accurate, there is at least a 90.00% chance there is a difference in performance between baseline and comparison variants.
-
Its configuration does not mark it "erratic".

Summary
Change Type
How did you test this PR?
References