1use std::sync::Arc;
16
17use chrono::DateTime;
18use chrono::Utc;
19use parking_lot::RwLock;
20use serde::Deserialize;
21use serde::Serialize;
22
23use crate::config::LogsExporter;
24use crate::config::MetricsExporter;
25use crate::config::ObservabilityConfig;
26use crate::config::SubscriberInstallStatus;
27use crate::config::TraceExporter;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum ObservabilityStatusValueV1 {
32 Initializing,
33 Ready,
34 Disabled,
35 Unknown,
36 NotInstrumented,
37 InProgress,
38 Shutdown,
39 Failed,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct ObservabilityCompiledFeaturesV1 {
44 pub metrics: bool,
45 pub traces: bool,
46 pub logs: bool,
47 pub otlp_grpc: bool,
48 pub prometheus: bool,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct ObservabilitySignalStatusV1 {
53 pub enabled: bool,
54 pub exporter: String,
55 pub initialization: ObservabilityStatusValueV1,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ObservabilitySubscriberStatusV1 {
60 pub attempted: bool,
61 pub installed: bool,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct ObservabilityStatusViewV1 {
70 pub schema_version: String,
71 pub observed_at: DateTime<Utc>,
72 pub enabled: bool,
73 pub compiled_features: ObservabilityCompiledFeaturesV1,
74 pub metrics: ObservabilitySignalStatusV1,
75 pub traces: ObservabilitySignalStatusV1,
76 pub logs: ObservabilitySignalStatusV1,
77 pub otlp_protocol: String,
78 pub otlp_endpoint: Option<String>,
79 pub subscriber: ObservabilitySubscriberStatusV1,
80 pub initialization: ObservabilityStatusValueV1,
81 pub export: ObservabilityStatusValueV1,
82 pub queue: ObservabilityStatusValueV1,
83 pub drops: ObservabilityStatusValueV1,
84 pub shutdown: ObservabilityStatusValueV1,
85}
86
87impl ObservabilityStatusViewV1 {
88 pub const SCHEMA_VERSION: &'static str = "rocketmq.observability-status.v1";
89}
90
91#[derive(Debug, Clone)]
92pub struct ObservabilityStatusHandle {
93 inner: Arc<RwLock<ObservabilityStatusState>>,
94}
95
96#[derive(Debug, Clone)]
97struct ObservabilityStatusState {
98 enabled: bool,
99 metrics: ObservabilitySignalStatusV1,
100 traces: ObservabilitySignalStatusV1,
101 logs: ObservabilitySignalStatusV1,
102 otlp_protocol: String,
103 otlp_endpoint: Option<String>,
104 subscriber: ObservabilitySubscriberStatusV1,
105 initialization: ObservabilityStatusValueV1,
106 export: ObservabilityStatusValueV1,
107 queue: ObservabilityStatusValueV1,
108 drops: ObservabilityStatusValueV1,
109 shutdown: ObservabilityStatusValueV1,
110}
111
112impl Default for ObservabilityStatusHandle {
113 fn default() -> Self {
114 Self::from_config(&ObservabilityConfig::default())
115 }
116}
117
118impl ObservabilityStatusHandle {
119 pub(crate) fn from_config(config: &ObservabilityConfig) -> Self {
120 let initialization = if config.enabled {
121 ObservabilityStatusValueV1::Initializing
122 } else {
123 ObservabilityStatusValueV1::Disabled
124 };
125 let export = if config.enabled {
126 ObservabilityStatusValueV1::Unknown
127 } else {
128 ObservabilityStatusValueV1::Disabled
129 };
130 Self {
131 inner: Arc::new(RwLock::new(ObservabilityStatusState {
132 enabled: config.enabled,
133 metrics: metric_status(config),
134 traces: trace_status(config),
135 logs: log_status(config),
136 otlp_protocol: otlp_protocol(config),
137 otlp_endpoint: uses_otlp(config).then(|| sanitize_endpoint(config.otlp.endpoint.as_str())),
138 subscriber: ObservabilitySubscriberStatusV1 {
139 attempted: false,
140 installed: false,
141 },
142 initialization,
143 export,
144 queue: status_when_enabled(config, ObservabilityStatusValueV1::NotInstrumented),
145 drops: status_when_enabled(config, ObservabilityStatusValueV1::NotInstrumented),
146 shutdown: status_when_enabled(config, ObservabilityStatusValueV1::Unknown),
147 })),
148 }
149 }
150
151 pub fn view(&self) -> ObservabilityStatusViewV1 {
152 let state = self.inner.read().clone();
153 ObservabilityStatusViewV1 {
154 schema_version: ObservabilityStatusViewV1::SCHEMA_VERSION.to_string(),
155 observed_at: Utc::now(),
156 enabled: state.enabled,
157 compiled_features: compiled_features(),
158 metrics: state.metrics,
159 traces: state.traces,
160 logs: state.logs,
161 otlp_protocol: state.otlp_protocol,
162 otlp_endpoint: state.otlp_endpoint,
163 subscriber: state.subscriber,
164 initialization: state.initialization,
165 export: state.export,
166 queue: state.queue,
167 drops: state.drops,
168 shutdown: state.shutdown,
169 }
170 }
171
172 pub(crate) fn mark_initialized(&self, subscriber: SubscriberInstallStatus) {
173 let mut state = self.inner.write();
174 state.subscriber = ObservabilitySubscriberStatusV1 {
175 attempted: subscriber.attempted,
176 installed: subscriber.installed,
177 };
178 state.initialization = if state.enabled {
179 ObservabilityStatusValueV1::Ready
180 } else {
181 ObservabilityStatusValueV1::Disabled
182 };
183 mark_signal_initialized(&mut state.metrics);
184 mark_signal_initialized(&mut state.traces);
185 mark_signal_initialized(&mut state.logs);
186 state.export = if !state.enabled || !any_signal_enabled(&state) {
187 ObservabilityStatusValueV1::Disabled
188 } else if uses_remote_exporter(&state) {
189 ObservabilityStatusValueV1::Unknown
190 } else {
191 ObservabilityStatusValueV1::Ready
192 };
193 }
194
195 pub(crate) fn mark_shutdown_started(&self) {
196 self.inner.write().shutdown = ObservabilityStatusValueV1::InProgress;
197 }
198
199 pub(crate) fn mark_shutdown_finished(&self, healthy: bool) {
200 self.inner.write().shutdown = if healthy {
201 ObservabilityStatusValueV1::Shutdown
202 } else {
203 ObservabilityStatusValueV1::Failed
204 };
205 }
206}
207
208fn mark_signal_initialized(signal: &mut ObservabilitySignalStatusV1) {
209 if signal.enabled && signal.initialization != ObservabilityStatusValueV1::NotInstrumented {
210 signal.initialization = ObservabilityStatusValueV1::Ready;
211 }
212}
213
214fn metric_status(config: &ObservabilityConfig) -> ObservabilitySignalStatusV1 {
215 signal_status(
216 config.enabled && config.metrics.enabled,
217 match config.metrics.exporter {
218 MetricsExporter::Disable => "disabled",
219 MetricsExporter::OtlpGrpc => "otlp_grpc",
220 MetricsExporter::Prometheus => "prometheus",
221 MetricsExporter::Log => "log",
222 },
223 cfg!(feature = "otel-metrics"),
224 )
225}
226
227fn trace_status(config: &ObservabilityConfig) -> ObservabilitySignalStatusV1 {
228 signal_status(
229 config.enabled && config.traces.enabled,
230 match config.traces.exporter {
231 TraceExporter::Disable => "disabled",
232 TraceExporter::OtlpGrpc => "otlp_grpc",
233 TraceExporter::Log => "log",
234 },
235 cfg!(feature = "otel-traces"),
236 )
237}
238
239fn log_status(config: &ObservabilityConfig) -> ObservabilitySignalStatusV1 {
240 signal_status(
241 config.enabled && config.logs.enabled,
242 match config.logs.exporter {
243 LogsExporter::Disable => "disabled",
244 LogsExporter::OtlpGrpc => "otlp_grpc",
245 LogsExporter::Log => "log",
246 },
247 cfg!(feature = "otel-logs"),
248 )
249}
250
251fn signal_status(enabled: bool, exporter: &str, compiled: bool) -> ObservabilitySignalStatusV1 {
252 ObservabilitySignalStatusV1 {
253 enabled,
254 exporter: exporter.to_string(),
255 initialization: if !enabled {
256 ObservabilityStatusValueV1::Disabled
257 } else if compiled {
258 ObservabilityStatusValueV1::Initializing
259 } else {
260 ObservabilityStatusValueV1::NotInstrumented
261 },
262 }
263}
264
265fn uses_remote_exporter(state: &ObservabilityStatusState) -> bool {
266 [&state.metrics, &state.traces, &state.logs]
267 .into_iter()
268 .any(|signal| signal.enabled && signal.exporter == "otlp_grpc")
269}
270
271fn any_signal_enabled(state: &ObservabilityStatusState) -> bool {
272 state.metrics.enabled || state.traces.enabled || state.logs.enabled
273}
274
275fn status_when_enabled(
276 config: &ObservabilityConfig,
277 enabled_status: ObservabilityStatusValueV1,
278) -> ObservabilityStatusValueV1 {
279 if config.enabled {
280 enabled_status
281 } else {
282 ObservabilityStatusValueV1::Disabled
283 }
284}
285
286pub(crate) fn uses_otlp(config: &ObservabilityConfig) -> bool {
287 config.enabled
288 && ((config.metrics.enabled && config.metrics.exporter == MetricsExporter::OtlpGrpc)
289 || (config.traces.enabled && config.traces.exporter == TraceExporter::OtlpGrpc)
290 || (config.logs.enabled && config.logs.exporter == LogsExporter::OtlpGrpc))
291}
292
293fn otlp_protocol(config: &ObservabilityConfig) -> String {
294 match config.otlp.protocol {
295 crate::config::OtlpProtocol::Grpc => "grpc",
296 crate::config::OtlpProtocol::HttpBinary => "http_binary",
297 crate::config::OtlpProtocol::HttpJson => "http_json",
298 }
299 .to_string()
300}
301
302fn sanitize_endpoint(endpoint: &str) -> String {
303 let Some((scheme, _rest)) = endpoint.trim().split_once("://") else {
304 return "<redacted>".to_string();
305 };
306 match scheme {
307 "http" | "https" | "grpc" => format!("{scheme}://<redacted>"),
308 _ => "<redacted>".to_string(),
309 }
310}
311
312fn compiled_features() -> ObservabilityCompiledFeaturesV1 {
313 ObservabilityCompiledFeaturesV1 {
314 metrics: cfg!(feature = "otel-metrics"),
315 traces: cfg!(feature = "otel-traces"),
316 logs: cfg!(feature = "otel-logs"),
317 otlp_grpc: cfg!(feature = "otlp-grpc"),
318 prometheus: cfg!(feature = "prometheus"),
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn endpoint_status_does_not_disclose_authority_or_credentials() {
328 let mut config = ObservabilityConfig {
329 enabled: true,
330 ..ObservabilityConfig::default()
331 };
332 config.metrics.enabled = true;
333 config.metrics.exporter = MetricsExporter::OtlpGrpc;
334 config.otlp.endpoint = "https://user:secret@collector.internal:4317/v1/metrics".to_string();
335
336 let view = ObservabilityStatusHandle::from_config(&config).view();
337 let json = serde_json::to_string(&view).expect("status should serialize");
338
339 assert_eq!(view.otlp_endpoint.as_deref(), Some("https://<redacted>"));
340 assert!(!json.contains("secret"));
341 assert!(!json.contains("collector.internal"));
342 }
343
344 #[test]
345 fn unavailable_exporter_health_is_reported_honestly() {
346 let mut config = ObservabilityConfig {
347 enabled: true,
348 ..ObservabilityConfig::default()
349 };
350 config.metrics.enabled = true;
351 config.metrics.exporter = MetricsExporter::OtlpGrpc;
352 let handle = ObservabilityStatusHandle::from_config(&config);
353 handle.mark_initialized(SubscriberInstallStatus::default());
354
355 let view = handle.view();
356 assert_eq!(view.initialization, ObservabilityStatusValueV1::Ready);
357 assert_eq!(view.export, ObservabilityStatusValueV1::Unknown);
358 assert_eq!(view.queue, ObservabilityStatusValueV1::NotInstrumented);
359 assert_eq!(view.drops, ObservabilityStatusValueV1::NotInstrumented);
360 }
361
362 #[test]
363 fn global_disable_does_not_report_signal_initialization_or_export_health() {
364 let mut config = ObservabilityConfig::default();
365 config.metrics.enabled = true;
366 config.metrics.exporter = MetricsExporter::OtlpGrpc;
367 let handle = ObservabilityStatusHandle::from_config(&config);
368 handle.mark_initialized(SubscriberInstallStatus::default());
369
370 let view = handle.view();
371
372 assert_eq!(view.initialization, ObservabilityStatusValueV1::Disabled);
373 assert_eq!(view.metrics.initialization, ObservabilityStatusValueV1::Disabled);
374 assert_eq!(view.export, ObservabilityStatusValueV1::Disabled);
375 assert_eq!(view.queue, ObservabilityStatusValueV1::Disabled);
376 assert_eq!(view.drops, ObservabilityStatusValueV1::Disabled);
377 assert_eq!(view.shutdown, ObservabilityStatusValueV1::Disabled);
378 assert_eq!(view.otlp_endpoint, None);
379 }
380}