strut_sentry/
integration.rs

1use crate::SentryConfig;
2use sentry::ClientInitGuard as SentryGuard;
3use strut_core::{AppContext, AppProfile, AppReplica, AppSpindown, AppSpindownToken};
4use tokio::runtime::Runtime;
5
6/// A facade for integrating with Sentry.
7pub struct SentryIntegration;
8
9impl SentryIntegration {
10    /// Initializes Sentry integration and returns the
11    /// [client guard](SentryGuard).
12    ///
13    /// The behavior of the Sentry client is partly configurable from the provided
14    /// [`SentryConfig`].
15    pub fn init(config: impl AsRef<SentryConfig>) -> SentryGuard {
16        let config = config.as_ref();
17
18        let guard = sentry::init((
19            config.dsn().unsecure(),
20            sentry::ClientOptions {
21                debug: config.debug(),
22                release: sentry::release_name!(),
23                environment: Some(AppProfile::active().as_str().into()),
24                sample_rate: config.sample_rate(),
25                traces_sample_rate: config.traces_sample_rate(),
26                max_breadcrumbs: config.max_breadcrumbs(),
27                attach_stacktrace: config.attach_stacktrace(),
28                shutdown_timeout: config.shutdown_timeout(),
29                ..Default::default()
30            },
31        ));
32
33        sentry::configure_scope(|scope| {
34            scope.set_tag(
35                "replica_index",
36                AppReplica::index()
37                    .map(|index| index.to_string())
38                    .unwrap_or_else(|| "unset".into()),
39            );
40
41            scope.set_tag("replica_lifetime_id", AppReplica::lifetime_id());
42        });
43
44        guard
45    }
46
47    /// Schedules flushing of unsend Sentry events (if any) after the
48    /// [application context](AppContext) is terminated. The flushing is
49    /// triggered by dropping the given [Sentry guard](SentryGuard).
50    ///
51    /// This scheduling involves spawning an asynchronous task. To avoid
52    /// accidentally calling this function outside of Tokio context, this
53    /// function explicitly takes a [runtime](Runtime) as an argument.
54    pub fn schedule_flushing(runtime: &Runtime, sentry_guard: SentryGuard) {
55        let spindown_token = AppSpindown::register("sentry-integration");
56
57        runtime.spawn(Self::await_shutdown(sentry_guard, spindown_token));
58    }
59
60    /// Awaits for [`AppContext`] to get terminated, then drops the provided
61    /// Sentry guard. Dropping the guard flushes unsent Sentry events, however it
62    /// does so synchronously (blocks the current thread). To let the flushing
63    /// run asynchronously, we [off-load](tokio::task::spawn_blocking) it to a
64    /// blocking thread pool.
65    async fn await_shutdown(sentry_guard: SentryGuard, spindown_token: AppSpindownToken) {
66        // Wait until the global context is terminated
67        AppContext::terminated().await;
68
69        // Initiate dropping of Sentry guard on a blocking thread pool
70        tokio::task::spawn_blocking(move || Self::drop_guard(sentry_guard, spindown_token));
71    }
72
73    /// Drops the given [Sentry guard](SentryGuard) first, then the given
74    /// [`AppSpindownToken`].
75    fn drop_guard(guard: SentryGuard, _spindown_token: AppSpindownToken) {
76        // Drop the Sentry guard: this will synchronously flush all events within Sentry’s own timeout
77        drop(guard);
78
79        // The spindown token will punch itself out when dropped
80    }
81}