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}