Skip to main content

wire_framework/vlog/
mod.rs

1//! This crate contains the observability subsystem.
2//! It is responsible for providing a centralized interface for consistent observability configuration.
3
4use std::time::Duration;
5
6use ::sentry::ClientInitGuard;
7use eyre::Context as _;
8use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
9
10pub mod logs;
11pub mod opentelemetry;
12pub mod prometheus;
13pub mod sentry;
14
15pub use logs::Logs;
16pub use opentelemetry::OpenTelemetry;
17pub use sentry::Sentry;
18
19/// Builder for the observability subsystem.
20/// Currently capable of configuring logging output and sentry integration.
21#[derive(Debug, Default)]
22pub struct ObservabilityBuilder {
23    logs: Option<Logs>,
24    opentelemetry_layer: Option<OpenTelemetry>,
25    sentry: Option<Sentry>,
26}
27
28/// Guard for the observability subsystem.
29/// Releases configured integrations upon being dropped.
30pub struct ObservabilityGuard {
31    /// Opentelemetry traces provider
32    otlp_tracing_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
33    /// Opentelemetry logs provider
34    otlp_logging_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
35    /// Sentry client guard
36    sentry_guard: Option<ClientInitGuard>,
37}
38
39impl ObservabilityGuard {
40    /// Forces flushing of pending events.
41    /// This method is blocking.
42    pub fn force_flush(&self) {
43        // We don't want to wait for too long.
44        const FLUSH_TIMEOUT: Duration = Duration::from_secs(1);
45
46        if let Some(sentry_guard) = &self.sentry_guard {
47            sentry_guard.flush(Some(FLUSH_TIMEOUT));
48            tracing::info!("Sentry events are flushed");
49        }
50
51        if let Some(provider) = &self.otlp_tracing_provider {
52            if let Err(err) = provider.force_flush() {
53                tracing::warn!("Flushing the spans failed: {err:?}");
54            }
55            tracing::info!("Spans are flushed");
56        }
57
58        if let Some(provider) = &self.otlp_logging_provider {
59            if let Err(err) = provider.force_flush() {
60                tracing::warn!("Flushing the logs failed: {err:?}");
61            }
62            tracing::info!("Logs are flushed");
63        }
64    }
65
66    /// Shutdown the observability subsystem.
67    /// It will stop any background tasks and release resources.
68    pub fn shutdown(&mut self) {
69        // We don't want to wait for too long.
70        const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1);
71
72        // `take` here and below ensures that we don't have any access to the deinitialized resources.
73        if let Some(sentry_guard) = self.sentry_guard.take() {
74            sentry_guard.close(Some(SHUTDOWN_TIMEOUT));
75            tracing::info!("Sentry client is shut down");
76        }
77        if let Some(provider) = self.otlp_tracing_provider.take() {
78            if let Err(err) = provider.shutdown() {
79                tracing::warn!("Shutting down the OTLP tracing provider failed: {err:?}");
80            } else {
81                tracing::info!("OTLP tracing provider is shut down");
82            }
83        }
84        if let Some(provider) = self.otlp_logging_provider.take() {
85            if let Err(err) = provider.shutdown() {
86                tracing::warn!("Shutting down the OTLP logs provider failed: {err:?}");
87            } else {
88                tracing::info!("OTLP logs provider is shut down");
89            }
90        }
91    }
92}
93
94impl Drop for ObservabilityGuard {
95    fn drop(&mut self) {
96        self.force_flush();
97        self.shutdown();
98    }
99}
100
101impl std::fmt::Debug for ObservabilityGuard {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        f.debug_struct("ObservabilityGuard").finish()
104    }
105}
106
107impl ObservabilityBuilder {
108    /// Creates a new builder with default values.
109    pub fn new() -> Self {
110        Self::default()
111    }
112
113    pub fn with_logs(mut self, logs: Option<Logs>) -> Self {
114        self.logs = logs;
115        self
116    }
117
118    pub fn with_opentelemetry(mut self, opentelemetry: Option<OpenTelemetry>) -> Self {
119        self.opentelemetry_layer = opentelemetry;
120        self
121    }
122
123    pub fn with_sentry(mut self, sentry: Option<Sentry>) -> Self {
124        self.sentry = sentry;
125        self
126    }
127
128    /// Tries to initialize the observability subsystem. Returns an error if it's already initialized.
129    /// This is mostly useful in tests.
130    pub fn try_build(self) -> eyre::Result<ObservabilityGuard> {
131        let logs = self.logs.unwrap_or_default();
132        logs.install_panic_hook();
133
134        // For now we use logs filter as a global filter for subscriber.
135        // Later we may want to enforce each layer to have its own filter.
136        let global_filter = logs.build_filter();
137
138        let logs_layer = logs.into_layer();
139        let (otlp_tracing_provider, otlp_tracing_layer) = self
140            .opentelemetry_layer
141            .as_ref()
142            .and_then(|layer| layer.tracing_layer())
143            .unzip();
144        let (otlp_logging_provider, otlp_logging_layer) = self
145            .opentelemetry_layer
146            .and_then(|layer| layer.logs_layer())
147            .unzip();
148
149        tracing_subscriber::registry()
150            .with(global_filter)
151            .with(logs_layer)
152            .with(otlp_tracing_layer)
153            .with(otlp_logging_layer)
154            .try_init()
155            .context("failed installing global tracer / logger")?;
156
157        let sentry_guard = self.sentry.map(|sentry| sentry.install());
158
159        Ok(ObservabilityGuard {
160            otlp_tracing_provider,
161            otlp_logging_provider,
162            sentry_guard,
163        })
164    }
165
166    /// Initializes the observability subsystem.
167    pub fn build(self) -> ObservabilityGuard {
168        self.try_build().unwrap()
169    }
170}