Skip to main content

rocketmq_observability/
logging.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
136/// Installs process telemetry with an explicit owner for runtime-backed exporters.
137pub 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
155/// Installs process telemetry with a resolved filter and an explicit task owner.
156pub 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
546/// The blocking authority that runs the final provider flush.
547///
548/// A service context is used while its scope still admits work; a drain lease reserved
549/// from that scope carries the same allowance into owner finalization.
550enum 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
577/// A reservation made before the owner knows its shutdown deadline only has to outlive the
578/// process: the deadline the scope installs when it stops becomes the effective bound.
579const UNKNOWN_SHUTDOWN_DEADLINE_HORIZON: Duration = Duration::from_secs(u32::MAX as u64);
580
581/// Reserves one blocking submission for a final telemetry flush owned by `service_context`.
582///
583/// Owner finalization reaches telemetry after the owning service task group has been shut
584/// down, and that closed scope refuses ordinary submissions. Reserving the allowance while
585/// the scope is still open keeps the last flush admissible, bounded by `deadline` or by the
586/// shutdown deadline the scope installs later, whichever is earlier. An owner that reserves
587/// before its shutdown deadline exists, because its flush runs after the service scope has
588/// already closed, passes `None`.
589///
590/// The caller falls back to [`TelemetryRuntimeGuard::shutdown_with_service_context`] and
591/// reports that outcome when the allowance cannot be reserved.
592#[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    /// Returns the cloneable business telemetry capability owned by this runtime.
646    #[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    /// Flushes logging and telemetry providers without exceeding one shared timeout budget.
689    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        // Drop local file logging guards before shutting down OpenTelemetry providers so
702        // non-blocking file writers are flushed and joined under explicit runtime ownership.
703        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    /// Gracefully joins runtime-backed exporters and runs blocking provider flushes on the
724    /// service's owned blocking executor within one shared timeout budget.
725    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    /// Runs the same flush as [`Self::shutdown_with_service_context`] on a drain lease
735    /// reserved from the owning scope while that scope was still open.
736    ///
737    /// Owner finalization reaches telemetry after the owning task group has already been
738    /// shut down, and the closed scope then refuses ordinary submissions. The lease carries
739    /// the reserved allowance past that point without reopening the scope.
740    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        // Do not block or spawn during unwinding/implicit drop. Provider flushing and reports are
816        // available only through explicit shutdown.
817        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}