1use std::num::NonZeroUsize;
16use std::time::Duration;
17
18use tracing_appender::non_blocking::ErrorCounter;
19use tracing_appender::non_blocking::NonBlockingBuilder;
20use tracing_appender::non_blocking::WorkerGuard;
21use tracing_appender::rolling::RollingFileAppender;
22use tracing_appender::rolling::Rotation;
23use tracing_subscriber::filter::EnvFilter;
24use tracing_subscriber::layer::SubscriberExt;
25use tracing_subscriber::reload;
26use tracing_subscriber::Layer;
27use tracing_subscriber::Registry;
28
29use rocketmq_runtime::BlockingDrainLease;
30use rocketmq_runtime::ChildServiceContext;
31use rocketmq_runtime::RuntimeResult;
32use rocketmq_runtime::ShutdownDeadline;
33use serde::Serialize;
34
35use crate::config::ConsoleLogConfig;
36use crate::config::FileLogConfig;
37use crate::config::LogFormat;
38use crate::config::LogRotation;
39use crate::config::SubscriberInstallPolicy;
40use crate::config::SubscriberInstallStatus;
41use crate::config::TelemetryBootstrapConfig;
42use crate::error::ObservabilityError;
43use crate::init::TelemetryProviderGuard;
44use crate::init::TelemetryProviderShutdownReport;
45use crate::LogFilterHandle;
46use crate::LogFilterInputs;
47use crate::LogFilterResolver;
48use crate::ResolvedLogFilter;
49use crate::TelemetryHandle;
50
51pub type BoxedRegistryLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
52
53#[derive(Default)]
54pub struct LoggingGuard {
55 worker_guards: Vec<WorkerGuard>,
56 dropped_log_counters: Vec<ErrorCounter>,
57}
58
59impl LoggingGuard {
60 pub fn noop() -> Self {
61 Self::new(Vec::new(), Vec::new())
62 }
63
64 pub(crate) fn new(worker_guards: Vec<WorkerGuard>, dropped_log_counters: Vec<ErrorCounter>) -> Self {
65 Self {
66 worker_guards,
67 dropped_log_counters,
68 }
69 }
70
71 pub(crate) fn append(&mut self, mut other: Self) {
72 self.worker_guards.append(&mut other.worker_guards);
73 self.dropped_log_counters.append(&mut other.dropped_log_counters);
74 }
75
76 pub fn dropped_log_lines(&self) -> usize {
77 self.dropped_log_counters.iter().map(ErrorCounter::dropped_lines).sum()
78 }
79
80 pub fn file_sink_count(&self) -> usize {
81 self.worker_guards.len()
82 }
83}
84
85pub struct FileLogLayer {
86 layer: BoxedRegistryLayer,
87 logging_guard: LoggingGuard,
88}
89
90impl FileLogLayer {
91 fn new(layer: BoxedRegistryLayer, logging_guard: LoggingGuard) -> Self {
92 Self { layer, logging_guard }
93 }
94
95 pub fn logging_guard(&self) -> &LoggingGuard {
96 &self.logging_guard
97 }
98
99 pub fn into_parts(self) -> (BoxedRegistryLayer, LoggingGuard) {
100 (self.layer, self.logging_guard)
101 }
102}
103
104pub fn build_env_filter(filter: &str) -> Result<EnvFilter, ObservabilityError> {
105 EnvFilter::try_new(filter).map_err(|error| ObservabilityError::invalid_log_filter(filter, error))
106}
107
108pub fn build_console_layer(config: &ConsoleLogConfig) -> Option<BoxedRegistryLayer> {
109 if !config.enabled {
110 return None;
111 }
112
113 let layer = tracing_subscriber::fmt::layer()
114 .with_writer(std::io::stderr)
115 .with_ansi(config.ansi)
116 .with_target(config.target)
117 .with_thread_ids(config.thread_ids)
118 .with_thread_names(config.thread_names)
119 .with_line_number(config.line_number);
120
121 match config.format {
122 LogFormat::Compact => Some(Box::new(layer.compact())),
123 LogFormat::Pretty => Some(Box::new(layer.pretty())),
124 LogFormat::Json => Some(Box::new(layer.json())),
125 }
126}
127
128pub fn install_global(config: &TelemetryBootstrapConfig) -> Result<TelemetryRuntimeGuard, ObservabilityError> {
129 let resolved_filter = LogFilterResolver::resolve(LogFilterInputs {
130 config: Some(config.logging.filter.as_str()),
131 ..LogFilterInputs::default()
132 })?;
133 install_global_with_filter(config, resolved_filter)
134}
135
136pub async fn install_global_with_service_context(
138 config: &TelemetryBootstrapConfig,
139 service_context: &ChildServiceContext,
140) -> Result<TelemetryRuntimeGuard, ObservabilityError> {
141 let resolved_filter = LogFilterResolver::resolve(LogFilterInputs {
142 config: Some(config.logging.filter.as_str()),
143 ..LogFilterInputs::default()
144 })?;
145 install_global_with_filter_and_service_context(config, resolved_filter, service_context).await
146}
147
148pub fn install_global_with_filter(
149 config: &TelemetryBootstrapConfig,
150 resolved_filter: ResolvedLogFilter,
151) -> Result<TelemetryRuntimeGuard, ObservabilityError> {
152 install_global_with_filter_inner(config, resolved_filter)
153}
154
155pub async fn install_global_with_filter_and_service_context(
157 config: &TelemetryBootstrapConfig,
158 resolved_filter: ResolvedLogFilter,
159 service_context: &ChildServiceContext,
160) -> Result<TelemetryRuntimeGuard, ObservabilityError> {
161 init_log_tracer();
162
163 let mut telemetry_guard =
164 crate::init::init_telemetry_providers_with_service_context(&config.observability, service_context)?;
165 let telemetry_handle = telemetry_guard.handle();
166 let (layers, filter, logging_guard) =
167 match build_subscriber_layers(config, resolved_filter.filter(), &telemetry_guard) {
168 Ok(layers) => layers,
169 Err(error) => {
170 return Err(cleanup_failed_scoped_install(
171 telemetry_guard,
172 LoggingGuard::noop(),
173 service_context,
174 error,
175 )
176 .await);
177 }
178 };
179
180 let (subscriber_install_status, log_filter_handle, log_filter_metrics) = install_subscriber_layers(
181 layers,
182 filter,
183 resolved_filter.clone(),
184 config.logging.reload.enabled,
185 config.observability.service_name.as_str(),
186 telemetry_handle,
187 );
188 telemetry_guard.set_subscriber_install_status(subscriber_install_status);
189
190 if let Err(error) = enforce_bootstrap_subscriber_policy(config, subscriber_install_status) {
191 drop(log_filter_handle);
192 drop(log_filter_metrics);
193 return Err(cleanup_failed_scoped_install(telemetry_guard, logging_guard, service_context, error).await);
194 }
195
196 if subscriber_install_status.installed {
197 if let Some(metrics) = log_filter_metrics.as_ref() {
198 metrics.set_active(None, resolved_filter.source());
199 }
200 }
201
202 #[cfg(feature = "prometheus")]
203 if let Err(error) = telemetry_guard.start_prometheus_endpoint(&config.observability, service_context) {
204 drop(log_filter_handle);
205 drop(log_filter_metrics);
206 return Err(cleanup_failed_scoped_install(telemetry_guard, logging_guard, service_context, error).await);
207 }
208
209 Ok(TelemetryRuntimeGuard::new_with_log_filter_runtime(
210 telemetry_guard,
211 logging_guard,
212 log_filter_handle,
213 log_filter_metrics,
214 ))
215}
216
217fn install_global_with_filter_inner(
218 config: &TelemetryBootstrapConfig,
219 resolved_filter: ResolvedLogFilter,
220) -> Result<TelemetryRuntimeGuard, ObservabilityError> {
221 init_log_tracer();
222
223 let mut telemetry_guard = crate::init::init_telemetry_providers(&config.observability)?;
224 let telemetry_handle = telemetry_guard.handle();
225 let (layers, filter, logging_guard) =
226 match build_subscriber_layers(config, resolved_filter.filter(), &telemetry_guard) {
227 Ok(layers) => layers,
228 Err(error) => {
229 let _ = telemetry_guard.shutdown();
230 return Err(error);
231 }
232 };
233
234 let (subscriber_install_status, log_filter_handle, log_filter_metrics) = install_subscriber_layers(
235 layers,
236 filter,
237 resolved_filter.clone(),
238 config.logging.reload.enabled,
239 config.observability.service_name.as_str(),
240 telemetry_handle,
241 );
242 telemetry_guard.set_subscriber_install_status(subscriber_install_status);
243 telemetry_guard.mark_status_initialized();
244
245 if let Err(error) = enforce_bootstrap_subscriber_policy(config, subscriber_install_status) {
246 let _ = telemetry_guard.shutdown();
247 drop(logging_guard);
248 return Err(error);
249 }
250
251 if subscriber_install_status.installed {
252 if let Some(metrics) = log_filter_metrics.as_ref() {
253 metrics.set_active(None, resolved_filter.source());
254 }
255 }
256
257 Ok(TelemetryRuntimeGuard::new_with_log_filter_runtime(
258 telemetry_guard,
259 logging_guard,
260 log_filter_handle,
261 log_filter_metrics,
262 ))
263}
264
265async fn cleanup_failed_scoped_install(
266 telemetry_guard: TelemetryProviderGuard,
267 logging_guard: LoggingGuard,
268 service_context: &ChildServiceContext,
269 primary_error: ObservabilityError,
270) -> ObservabilityError {
271 let report = TelemetryRuntimeGuard::new(telemetry_guard, logging_guard)
272 .shutdown_with_service_context(
273 service_context,
274 std::time::Duration::from_millis(crate::exporter::outage::DEFAULT_SHUTDOWN_TIMEOUT_MILLIS),
275 )
276 .await;
277 if !report.is_healthy() {
278 tracing::warn!(
279 error = %primary_error,
280 report = %report.to_json(),
281 "scoped telemetry initialization failed and rollback was unhealthy"
282 );
283 }
284 primary_error
285}
286
287fn init_log_tracer() {
288 if let Err(error) = tracing_log::LogTracer::init() {
289 tracing::debug!(
290 target: "rocketmq_observability",
291 %error,
292 "log tracer was already initialized; keeping the existing log facade bridge"
293 );
294 }
295}
296
297fn build_subscriber_layers(
298 config: &TelemetryBootstrapConfig,
299 resolved_filter: &str,
300 _telemetry_guard: &TelemetryProviderGuard,
301) -> Result<(Vec<BoxedRegistryLayer>, Option<EnvFilter>, LoggingGuard), ObservabilityError> {
302 let mut layers = Vec::new();
303 let mut logging_guard = LoggingGuard::noop();
304
305 if config.logging.enabled {
306 if let Some(console_layer) = build_console_layer(&config.logging.console) {
307 layers.push(console_layer);
308 }
309
310 if let Some(file_layer) = build_file_layer(&config.logging.file)? {
311 let (layer, file_logging_guard) = file_layer.into_parts();
312 layers.push(layer);
313 logging_guard.append(file_logging_guard);
314 }
315 }
316
317 #[cfg(feature = "otel-traces")]
318 if let Some(tracer_provider) = _telemetry_guard.tracer_provider() {
319 layers.push(Box::new(crate::trace::build_tracing_layer(
320 &config.observability,
321 tracer_provider,
322 )));
323 }
324
325 #[cfg(feature = "otel-logs")]
326 if let Some(logger_provider) = _telemetry_guard.logger_provider() {
327 layers.push(Box::new(crate::logs::bridge::build_logs_layer(logger_provider)));
328 }
329
330 let filter = if layers.is_empty() {
331 None
332 } else {
333 Some(build_env_filter(resolved_filter)?)
334 };
335 Ok((layers, filter, logging_guard))
336}
337
338fn compose_subscriber_layers(
339 layers: Vec<BoxedRegistryLayer>,
340 filter: Option<EnvFilter>,
341) -> impl tracing::Subscriber + Send + Sync + 'static {
342 tracing_subscriber::registry().with(layers).with(filter)
343}
344
345fn install_subscriber_layers(
346 layers: Vec<BoxedRegistryLayer>,
347 filter: Option<EnvFilter>,
348 resolved_filter: ResolvedLogFilter,
349 reload_enabled: bool,
350 service_name: &str,
351 telemetry: TelemetryHandle,
352) -> (
353 SubscriberInstallStatus,
354 Option<LogFilterHandle>,
355 Option<crate::metrics::log_filter::LogFilterMetrics>,
356) {
357 if layers.is_empty() && filter.is_none() {
358 return (SubscriberInstallStatus::default(), None, None);
359 }
360
361 let metrics = crate::metrics::log_filter::LogFilterMetrics::new(telemetry, service_name);
362 if reload_enabled {
363 if let Some(filter) = filter {
364 let (filter_layer, reload_handle) = reload::Layer::new(filter);
365 let result =
366 tracing::subscriber::set_global_default(tracing_subscriber::registry().with(layers).with(filter_layer));
367 let status = SubscriberInstallStatus::attempted(result.is_ok());
368 let handle = status.installed.then(|| {
369 LogFilterHandle::new(resolved_filter, metrics.clone(), move |filter| {
370 reload_handle
371 .reload(filter)
372 .map_err(|error| ObservabilityError::logging_init(format!("log filter reload failed: {error}")))
373 })
374 });
375 let retained_metrics = status.installed.then_some(metrics);
376 return (status, handle, retained_metrics);
377 }
378 }
379
380 let result = tracing::subscriber::set_global_default(compose_subscriber_layers(layers, filter));
381 let status = SubscriberInstallStatus::attempted(result.is_ok());
382 let retained_metrics = status.installed.then_some(metrics);
383 (status, None, retained_metrics)
384}
385
386fn enforce_bootstrap_subscriber_policy(
387 config: &TelemetryBootstrapConfig,
388 status: SubscriberInstallStatus,
389) -> Result<(), ObservabilityError> {
390 if !status.attempted || status.installed {
391 return Ok(());
392 }
393
394 match config.observability.subscriber_install_policy {
395 SubscriberInstallPolicy::Required => Err(ObservabilityError::subscriber_install_failed(
396 status.attempted,
397 status.installed,
398 )),
399 SubscriberInstallPolicy::BestEffort => {
400 tracing::warn!(
401 target: "rocketmq_observability",
402 attempted = status.attempted,
403 installed = status.installed,
404 "tracing subscriber was already initialized; unified logging and OpenTelemetry layers were not installed"
405 );
406 Ok(())
407 }
408 }
409}
410
411pub fn build_file_layer(config: &FileLogConfig) -> Result<Option<FileLogLayer>, ObservabilityError> {
412 if !config.enabled {
413 return Ok(None);
414 }
415
416 if config.directory.trim().is_empty() {
417 return Err(ObservabilityError::logging_init("file log directory must not be empty"));
418 }
419
420 if config.file_name_prefix.trim().is_empty() {
421 return Err(ObservabilityError::logging_init(
422 "file log name prefix must not be empty",
423 ));
424 }
425
426 let appender = build_rolling_file_appender(config)?;
427 let (writer, worker_guard) = NonBlockingBuilder::default()
428 .buffered_lines_limit(config.non_blocking.buffered_lines_limit)
429 .lossy(config.non_blocking.lossy)
430 .finish(appender);
431 let error_counter = writer.error_counter();
432
433 let layer = tracing_subscriber::fmt::layer()
434 .with_writer(writer)
435 .with_ansi(false)
436 .with_target(true)
437 .with_thread_ids(true)
438 .with_thread_names(true)
439 .with_line_number(true);
440
441 let layer = match config.format {
442 LogFormat::Compact => Box::new(layer.compact()) as BoxedRegistryLayer,
443 LogFormat::Pretty => Box::new(layer.pretty()) as BoxedRegistryLayer,
444 LogFormat::Json => Box::new(layer.json()) as BoxedRegistryLayer,
445 };
446
447 Ok(Some(FileLogLayer::new(
448 layer,
449 LoggingGuard::new(vec![worker_guard], vec![error_counter]),
450 )))
451}
452
453fn build_rolling_file_appender(config: &FileLogConfig) -> Result<RollingFileAppender, ObservabilityError> {
454 RollingFileAppender::builder()
455 .rotation(to_tracing_rotation(config.rotation))
456 .filename_prefix(config.file_name_prefix.as_str())
457 .build(config.directory.as_str())
458 .map_err(ObservabilityError::logging_init)
459}
460
461fn to_tracing_rotation(rotation: LogRotation) -> Rotation {
462 match rotation {
463 LogRotation::Never => Rotation::NEVER,
464 LogRotation::Hourly => Rotation::HOURLY,
465 LogRotation::Daily => Rotation::DAILY,
466 }
467}
468
469#[must_use = "telemetry providers and logging workers must be shut down explicitly"]
470pub struct TelemetryRuntimeGuard {
471 telemetry_guard: TelemetryProviderGuard,
472 handle: TelemetryHandle,
473 logging_guard: LoggingGuard,
474 log_filter_handle: Option<LogFilterHandle>,
475 log_filter_metrics: Option<crate::metrics::log_filter::LogFilterMetrics>,
476}
477
478impl std::fmt::Debug for TelemetryRuntimeGuard {
479 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 formatter
481 .debug_struct("TelemetryRuntimeGuard")
482 .field("telemetry_state", &self.handle.state())
483 .field("subscriber_install_status", &self.subscriber_install_status())
484 .field("file_sink_count", &self.logging_guard.file_sink_count())
485 .field("dropped_log_lines", &self.logging_guard.dropped_log_lines())
486 .field("log_filter_metrics_active", &self.log_filter_metrics.is_some())
487 .finish_non_exhaustive()
488 }
489}
490
491#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
492pub struct TelemetryShutdownReport {
493 pub subscriber_installed: bool,
494 pub file_log_enabled: bool,
495 pub dropped_log_lines: usize,
496 pub logs_shutdown_ok: bool,
497 pub traces_shutdown_ok: bool,
498 pub metrics_shutdown_ok: bool,
499 pub logs_shutdown_error: Option<String>,
500 pub traces_shutdown_error: Option<String>,
501 pub metrics_shutdown_error: Option<String>,
502}
503
504impl TelemetryShutdownReport {
505 fn new(
506 subscriber_install_status: SubscriberInstallStatus,
507 file_log_enabled: bool,
508 dropped_log_lines: usize,
509 provider_report: TelemetryProviderShutdownReport,
510 ) -> Self {
511 Self {
512 subscriber_installed: subscriber_install_status.installed,
513 file_log_enabled,
514 dropped_log_lines,
515 logs_shutdown_ok: provider_report.logs_shutdown_ok,
516 traces_shutdown_ok: provider_report.traces_shutdown_ok,
517 metrics_shutdown_ok: provider_report.metrics_shutdown_ok,
518 logs_shutdown_error: provider_report.logs_shutdown_error,
519 traces_shutdown_error: provider_report.traces_shutdown_error,
520 metrics_shutdown_error: provider_report.metrics_shutdown_error,
521 }
522 }
523
524 pub fn is_healthy(&self) -> bool {
525 self.logs_shutdown_ok && self.traces_shutdown_ok && self.metrics_shutdown_ok
526 }
527
528 pub fn into_result(self) -> Result<Self, ObservabilityError> {
529 if let Some(error) = self.logs_shutdown_error.as_ref() {
530 return Err(ObservabilityError::logs_shutdown(error));
531 }
532 if let Some(error) = self.traces_shutdown_error.as_ref() {
533 return Err(ObservabilityError::traces_shutdown(error));
534 }
535 if let Some(error) = self.metrics_shutdown_error.as_ref() {
536 return Err(ObservabilityError::metrics_shutdown(error));
537 }
538 Ok(self)
539 }
540
541 pub fn to_json(&self) -> String {
542 serde_json::to_string_pretty(self).unwrap_or_else(|error| format!("{{\"serialization_error\":\"{error}\"}}"))
543 }
544}
545
546enum TelemetryFlushAuthority<'a> {
551 ServiceContext(&'a ChildServiceContext),
552 Drain(BlockingDrainLease),
553}
554
555impl TelemetryFlushAuthority<'_> {
556 async fn flush<F>(
557 self,
558 name: &'static str,
559 deadline: ShutdownDeadline,
560 operation: F,
561 ) -> RuntimeResult<TelemetryShutdownReport>
562 where
563 F: FnOnce() -> TelemetryShutdownReport + Send + 'static,
564 {
565 match self {
566 Self::ServiceContext(service_context) => {
567 service_context
568 .metadata_io()
569 .spawn_io_until(name, deadline, operation)
570 .await
571 }
572 Self::Drain(lease) => lease.spawn_io_until(name, deadline, operation).await,
573 }
574 }
575}
576
577const UNKNOWN_SHUTDOWN_DEADLINE_HORIZON: Duration = Duration::from_secs(u32::MAX as u64);
580
581#[must_use]
593pub fn reserve_telemetry_flush_lease(
594 service_context: &ChildServiceContext,
595 deadline: Option<ShutdownDeadline>,
596) -> Option<BlockingDrainLease> {
597 let deadline = deadline.unwrap_or_else(|| ShutdownDeadline::after(UNKNOWN_SHUTDOWN_DEADLINE_HORIZON));
598 match service_context
599 .metadata_io()
600 .try_drain_lease(deadline, NonZeroUsize::MIN)
601 {
602 Ok(lease) => Some(lease),
603 Err(error) => {
604 tracing::warn!(
605 error = %error,
606 "final telemetry flush could not reserve its blocking allowance"
607 );
608 None
609 }
610 }
611}
612
613impl TelemetryRuntimeGuard {
614 pub(crate) fn new(telemetry_guard: TelemetryProviderGuard, logging_guard: LoggingGuard) -> Self {
615 let handle = telemetry_guard.handle();
616 Self {
617 telemetry_guard,
618 handle,
619 logging_guard,
620 log_filter_handle: None,
621 log_filter_metrics: None,
622 }
623 }
624
625 fn new_with_log_filter_runtime(
626 telemetry_guard: TelemetryProviderGuard,
627 logging_guard: LoggingGuard,
628 log_filter_handle: Option<LogFilterHandle>,
629 log_filter_metrics: Option<crate::metrics::log_filter::LogFilterMetrics>,
630 ) -> Self {
631 let handle = telemetry_guard.handle();
632 Self {
633 telemetry_guard,
634 handle,
635 logging_guard,
636 log_filter_handle,
637 log_filter_metrics,
638 }
639 }
640
641 pub fn noop() -> Self {
642 Self::new(TelemetryProviderGuard::noop(), LoggingGuard::noop())
643 }
644
645 #[must_use]
647 pub fn handle(&self) -> TelemetryHandle {
648 self.handle.clone()
649 }
650
651 pub fn logging_guard(&self) -> &LoggingGuard {
652 &self.logging_guard
653 }
654
655 pub fn log_filter_handle(&self) -> Option<LogFilterHandle> {
656 self.log_filter_handle.clone()
657 }
658
659 pub fn subscriber_install_status(&self) -> SubscriberInstallStatus {
660 self.telemetry_guard.subscriber_install_status()
661 }
662
663 pub fn status_handle(&self) -> crate::ObservabilityStatusHandle {
664 self.telemetry_guard.status_handle()
665 }
666
667 pub fn dropped_log_lines(&self) -> usize {
668 self.logging_guard.dropped_log_lines()
669 }
670
671 #[cfg(feature = "prometheus")]
672 pub fn prometheus_local_addr(&self) -> Option<std::net::SocketAddr> {
673 self.telemetry_guard.prometheus_local_addr()
674 }
675
676 #[cfg(feature = "prometheus")]
677 #[doc(hidden)]
678 pub fn prometheus_task_count(&self) -> usize {
679 self.telemetry_guard.prometheus_task_count()
680 }
681
682 pub fn shutdown(self) -> TelemetryShutdownReport {
683 self.shutdown_with_timeout(std::time::Duration::from_millis(
684 crate::exporter::outage::DEFAULT_SHUTDOWN_TIMEOUT_MILLIS,
685 ))
686 }
687
688 pub fn shutdown_with_timeout(mut self, timeout: std::time::Duration) -> TelemetryShutdownReport {
690 self.handle.begin_closing();
691 let subscriber_install_status = self.telemetry_guard.subscriber_install_status();
692 let file_log_enabled = self.logging_guard.file_sink_count() > 0;
693 let dropped_log_lines = self.logging_guard.dropped_log_lines();
694 if dropped_log_lines > 0 {
695 tracing::warn!(
696 dropped_log_lines,
697 "telemetry logging guard reported dropped log lines before shutdown"
698 );
699 }
700
701 let logging_guard = std::mem::replace(&mut self.logging_guard, LoggingGuard::noop());
704 drop(logging_guard);
705 let telemetry_guard = std::mem::replace(&mut self.telemetry_guard, TelemetryProviderGuard::noop());
706 let provider_report = telemetry_guard.shutdown_with_report_timeout(timeout);
707 let provider_shutdown_healthy = provider_report.is_healthy();
708 let report = TelemetryShutdownReport::new(
709 subscriber_install_status,
710 file_log_enabled,
711 dropped_log_lines,
712 provider_report,
713 );
714 if !provider_shutdown_healthy {
715 tracing::warn!(
716 report = %report.to_json(),
717 "telemetry provider shutdown report is unhealthy"
718 );
719 }
720 report
721 }
722
723 pub async fn shutdown_with_service_context(
726 self,
727 service_context: &ChildServiceContext,
728 timeout: std::time::Duration,
729 ) -> TelemetryShutdownReport {
730 self.shutdown_with_flush_authority(TelemetryFlushAuthority::ServiceContext(service_context), timeout)
731 .await
732 }
733
734 pub async fn shutdown_with_drain_lease(
741 self,
742 lease: BlockingDrainLease,
743 timeout: std::time::Duration,
744 ) -> TelemetryShutdownReport {
745 self.shutdown_with_flush_authority(TelemetryFlushAuthority::Drain(lease), timeout)
746 .await
747 }
748
749 async fn shutdown_with_flush_authority(
750 self,
751 authority: TelemetryFlushAuthority<'_>,
752 timeout: std::time::Duration,
753 ) -> TelemetryShutdownReport {
754 #[cfg(feature = "prometheus")]
755 let mut telemetry_runtime = self;
756 #[cfg(not(feature = "prometheus"))]
757 let telemetry_runtime = self;
758
759 telemetry_runtime.handle.begin_closing();
760 let deadline = ShutdownDeadline::after(timeout);
761 let subscriber_install_status = telemetry_runtime.telemetry_guard.subscriber_install_status();
762 let file_log_enabled = telemetry_runtime.logging_guard.file_sink_count() > 0;
763 let dropped_log_lines = telemetry_runtime.logging_guard.dropped_log_lines();
764 let surviving_handle = telemetry_runtime.handle.clone();
765
766 #[cfg(feature = "prometheus")]
767 let prometheus_shutdown_error = if let Some(prometheus_http) =
768 telemetry_runtime.telemetry_guard.take_prometheus_http()
769 {
770 let report = prometheus_http.shutdown_gracefully(deadline.remaining()).await;
771 (!report.is_healthy()).then(|| format!("Prometheus endpoint shutdown was unhealthy: {}", report.to_json()))
772 } else {
773 None
774 };
775
776 let shutdown_result = authority
777 .flush("observability.provider-shutdown", deadline, move || {
778 telemetry_runtime.shutdown_with_timeout(deadline.remaining())
779 })
780 .await;
781
782 let report = match shutdown_result {
783 Ok(report) => report,
784 Err(error) => {
785 surviving_handle.mark_closed();
786 let mut provider_report = TelemetryProviderShutdownReport::default();
787 provider_report
788 .record_runtime_shutdown_error(format!("owned blocking provider shutdown failed: {error}"));
789 TelemetryShutdownReport::new(
790 subscriber_install_status,
791 file_log_enabled,
792 dropped_log_lines,
793 provider_report,
794 )
795 }
796 };
797 #[cfg(feature = "prometheus")]
798 let mut report = report;
799
800 #[cfg(feature = "prometheus")]
801 if let Some(error) = prometheus_shutdown_error {
802 report.metrics_shutdown_ok = false;
803 report.metrics_shutdown_error = Some(match report.metrics_shutdown_error.take() {
804 Some(existing) => format!("{existing}; {error}"),
805 None => error,
806 });
807 }
808
809 report
810 }
811}
812
813impl Drop for TelemetryRuntimeGuard {
814 fn drop(&mut self) {
815 self.handle.begin_closing();
818 self.handle.mark_closed();
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use crate::ObservabilityConfig;
826 use std::fs;
827 use std::path::PathBuf;
828 use std::sync::atomic::AtomicU64;
829 use std::sync::atomic::Ordering;
830 use std::sync::Arc;
831 use std::sync::Mutex;
832 use std::time::SystemTime;
833 use std::time::UNIX_EPOCH;
834
835 static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
836
837 #[derive(Clone)]
838 struct RecordingLayer {
839 levels: Arc<Mutex<Vec<tracing::Level>>>,
840 }
841
842 impl Layer<Registry> for RecordingLayer {
843 fn on_event(&self, event: &tracing::Event<'_>, _context: tracing_subscriber::layer::Context<'_, Registry>) {
844 self.levels
845 .lock()
846 .expect("recorded levels lock should remain available")
847 .push(*event.metadata().level());
848 }
849 }
850
851 #[test]
852 fn noop_logging_guard_reports_zero_dropped_lines() {
853 let guard = LoggingGuard::noop();
854
855 assert_eq!(guard.file_sink_count(), 0);
856 assert_eq!(guard.dropped_log_lines(), 0);
857 }
858
859 #[test]
860 fn dropping_runtime_guard_closes_surviving_handle_clones() {
861 let config = ObservabilityConfig {
862 enabled: true,
863 ..ObservabilityConfig::default()
864 };
865 let telemetry_guard =
866 crate::init::init_telemetry_providers(&config).expect("telemetry provider guard should initialize");
867 let runtime = TelemetryRuntimeGuard::new(telemetry_guard, LoggingGuard::noop());
868 let handle = runtime.handle();
869
870 assert_eq!(handle.state(), crate::TelemetryState::Active);
871 drop(runtime);
872
873 assert_eq!(handle.state(), crate::TelemetryState::Closed);
874 }
875
876 #[test]
877 fn logging_guard_tracks_worker_guards_without_static_storage() {
878 let (writer, worker_guard) = tracing_appender::non_blocking(std::io::sink());
879 let error_counter = writer.error_counter();
880 let guard = LoggingGuard::new(vec![worker_guard], vec![error_counter]);
881
882 assert_eq!(guard.file_sink_count(), 1);
883 assert_eq!(guard.dropped_log_lines(), 0);
884 }
885
886 #[test]
887 fn env_filter_accepts_target_directives() {
888 let filter =
889 build_env_filter("rocketmq_store=debug,rocketmq_transport=warn").expect("target directives should parse");
890
891 assert!(filter.to_string().contains("rocketmq_store=debug"));
892 }
893
894 #[test]
895 fn global_info_filter_suppresses_debug_events_inside_info_spans() {
896 let levels = Arc::new(Mutex::new(Vec::new()));
897 let layers = vec![Box::new(RecordingLayer {
898 levels: Arc::clone(&levels),
899 }) as BoxedRegistryLayer];
900 let filter = Some(build_env_filter("info").expect("info filter should parse"));
901 let subscriber = compose_subscriber_layers(layers, filter);
902
903 tracing::subscriber::with_default(subscriber, || {
904 let span = tracing::info_span!("info_filter_regression");
905 let _guard = span.enter();
906 tracing::debug!("debug event should be filtered");
907 tracing::info!("info event should be recorded");
908 });
909
910 assert_eq!(
911 *levels.lock().expect("recorded levels lock should remain available"),
912 vec![tracing::Level::INFO]
913 );
914 }
915
916 #[test]
917 fn invalid_env_filter_returns_typed_error() {
918 let error = build_env_filter("rocketmq_store==debug").expect_err("invalid filter should fail");
919
920 assert_eq!(error.code(), rocketmq_error::OBSERVABILITY_LOG_FILTER_INVALID.code());
921 }
922
923 #[test]
924 fn disabled_console_config_builds_no_layer() {
925 let config = ConsoleLogConfig {
926 enabled: false,
927 ..ConsoleLogConfig::default()
928 };
929
930 assert!(build_console_layer(&config).is_none());
931 }
932
933 #[test]
934 fn console_layer_builder_supports_all_formats() {
935 for format in [LogFormat::Compact, LogFormat::Pretty, LogFormat::Json] {
936 let config = ConsoleLogConfig {
937 format,
938 ..ConsoleLogConfig::default()
939 };
940
941 assert!(build_console_layer(&config).is_some());
942 }
943 }
944
945 #[test]
946 fn disabled_file_config_builds_no_layer_or_directory() {
947 let directory = unique_temp_log_dir("disabled_file_config_builds_no_layer_or_directory");
948 let config = FileLogConfig {
949 enabled: false,
950 directory: directory.to_string_lossy().into_owned(),
951 ..FileLogConfig::default()
952 };
953
954 let layer = build_file_layer(&config).expect("disabled file sink should not fail");
955
956 assert!(layer.is_none());
957 assert!(!directory.exists());
958 }
959
960 #[test]
961 fn file_layer_creates_missing_directory_and_tracks_guard() {
962 let directory = unique_temp_log_dir("file_layer_creates_missing_directory_and_tracks_guard");
963 let config = FileLogConfig {
964 enabled: true,
965 directory: directory.to_string_lossy().into_owned(),
966 file_name_prefix: "rocketmq-test.log".to_string(),
967 rotation: LogRotation::Never,
968 ..FileLogConfig::default()
969 };
970
971 let layer = build_file_layer(&config)
972 .expect("file sink should build")
973 .expect("enabled file sink should return a layer");
974
975 assert!(directory.exists());
976 assert_eq!(layer.logging_guard().file_sink_count(), 1);
977 assert_eq!(layer.logging_guard().dropped_log_lines(), 0);
978 assert!(directory.join("rocketmq-test.log").exists());
979
980 drop(layer);
981 cleanup_temp_log_dir(directory);
982 }
983
984 #[test]
985 fn file_layer_supports_rotation_variants() {
986 for rotation in [LogRotation::Never, LogRotation::Hourly, LogRotation::Daily] {
987 let directory = unique_temp_log_dir("file_layer_supports_rotation_variants");
988 let config = FileLogConfig {
989 enabled: true,
990 directory: directory.to_string_lossy().into_owned(),
991 file_name_prefix: "rocketmq-rotation.log".to_string(),
992 rotation,
993 ..FileLogConfig::default()
994 };
995
996 let layer = build_file_layer(&config)
997 .expect("file sink should build")
998 .expect("enabled file sink should return a layer");
999
1000 assert_eq!(layer.logging_guard().file_sink_count(), 1);
1001 assert!(
1002 fs::read_dir(&directory)
1003 .expect("log directory should be readable")
1004 .next()
1005 .is_some(),
1006 "rotation {rotation:?} should create a log file"
1007 );
1008
1009 drop(layer);
1010 cleanup_temp_log_dir(directory);
1011 }
1012 }
1013
1014 #[test]
1015 fn file_layer_writes_without_ansi_escape_codes() {
1016 let directory = unique_temp_log_dir("file_layer_writes_without_ansi_escape_codes");
1017 let config = FileLogConfig {
1018 enabled: true,
1019 directory: directory.to_string_lossy().into_owned(),
1020 file_name_prefix: "ansi-free.log".to_string(),
1021 rotation: LogRotation::Never,
1022 format: LogFormat::Compact,
1023 ..FileLogConfig::default()
1024 };
1025
1026 let layer = build_file_layer(&config)
1027 .expect("file sink should build")
1028 .expect("enabled file sink should return a layer");
1029 let (layer, logging_guard) = layer.into_parts();
1030 let subscriber = Registry::default().with(layer);
1031
1032 tracing::subscriber::with_default(subscriber, || {
1033 tracing::info!(target: "rocketmq_observability::logging::tests", "file sink event");
1034 });
1035 drop(logging_guard);
1036
1037 let contents =
1038 fs::read_to_string(directory.join("ansi-free.log")).expect("file sink output should be readable");
1039 assert!(contents.contains("file sink event"));
1040 assert!(!contents.contains('\u{1b}'));
1041
1042 cleanup_temp_log_dir(directory);
1043 }
1044
1045 #[test]
1046 fn invalid_file_layer_config_returns_typed_error() {
1047 let config = FileLogConfig {
1048 enabled: true,
1049 directory: String::new(),
1050 ..FileLogConfig::default()
1051 };
1052
1053 let error = match build_file_layer(&config) {
1054 Ok(_) => panic!("empty directory should fail"),
1055 Err(error) => error,
1056 };
1057
1058 assert_eq!(
1059 error.operation(),
1060 crate::error::ObservabilityOperation::InitializeSubscriber
1061 );
1062 }
1063
1064 #[test]
1065 fn noop_runtime_guard_exposes_zero_logging_and_subscriber_status() {
1066 let guard = TelemetryRuntimeGuard::noop();
1067
1068 assert_eq!(guard.dropped_log_lines(), 0);
1069 assert_eq!(
1070 guard.subscriber_install_status(),
1071 SubscriberInstallStatus {
1072 attempted: false,
1073 installed: false,
1074 }
1075 );
1076 }
1077
1078 #[test]
1079 fn noop_runtime_guard_shutdown_succeeds() {
1080 let report = TelemetryRuntimeGuard::noop().shutdown();
1081
1082 assert!(report.is_healthy());
1083 assert_eq!(report.dropped_log_lines, 0);
1084 assert!(!report.file_log_enabled);
1085 assert!(!report.subscriber_installed);
1086 report
1087 .into_result()
1088 .expect("noop runtime guard shutdown should succeed");
1089 }
1090
1091 #[test]
1092 fn noop_runtime_guard_honors_caller_shutdown_budget() {
1093 let report = TelemetryRuntimeGuard::noop().shutdown_with_timeout(std::time::Duration::ZERO);
1094
1095 assert!(report.is_healthy());
1096 }
1097
1098 #[test]
1099 fn runtime_shutdown_report_preserves_provider_failure() {
1100 let report = TelemetryShutdownReport {
1101 subscriber_installed: true,
1102 file_log_enabled: true,
1103 dropped_log_lines: 3,
1104 logs_shutdown_ok: false,
1105 traces_shutdown_ok: true,
1106 metrics_shutdown_ok: true,
1107 logs_shutdown_error: Some("logger provider failed".to_string()),
1108 traces_shutdown_error: None,
1109 metrics_shutdown_error: None,
1110 };
1111
1112 assert!(!report.is_healthy());
1113 let error = report.into_result().expect_err("log shutdown should fail");
1114 assert_eq!(error.operation(), crate::error::ObservabilityOperation::ShutdownLogs);
1115 }
1116
1117 #[tokio::test]
1118 async fn final_flush_remains_owned_after_its_observation_deadline() {
1119 use std::sync::{Arc, Condvar, Mutex};
1120 let context = rocketmq_runtime::RuntimeContext::from_current("flush-deadline");
1121 let service = context.service_context("telemetry");
1122 let deadline = ShutdownDeadline::after(Duration::from_millis(100));
1123 let lease = reserve_telemetry_flush_lease(&service, Some(deadline)).unwrap();
1124 assert!(service.task_group().shutdown_until(deadline).await.is_healthy());
1125 let gate = Arc::new((Mutex::new(false), Condvar::new()));
1126 struct Release(Arc<(Mutex<bool>, Condvar)>);
1127 impl Drop for Release {
1128 fn drop(&mut self) {
1129 *self.0 .0.lock().unwrap() = true;
1130 self.0 .1.notify_all();
1131 }
1132 }
1133 let release = Release(gate.clone());
1134 let (started, wait_started) = tokio::sync::oneshot::channel();
1135 let flush = TelemetryFlushAuthority::Drain(lease).flush("blocked-final-flush", deadline, move || {
1136 let _ = started.send(());
1137 let mut open = gate.0.lock().unwrap();
1138 while !*open {
1139 open = gate.1.wait(open).unwrap();
1140 }
1141 TelemetryRuntimeGuard::noop().shutdown()
1142 });
1143 let (result, started) = tokio::join!(flush, wait_started);
1144 started.unwrap();
1145 assert!(result.is_err());
1146 assert_eq!(service.metadata_io().blocking_still_running(), 1);
1147 drop(release);
1148 tokio::time::timeout(Duration::from_secs(2), async {
1149 while service.metadata_io().blocking_still_running() != 0 {
1150 tokio::task::yield_now().await;
1151 }
1152 })
1153 .await
1154 .unwrap();
1155 }
1156
1157 fn unique_temp_log_dir(test_name: &str) -> PathBuf {
1158 let id = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
1159 let nanos = SystemTime::now()
1160 .duration_since(UNIX_EPOCH)
1161 .expect("system clock should be after Unix epoch")
1162 .as_nanos();
1163 std::env::temp_dir().join(format!(
1164 "rocketmq-observability-{test_name}-{}-{id}-{nanos}",
1165 std::process::id()
1166 ))
1167 }
1168
1169 fn cleanup_temp_log_dir(directory: PathBuf) {
1170 if directory.exists() {
1171 fs::remove_dir_all(&directory).expect("temporary log directory should be removable");
1172 }
1173 }
1174}