rocketmq_observability/runtime_diagnostics/
service.rs1use std::sync::Arc;
16use std::time::Duration;
17
18use rocketmq_runtime::{
19 ChildServiceContext, RuntimeComponent, RuntimeDiagnosticsInputs, ShutdownDeadline, ShutdownReport,
20};
21use tokio::sync::OnceCell;
22
23use super::{read_token, serve, RuntimeDiagnosticsEndpointConfig, RuntimeDiagnosticsEndpointHandle};
24use crate::metrics::runtime::RuntimeMetricsRecorder;
25use crate::{ObservabilityError, TelemetryHandle};
26
27pub const RUNTIME_DIAGNOSTICS_MODE_ENV: &str = "ROCKETMQ_RUNTIME_DIAGNOSTICS_MODE";
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum RuntimeDiagnosticsMode {
32 Disabled,
33 MetricsOnly { sample_interval: Duration },
34 EndpointOnly(RuntimeDiagnosticsEndpointConfig),
35 EndpointAndMetrics(RuntimeDiagnosticsEndpointConfig),
36}
37
38impl RuntimeDiagnosticsMode {
39 pub fn from_env() -> Result<Self, ObservabilityError> {
50 match super::optional_env(RUNTIME_DIAGNOSTICS_MODE_ENV)?.as_deref() {
51 Some("disabled") => Ok(Self::Disabled),
52 Some("metrics_only") => Ok(Self::MetricsOnly {
53 sample_interval: super::parse_sample_interval()?,
54 }),
55 Some(mode @ ("endpoint_only" | "endpoint_and_metrics")) => {
56 let config = RuntimeDiagnosticsEndpointConfig::from_env()?.ok_or_else(|| {
57 ObservabilityError::invalid_config(
58 "runtime diagnostics endpoint mode requires bind and token settings",
59 )
60 })?;
61 Ok(if mode == "endpoint_only" {
62 Self::EndpointOnly(config)
63 } else {
64 Self::EndpointAndMetrics(config)
65 })
66 }
67 Some(_) => Err(ObservabilityError::invalid_config("unknown runtime diagnostics mode")),
68 None => Ok(RuntimeDiagnosticsEndpointConfig::from_env()?.map_or(Self::Disabled, Self::EndpointAndMetrics)),
69 }
70 }
71
72 fn endpoint(&self) -> Option<&RuntimeDiagnosticsEndpointConfig> {
73 match self {
74 Self::EndpointOnly(config) | Self::EndpointAndMetrics(config) => Some(config),
75 Self::Disabled | Self::MetricsOnly { .. } => None,
76 }
77 }
78
79 fn sample_interval(&self) -> Option<Duration> {
80 match self {
81 Self::MetricsOnly { sample_interval } => Some(*sample_interval),
82 Self::EndpointAndMetrics(config) => Some(config.sample_interval),
83 Self::Disabled | Self::EndpointOnly(_) => None,
84 }
85 }
86}
87
88pub trait RuntimeDiagnosticsDataProvider: Send + Sync {
95 fn snapshot(&self) -> RuntimeDiagnosticsInputs;
96}
97
98impl<F> RuntimeDiagnosticsDataProvider for F
99where
100 F: Fn() -> RuntimeDiagnosticsInputs + Send + Sync,
101{
102 fn snapshot(&self) -> RuntimeDiagnosticsInputs {
103 self()
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct RuntimeDiagnosticsStarted {
109 pub endpoint: Option<RuntimeDiagnosticsEndpointHandle>,
110 pub sampler_active: bool,
111}
112
113struct StartedConfiguration {
114 mode: RuntimeDiagnosticsMode,
115 handle: RuntimeDiagnosticsStarted,
116}
117
118struct ServiceInner {
119 context: ChildServiceContext,
120 observed: ChildServiceContext,
121 component: RuntimeComponent,
122 telemetry: TelemetryHandle,
123 provider: Arc<dyn RuntimeDiagnosticsDataProvider>,
124 started: OnceCell<StartedConfiguration>,
125}
126
127#[derive(Clone)]
134pub struct RuntimeDiagnosticsService {
135 inner: Arc<ServiceInner>,
136}
137
138impl RuntimeDiagnosticsService {
139 pub fn new(
140 parent: &ChildServiceContext,
141 component: RuntimeComponent,
142 telemetry: TelemetryHandle,
143 provider: Arc<dyn RuntimeDiagnosticsDataProvider>,
144 ) -> Self {
145 Self {
146 inner: Arc::new(ServiceInner {
147 context: parent.component("runtime-diagnostics"),
148 observed: parent.clone(),
149 component,
150 telemetry,
151 provider,
152 started: OnceCell::new(),
153 }),
154 }
155 }
156
157 pub async fn start(&self, mode: RuntimeDiagnosticsMode) -> Result<RuntimeDiagnosticsStarted, ObservabilityError> {
164 if self.inner.context.task_group().lifecycle_state() != rocketmq_runtime::TaskGroupLifecycleState::Open {
165 return Err(ObservabilityError::invalid_config(
166 "runtime diagnostics owner is closed",
167 ));
168 }
169 if mode
170 .sample_interval()
171 .is_some_and(|interval| interval.is_zero() || interval > Duration::from_secs(300))
172 {
173 return Err(ObservabilityError::invalid_config(
174 "runtime diagnostics sample interval must be positive and at most 300 seconds",
175 ));
176 }
177 let started = self
178 .inner
179 .started
180 .get_or_try_init(|| self.initialize(mode.clone()))
181 .await?;
182 if started.mode != mode {
183 return Err(ObservabilityError::invalid_config(
184 "runtime diagnostics service already has a different configuration",
185 ));
186 }
187 Ok(started.handle)
188 }
189
190 async fn initialize(&self, mode: RuntimeDiagnosticsMode) -> Result<StartedConfiguration, ObservabilityError> {
191 let context = &self.inner.context;
192 let bound = if let Some(config) = mode.endpoint() {
193 read_token(context, config.token_file.clone()).await?;
194 let listener = tokio::net::TcpListener::bind(config.bind_addr)
195 .await
196 .map_err(|_| ObservabilityError::invalid_config("runtime diagnostics listener cannot bind"))?;
197 let local_addr = listener.local_addr().map_err(|_| {
198 ObservabilityError::invalid_config("runtime diagnostics listener address is unavailable")
199 })?;
200 Some((listener, local_addr, config.token_file.clone()))
201 } else {
202 None
203 };
204 let recorder = if mode.sample_interval().is_some() {
205 RuntimeMetricsRecorder::from_handle(&self.inner.telemetry, self.inner.component)
206 } else {
207 RuntimeMetricsRecorder::noop(self.inner.component)
208 };
209 let sample_interval = mode.sample_interval().filter(|_| recorder.is_enabled());
210 let sampler_active = sample_interval.is_some();
211 if let Some(sample_interval) = sample_interval {
212 let sample_context = self.inner.observed.clone();
213 let component = self.inner.component;
214 let provider = self.inner.provider.clone();
215 let metrics = recorder.clone();
216 context
217 .scheduled_tasks("sampler")
218 .schedule(
219 rocketmq_runtime::ScheduledTaskConfig::fixed_delay("runtime-diagnostics.sample", sample_interval),
220 rocketmq_runtime::ScheduledExecutionPolicy::serial(rocketmq_runtime::MissedTickPolicy::Skip),
221 move || {
222 let context = sample_context.clone();
223 let provider = provider.clone();
224 let metrics = metrics.clone();
225 async move {
226 metrics.record_snapshot_v2(&context.diagnostics_view_v2(component, provider.snapshot()));
227 }
228 },
229 )
230 .map_err(|_| ObservabilityError::invalid_config("runtime diagnostics sampler cannot start"))?;
231 }
232 let endpoint = if let Some((listener, local_addr, token_file)) = bound {
233 let cancellation = context.task_group().cancellation_token();
234 let endpoint_context = super::EndpointContext {
235 owner: context.clone(),
236 observed: self.inner.observed.clone(),
237 component: self.inner.component,
238 token_file,
239 metrics: recorder,
240 provider: self.inner.provider.clone(),
241 };
242 if context
243 .spawn_service("runtime-diagnostics.endpoint", async move {
244 serve(listener, endpoint_context, cancellation).await;
245 })
246 .is_err()
247 {
248 context.task_group().shutdown(Duration::ZERO).await;
249 return Err(ObservabilityError::invalid_config(
250 "runtime diagnostics endpoint cannot start",
251 ));
252 }
253 Some(RuntimeDiagnosticsEndpointHandle { local_addr })
254 } else {
255 None
256 };
257 Ok(StartedConfiguration {
258 mode,
259 handle: RuntimeDiagnosticsStarted {
260 endpoint,
261 sampler_active,
262 },
263 })
264 }
265
266 pub async fn shutdown_until(&self, deadline: ShutdownDeadline) -> ShutdownReport {
267 self.inner.context.task_group().shutdown_until(deadline).await
268 }
269}